mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-03-31 23:05:41 +00:00
Rewrite conversation JSON parsing code.
This commit is contained in:
parent
401c200070
commit
fa556dc1f2
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -110,6 +110,11 @@
|
||||||
"target": "windows",
|
"target": "windows",
|
||||||
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Windows / Debug (Conversation Test)",
|
||||||
|
"target": "windows",
|
||||||
|
"args": ["-debug", "-DDIALOGUE"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Windows / Debug (Straight to Chart Editor)",
|
"label": "Windows / Debug (Straight to Chart Editor)",
|
||||||
"target": "windows",
|
"target": "windows",
|
||||||
|
|
|
@ -108,7 +108,6 @@
|
||||||
<haxelib name="hxCodec" /> <!-- Video playback -->
|
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||||
|
|
||||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||||
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
|
||||||
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
||||||
|
|
||||||
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
|
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
|
||||||
|
|
5
hmm.json
5
hmm.json
|
@ -156,11 +156,6 @@
|
||||||
"name": "thx.semver",
|
"name": "thx.semver",
|
||||||
"type": "haxelib",
|
"type": "haxelib",
|
||||||
"version": "0.2.2"
|
"version": "0.2.2"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tink_json",
|
|
||||||
"type": "haxelib",
|
|
||||||
"version": "0.11.0"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,9 @@ import funkin.data.level.LevelRegistry;
|
||||||
import funkin.data.notestyle.NoteStyleRegistry;
|
import funkin.data.notestyle.NoteStyleRegistry;
|
||||||
import funkin.data.event.SongEventRegistry;
|
import funkin.data.event.SongEventRegistry;
|
||||||
import funkin.data.stage.StageRegistry;
|
import funkin.data.stage.StageRegistry;
|
||||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
import funkin.data.dialogue.ConversationRegistry;
|
||||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
import funkin.data.dialogue.DialogueBoxRegistry;
|
||||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
import funkin.data.dialogue.SpeakerRegistry;
|
||||||
import funkin.data.song.SongRegistry;
|
import funkin.data.song.SongRegistry;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
|
@ -208,22 +208,30 @@ class InitState extends FlxState
|
||||||
// GAME DATA PARSING
|
// GAME DATA PARSING
|
||||||
//
|
//
|
||||||
|
|
||||||
|
trace('Parsing game data...');
|
||||||
|
|
||||||
|
var perf_gameDataParse_start = haxe.Timer.stamp();
|
||||||
|
|
||||||
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
|
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
|
||||||
// to ensure build macros work properly.
|
// to ensure build macros work properly.
|
||||||
SongRegistry.instance.loadEntries();
|
SongRegistry.instance.loadEntries();
|
||||||
LevelRegistry.instance.loadEntries();
|
LevelRegistry.instance.loadEntries();
|
||||||
NoteStyleRegistry.instance.loadEntries();
|
NoteStyleRegistry.instance.loadEntries();
|
||||||
SongEventRegistry.loadEventCache();
|
SongEventRegistry.loadEventCache();
|
||||||
ConversationDataParser.loadConversationCache();
|
ConversationRegistry.instance.loadEntries();
|
||||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
DialogueBoxRegistry.instance.loadEntries();
|
||||||
SpeakerDataParser.loadSpeakerCache();
|
SpeakerRegistry.instance.loadEntries();
|
||||||
StageRegistry.instance.loadEntries();
|
StageRegistry.instance.loadEntries();
|
||||||
CharacterDataParser.loadCharacterCache();
|
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||||
|
|
||||||
ModuleHandler.buildModuleCallbacks();
|
ModuleHandler.buildModuleCallbacks();
|
||||||
ModuleHandler.loadModuleCache();
|
ModuleHandler.loadModuleCache();
|
||||||
|
|
||||||
ModuleHandler.callOnCreate();
|
ModuleHandler.callOnCreate();
|
||||||
|
|
||||||
|
var perf_gameDataParse_end = haxe.Timer.stamp();
|
||||||
|
|
||||||
|
trace('Done parsing game data. Duration: ${perf_gameDataParse_end - perf_gameDataParse_start} seconds');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -241,6 +249,8 @@ class InitState extends FlxState
|
||||||
startLevel(defineLevel(), defineDifficulty());
|
startLevel(defineLevel(), defineDifficulty());
|
||||||
#elseif FREEPLAY // -DFREEPLAY
|
#elseif FREEPLAY // -DFREEPLAY
|
||||||
FlxG.switchState(new FreeplayState());
|
FlxG.switchState(new FreeplayState());
|
||||||
|
#elseif DIALOGUE // -DDIALOGUE
|
||||||
|
FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
|
||||||
#elseif ANIMATE // -DANIMATE
|
#elseif ANIMATE // -DANIMATE
|
||||||
FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
|
FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
|
||||||
#elseif WAVEFORM // -DWAVEFORM
|
#elseif WAVEFORM // -DWAVEFORM
|
||||||
|
|
|
@ -120,6 +120,68 @@ class DataParse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function backdropData(json:Json, name:String):BackdropData
|
||||||
|
{
|
||||||
|
switch (json.value)
|
||||||
|
{
|
||||||
|
case JObject(fields):
|
||||||
|
var result:BackdropData = {};
|
||||||
|
var backdropType:String = '';
|
||||||
|
|
||||||
|
for (field in fields)
|
||||||
|
{
|
||||||
|
switch (field.name)
|
||||||
|
{
|
||||||
|
case 'backdropType':
|
||||||
|
backdropType = Tools.getValue(field.value);
|
||||||
|
}
|
||||||
|
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (backdropType)
|
||||||
|
{
|
||||||
|
case 'solid':
|
||||||
|
return SOLID(result);
|
||||||
|
default:
|
||||||
|
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function outroData(json:Json, name:String):OutroData
|
||||||
|
{
|
||||||
|
switch (json.value)
|
||||||
|
{
|
||||||
|
case JObject(fields):
|
||||||
|
var result:OutroData = {};
|
||||||
|
var outroType:String = '';
|
||||||
|
|
||||||
|
for (field in fields)
|
||||||
|
{
|
||||||
|
switch (field.name)
|
||||||
|
{
|
||||||
|
case 'outroType':
|
||||||
|
outroType = Tools.getValue(field.value);
|
||||||
|
}
|
||||||
|
Reflect.setField(result, field.name, Tools.getValue(field.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (outroType)
|
||||||
|
{
|
||||||
|
case 'none':
|
||||||
|
return NONE(result);
|
||||||
|
case 'fade':
|
||||||
|
return FADE(result);
|
||||||
|
default:
|
||||||
|
throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
|
||||||
* Used by the FNF legacy JSON importer.
|
* Used by the FNF legacy JSON importer.
|
||||||
|
|
|
@ -273,11 +273,11 @@ class PolymodHandler
|
||||||
LevelRegistry.instance.loadEntries();
|
LevelRegistry.instance.loadEntries();
|
||||||
NoteStyleRegistry.instance.loadEntries();
|
NoteStyleRegistry.instance.loadEntries();
|
||||||
SongEventRegistry.loadEventCache();
|
SongEventRegistry.loadEventCache();
|
||||||
ConversationDataParser.loadConversationCache();
|
ConversationRegistry.instance.loadEntries();
|
||||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
DialogueBoxRegistry.instance.loadEntries();
|
||||||
SpeakerDataParser.loadSpeakerCache();
|
SpeakerRegistry.instance.loadEntries();
|
||||||
StageRegistry.instance.loadEntries();
|
StageRegistry.instance.loadEntries();
|
||||||
CharacterDataParser.loadCharacterCache();
|
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||||
ModuleHandler.loadModuleCache();
|
ModuleHandler.loadModuleCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.character.BaseCharacter;
|
import funkin.play.character.BaseCharacter;
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
import funkin.play.cutscene.dialogue.Conversation;
|
import funkin.play.cutscene.dialogue.Conversation;
|
||||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
import funkin.data.dialogue.ConversationRegistry;
|
||||||
import funkin.play.cutscene.VanillaCutscenes;
|
import funkin.play.cutscene.VanillaCutscenes;
|
||||||
import funkin.play.cutscene.VideoCutscene;
|
import funkin.play.cutscene.VideoCutscene;
|
||||||
import funkin.data.event.SongEventRegistry;
|
import funkin.data.event.SongEventRegistry;
|
||||||
|
@ -1662,7 +1662,7 @@ class PlayState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
isInCutscene = true;
|
isInCutscene = true;
|
||||||
|
|
||||||
currentConversation = ConversationDataParser.fetchConversation(conversationId);
|
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
|
||||||
if (currentConversation == null) return;
|
if (currentConversation == null) return;
|
||||||
|
|
||||||
currentConversation.completeCallback = onConversationComplete;
|
currentConversation.completeCallback = onConversationComplete;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import funkin.modding.IScriptedClass.IEventHandler;
|
||||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||||
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
|
import funkin.data.dialogue.ConversationData.DialogueEntryData;
|
||||||
import flixel.addons.display.FlxPieDial;
|
import flixel.addons.display.FlxPieDial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import funkin.util.SerializerUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a conversation.
|
|
||||||
* Includes what speakers are in the conversation, and what phrases they say.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class ConversationData
|
|
||||||
{
|
|
||||||
public var version:String;
|
|
||||||
public var backdrop:BackdropData;
|
|
||||||
public var outro:OutroData;
|
|
||||||
public var music:MusicData;
|
|
||||||
public var dialogue:Array<DialogueEntryData>;
|
|
||||||
|
|
||||||
public function new(version:String, backdrop:BackdropData, outro:OutroData, music:MusicData, dialogue:Array<DialogueEntryData>)
|
|
||||||
{
|
|
||||||
this.version = version;
|
|
||||||
this.backdrop = backdrop;
|
|
||||||
this.outro = outro;
|
|
||||||
this.music = music;
|
|
||||||
this.dialogue = dialogue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromString(i:String):ConversationData
|
|
||||||
{
|
|
||||||
if (i == null || i == '') return null;
|
|
||||||
var data:
|
|
||||||
{
|
|
||||||
version:String,
|
|
||||||
backdrop:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
|
||||||
?outro:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
|
||||||
?music:Dynamic, // TODO: tink.Json doesn't like when these are typed
|
|
||||||
dialogue:Array<Dynamic> // TODO: tink.Json doesn't like when these are typed
|
|
||||||
} = tink.Json.parse(i);
|
|
||||||
return fromJson(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):ConversationData
|
|
||||||
{
|
|
||||||
// TODO: Check version and perform migrations if necessary.
|
|
||||||
if (j == null) return null;
|
|
||||||
return new ConversationData(j.version, BackdropData.fromJson(j.backdrop), OutroData.fromJson(j.outro), MusicData.fromJson(j.music),
|
|
||||||
j.dialogue.map(d -> DialogueEntryData.fromJson(d)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
version: this.version,
|
|
||||||
backdrop: this.backdrop.toJson(),
|
|
||||||
dialogue: this.dialogue.map(d -> d.toJson())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a single dialogue entry.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class DialogueEntryData
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The speaker who says this phrase.
|
|
||||||
*/
|
|
||||||
public var speaker:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The animation the speaker will play.
|
|
||||||
*/
|
|
||||||
public var speakerAnimation:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The text box that will appear.
|
|
||||||
*/
|
|
||||||
public var box:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The animation the dialogue box will play.
|
|
||||||
*/
|
|
||||||
public var boxAnimation:String;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The lines of text that will appear in the text box.
|
|
||||||
*/
|
|
||||||
public var text:Array<String>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The relative speed at which the text will scroll.
|
|
||||||
* @default 1.0
|
|
||||||
*/
|
|
||||||
public var speed:Float = 1.0;
|
|
||||||
|
|
||||||
public function new(speaker:String, speakerAnimation:String, box:String, boxAnimation:String, text:Array<String>, speed:Float = null)
|
|
||||||
{
|
|
||||||
this.speaker = speaker;
|
|
||||||
this.speakerAnimation = speakerAnimation;
|
|
||||||
this.box = box;
|
|
||||||
this.boxAnimation = boxAnimation;
|
|
||||||
this.text = text;
|
|
||||||
if (speed != null) this.speed = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):DialogueEntryData
|
|
||||||
{
|
|
||||||
if (j == null) return null;
|
|
||||||
return new DialogueEntryData(j.speaker, j.speakerAnimation, j.box, j.boxAnimation, j.text, j.speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
var result:Dynamic =
|
|
||||||
{
|
|
||||||
speaker: this.speaker,
|
|
||||||
speakerAnimation: this.speakerAnimation,
|
|
||||||
box: this.box,
|
|
||||||
boxAnimation: this.boxAnimation,
|
|
||||||
text: this.text,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.speed != 1.0) result.speed = this.speed;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a backdrop.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.BackdropData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class BackdropData
|
|
||||||
{
|
|
||||||
public var type:BackdropType;
|
|
||||||
public var data:Dynamic;
|
|
||||||
|
|
||||||
public function new(typeStr:String, data:Dynamic)
|
|
||||||
{
|
|
||||||
this.type = typeStr;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):BackdropData
|
|
||||||
{
|
|
||||||
if (j == null) return null;
|
|
||||||
return new BackdropData(j.type, j.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
type: this.type,
|
|
||||||
data: this.data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum abstract BackdropType(String) from String to String
|
|
||||||
{
|
|
||||||
public var SOLID:BackdropType = 'solid';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a music track.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.MusicData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class MusicData
|
|
||||||
{
|
|
||||||
public var asset:String;
|
|
||||||
|
|
||||||
public var fadeTime:Float;
|
|
||||||
|
|
||||||
@:optional
|
|
||||||
@:default(false)
|
|
||||||
public var looped:Bool;
|
|
||||||
|
|
||||||
public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
|
|
||||||
{
|
|
||||||
this.asset = asset;
|
|
||||||
this.looped = looped;
|
|
||||||
this.fadeTime = fadeTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):MusicData
|
|
||||||
{
|
|
||||||
if (j == null) return null;
|
|
||||||
return new MusicData(j.asset, j.looped, j.fadeTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
asset: this.asset,
|
|
||||||
looped: this.looped,
|
|
||||||
fadeTime: this.fadeTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about an outro.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.OutroData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class OutroData
|
|
||||||
{
|
|
||||||
public var type:OutroType;
|
|
||||||
public var data:Dynamic;
|
|
||||||
|
|
||||||
public function new(?typeStr:String, data:Dynamic)
|
|
||||||
{
|
|
||||||
this.type = typeStr ?? OutroType.NONE;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):OutroData
|
|
||||||
{
|
|
||||||
if (j == null) return null;
|
|
||||||
return new OutroData(j.type, j.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
type: this.type,
|
|
||||||
data: this.data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum abstract OutroType(String) from String to String
|
|
||||||
{
|
|
||||||
public var NONE:OutroType = 'none';
|
|
||||||
public var FADE:OutroType = 'fade';
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import openfl.Assets;
|
|
||||||
import funkin.util.assets.DataAssets;
|
|
||||||
import funkin.play.cutscene.dialogue.ScriptedConversation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains utilities for loading and parsing conversation data.
|
|
||||||
* TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
|
|
||||||
*/
|
|
||||||
class ConversationDataParser
|
|
||||||
{
|
|
||||||
public static final CONVERSATION_DATA_VERSION:String = '1.0.0';
|
|
||||||
public static final CONVERSATION_DATA_VERSION_RULE:String = '1.0.x';
|
|
||||||
|
|
||||||
static final conversationCache:Map<String, Conversation> = new Map<String, Conversation>();
|
|
||||||
static final conversationScriptedClass:Map<String, String> = new Map<String, String>();
|
|
||||||
|
|
||||||
static final DEFAULT_CONVERSATION_ID:String = 'UNKNOWN';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and preloads the game's conversation data and scripts when the game starts.
|
|
||||||
*
|
|
||||||
* If you want to force conversations to be reloaded, you can just call this function again.
|
|
||||||
*/
|
|
||||||
public static function loadConversationCache():Void
|
|
||||||
{
|
|
||||||
clearConversationCache();
|
|
||||||
trace('Loading dialogue conversation cache...');
|
|
||||||
|
|
||||||
//
|
|
||||||
// SCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
var scriptedConversationClassNames:Array<String> = ScriptedConversation.listScriptClasses();
|
|
||||||
trace(' Instantiating ${scriptedConversationClassNames.length} scripted conversations...');
|
|
||||||
for (conversationCls in scriptedConversationClassNames)
|
|
||||||
{
|
|
||||||
var conversation:Conversation = ScriptedConversation.init(conversationCls, DEFAULT_CONVERSATION_ID);
|
|
||||||
if (conversation != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded scripted conversation: ${conversationCls}');
|
|
||||||
// Disable the rendering logic for conversation until it's loaded.
|
|
||||||
// Note that kill() =/= destroy()
|
|
||||||
conversation.kill();
|
|
||||||
|
|
||||||
// Then store it.
|
|
||||||
conversationCache.set(conversation.conversationId, conversation);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace(' Failed to instantiate scripted conversation class: ${conversationCls}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// UNSCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
// Scripts refers to code here, not the actual dialogue.
|
|
||||||
var conversationIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/conversations/');
|
|
||||||
// Filter out conversations that are scripted.
|
|
||||||
var unscriptedConversationIds:Array<String> = conversationIdList.filter(function(conversationId:String):Bool {
|
|
||||||
return !conversationCache.exists(conversationId);
|
|
||||||
});
|
|
||||||
trace(' Fetching data for ${unscriptedConversationIds.length} conversations...');
|
|
||||||
for (conversationId in unscriptedConversationIds)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var conversation:Conversation = new Conversation(conversationId);
|
|
||||||
// Say something offensive to kill the conversation.
|
|
||||||
// We will revive it later.
|
|
||||||
conversation.kill();
|
|
||||||
if (conversation != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded conversation data: ${conversation.conversationId}');
|
|
||||||
conversationCache.set(conversation.conversationId, conversation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace(e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches data for a conversation and returns a Conversation instance,
|
|
||||||
* ready to be displayed.
|
|
||||||
* @param conversationId The ID of the conversation to fetch.
|
|
||||||
* @return The conversation instance, or null if the conversation was not found.
|
|
||||||
*/
|
|
||||||
public static function fetchConversation(conversationId:String):Null<Conversation>
|
|
||||||
{
|
|
||||||
if (conversationId != null && conversationId != '' && conversationCache.exists(conversationId))
|
|
||||||
{
|
|
||||||
trace('Successfully fetched conversation: ${conversationId}');
|
|
||||||
var conversation:Conversation = conversationCache.get(conversationId);
|
|
||||||
// ...ANYway...
|
|
||||||
conversation.revive();
|
|
||||||
return conversation;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to fetch conversation, not found in cache: ${conversationId}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function clearConversationCache():Void
|
|
||||||
{
|
|
||||||
if (conversationCache != null)
|
|
||||||
{
|
|
||||||
for (conversation in conversationCache)
|
|
||||||
{
|
|
||||||
conversation.destroy();
|
|
||||||
}
|
|
||||||
conversationCache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function listConversationIds():Array<String>
|
|
||||||
{
|
|
||||||
return conversationCache.keys().array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a conversation's JSON file, parse its data, and return it.
|
|
||||||
*
|
|
||||||
* @param conversationId The conversation to load.
|
|
||||||
* @return The conversation data, or null if validation failed.
|
|
||||||
*/
|
|
||||||
public static function parseConversationData(conversationId:String):Null<ConversationData>
|
|
||||||
{
|
|
||||||
trace('Parsing conversation data: ${conversationId}');
|
|
||||||
var rawJson:String = loadConversationFile(conversationId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var conversationData:ConversationData = ConversationData.fromString(rawJson);
|
|
||||||
return conversationData;
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace('Failed to parse conversation ($conversationId).');
|
|
||||||
trace(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function loadConversationFile(conversationPath:String):String
|
|
||||||
{
|
|
||||||
var conversationFilePath:String = Paths.json('dialogue/conversations/${conversationPath}');
|
|
||||||
var rawJson:String = Assets.getText(conversationFilePath).trim();
|
|
||||||
|
|
||||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
|
||||||
{
|
|
||||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawJson;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import flixel.FlxState;
|
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
|
||||||
import funkin.modding.events.ScriptEvent;
|
|
||||||
import flixel.util.FlxColor;
|
|
||||||
import funkin.ui.MusicBeatState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A state with displays a conversation with no background.
|
|
||||||
* Used for testing.
|
|
||||||
* @param conversationId The conversation to display.
|
|
||||||
*/
|
|
||||||
class ConversationDebugState extends MusicBeatState
|
|
||||||
{
|
|
||||||
final conversationId:String = 'senpai';
|
|
||||||
|
|
||||||
var conversation:Conversation;
|
|
||||||
|
|
||||||
public function new()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
|
|
||||||
// TODO: Fix this BS
|
|
||||||
Paths.setCurrentLevel('week6');
|
|
||||||
}
|
|
||||||
|
|
||||||
public override function create():Void
|
|
||||||
{
|
|
||||||
conversation = ConversationDataParser.fetchConversation(conversationId);
|
|
||||||
conversation.completeCallback = onConversationComplete;
|
|
||||||
add(conversation);
|
|
||||||
|
|
||||||
ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConversationComplete():Void
|
|
||||||
{
|
|
||||||
remove(conversation);
|
|
||||||
conversation = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override function update(elapsed:Float):Void
|
|
||||||
{
|
|
||||||
super.update(elapsed);
|
|
||||||
|
|
||||||
if (conversation != null)
|
|
||||||
{
|
|
||||||
if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
|
|
||||||
|
|
||||||
if (controls.CUTSCENE_SKIP)
|
|
||||||
{
|
|
||||||
conversation.trySkipConversation(elapsed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
conversation.trySkipConversation(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -235,7 +235,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||||
{
|
{
|
||||||
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
||||||
textDisplay.fieldWidth = boxData.text.width;
|
textDisplay.fieldWidth = boxData.text.width;
|
||||||
textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
|
textDisplay.setFormat(boxData.text.fontFamily, boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
|
||||||
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
|
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
|
||||||
textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
|
textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
|
||||||
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
|
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import funkin.data.animation.AnimationData;
|
|
||||||
import funkin.util.SerializerUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a text box.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class DialogueBoxData
|
|
||||||
{
|
|
||||||
public var version:String;
|
|
||||||
public var name:String;
|
|
||||||
public var assetPath:String;
|
|
||||||
public var flipX:Bool;
|
|
||||||
public var flipY:Bool;
|
|
||||||
public var isPixel:Bool;
|
|
||||||
public var offsets:Array<Float>;
|
|
||||||
public var text:DialogueBoxTextData;
|
|
||||||
public var scale:Float;
|
|
||||||
public var animations:Array<AnimationData>;
|
|
||||||
|
|
||||||
public function new(version:String, name:String, assetPath:String, flipX:Bool = false, flipY:Bool = false, isPixel:Bool = false, offsets:Null<Array<Float>>,
|
|
||||||
text:DialogueBoxTextData, scale:Float = 1.0, animations:Array<AnimationData>)
|
|
||||||
{
|
|
||||||
this.version = version;
|
|
||||||
this.name = name;
|
|
||||||
this.assetPath = assetPath;
|
|
||||||
this.flipX = flipX;
|
|
||||||
this.flipY = flipY;
|
|
||||||
this.isPixel = isPixel;
|
|
||||||
this.offsets = offsets ?? [0, 0];
|
|
||||||
this.text = text;
|
|
||||||
this.scale = scale;
|
|
||||||
this.animations = animations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromString(i:String):DialogueBoxData
|
|
||||||
{
|
|
||||||
if (i == null || i == '') return null;
|
|
||||||
var data:
|
|
||||||
{
|
|
||||||
version:String,
|
|
||||||
name:String,
|
|
||||||
assetPath:String,
|
|
||||||
flipX:Bool,
|
|
||||||
flipY:Bool,
|
|
||||||
isPixel:Bool,
|
|
||||||
?offsets:Array<Float>,
|
|
||||||
text:Dynamic,
|
|
||||||
scale:Float,
|
|
||||||
animations:Array<AnimationData>
|
|
||||||
} = tink.Json.parse(i);
|
|
||||||
return fromJson(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):DialogueBoxData
|
|
||||||
{
|
|
||||||
// TODO: Check version and perform migrations if necessary.
|
|
||||||
if (j == null) return null;
|
|
||||||
return new DialogueBoxData(j.version, j.name, j.assetPath, j.flipX, j.flipY, j.isPixel, j.offsets, DialogueBoxTextData.fromJson(j.text), j.scale,
|
|
||||||
j.animations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
version: this.version,
|
|
||||||
name: this.name,
|
|
||||||
assetPath: this.assetPath,
|
|
||||||
flipX: this.flipX,
|
|
||||||
flipY: this.flipY,
|
|
||||||
isPixel: this.isPixel,
|
|
||||||
offsets: this.offsets,
|
|
||||||
scale: this.scale,
|
|
||||||
animations: this.animations
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about text in a text box.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxTextData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class DialogueBoxTextData
|
|
||||||
{
|
|
||||||
public var offsets:Array<Float>;
|
|
||||||
public var width:Int;
|
|
||||||
public var size:Int;
|
|
||||||
public var color:String;
|
|
||||||
public var shadowColor:Null<String>;
|
|
||||||
public var shadowWidth:Null<Int>;
|
|
||||||
|
|
||||||
public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>)
|
|
||||||
{
|
|
||||||
this.offsets = offsets ?? [0, 0];
|
|
||||||
this.width = width ?? 300;
|
|
||||||
this.size = size ?? 32;
|
|
||||||
this.color = color;
|
|
||||||
this.shadowColor = shadowColor;
|
|
||||||
this.shadowWidth = shadowWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):DialogueBoxTextData
|
|
||||||
{
|
|
||||||
// TODO: Check version and perform migrations if necessary.
|
|
||||||
if (j == null) return null;
|
|
||||||
return new DialogueBoxTextData(j.offsets, j.width, j.size, j.color, j.shadowColor, j.shadowWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
offsets: this.offsets,
|
|
||||||
width: this.width,
|
|
||||||
size: this.size,
|
|
||||||
color: this.color,
|
|
||||||
shadowColor: this.shadowColor,
|
|
||||||
shadowWidth: this.shadowWidth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import openfl.Assets;
|
|
||||||
import funkin.util.assets.DataAssets;
|
|
||||||
import funkin.play.cutscene.dialogue.DialogueBox;
|
|
||||||
import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains utilities for loading and parsing dialogueBox data.
|
|
||||||
*/
|
|
||||||
class DialogueBoxDataParser
|
|
||||||
{
|
|
||||||
public static final DIALOGUE_BOX_DATA_VERSION:String = '1.0.0';
|
|
||||||
public static final DIALOGUE_BOX_DATA_VERSION_RULE:String = '1.0.x';
|
|
||||||
|
|
||||||
static final dialogueBoxCache:Map<String, DialogueBox> = new Map<String, DialogueBox>();
|
|
||||||
|
|
||||||
static final dialogueBoxScriptedClass:Map<String, String> = new Map<String, String>();
|
|
||||||
|
|
||||||
static final DEFAULT_DIALOGUE_BOX_ID:String = 'UNKNOWN';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and preloads the game's dialogueBox data and scripts when the game starts.
|
|
||||||
*
|
|
||||||
* If you want to force dialogue boxes to be reloaded, you can just call this function again.
|
|
||||||
*/
|
|
||||||
public static function loadDialogueBoxCache():Void
|
|
||||||
{
|
|
||||||
clearDialogueBoxCache();
|
|
||||||
trace('Loading dialogue box cache...');
|
|
||||||
|
|
||||||
//
|
|
||||||
// SCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
var scriptedDialogueBoxClassNames:Array<String> = ScriptedDialogueBox.listScriptClasses();
|
|
||||||
trace(' Instantiating ${scriptedDialogueBoxClassNames.length} scripted dialogue boxes...');
|
|
||||||
for (dialogueBoxCls in scriptedDialogueBoxClassNames)
|
|
||||||
{
|
|
||||||
var dialogueBox:DialogueBox = ScriptedDialogueBox.init(dialogueBoxCls, DEFAULT_DIALOGUE_BOX_ID);
|
|
||||||
if (dialogueBox != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded scripted dialogue box: ${dialogueBox.dialogueBoxName}');
|
|
||||||
// Disable the rendering logic for dialogueBox until it's loaded.
|
|
||||||
// Note that kill() =/= destroy()
|
|
||||||
dialogueBox.kill();
|
|
||||||
|
|
||||||
// Then store it.
|
|
||||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace(' Failed to instantiate scripted dialogueBox class: ${dialogueBoxCls}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// UNSCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
// Scripts refers to code here, not the actual dialogue.
|
|
||||||
var dialogueBoxIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/boxes/');
|
|
||||||
// Filter out dialogue boxes that are scripted.
|
|
||||||
var unscriptedDialogueBoxIds:Array<String> = dialogueBoxIdList.filter(function(dialogueBoxId:String):Bool {
|
|
||||||
return !dialogueBoxCache.exists(dialogueBoxId);
|
|
||||||
});
|
|
||||||
trace(' Fetching data for ${unscriptedDialogueBoxIds.length} dialogue boxes...');
|
|
||||||
for (dialogueBoxId in unscriptedDialogueBoxIds)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dialogueBox:DialogueBox = new DialogueBox(dialogueBoxId);
|
|
||||||
if (dialogueBox != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded dialogueBox data: ${dialogueBox.dialogueBoxName}');
|
|
||||||
dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace(e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches data for a dialogueBox and returns a DialogueBox instance,
|
|
||||||
* ready to be displayed.
|
|
||||||
* @param dialogueBoxId The ID of the dialogueBox to fetch.
|
|
||||||
* @return The dialogueBox instance, or null if the dialogueBox was not found.
|
|
||||||
*/
|
|
||||||
public static function fetchDialogueBox(dialogueBoxId:String):Null<DialogueBox>
|
|
||||||
{
|
|
||||||
if (dialogueBoxId != null && dialogueBoxId != '' && dialogueBoxCache.exists(dialogueBoxId))
|
|
||||||
{
|
|
||||||
trace('Successfully fetched dialogueBox: ${dialogueBoxId}');
|
|
||||||
var dialogueBox:DialogueBox = dialogueBoxCache.get(dialogueBoxId);
|
|
||||||
dialogueBox.revive();
|
|
||||||
return dialogueBox;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to fetch dialogueBox, not found in cache: ${dialogueBoxId}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function clearDialogueBoxCache():Void
|
|
||||||
{
|
|
||||||
if (dialogueBoxCache != null)
|
|
||||||
{
|
|
||||||
for (dialogueBox in dialogueBoxCache)
|
|
||||||
{
|
|
||||||
dialogueBox.destroy();
|
|
||||||
}
|
|
||||||
dialogueBoxCache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function listDialogueBoxIds():Array<String>
|
|
||||||
{
|
|
||||||
return dialogueBoxCache.keys().array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a dialogueBox's JSON file, parse its data, and return it.
|
|
||||||
*
|
|
||||||
* @param dialogueBoxId The dialogueBox to load.
|
|
||||||
* @return The dialogueBox data, or null if validation failed.
|
|
||||||
*/
|
|
||||||
public static function parseDialogueBoxData(dialogueBoxId:String):Null<DialogueBoxData>
|
|
||||||
{
|
|
||||||
var rawJson:String = loadDialogueBoxFile(dialogueBoxId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dialogueBoxData:DialogueBoxData = DialogueBoxData.fromString(rawJson);
|
|
||||||
return dialogueBoxData;
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace('Failed to parse dialogueBox ($dialogueBoxId).');
|
|
||||||
trace(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function loadDialogueBoxFile(dialogueBoxPath:String):String
|
|
||||||
{
|
|
||||||
var dialogueBoxFilePath:String = Paths.json('dialogue/boxes/${dialogueBoxPath}');
|
|
||||||
var rawJson:String = Assets.getText(dialogueBoxFilePath).trim();
|
|
||||||
|
|
||||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
|
||||||
{
|
|
||||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawJson;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,10 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A script that can be tied to a Conversation.
|
||||||
|
* Create a scripted class that extends Conversation to use this.
|
||||||
|
* This allows you to customize how a specific conversation appears and behaves.
|
||||||
|
* Someone clever could use this to add branching dialogue I think.
|
||||||
|
*/
|
||||||
@:hscriptClass
|
@:hscriptClass
|
||||||
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
|
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A script that can be tied to a DialogueBox.
|
||||||
|
* Create a scripted class that extends DialogueBox to use this.
|
||||||
|
* This allows you to customize how a specific dialogue box appears.
|
||||||
|
*/
|
||||||
@:hscriptClass
|
@:hscriptClass
|
||||||
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
|
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import funkin.data.animation.AnimationData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data about a conversation.
|
|
||||||
* Includes what speakers are in the conversation, and what phrases they say.
|
|
||||||
*/
|
|
||||||
@:jsonParse(j -> funkin.play.cutscene.dialogue.SpeakerData.fromJson(j))
|
|
||||||
@:jsonStringify(v -> v.toJson())
|
|
||||||
class SpeakerData
|
|
||||||
{
|
|
||||||
public var version:String;
|
|
||||||
public var name:String;
|
|
||||||
public var assetPath:String;
|
|
||||||
public var flipX:Bool;
|
|
||||||
public var isPixel:Bool;
|
|
||||||
public var offsets:Array<Float>;
|
|
||||||
public var scale:Float;
|
|
||||||
public var animations:Array<AnimationData>;
|
|
||||||
|
|
||||||
public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false,
|
|
||||||
isPixel:Bool = false, ?scale:Float = 1.0)
|
|
||||||
{
|
|
||||||
this.version = version;
|
|
||||||
this.name = name;
|
|
||||||
this.assetPath = assetPath;
|
|
||||||
this.animations = animations;
|
|
||||||
|
|
||||||
this.offsets = offsets;
|
|
||||||
if (this.offsets == null || this.offsets == []) this.offsets = [0, 0];
|
|
||||||
|
|
||||||
this.flipX = flipX;
|
|
||||||
this.isPixel = isPixel;
|
|
||||||
this.scale = scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromString(i:String):SpeakerData
|
|
||||||
{
|
|
||||||
if (i == null || i == '') return null;
|
|
||||||
var data:
|
|
||||||
{
|
|
||||||
version:String,
|
|
||||||
name:String,
|
|
||||||
assetPath:String,
|
|
||||||
animations:Array<AnimationData>,
|
|
||||||
?offsets:Array<Float>,
|
|
||||||
?flipX:Bool,
|
|
||||||
?isPixel:Bool,
|
|
||||||
?scale:Float
|
|
||||||
} = tink.Json.parse(i);
|
|
||||||
return fromJson(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function fromJson(j:Dynamic):SpeakerData
|
|
||||||
{
|
|
||||||
// TODO: Check version and perform migrations if necessary.
|
|
||||||
if (j == null) return null;
|
|
||||||
return new SpeakerData(j.version, j.name, j.assetPath, j.animations, j.offsets, j.flipX, j.isPixel, j.scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toJson():Dynamic
|
|
||||||
{
|
|
||||||
var result:Dynamic =
|
|
||||||
{
|
|
||||||
version: this.version,
|
|
||||||
name: this.name,
|
|
||||||
assetPath: this.assetPath,
|
|
||||||
animations: this.animations,
|
|
||||||
flipX: this.flipX,
|
|
||||||
isPixel: this.isPixel
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.scale != 1.0) result.scale = this.scale;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
package funkin.play.cutscene.dialogue;
|
|
||||||
|
|
||||||
import openfl.Assets;
|
|
||||||
import funkin.util.assets.DataAssets;
|
|
||||||
import funkin.play.cutscene.dialogue.Speaker;
|
|
||||||
import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains utilities for loading and parsing speaker data.
|
|
||||||
*/
|
|
||||||
class SpeakerDataParser
|
|
||||||
{
|
|
||||||
public static final SPEAKER_DATA_VERSION:String = '1.0.0';
|
|
||||||
public static final SPEAKER_DATA_VERSION_RULE:String = '1.0.x';
|
|
||||||
|
|
||||||
static final speakerCache:Map<String, Speaker> = new Map<String, Speaker>();
|
|
||||||
|
|
||||||
static final speakerScriptedClass:Map<String, String> = new Map<String, String>();
|
|
||||||
|
|
||||||
static final DEFAULT_SPEAKER_ID:String = 'UNKNOWN';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and preloads the game's speaker data and scripts when the game starts.
|
|
||||||
*
|
|
||||||
* If you want to force speakers to be reloaded, you can just call this function again.
|
|
||||||
*/
|
|
||||||
public static function loadSpeakerCache():Void
|
|
||||||
{
|
|
||||||
clearSpeakerCache();
|
|
||||||
trace('Loading dialogue speaker cache...');
|
|
||||||
|
|
||||||
//
|
|
||||||
// SCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
var scriptedSpeakerClassNames:Array<String> = ScriptedSpeaker.listScriptClasses();
|
|
||||||
trace(' Instantiating ${scriptedSpeakerClassNames.length} scripted speakers...');
|
|
||||||
for (speakerCls in scriptedSpeakerClassNames)
|
|
||||||
{
|
|
||||||
var speaker:Speaker = ScriptedSpeaker.init(speakerCls, DEFAULT_SPEAKER_ID);
|
|
||||||
if (speaker != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded scripted speaker: ${speaker.speakerName}');
|
|
||||||
// Disable the rendering logic for speaker until it's loaded.
|
|
||||||
// Note that kill() =/= destroy()
|
|
||||||
speaker.kill();
|
|
||||||
|
|
||||||
// Then store it.
|
|
||||||
speakerCache.set(speaker.speakerId, speaker);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace(' Failed to instantiate scripted speaker class: ${speakerCls}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// UNSCRIPTED CONVERSATIONS
|
|
||||||
//
|
|
||||||
// Scripts refers to code here, not the actual dialogue.
|
|
||||||
var speakerIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/speakers/');
|
|
||||||
// Filter out speakers that are scripted.
|
|
||||||
var unscriptedSpeakerIds:Array<String> = speakerIdList.filter(function(speakerId:String):Bool {
|
|
||||||
return !speakerCache.exists(speakerId);
|
|
||||||
});
|
|
||||||
trace(' Fetching data for ${unscriptedSpeakerIds.length} speakers...');
|
|
||||||
for (speakerId in unscriptedSpeakerIds)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var speaker:Speaker = new Speaker(speakerId);
|
|
||||||
if (speaker != null)
|
|
||||||
{
|
|
||||||
trace(' Loaded speaker data: ${speaker.speakerName}');
|
|
||||||
speakerCache.set(speaker.speakerId, speaker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace(e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches data for a speaker and returns a Speaker instance,
|
|
||||||
* ready to be displayed.
|
|
||||||
* @param speakerId The ID of the speaker to fetch.
|
|
||||||
* @return The speaker instance, or null if the speaker was not found.
|
|
||||||
*/
|
|
||||||
public static function fetchSpeaker(speakerId:String):Null<Speaker>
|
|
||||||
{
|
|
||||||
if (speakerId != null && speakerId != '' && speakerCache.exists(speakerId))
|
|
||||||
{
|
|
||||||
trace('Successfully fetched speaker: ${speakerId}');
|
|
||||||
var speaker:Speaker = speakerCache.get(speakerId);
|
|
||||||
speaker.revive();
|
|
||||||
return speaker;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
trace('Failed to fetch speaker, not found in cache: ${speakerId}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function clearSpeakerCache():Void
|
|
||||||
{
|
|
||||||
if (speakerCache != null)
|
|
||||||
{
|
|
||||||
for (speaker in speakerCache)
|
|
||||||
{
|
|
||||||
speaker.destroy();
|
|
||||||
}
|
|
||||||
speakerCache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function listSpeakerIds():Array<String>
|
|
||||||
{
|
|
||||||
return speakerCache.keys().array();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a speaker's JSON file, parse its data, and return it.
|
|
||||||
*
|
|
||||||
* @param speakerId The speaker to load.
|
|
||||||
* @return The speaker data, or null if validation failed.
|
|
||||||
*/
|
|
||||||
public static function parseSpeakerData(speakerId:String):Null<SpeakerData>
|
|
||||||
{
|
|
||||||
var rawJson:String = loadSpeakerFile(speakerId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var speakerData:SpeakerData = SpeakerData.fromString(rawJson);
|
|
||||||
return speakerData;
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
trace('Failed to parse speaker ($speakerId).');
|
|
||||||
trace(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static function loadSpeakerFile(speakerPath:String):String
|
|
||||||
{
|
|
||||||
var speakerFilePath:String = Paths.json('dialogue/speakers/${speakerPath}');
|
|
||||||
var rawJson:String = Assets.getText(speakerFilePath).trim();
|
|
||||||
|
|
||||||
while (!rawJson.endsWith('}') && rawJson.length > 0)
|
|
||||||
{
|
|
||||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawJson;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,6 @@
|
||||||
<haxelib name="hxCodec" /> <!-- Video playback -->
|
<haxelib name="hxCodec" /> <!-- Video playback -->
|
||||||
<haxelib name="thx.semver" /> <!-- Semantic version handling -->
|
<haxelib name="thx.semver" /> <!-- Semantic version handling -->
|
||||||
<haxelib name="json2object" /> <!-- JSON parsing -->
|
<haxelib name="json2object" /> <!-- JSON parsing -->
|
||||||
<haxelib name="tink_json" /> <!-- JSON parsing -->
|
|
||||||
|
|
||||||
<!-- Test dependencies -->
|
<!-- Test dependencies -->
|
||||||
<haxelib name="munit" /> <!-- Unit test execution -->
|
<haxelib name="munit" /> <!-- Unit test execution -->
|
||||||
|
|
Loading…
Reference in a new issue