mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-07-12 06:10:23 +00:00
Implemented playable character registry, added Freeplay character filtering, added alt instrumental support
This commit is contained in:
parent
6f2f2a9aa4
commit
60e741434c
2
assets
2
assets
|
@ -1 +1 @@
|
||||||
Subproject commit 2e1594ee4c04c7148628bae471bdd061c9deb6b7
|
Subproject commit fece99b3b121045fb2f6f02dba485201b32f1c87
|
|
@ -1,5 +1,6 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
|
import funkin.data.freeplay.player.PlayerRegistry;
|
||||||
import funkin.ui.debug.charting.ChartEditorState;
|
import funkin.ui.debug.charting.ChartEditorState;
|
||||||
import funkin.ui.transition.LoadingState;
|
import funkin.ui.transition.LoadingState;
|
||||||
import flixel.FlxState;
|
import flixel.FlxState;
|
||||||
|
@ -164,6 +165,7 @@ class InitState extends FlxState
|
||||||
SongRegistry.instance.loadEntries();
|
SongRegistry.instance.loadEntries();
|
||||||
LevelRegistry.instance.loadEntries();
|
LevelRegistry.instance.loadEntries();
|
||||||
NoteStyleRegistry.instance.loadEntries();
|
NoteStyleRegistry.instance.loadEntries();
|
||||||
|
PlayerRegistry.instance.loadEntries();
|
||||||
ConversationRegistry.instance.loadEntries();
|
ConversationRegistry.instance.loadEntries();
|
||||||
DialogueBoxRegistry.instance.loadEntries();
|
DialogueBoxRegistry.instance.loadEntries();
|
||||||
SpeakerRegistry.instance.loadEntries();
|
SpeakerRegistry.instance.loadEntries();
|
||||||
|
|
9
source/funkin/data/freeplay/player/CHANGELOG.md
Normal file
9
source/funkin/data/freeplay/player/CHANGELOG.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Freeplay Playable Character Data Schema Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0]
|
||||||
|
Initial release.
|
63
source/funkin/data/freeplay/player/PlayerData.hx
Normal file
63
source/funkin/data/freeplay/player/PlayerData.hx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package funkin.data.freeplay.player;
|
||||||
|
|
||||||
|
import funkin.data.animation.AnimationData;
|
||||||
|
|
||||||
|
@:nullSafety
|
||||||
|
class PlayerData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The sematic version number of the player data JSON format.
|
||||||
|
* Supports fancy comparisons like NPM does it's neat.
|
||||||
|
*/
|
||||||
|
@:default(funkin.data.freeplay.player.PlayerRegistry.PLAYER_DATA_VERSION)
|
||||||
|
public var version:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A readable name for this playable character.
|
||||||
|
*/
|
||||||
|
public var name:String = 'Unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The character IDs this character is associated with.
|
||||||
|
* Only songs that use these characters will show up in Freeplay.
|
||||||
|
*/
|
||||||
|
@:default([])
|
||||||
|
public var ownedChars:Array<String> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show songs with character IDs that aren't associated with any specific character.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(false)
|
||||||
|
public var showUnownedChars:Bool = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this character is unlocked by default.
|
||||||
|
* Use a ScriptedPlayableCharacter to add custom logic.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
@:default(true)
|
||||||
|
public var unlocked:Bool = true;
|
||||||
|
|
||||||
|
public function new()
|
||||||
|
{
|
||||||
|
this.version = PlayerRegistry.PLAYER_DATA_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this StageData into a JSON string.
|
||||||
|
*/
|
||||||
|
public function serialize(pretty:Bool = true):String
|
||||||
|
{
|
||||||
|
// Update generatedBy and version before writing.
|
||||||
|
updateVersionToLatest();
|
||||||
|
|
||||||
|
var writer = new json2object.JsonWriter<PlayerData>();
|
||||||
|
return writer.write(this, pretty ? ' ' : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateVersionToLatest():Void
|
||||||
|
{
|
||||||
|
this.version = PlayerRegistry.PLAYER_DATA_VERSION;
|
||||||
|
}
|
||||||
|
}
|
151
source/funkin/data/freeplay/player/PlayerRegistry.hx
Normal file
151
source/funkin/data/freeplay/player/PlayerRegistry.hx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package funkin.data.freeplay.player;
|
||||||
|
|
||||||
|
import funkin.data.freeplay.player.PlayerData;
|
||||||
|
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||||
|
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
|
||||||
|
|
||||||
|
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current version string for the stage data format.
|
||||||
|
* Handle breaking changes by incrementing this value
|
||||||
|
* and adding migration to the `migratePlayerData()` function.
|
||||||
|
*/
|
||||||
|
public static final PLAYER_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||||
|
|
||||||
|
public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||||
|
|
||||||
|
public static var instance(get, never):PlayerRegistry;
|
||||||
|
static var _instance:Null<PlayerRegistry> = null;
|
||||||
|
|
||||||
|
static function get_instance():PlayerRegistry
|
||||||
|
{
|
||||||
|
if (_instance == null) _instance = new PlayerRegistry();
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping between stage character IDs and Freeplay playable character IDs.
|
||||||
|
*/
|
||||||
|
var ownedCharacterIds:Map<String, String> = [];
|
||||||
|
|
||||||
|
public function new()
|
||||||
|
{
|
||||||
|
super('PLAYER', 'players', PLAYER_DATA_VERSION_RULE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function loadEntries():Void
|
||||||
|
{
|
||||||
|
super.loadEntries();
|
||||||
|
|
||||||
|
for (playerId in listEntryIds())
|
||||||
|
{
|
||||||
|
var player = fetchEntry(playerId);
|
||||||
|
if (player == null) continue;
|
||||||
|
|
||||||
|
var currentPlayerCharIds = player.getOwnedCharacterIds();
|
||||||
|
for (characterId in currentPlayerCharIds)
|
||||||
|
{
|
||||||
|
ownedCharacterIds.set(characterId, playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the playable character associated with a given stage character.
|
||||||
|
* @param characterId The stage character ID.
|
||||||
|
* @return The playable character.
|
||||||
|
*/
|
||||||
|
public function getCharacterOwnerId(characterId:String):String
|
||||||
|
{
|
||||||
|
return ownedCharacterIds[characterId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the given stage character is associated with a specific playable character.
|
||||||
|
* If so, the level should only appear if that character is selected in Freeplay.
|
||||||
|
* @param characterId The stage character ID.
|
||||||
|
* @return Whether the character is owned by any one character.
|
||||||
|
*/
|
||||||
|
public function isCharacterOwned(characterId:String):Bool
|
||||||
|
{
|
||||||
|
return ownedCharacterIds.exists(characterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||||
|
*/
|
||||||
|
public function parseEntryData(id:String):Null<PlayerData>
|
||||||
|
{
|
||||||
|
// JsonParser does not take type parameters,
|
||||||
|
// otherwise this function would be in BaseRegistry.
|
||||||
|
var parser = new json2object.JsonParser<PlayerData>();
|
||||||
|
parser.ignoreUnknownVariables = false;
|
||||||
|
|
||||||
|
switch (loadEntryFile(id))
|
||||||
|
{
|
||||||
|
case {fileName: fileName, contents: contents}:
|
||||||
|
parser.fromJson(contents, fileName);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parser.errors.length > 0)
|
||||||
|
{
|
||||||
|
printErrors(parser.errors, id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parser.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and validate the JSON data and produce the corresponding data object.
|
||||||
|
*
|
||||||
|
* NOTE: Must be implemented on the implementation class.
|
||||||
|
* @param contents The JSON as a string.
|
||||||
|
* @param fileName An optional file name for error reporting.
|
||||||
|
*/
|
||||||
|
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<PlayerData>
|
||||||
|
{
|
||||||
|
var parser = new json2object.JsonParser<PlayerData>();
|
||||||
|
parser.ignoreUnknownVariables = false;
|
||||||
|
parser.fromJson(contents, fileName);
|
||||||
|
|
||||||
|
if (parser.errors.length > 0)
|
||||||
|
{
|
||||||
|
printErrors(parser.errors, fileName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parser.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScriptedEntry(clsName:String):PlayableCharacter
|
||||||
|
{
|
||||||
|
return ScriptedPlayableCharacter.init(clsName, "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptedClassNames():Array<String>
|
||||||
|
{
|
||||||
|
return ScriptedPlayableCharacter.listScriptClasses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all the playable characters from the base game, in order.
|
||||||
|
*/
|
||||||
|
public function listBaseGamePlayerIds():Array<String>
|
||||||
|
{
|
||||||
|
return ["bf", "pico"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of all installed playable characters that are not from the base game.
|
||||||
|
*/
|
||||||
|
public function listModdedPlayerIds():Array<String>
|
||||||
|
{
|
||||||
|
return listEntryIds().filter(function(id:String):Bool {
|
||||||
|
return listBaseGamePlayerIds().indexOf(id) == -1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import funkin.data.event.SongEventRegistry;
|
||||||
import funkin.data.story.level.LevelRegistry;
|
import funkin.data.story.level.LevelRegistry;
|
||||||
import funkin.data.notestyle.NoteStyleRegistry;
|
import funkin.data.notestyle.NoteStyleRegistry;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
|
import funkin.data.freeplay.player.PlayerRegistry;
|
||||||
import funkin.data.stage.StageRegistry;
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.data.freeplay.album.AlbumRegistry;
|
import funkin.data.freeplay.album.AlbumRegistry;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
|
@ -369,15 +370,18 @@ class PolymodHandler
|
||||||
|
|
||||||
// These MUST be imported at the top of the file and not referred to by fully qualified name,
|
// These MUST be imported at the top of the file and not referred to by fully qualified name,
|
||||||
// to ensure build macros work properly.
|
// to ensure build macros work properly.
|
||||||
|
SongEventRegistry.loadEventCache();
|
||||||
|
|
||||||
SongRegistry.instance.loadEntries();
|
SongRegistry.instance.loadEntries();
|
||||||
LevelRegistry.instance.loadEntries();
|
LevelRegistry.instance.loadEntries();
|
||||||
NoteStyleRegistry.instance.loadEntries();
|
NoteStyleRegistry.instance.loadEntries();
|
||||||
SongEventRegistry.loadEventCache();
|
PlayerRegistry.instance.loadEntries();
|
||||||
ConversationRegistry.instance.loadEntries();
|
ConversationRegistry.instance.loadEntries();
|
||||||
DialogueBoxRegistry.instance.loadEntries();
|
DialogueBoxRegistry.instance.loadEntries();
|
||||||
SpeakerRegistry.instance.loadEntries();
|
SpeakerRegistry.instance.loadEntries();
|
||||||
AlbumRegistry.instance.loadEntries();
|
AlbumRegistry.instance.loadEntries();
|
||||||
StageRegistry.instance.loadEntries();
|
StageRegistry.instance.loadEntries();
|
||||||
|
|
||||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||||
ModuleHandler.loadModuleCache();
|
ModuleHandler.loadModuleCache();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import funkin.data.song.SongData.SongTimeFormat;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||||
import funkin.util.SortUtil;
|
import funkin.util.SortUtil;
|
||||||
import openfl.utils.Assets;
|
import openfl.utils.Assets;
|
||||||
|
|
||||||
|
@ -401,11 +402,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFirstValidVariation(?diffId:String, ?possibleVariations:Array<String>):Null<String>
|
public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array<String>):Null<String>
|
||||||
{
|
{
|
||||||
if (possibleVariations == null)
|
if (possibleVariations == null)
|
||||||
{
|
{
|
||||||
possibleVariations = variations;
|
possibleVariations = getVariationsByCharacter(currentCharacter);
|
||||||
possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST));
|
possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST));
|
||||||
}
|
}
|
||||||
if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
|
if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
|
||||||
|
@ -422,22 +423,29 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||||
/**
|
/**
|
||||||
* Given that this character is selected in the Freeplay menu,
|
* Given that this character is selected in the Freeplay menu,
|
||||||
* which variations should be available?
|
* which variations should be available?
|
||||||
* @param charId The character ID to query.
|
* @param char The playable character to query.
|
||||||
* @return An array of available variations.
|
* @return An array of available variations.
|
||||||
*/
|
*/
|
||||||
public function getVariationsByCharId(?charId:String):Array<String>
|
public function getVariationsByCharacter(?char:PlayableCharacter):Array<String>
|
||||||
{
|
{
|
||||||
if (charId == null) charId = Constants.DEFAULT_CHARACTER;
|
if (char == null) return variations;
|
||||||
|
|
||||||
if (variations.contains(charId))
|
var result = [];
|
||||||
|
trace('Evaluating variations for ${this.id} ${char.id}: ${this.variations}');
|
||||||
|
for (variation in variations)
|
||||||
{
|
{
|
||||||
return [charId];
|
var metadata = _metadata.get(variation);
|
||||||
}
|
|
||||||
else
|
var playerCharId = metadata?.playData?.characters?.player;
|
||||||
{
|
if (playerCharId == null) continue;
|
||||||
// TODO: How to exclude character variations while keeping other custom variations?
|
|
||||||
return variations;
|
if (char.shouldShowCharacter(playerCharId))
|
||||||
|
{
|
||||||
|
result.push(variation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -455,6 +463,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
||||||
if (variationIds == null) variationIds = [];
|
if (variationIds == null) variationIds = [];
|
||||||
if (variationId != null) variationIds.push(variationId);
|
if (variationId != null) variationIds.push(variationId);
|
||||||
|
|
||||||
|
if (variationIds.length == 0) return [];
|
||||||
|
|
||||||
// The difficulties array contains entries like 'normal', 'nightmare-erect', and 'normal-pico',
|
// The difficulties array contains entries like 'normal', 'nightmare-erect', and 'normal-pico',
|
||||||
// so we have to map it to the actual difficulty names.
|
// so we have to map it to the actual difficulty names.
|
||||||
// We also filter out difficulties that don't match the variation or that don't exist.
|
// We also filter out difficulties that don't match the variation or that don't exist.
|
||||||
|
|
|
@ -808,8 +808,11 @@ class ChartEditorDialogHandler
|
||||||
}
|
}
|
||||||
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
|
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
|
||||||
#if FILE_DROP_SUPPORTED
|
#if FILE_DROP_SUPPORTED
|
||||||
state.addDropHandler({component: songVariationMetadataEntry, handler: onDropFileMetadataVariation.bind(variation)
|
state.addDropHandler(
|
||||||
.bind(songVariationMetadataEntryLabel)});
|
{
|
||||||
|
component: songVariationMetadataEntry,
|
||||||
|
handler: onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel)
|
||||||
|
});
|
||||||
#end
|
#end
|
||||||
chartContainerB.addComponent(songVariationMetadataEntry);
|
chartContainerB.addComponent(songVariationMetadataEntry);
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import flixel.util.FlxTimer;
|
||||||
import funkin.audio.FunkinSound;
|
import funkin.audio.FunkinSound;
|
||||||
import funkin.data.story.level.LevelRegistry;
|
import funkin.data.story.level.LevelRegistry;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
|
import funkin.data.freeplay.player.PlayerRegistry;
|
||||||
import funkin.graphics.FunkinCamera;
|
import funkin.graphics.FunkinCamera;
|
||||||
import funkin.graphics.FunkinSprite;
|
import funkin.graphics.FunkinSprite;
|
||||||
import funkin.graphics.shaders.AngleMask;
|
import funkin.graphics.shaders.AngleMask;
|
||||||
|
@ -47,6 +48,7 @@ import lime.utils.Assets;
|
||||||
import flixel.tweens.misc.ShakeTween;
|
import flixel.tweens.misc.ShakeTween;
|
||||||
import funkin.effects.IntervalShake;
|
import funkin.effects.IntervalShake;
|
||||||
import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
|
import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
|
||||||
|
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters used to initialize the FreeplayState.
|
* Parameters used to initialize the FreeplayState.
|
||||||
|
@ -102,7 +104,9 @@ class FreeplayState extends MusicBeatSubState
|
||||||
* The current character for this FreeplayState.
|
* The current character for this FreeplayState.
|
||||||
* You can't change this without transitioning to a new FreeplayState.
|
* You can't change this without transitioning to a new FreeplayState.
|
||||||
*/
|
*/
|
||||||
final currentCharacter:String;
|
final currentCharacterId:String;
|
||||||
|
|
||||||
|
final currentCharacter:PlayableCharacter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For the audio preview, the duration of the fade-in effect.
|
* For the audio preview, the duration of the fade-in effect.
|
||||||
|
@ -205,7 +209,8 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
||||||
{
|
{
|
||||||
currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
|
currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER;
|
||||||
|
currentCharacter = PlayerRegistry.instance.fetchEntry(currentCharacterId);
|
||||||
|
|
||||||
fromResultsParams = params?.fromResults;
|
fromResultsParams = params?.fromResults;
|
||||||
|
|
||||||
|
@ -290,11 +295,10 @@ class FreeplayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only display songs which actually have available difficulties for the current character.
|
// Only display songs which actually have available difficulties for the current character.
|
||||||
var displayedVariations = song.getVariationsByCharId(currentCharacter);
|
var displayedVariations = song.getVariationsByCharacter(currentCharacter);
|
||||||
trace(songId);
|
trace('Displayed Variations (${songId}): $displayedVariations');
|
||||||
trace(displayedVariations);
|
|
||||||
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
|
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
|
||||||
trace(availableDifficultiesForSong);
|
trace('Available Difficulties: $availableDifficultiesForSong');
|
||||||
if (availableDifficultiesForSong.length == 0) continue;
|
if (availableDifficultiesForSong.length == 0) continue;
|
||||||
|
|
||||||
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
|
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
|
||||||
|
@ -454,7 +458,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Replace this.
|
// TODO: Replace this.
|
||||||
if (currentCharacter == 'pico') dj.visible = false;
|
if (currentCharacterId == 'pico') dj.visible = false;
|
||||||
|
|
||||||
add(dj);
|
add(dj);
|
||||||
|
|
||||||
|
@ -1195,6 +1199,16 @@ class FreeplayState extends MusicBeatSubState
|
||||||
rankAnimStart(fromResultsParams);
|
rankAnimStart(fromResultsParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FlxG.keys.justPressed.P)
|
||||||
|
{
|
||||||
|
FlxG.switchState(FreeplayState.build(
|
||||||
|
{
|
||||||
|
{
|
||||||
|
character: currentCharacterId == "pico" ? "bf" : "pico",
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// if (FlxG.keys.justPressed.H)
|
// if (FlxG.keys.justPressed.H)
|
||||||
// {
|
// {
|
||||||
// rankDisplayNew(fromResultsParams);
|
// rankDisplayNew(fromResultsParams);
|
||||||
|
@ -1302,9 +1316,9 @@ class FreeplayState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
|
|
||||||
var upP:Bool = controls.UI_UP_P && !FlxG.keys.pressed.CONTROL;
|
var upP:Bool = controls.UI_UP_P;
|
||||||
var downP:Bool = controls.UI_DOWN_P && !FlxG.keys.pressed.CONTROL;
|
var downP:Bool = controls.UI_DOWN_P;
|
||||||
var accepted:Bool = controls.ACCEPT && !FlxG.keys.pressed.CONTROL;
|
var accepted:Bool = controls.ACCEPT;
|
||||||
|
|
||||||
if (FlxG.onMobile)
|
if (FlxG.onMobile)
|
||||||
{
|
{
|
||||||
|
@ -1378,7 +1392,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
#end
|
#end
|
||||||
|
|
||||||
if (!FlxG.keys.pressed.CONTROL && (controls.UI_UP || controls.UI_DOWN))
|
if ((controls.UI_UP || controls.UI_DOWN))
|
||||||
{
|
{
|
||||||
if (spamming)
|
if (spamming)
|
||||||
{
|
{
|
||||||
|
@ -1440,13 +1454,13 @@ class FreeplayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
#end
|
#end
|
||||||
|
|
||||||
if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL)
|
if (controls.UI_LEFT_P)
|
||||||
{
|
{
|
||||||
dj.resetAFKTimer();
|
dj.resetAFKTimer();
|
||||||
changeDiff(-1);
|
changeDiff(-1);
|
||||||
generateSongList(currentFilter, true);
|
generateSongList(currentFilter, true);
|
||||||
}
|
}
|
||||||
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
|
if (controls.UI_RIGHT_P)
|
||||||
{
|
{
|
||||||
dj.resetAFKTimer();
|
dj.resetAFKTimer();
|
||||||
changeDiff(1);
|
changeDiff(1);
|
||||||
|
@ -1720,7 +1734,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var targetDifficultyId:String = currentDifficulty;
|
var targetDifficultyId:String = currentDifficulty;
|
||||||
var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId);
|
var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter);
|
||||||
PlayStatePlaylist.campaignId = cap.songData.levelId;
|
PlayStatePlaylist.campaignId = cap.songData.levelId;
|
||||||
|
|
||||||
var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation);
|
var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation);
|
||||||
|
@ -1730,8 +1744,18 @@ class FreeplayState extends MusicBeatSubState
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Change this with alternate instrumentals
|
var baseInstrumentalId:String = targetDifficulty?.characters?.instrumental ?? '';
|
||||||
var targetInstId:String = targetDifficulty.characters.instrumental;
|
var altInstrumentalIds:Array<String> = targetDifficulty?.characters?.altInstrumentals ?? [];
|
||||||
|
|
||||||
|
var targetInstId:String = baseInstrumentalId;
|
||||||
|
|
||||||
|
// TODO: Make this a UI element.
|
||||||
|
#if (debug || FORCE_DEBUG_VERSION)
|
||||||
|
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
|
||||||
|
{
|
||||||
|
targetInstId = altInstrumentalIds[0];
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
// Visual and audio effects.
|
// Visual and audio effects.
|
||||||
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||||
|
@ -1883,9 +1907,23 @@ class FreeplayState extends MusicBeatSubState
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var previewSong:Null<Song> = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId);
|
var previewSong:Null<Song> = SongRegistry.instance.fetchEntry(daSongCapsule.songData.songId);
|
||||||
var instSuffix:String = previewSong?.getDifficulty(currentDifficulty,
|
var songDifficulty = previewSong?.getDifficulty(currentDifficulty,
|
||||||
previewSong?.getVariationsByCharId(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST)?.characters?.instrumental ?? '';
|
previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST);
|
||||||
|
var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? '';
|
||||||
|
var altInstrumentalIds:Array<String> = songDifficulty?.characters?.altInstrumentals ?? [];
|
||||||
|
|
||||||
|
var instSuffix:String = baseInstrumentalId;
|
||||||
|
|
||||||
|
// TODO: Make this a UI element.
|
||||||
|
#if (debug || FORCE_DEBUG_VERSION)
|
||||||
|
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
|
||||||
|
{
|
||||||
|
instSuffix = altInstrumentalIds[0];
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
instSuffix = (instSuffix != '') ? '-$instSuffix' : '';
|
instSuffix = (instSuffix != '') ? '-$instSuffix' : '';
|
||||||
|
|
||||||
FunkinSound.playMusic(daSongCapsule.songData.songId,
|
FunkinSound.playMusic(daSongCapsule.songData.songId,
|
||||||
{
|
{
|
||||||
startingVolume: 0.0,
|
startingVolume: 0.0,
|
||||||
|
@ -1913,7 +1951,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
|
public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
|
||||||
{
|
{
|
||||||
var result:MainMenuState;
|
var result:MainMenuState;
|
||||||
if (params?.fromResults.playRankAnim) result = new MainMenuState(true);
|
if (params?.fromResults?.playRankAnim) result = new MainMenuState(true);
|
||||||
else
|
else
|
||||||
result = new MainMenuState(false);
|
result = new MainMenuState(false);
|
||||||
|
|
||||||
|
@ -1951,8 +1989,8 @@ class DifficultySelector extends FlxSprite
|
||||||
|
|
||||||
override function update(elapsed:Float):Void
|
override function update(elapsed:Float):Void
|
||||||
{
|
{
|
||||||
if (flipX && controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
|
if (flipX && controls.UI_RIGHT_P) moveShitDown();
|
||||||
if (!flipX && controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
|
if (!flipX && controls.UI_LEFT_P) moveShitDown();
|
||||||
|
|
||||||
super.update(elapsed);
|
super.update(elapsed);
|
||||||
}
|
}
|
||||||
|
|
108
source/funkin/ui/freeplay/charselect/PlayableCharacter.hx
Normal file
108
source/funkin/ui/freeplay/charselect/PlayableCharacter.hx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package funkin.ui.freeplay.charselect;
|
||||||
|
|
||||||
|
import funkin.data.IRegistryEntry;
|
||||||
|
import funkin.data.freeplay.player.PlayerData;
|
||||||
|
import funkin.data.freeplay.player.PlayerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object used to retrieve data about a playable character (also known as "weeks").
|
||||||
|
* Can be scripted to override each function, for custom behavior.
|
||||||
|
*/
|
||||||
|
class PlayableCharacter implements IRegistryEntry<PlayerData>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The ID of the playable character.
|
||||||
|
*/
|
||||||
|
public final id:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playable character data as parsed from the JSON file.
|
||||||
|
*/
|
||||||
|
public final _data:PlayerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param id The ID of the JSON file to parse.
|
||||||
|
*/
|
||||||
|
public function new(id:String)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
_data = _fetchData(id);
|
||||||
|
|
||||||
|
if (_data == null)
|
||||||
|
{
|
||||||
|
throw 'Could not parse playable character data for id: $id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the readable name of the playable character.
|
||||||
|
*/
|
||||||
|
public function getName():String
|
||||||
|
{
|
||||||
|
// TODO: Maybe add localization support?
|
||||||
|
return _data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of stage character IDs associated with this playable character.
|
||||||
|
* @return The list of associated character IDs
|
||||||
|
*/
|
||||||
|
public function getOwnedCharacterIds():Array<String>
|
||||||
|
{
|
||||||
|
return _data.ownedChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `true` if, when this character is selected in Freeplay,
|
||||||
|
* songs unassociated with a specific character should appear.
|
||||||
|
*/
|
||||||
|
public function shouldShowUnownedChars():Bool
|
||||||
|
{
|
||||||
|
return _data.showUnownedChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shouldShowCharacter(id:String):Bool
|
||||||
|
{
|
||||||
|
if (_data.ownedChars.contains(id))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_data.showUnownedChars)
|
||||||
|
{
|
||||||
|
var result = !PlayerRegistry.instance.isCharacterOwned(id);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this character is unlocked.
|
||||||
|
*/
|
||||||
|
public function isUnlocked():Bool
|
||||||
|
{
|
||||||
|
return _data.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the character is destroyed.
|
||||||
|
* TODO: Document when this gets called
|
||||||
|
*/
|
||||||
|
public function destroy():Void {}
|
||||||
|
|
||||||
|
public function toString():String
|
||||||
|
{
|
||||||
|
return 'PlayableCharacter($id)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve and parse the JSON data for a playable character by ID.
|
||||||
|
* @param id The ID of the character
|
||||||
|
* @return The parsed player data, or null if not found or invalid
|
||||||
|
*/
|
||||||
|
static function _fetchData(id:String):Null<PlayerData>
|
||||||
|
{
|
||||||
|
return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package funkin.ui.freeplay.charselect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A script that can be tied to a PlayableCharacter.
|
||||||
|
* Create a scripted class that extends PlayableCharacter to use this.
|
||||||
|
*/
|
||||||
|
@:hscriptClass
|
||||||
|
class ScriptedPlayableCharacter extends funkin.ui.freeplay.charselect.PlayableCharacter implements polymod.hscript.HScriptedClass {}
|
|
@ -24,7 +24,6 @@ class VersionUtil
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var versionRaw:thx.semver.Version.SemVer = version;
|
var versionRaw:thx.semver.Version.SemVer = version;
|
||||||
trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}');
|
|
||||||
return version.satisfies(versionRule);
|
return version.satisfies(versionRule);
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
|
|
Loading…
Reference in a new issue