2022-03-17 01:40:08 -04:00
|
|
|
package funkin.play.character;
|
|
|
|
|
2023-07-13 20:25:01 -04:00
|
|
|
import funkin.data.animation.AnimationData;
|
2022-03-21 00:19:05 -04:00
|
|
|
import funkin.modding.events.ScriptEvent;
|
|
|
|
import funkin.modding.events.ScriptEventDispatcher;
|
2023-02-21 20:58:15 -05:00
|
|
|
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;
|
2022-03-21 00:19:05 -04:00
|
|
|
import funkin.play.character.ScriptedCharacter.ScriptedBaseCharacter;
|
|
|
|
import funkin.play.character.ScriptedCharacter.ScriptedMultiSparrowCharacter;
|
|
|
|
import funkin.play.character.ScriptedCharacter.ScriptedPackerCharacter;
|
|
|
|
import funkin.play.character.ScriptedCharacter.ScriptedSparrowCharacter;
|
2022-05-02 23:00:00 -04:00
|
|
|
import funkin.util.assets.DataAssets;
|
2023-02-21 20:58:15 -05:00
|
|
|
import funkin.util.VersionUtil;
|
2022-03-21 00:19:05 -04:00
|
|
|
import haxe.Json;
|
|
|
|
import openfl.utils.Assets;
|
2022-03-17 01:40:08 -04:00
|
|
|
|
|
|
|
class CharacterDataParser
|
|
|
|
{
|
2023-01-22 19:55:30 -05:00
|
|
|
/**
|
|
|
|
* The current version string for the stage data format.
|
|
|
|
* Handle breaking changes by incrementing this value
|
|
|
|
* and adding migration to the `migrateStageData()` function.
|
2023-10-11 01:04:56 -04:00
|
|
|
*
|
|
|
|
* - Version 1.0.1 adds `death.cameraOffsets`
|
2023-01-22 19:55:30 -05:00
|
|
|
*/
|
2023-10-11 01:04:56 -04:00
|
|
|
public static final CHARACTER_DATA_VERSION:String = '1.0.1';
|
2023-01-22 19:55:30 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The current version rule check for the stage data format.
|
|
|
|
*/
|
2023-02-21 20:58:15 -05:00
|
|
|
public static final CHARACTER_DATA_VERSION_RULE:String = '1.0.x';
|
2023-01-22 19:55:30 -05:00
|
|
|
|
|
|
|
static final characterCache:Map<String, CharacterData> = new Map<String, CharacterData>();
|
|
|
|
static final characterScriptedClass:Map<String, String> = new Map<String, String>();
|
|
|
|
|
|
|
|
static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses and preloads the game's stage data and scripts when the game starts.
|
2023-06-08 16:30:45 -04:00
|
|
|
*
|
2023-01-22 19:55:30 -05:00
|
|
|
* If you want to force stages to be reloaded, you can just call this function again.
|
|
|
|
*/
|
|
|
|
public static function loadCharacterCache():Void
|
|
|
|
{
|
|
|
|
// Clear any stages that are cached if there were any.
|
|
|
|
clearCharacterCache();
|
2024-02-07 18:45:13 -05:00
|
|
|
trace('[CHARACTER] Parsing all entries...');
|
2023-01-22 19:55:30 -05:00
|
|
|
|
|
|
|
//
|
|
|
|
// UNSCRIPTED CHARACTERS
|
|
|
|
//
|
|
|
|
var charIdList:Array<String> = DataAssets.listDataFilesInPath('characters/');
|
2023-02-21 20:58:15 -05:00
|
|
|
var unscriptedCharIds:Array<String> = charIdList.filter(function(charId:String):Bool {
|
2023-01-22 19:55:30 -05:00
|
|
|
return !characterCache.exists(charId);
|
|
|
|
});
|
|
|
|
trace(' Fetching data for ${unscriptedCharIds.length} characters...');
|
|
|
|
for (charId in unscriptedCharIds)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var charData:CharacterData = parseCharacterData(charId);
|
|
|
|
if (charData != null)
|
|
|
|
{
|
|
|
|
trace(' Loaded character data: ${charId}');
|
|
|
|
characterCache.set(charId, charData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
// Assume error was already logged.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// SCRIPTED CHARACTERS
|
|
|
|
//
|
|
|
|
|
|
|
|
// Fuck I wish scripted classes supported static functions.
|
|
|
|
|
|
|
|
var scriptedCharClassNames1:Array<String> = ScriptedSparrowCharacter.listScriptClasses();
|
|
|
|
if (scriptedCharClassNames1.length > 0)
|
|
|
|
{
|
|
|
|
trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
|
|
|
|
for (charCls in scriptedCharClassNames1)
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
try
|
|
|
|
{
|
|
|
|
var character:SparrowCharacter = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
2023-04-20 13:48:15 -04:00
|
|
|
trace(' Initialized character ${character.characterName}');
|
2023-02-21 20:58:15 -05:00
|
|
|
characterScriptedClass.set(character.characterId, charCls);
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
trace(' FAILED to instantiate scripted Sparrow character: ${charCls}');
|
|
|
|
trace(e);
|
|
|
|
}
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var scriptedCharClassNames2:Array<String> = ScriptedPackerCharacter.listScriptClasses();
|
|
|
|
if (scriptedCharClassNames2.length > 0)
|
|
|
|
{
|
|
|
|
trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
|
|
|
|
for (charCls in scriptedCharClassNames2)
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
try
|
|
|
|
{
|
|
|
|
var character:PackerCharacter = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
|
|
|
|
characterScriptedClass.set(character.characterId, charCls);
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
trace(' FAILED to instantiate scripted Packer character: ${charCls}');
|
|
|
|
trace(e);
|
|
|
|
}
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var scriptedCharClassNames3:Array<String> = ScriptedMultiSparrowCharacter.listScriptClasses();
|
|
|
|
if (scriptedCharClassNames3.length > 0)
|
|
|
|
{
|
|
|
|
trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
|
|
|
|
for (charCls in scriptedCharClassNames3)
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
try
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
var character:MultiSparrowCharacter = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
|
|
|
|
characterScriptedClass.set(character.characterId, charCls);
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
trace(' FAILED to instantiate scripted Multi-Sparrow character: ${charCls}');
|
|
|
|
trace(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var scriptedCharClassNames4:Array<String> = ScriptedAnimateAtlasCharacter.listScriptClasses();
|
|
|
|
if (scriptedCharClassNames4.length > 0)
|
|
|
|
{
|
|
|
|
trace(' Instantiating ${scriptedCharClassNames4.length} (Animate Atlas) scripted characters...');
|
|
|
|
for (charCls in scriptedCharClassNames4)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var character:AnimateAtlasCharacter = ScriptedAnimateAtlasCharacter.init(charCls, DEFAULT_CHAR_ID);
|
|
|
|
characterScriptedClass.set(character.characterId, charCls);
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
trace(' FAILED to instantiate scripted Animate Atlas character: ${charCls}');
|
|
|
|
trace(e);
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: Only instantiate the ones not populated above.
|
|
|
|
// ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
|
|
|
|
var scriptedCharClassNames:Array<String> = ScriptedBaseCharacter.listScriptClasses();
|
2023-02-21 20:58:15 -05:00
|
|
|
scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool {
|
2023-01-22 19:55:30 -05:00
|
|
|
return !(scriptedCharClassNames1.contains(charCls)
|
|
|
|
|| scriptedCharClassNames2.contains(charCls)
|
2023-02-21 20:58:15 -05:00
|
|
|
|| scriptedCharClassNames3.contains(charCls)
|
|
|
|
|| scriptedCharClassNames4.contains(charCls));
|
2023-01-22 19:55:30 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
if (scriptedCharClassNames.length > 0)
|
|
|
|
{
|
|
|
|
trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
|
|
|
|
for (charCls in scriptedCharClassNames)
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
var character:BaseCharacter = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID, Custom);
|
2023-01-22 19:55:30 -05:00
|
|
|
if (character == null)
|
|
|
|
{
|
|
|
|
trace(' Failed to instantiate scripted character: ${charCls}');
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
trace(' Successfully instantiated scripted character: ${charCls}');
|
|
|
|
characterScriptedClass.set(character.characterId, charCls);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
|
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Fetches data for a character and returns a BaseCharacter instance,
|
|
|
|
* ready to be added to the scene.
|
|
|
|
* @param charId The character ID to fetch.
|
|
|
|
* @return The character instance, or null if the character was not found.
|
|
|
|
*/
|
2023-08-28 15:03:29 -04:00
|
|
|
public static function fetchCharacter(charId:String, debug:Bool = false):Null<BaseCharacter>
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
if (charId == null || charId == '' || !characterCache.exists(charId))
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
// Gracefully handle songs that don't use this character,
|
|
|
|
// or throw an error if the character is missing.
|
|
|
|
|
|
|
|
if (charId != null && charId != '') trace('Failed to build character, not found in cache: ${charId}');
|
2023-01-22 19:55:30 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
var charData:CharacterData = characterCache.get(charId);
|
|
|
|
var charScriptClass:String = characterScriptedClass.get(charId);
|
2023-01-22 19:55:30 -05:00
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
var char:BaseCharacter;
|
2023-01-22 19:55:30 -05:00
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
if (charScriptClass != null)
|
|
|
|
{
|
|
|
|
switch (charData.renderType)
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
case CharacterRenderType.AnimateAtlas:
|
|
|
|
char = ScriptedAnimateAtlasCharacter.init(charScriptClass, charId);
|
|
|
|
case CharacterRenderType.MultiSparrow:
|
|
|
|
char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
|
|
|
|
case CharacterRenderType.Sparrow:
|
|
|
|
char = ScriptedSparrowCharacter.init(charScriptClass, charId);
|
|
|
|
case CharacterRenderType.Packer:
|
|
|
|
char = ScriptedPackerCharacter.init(charScriptClass, charId);
|
|
|
|
default:
|
|
|
|
// We're going to assume that the script class does the rendering.
|
|
|
|
char = ScriptedBaseCharacter.init(charScriptClass, charId, CharacterRenderType.Custom);
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
2023-02-21 20:58:15 -05:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
switch (charData.renderType)
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
case CharacterRenderType.AnimateAtlas:
|
|
|
|
char = new AnimateAtlasCharacter(charId);
|
|
|
|
case CharacterRenderType.MultiSparrow:
|
|
|
|
char = new MultiSparrowCharacter(charId);
|
|
|
|
case CharacterRenderType.Sparrow:
|
|
|
|
char = new SparrowCharacter(charId);
|
|
|
|
case CharacterRenderType.Packer:
|
|
|
|
char = new PackerCharacter(charId);
|
|
|
|
default:
|
|
|
|
trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
|
|
|
|
char = new BaseCharacter(charId, CharacterRenderType.Custom);
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
}
|
2023-02-21 20:58:15 -05:00
|
|
|
|
|
|
|
if (char == null)
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
trace('Failed to instantiate character: ${charId}');
|
2023-01-22 19:55:30 -05:00
|
|
|
return null;
|
|
|
|
}
|
2023-02-21 20:58:15 -05:00
|
|
|
|
2023-05-26 17:10:08 -04:00
|
|
|
trace('Successfully instantiated character (${debug ? 'debug' : 'stable'}): ${charId}');
|
|
|
|
|
|
|
|
char.debug = debug;
|
2023-02-21 20:58:15 -05:00
|
|
|
|
|
|
|
// Call onCreate only in the fetchCharacter() function, not at application initialization.
|
2023-10-26 05:46:22 -04:00
|
|
|
ScriptEventDispatcher.callEvent(char, new ScriptEvent(CREATE));
|
2023-02-21 20:58:15 -05:00
|
|
|
|
|
|
|
return char;
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Fetches just the character data for a character.
|
|
|
|
* @param charId The character ID to fetch.
|
|
|
|
* @return The character data, or null if the character was not found.
|
|
|
|
*/
|
2023-01-22 19:55:30 -05:00
|
|
|
public static function fetchCharacterData(charId:String):Null<CharacterData>
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
if (characterCache.exists(charId)) return characterCache.get(charId);
|
|
|
|
|
|
|
|
return null;
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Lists all the valid character IDs.
|
|
|
|
* @return An array of character IDs.
|
|
|
|
*/
|
2023-01-22 19:55:30 -05:00
|
|
|
public static function listCharacterIds():Array<String>
|
|
|
|
{
|
|
|
|
return characterCache.keys().array();
|
|
|
|
}
|
|
|
|
|
2023-12-12 05:24:43 -05:00
|
|
|
/**
|
|
|
|
* TODO: Hardcode this.
|
|
|
|
*/
|
2023-12-11 00:19:33 -05:00
|
|
|
public static function getCharPixelIconAsset(char:String):String
|
|
|
|
{
|
|
|
|
var icon:String = char;
|
|
|
|
|
|
|
|
switch (icon)
|
|
|
|
{
|
|
|
|
case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf":
|
|
|
|
icon = "bf";
|
|
|
|
case "monster-christmas":
|
|
|
|
icon = "monster";
|
2023-12-12 05:24:43 -05:00
|
|
|
case "mom" | "mom-car":
|
2023-12-11 00:19:33 -05:00
|
|
|
icon = "mommy";
|
2023-12-12 05:24:43 -05:00
|
|
|
case "pico-blazin" | "pico-playable" | "pico-speaker":
|
2023-12-11 00:19:33 -05:00
|
|
|
icon = "pico";
|
|
|
|
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen":
|
|
|
|
icon = "gf";
|
|
|
|
case "dad":
|
|
|
|
icon = "daddy";
|
|
|
|
case "darnell-blazin":
|
|
|
|
icon = "darnell";
|
|
|
|
case "senpai-angry":
|
|
|
|
icon = "senpai";
|
2024-06-25 18:07:09 -04:00
|
|
|
case "tankman-atlas":
|
|
|
|
icon = "tankman";
|
2023-12-11 00:19:33 -05:00
|
|
|
}
|
|
|
|
|
2024-02-15 17:25:28 -05:00
|
|
|
var path = Paths.image("freeplay/icons/" + icon + "pixel");
|
|
|
|
if (Assets.exists(path)) return path;
|
|
|
|
|
|
|
|
// TODO: Hardcode some additional behavior or a fallback.
|
|
|
|
return null;
|
2023-12-11 00:19:33 -05:00
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Clears the character data cache.
|
|
|
|
*/
|
2023-01-22 19:55:30 -05:00
|
|
|
static function clearCharacterCache():Void
|
|
|
|
{
|
|
|
|
if (characterCache != null)
|
|
|
|
{
|
|
|
|
characterCache.clear();
|
|
|
|
}
|
|
|
|
if (characterScriptedClass != null)
|
|
|
|
{
|
|
|
|
characterScriptedClass.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-02-21 20:58:15 -05:00
|
|
|
* Load a character's JSON file and parse its data.
|
2023-06-08 16:30:45 -04:00
|
|
|
*
|
2023-01-22 19:55:30 -05:00
|
|
|
* @param charId The character to load.
|
|
|
|
* @return The character data, or null if validation failed.
|
|
|
|
*/
|
|
|
|
public static function parseCharacterData(charId:String):Null<CharacterData>
|
|
|
|
{
|
|
|
|
var rawJson:String = loadCharacterFile(charId);
|
|
|
|
|
|
|
|
var charData:CharacterData = migrateCharacterData(rawJson, charId);
|
|
|
|
|
|
|
|
return validateCharacterData(charId, charData);
|
|
|
|
}
|
|
|
|
|
|
|
|
static function loadCharacterFile(charPath:String):String
|
|
|
|
{
|
|
|
|
var charFilePath:String = Paths.json('characters/${charPath}');
|
|
|
|
var rawJson = Assets.getText(charFilePath).trim();
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
while (!StringTools.endsWith(rawJson, '}'))
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
|
|
|
rawJson = rawJson.substr(0, rawJson.length - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return rawJson;
|
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
static function migrateCharacterData(rawJson:String, charId:String):Null<CharacterData>
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
|
|
|
// If you update the character data format in a breaking way,
|
|
|
|
// handle migration here by checking the `version` value.
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
var charData:CharacterData = cast Json.parse(rawJson);
|
|
|
|
return charData;
|
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
|
|
|
trace(' Error parsing data for character: ${charId}');
|
|
|
|
trace(' ${e}');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-03-08 20:56:34 -05:00
|
|
|
* The default time the character should sing for, in steps.
|
2023-01-22 19:55:30 -05:00
|
|
|
* Values that are too low will cause the character to stop singing between notes.
|
2024-03-08 20:56:34 -05:00
|
|
|
* Values that are too high will cause the character to hold their singing pose for too long after they're done.
|
|
|
|
* @default `8 steps`
|
2023-01-22 19:55:30 -05:00
|
|
|
*/
|
2024-03-08 20:56:34 -05:00
|
|
|
static final DEFAULT_SINGTIME:Float = 8.0;
|
2023-01-22 19:55:30 -05:00
|
|
|
|
|
|
|
static final DEFAULT_DANCEEVERY:Int = 1;
|
|
|
|
static final DEFAULT_FLIPX:Bool = false;
|
|
|
|
static final DEFAULT_FLIPY:Bool = false;
|
|
|
|
static final DEFAULT_FRAMERATE:Int = 24;
|
|
|
|
static final DEFAULT_ISPIXEL:Bool = false;
|
|
|
|
static final DEFAULT_LOOP:Bool = false;
|
2023-02-21 20:58:15 -05:00
|
|
|
static final DEFAULT_NAME:String = 'Untitled Character';
|
2023-01-22 19:55:30 -05:00
|
|
|
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
|
|
|
|
static final DEFAULT_HEALTHICON_OFFSETS:Array<Int> = [0, 25];
|
2023-02-21 20:58:15 -05:00
|
|
|
static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.Sparrow;
|
2023-01-22 19:55:30 -05:00
|
|
|
static final DEFAULT_SCALE:Float = 1;
|
|
|
|
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
2023-02-21 20:58:15 -05:00
|
|
|
static final DEFAULT_STARTINGANIM:String = 'idle';
|
2023-01-22 19:55:30 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set unspecified parameters to their defaults.
|
|
|
|
* If the parameter is mandatory, print an error message.
|
2023-06-08 16:30:45 -04:00
|
|
|
* @param id
|
|
|
|
* @param input
|
2023-01-22 19:55:30 -05:00
|
|
|
* @return The validated character data
|
|
|
|
*/
|
|
|
|
static function validateCharacterData(id:String, input:CharacterData):Null<CharacterData>
|
|
|
|
{
|
|
|
|
if (input == null)
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
trace('ERROR: Could not parse character data for "${id}".');
|
2023-01-22 19:55:30 -05:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.version == null)
|
|
|
|
{
|
|
|
|
trace('WARN: No semantic version specified for character data file "$id", assuming ${CHARACTER_DATA_VERSION}');
|
|
|
|
input.version = CHARACTER_DATA_VERSION;
|
|
|
|
}
|
|
|
|
|
2023-08-22 04:27:30 -04:00
|
|
|
if (!VersionUtil.validateVersionStr(input.version, CHARACTER_DATA_VERSION_RULE))
|
2023-01-22 19:55:30 -05:00
|
|
|
{
|
|
|
|
trace('ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.name == null)
|
|
|
|
{
|
|
|
|
trace('WARN: Character data for "$id" missing name');
|
|
|
|
input.name = DEFAULT_NAME;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.renderType == null)
|
|
|
|
{
|
|
|
|
input.renderType = DEFAULT_RENDERTYPE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.assetPath == null)
|
|
|
|
{
|
|
|
|
trace('ERROR: Could not load character data for "$id": missing assetPath');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.offsets == null)
|
|
|
|
{
|
|
|
|
input.offsets = DEFAULT_OFFSETS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.cameraOffsets == null)
|
|
|
|
{
|
|
|
|
input.cameraOffsets = DEFAULT_OFFSETS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.healthIcon == null)
|
|
|
|
{
|
2023-01-22 22:25:45 -05:00
|
|
|
input.healthIcon =
|
|
|
|
{
|
|
|
|
id: null,
|
|
|
|
scale: null,
|
|
|
|
flipX: null,
|
2023-05-26 17:10:08 -04:00
|
|
|
isPixel: null,
|
2023-01-22 22:25:45 -05:00
|
|
|
offsets: null
|
|
|
|
};
|
2023-01-22 19:55:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (input.healthIcon.id == null)
|
|
|
|
{
|
|
|
|
input.healthIcon.id = id;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.healthIcon.scale == null)
|
|
|
|
{
|
|
|
|
input.healthIcon.scale = DEFAULT_SCALE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.healthIcon.flipX == null)
|
|
|
|
{
|
|
|
|
input.healthIcon.flipX = DEFAULT_FLIPX;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.healthIcon.offsets == null)
|
|
|
|
{
|
|
|
|
input.healthIcon.offsets = DEFAULT_OFFSETS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.startingAnimation == null)
|
|
|
|
{
|
|
|
|
input.startingAnimation = DEFAULT_STARTINGANIM;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.scale == null)
|
|
|
|
{
|
|
|
|
input.scale = DEFAULT_SCALE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.isPixel == null)
|
|
|
|
{
|
|
|
|
input.isPixel = DEFAULT_ISPIXEL;
|
|
|
|
}
|
|
|
|
|
2023-05-26 17:10:08 -04:00
|
|
|
if (input.healthIcon.isPixel == null)
|
|
|
|
{
|
|
|
|
input.healthIcon.isPixel = input.isPixel;
|
|
|
|
}
|
|
|
|
|
2023-01-22 19:55:30 -05:00
|
|
|
if (input.danceEvery == null)
|
|
|
|
{
|
|
|
|
input.danceEvery = DEFAULT_DANCEEVERY;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.singTime == null)
|
|
|
|
{
|
|
|
|
input.singTime = DEFAULT_SINGTIME;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.animations == null || input.animations.length == 0)
|
|
|
|
{
|
|
|
|
trace('ERROR: Could not load character data for "$id": missing animations');
|
|
|
|
input.animations = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.flipX == null)
|
|
|
|
{
|
|
|
|
input.flipX = DEFAULT_FLIPX;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (input.animations.length == 0 && input.startingAnimation != null)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (inputAnimation in input.animations)
|
|
|
|
{
|
|
|
|
if (inputAnimation.name == null)
|
|
|
|
{
|
|
|
|
trace('ERROR: Could not load character data for "$id": missing animation name for prop "${input.name}"');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputAnimation.frameRate == null)
|
|
|
|
{
|
|
|
|
inputAnimation.frameRate = DEFAULT_FRAMERATE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputAnimation.offsets == null)
|
|
|
|
{
|
|
|
|
inputAnimation.offsets = DEFAULT_OFFSETS;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputAnimation.looped == null)
|
|
|
|
{
|
|
|
|
inputAnimation.looped = DEFAULT_LOOP;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputAnimation.flipX == null)
|
|
|
|
{
|
|
|
|
inputAnimation.flipX = DEFAULT_FLIPX;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inputAnimation.flipY == null)
|
|
|
|
{
|
|
|
|
inputAnimation.flipY = DEFAULT_FLIPY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// All good!
|
|
|
|
return input;
|
|
|
|
}
|
2022-03-17 01:40:08 -04:00
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Describes the available rendering types for a character.
|
|
|
|
*/
|
2022-03-17 01:40:08 -04:00
|
|
|
enum abstract CharacterRenderType(String) from String to String
|
|
|
|
{
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* Renders the character using a single spritesheet and XML data.
|
|
|
|
*/
|
|
|
|
public var Sparrow = 'sparrow';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the character using a single spritesheet and TXT data.
|
|
|
|
*/
|
|
|
|
public var Packer = 'packer';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the character using multiple spritesheets and XML data.
|
|
|
|
*/
|
|
|
|
public var MultiSparrow = 'multisparrow';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the character using a spritesheet of symbols and JSON data.
|
|
|
|
*/
|
|
|
|
public var AnimateAtlas = 'animateatlas';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Renders the character using a custom method.
|
|
|
|
*/
|
|
|
|
public var Custom = 'custom';
|
2022-03-17 01:40:08 -04:00
|
|
|
}
|
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* The JSON data schema used to define a character.
|
|
|
|
*/
|
2022-03-17 01:40:08 -04:00
|
|
|
typedef CharacterData =
|
|
|
|
{
|
2023-01-22 19:55:30 -05:00
|
|
|
/**
|
|
|
|
* The sematic version number of the character data JSON format.
|
|
|
|
*/
|
|
|
|
var version:String;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The readable name of the character.
|
|
|
|
*/
|
|
|
|
var name:String;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The type of rendering system to use for the character.
|
|
|
|
* @default sparrow
|
|
|
|
*/
|
|
|
|
var renderType:CharacterRenderType;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Behavior varies by render type:
|
|
|
|
* - SPARROW: Path to retrieve both the spritesheet and the XML data from.
|
|
|
|
* - PACKER: Path to retrieve both the spritsheet and the TXT data from.
|
|
|
|
*/
|
|
|
|
var assetPath:String;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The scale of the graphic as a float.
|
|
|
|
* Pro tip: On pixel-art levels, save the sprites small and set this value to 6 or so to save memory.
|
|
|
|
* @default 1
|
|
|
|
*/
|
|
|
|
var scale:Null<Float>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Optional data about the health icon for the character.
|
|
|
|
*/
|
|
|
|
var healthIcon:Null<HealthIconData>;
|
|
|
|
|
2023-10-11 01:04:56 -04:00
|
|
|
var death:Null<DeathData>;
|
|
|
|
|
2023-01-22 19:55:30 -05:00
|
|
|
/**
|
|
|
|
* The global offset to the character's position, in pixels.
|
|
|
|
* @default [0, 0]
|
|
|
|
*/
|
|
|
|
var offsets:Null<Array<Float>>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The amount to offset the camera by while focusing on this character.
|
|
|
|
* Default value focuses on the character directly.
|
|
|
|
* @default [0, 0]
|
|
|
|
*/
|
|
|
|
var cameraOffsets:Array<Float>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Setting this to true disables anti-aliasing for the character.
|
|
|
|
* @default false
|
|
|
|
*/
|
|
|
|
var isPixel:Null<Bool>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The frequency at which the character will play its idle animation, in beats.
|
|
|
|
* Increasing this number will make the character dance less often.
|
2023-06-08 16:30:45 -04:00
|
|
|
*
|
2023-01-22 19:55:30 -05:00
|
|
|
* @default 1
|
|
|
|
*/
|
|
|
|
var danceEvery:Null<Int>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The minimum duration that a character will play a note animation for, in beats.
|
|
|
|
* If this number is too low, you may see the character start playing the idle animation between notes.
|
|
|
|
* If this number is too high, you may see the the character play the sing animation for too long after the notes are gone.
|
2023-06-08 16:30:45 -04:00
|
|
|
*
|
2023-01-22 19:55:30 -05:00
|
|
|
* Examples:
|
|
|
|
* - Daddy Dearest uses a value of `1.525`.
|
|
|
|
* @default 1.0
|
|
|
|
*/
|
|
|
|
var singTime:Null<Float>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* An optional array of animations which the character can play.
|
|
|
|
*/
|
|
|
|
var animations:Array<AnimationData>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If animations are used, this is the name of the animation to play first.
|
|
|
|
* @default idle
|
|
|
|
*/
|
|
|
|
var startingAnimation:Null<String>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not the whole ass sprite is flipped by default.
|
|
|
|
* Useful for characters that could also be played (Pico)
|
2023-06-08 16:30:45 -04:00
|
|
|
*
|
2023-01-22 19:55:30 -05:00
|
|
|
* @default false
|
|
|
|
*/
|
|
|
|
var flipX:Null<Bool>;
|
2022-03-17 01:40:08 -04:00
|
|
|
};
|
2022-03-26 22:18:26 -04:00
|
|
|
|
2023-02-21 20:58:15 -05:00
|
|
|
/**
|
|
|
|
* The JSON data schema used to define the health icon for a character.
|
|
|
|
*/
|
2022-03-26 22:18:26 -04:00
|
|
|
typedef HealthIconData =
|
|
|
|
{
|
2023-01-22 19:55:30 -05:00
|
|
|
/**
|
|
|
|
* The ID to use for the health icon.
|
|
|
|
* @default The character's ID
|
|
|
|
*/
|
|
|
|
var id:Null<String>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The scale of the health icon.
|
|
|
|
*/
|
|
|
|
var scale:Null<Float>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to flip the health icon horizontally.
|
|
|
|
* @default false
|
|
|
|
*/
|
|
|
|
var flipX:Null<Bool>;
|
|
|
|
|
2023-05-26 17:10:08 -04:00
|
|
|
/**
|
|
|
|
* Multiply scale by 6 and disable antialiasing
|
|
|
|
* @default false
|
|
|
|
*/
|
|
|
|
var isPixel:Null<Bool>;
|
|
|
|
|
2023-01-22 19:55:30 -05:00
|
|
|
/**
|
|
|
|
* The offset of the health icon, in pixels.
|
|
|
|
* @default [0, 25]
|
|
|
|
*/
|
|
|
|
var offsets:Null<Array<Float>>;
|
2022-03-26 22:18:26 -04:00
|
|
|
}
|
2023-10-11 01:04:56 -04:00
|
|
|
|
|
|
|
typedef DeathData =
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* The amount to offset the camera by while focusing on this character as they die.
|
|
|
|
* Default value focuses on the character's graphic midpoint.
|
|
|
|
* @default [0, 0]
|
|
|
|
*/
|
|
|
|
var ?cameraOffsets:Array<Float>;
|
2024-03-01 08:13:06 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The amount to zoom the camera by while focusing on this character as they die.
|
|
|
|
* Value is a multiplier of the default camera zoom for the stage.
|
|
|
|
* @default 1.0
|
|
|
|
*/
|
|
|
|
var ?cameraZoom:Float;
|
2024-03-04 16:37:42 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Impose a delay between when the character reaches `0` health and when the death animation plays.
|
|
|
|
* @default 0.0
|
|
|
|
*/
|
|
|
|
var ?preTransitionDelay:Float;
|
2023-10-11 01:04:56 -04:00
|
|
|
}
|