533 lines
16 KiB
Haxe
533 lines
16 KiB
Haxe
package;
|
|
|
|
import flixel.FlxG;
|
|
import flixel.FlxSprite;
|
|
import flixel.addons.text.FlxTypeText;
|
|
import flixel.graphics.frames.FlxAtlasFrames;
|
|
import flixel.group.FlxSpriteGroup;
|
|
import flixel.input.FlxKeyManager;
|
|
import flixel.text.FlxText;
|
|
import flixel.util.FlxColor;
|
|
import flixel.util.FlxTimer;
|
|
import flixel.FlxSubState;
|
|
import haxe.Json;
|
|
import haxe.format.JsonParser;
|
|
#if sys
|
|
import sys.FileSystem;
|
|
import sys.io.File;
|
|
#end
|
|
import openfl.utils.Assets;
|
|
|
|
using StringTools;
|
|
|
|
typedef DialogueCharacterFile = {
|
|
var image:String;
|
|
var dialogue_pos:String;
|
|
|
|
var animations:Array<DialogueAnimArray>;
|
|
var position:Array<Float>;
|
|
var scale:Float;
|
|
}
|
|
|
|
typedef DialogueAnimArray = {
|
|
var anim:String;
|
|
var loop_name:String;
|
|
var loop_offsets:Array<Int>;
|
|
var idle_name:String;
|
|
var idle_offsets:Array<Int>;
|
|
}
|
|
|
|
// Gonna try to kind of make it compatible to Forever Engine,
|
|
// love u Shubs no homo :flushedh4:
|
|
typedef DialogueFile = {
|
|
var dialogue:Array<DialogueLine>;
|
|
}
|
|
|
|
typedef DialogueLine = {
|
|
var portrait:Null<String>;
|
|
var expression:Null<String>;
|
|
var text:Null<String>;
|
|
var boxState:Null<String>;
|
|
var speed:Null<Float>;
|
|
}
|
|
|
|
class DialogueCharacter extends FlxSprite
|
|
{
|
|
private static var IDLE_SUFFIX:String = '-IDLE';
|
|
public static var DEFAULT_CHARACTER:String = 'bf';
|
|
public static var DEFAULT_SCALE:Float = 0.7;
|
|
|
|
public var jsonFile:DialogueCharacterFile = null;
|
|
#if (haxe >= "4.0.0")
|
|
public var dialogueAnimations:Map<String, DialogueAnimArray> = new Map();
|
|
#else
|
|
public var dialogueAnimations:Map<String, DialogueAnimArray> = new Map<String, DialogueAnimArray>();
|
|
#end
|
|
|
|
public var startingPos:Float = 0; //For center characters, it works as the starting Y, for everything else it works as starting X
|
|
public var isGhost:Bool = false; //For the editor
|
|
public var curCharacter:String = 'bf';
|
|
|
|
public function new(x:Float = 0, y:Float = 0, character:String = null)
|
|
{
|
|
super(x, y);
|
|
|
|
if(character == null) character = DEFAULT_CHARACTER;
|
|
this.curCharacter = character;
|
|
|
|
reloadCharacterJson(character);
|
|
frames = Paths.getSparrowAtlas('dialogue/' + jsonFile.image);
|
|
reloadAnimations();
|
|
}
|
|
|
|
public function reloadCharacterJson(character:String) {
|
|
var characterPath:String = 'images/dialogue/' + character + '.json';
|
|
var rawJson = null;
|
|
|
|
#if MODS_ALLOWED
|
|
var path:String = Paths.modFolders(characterPath);
|
|
if (!FileSystem.exists(path)) {
|
|
path = Paths.getPreloadPath(characterPath);
|
|
}
|
|
|
|
if(!FileSystem.exists(path)) {
|
|
path = Paths.getPreloadPath('images/dialogue/' + DEFAULT_CHARACTER + '.json');
|
|
}
|
|
rawJson = File.getContent(path);
|
|
|
|
#else
|
|
var path:String = Paths.getPreloadPath(characterPath);
|
|
rawJson = Assets.getText(path);
|
|
#end
|
|
|
|
jsonFile = cast Json.parse(rawJson);
|
|
}
|
|
|
|
public function reloadAnimations() {
|
|
dialogueAnimations.clear();
|
|
if(jsonFile.animations != null && jsonFile.animations.length > 0) {
|
|
for (anim in jsonFile.animations) {
|
|
animation.addByPrefix(anim.anim, anim.loop_name, 24, isGhost);
|
|
animation.addByPrefix(anim.anim + IDLE_SUFFIX, anim.idle_name, 24, true);
|
|
dialogueAnimations.set(anim.anim, anim);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function playAnim(animName:String = null, playIdle:Bool = false) {
|
|
var leAnim:String = animName;
|
|
if(animName == null || !dialogueAnimations.exists(animName)) { //Anim is null, get a random animation
|
|
var arrayAnims:Array<String> = [];
|
|
for (anim in dialogueAnimations) {
|
|
arrayAnims.push(anim.anim);
|
|
}
|
|
if(arrayAnims.length > 0) {
|
|
leAnim = arrayAnims[FlxG.random.int(0, arrayAnims.length-1)];
|
|
}
|
|
}
|
|
|
|
if(dialogueAnimations.exists(leAnim) &&
|
|
(dialogueAnimations.get(leAnim).loop_name == null ||
|
|
dialogueAnimations.get(leAnim).loop_name.length < 1 ||
|
|
dialogueAnimations.get(leAnim).loop_name == dialogueAnimations.get(leAnim).idle_name)) {
|
|
playIdle = true;
|
|
}
|
|
animation.play(playIdle ? leAnim + IDLE_SUFFIX : leAnim, false);
|
|
|
|
if(dialogueAnimations.exists(leAnim)) {
|
|
var anim:DialogueAnimArray = dialogueAnimations.get(leAnim);
|
|
if(playIdle) {
|
|
offset.set(anim.idle_offsets[0], anim.idle_offsets[1]);
|
|
//trace('Setting idle offsets: ' + anim.idle_offsets);
|
|
} else {
|
|
offset.set(anim.loop_offsets[0], anim.loop_offsets[1]);
|
|
//trace('Setting loop offsets: ' + anim.loop_offsets);
|
|
}
|
|
} else {
|
|
offset.set(0, 0);
|
|
trace('Offsets not found! Dialogue character is badly formatted, anim: ' + leAnim + ', ' + (playIdle ? 'idle anim' : 'loop anim'));
|
|
}
|
|
}
|
|
|
|
public function animationIsLoop():Bool {
|
|
if(animation.curAnim == null) return false;
|
|
return !animation.curAnim.name.endsWith(IDLE_SUFFIX);
|
|
}
|
|
}
|
|
|
|
// TO DO: Clean code? Maybe? idk
|
|
class DialogueBoxPsych extends FlxSpriteGroup
|
|
{
|
|
var dialogue:Alphabet;
|
|
var dialogueList:DialogueFile = null;
|
|
|
|
public var finishThing:Void->Void;
|
|
public var nextDialogueThing:Void->Void = null;
|
|
public var skipDialogueThing:Void->Void = null;
|
|
var bgFade:FlxSprite = null;
|
|
var box:FlxSprite;
|
|
var textToType:String = '';
|
|
|
|
var arrayCharacters:Array<DialogueCharacter> = [];
|
|
|
|
var currentText:Int = 0;
|
|
var offsetPos:Float = -600;
|
|
|
|
var textBoxTypes:Array<String> = ['normal', 'angry'];
|
|
//var charPositionList:Array<String> = ['left', 'center', 'right'];
|
|
|
|
public function new(dialogueList:DialogueFile, ?song:String = null)
|
|
{
|
|
super();
|
|
|
|
if(song != null && song != '') {
|
|
FlxG.sound.playMusic(Paths.music(song), 0);
|
|
FlxG.sound.music.fadeIn(2, 0, 1);
|
|
}
|
|
|
|
bgFade = new FlxSprite(-500, -500).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.WHITE);
|
|
bgFade.scrollFactor.set();
|
|
bgFade.visible = true;
|
|
bgFade.alpha = 0;
|
|
add(bgFade);
|
|
|
|
this.dialogueList = dialogueList;
|
|
spawnCharacters();
|
|
|
|
box = new FlxSprite(70, 370);
|
|
box.frames = Paths.getSparrowAtlas('speech_bubble');
|
|
box.scrollFactor.set();
|
|
box.antialiasing = ClientPrefs.globalAntialiasing;
|
|
box.animation.addByPrefix('normal', 'speech bubble normal', 24);
|
|
box.animation.addByPrefix('normalOpen', 'Speech Bubble Normal Open', 24, false);
|
|
box.animation.addByPrefix('angry', 'AHH speech bubble', 24);
|
|
box.animation.addByPrefix('angryOpen', 'speech bubble loud open', 24, false);
|
|
box.animation.addByPrefix('center-normal', 'speech bubble middle', 24);
|
|
box.animation.addByPrefix('center-normalOpen', 'Speech Bubble Middle Open', 24, false);
|
|
box.animation.addByPrefix('center-angry', 'AHH Speech Bubble middle', 24);
|
|
box.animation.addByPrefix('center-angryOpen', 'speech bubble Middle loud open', 24, false);
|
|
box.animation.play('normal', true);
|
|
box.visible = false;
|
|
box.setGraphicSize(Std.int(box.width * 0.9));
|
|
box.updateHitbox();
|
|
add(box);
|
|
|
|
startNextDialog();
|
|
}
|
|
|
|
var dialogueStarted:Bool = false;
|
|
var dialogueEnded:Bool = false;
|
|
|
|
public static var LEFT_CHAR_X:Float = -60;
|
|
public static var RIGHT_CHAR_X:Float = -100;
|
|
public static var DEFAULT_CHAR_Y:Float = 60;
|
|
|
|
function spawnCharacters() {
|
|
#if (haxe >= "4.0.0")
|
|
var charsMap:Map<String, Bool> = new Map();
|
|
#else
|
|
var charsMap:Map<String, Bool> = new Map<String, Bool>();
|
|
#end
|
|
for (i in 0...dialogueList.dialogue.length) {
|
|
if(dialogueList.dialogue[i] != null) {
|
|
var charToAdd:String = dialogueList.dialogue[i].portrait;
|
|
if(!charsMap.exists(charToAdd) || !charsMap.get(charToAdd)) {
|
|
charsMap.set(charToAdd, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (individualChar in charsMap.keys()) {
|
|
var x:Float = LEFT_CHAR_X;
|
|
var y:Float = DEFAULT_CHAR_Y;
|
|
var char:DialogueCharacter = new DialogueCharacter(x + offsetPos, y, individualChar);
|
|
|
|
char.setGraphicSize(Std.int(char.width * DialogueCharacter.DEFAULT_SCALE * char.jsonFile.scale));
|
|
char.updateHitbox();
|
|
char.antialiasing = ClientPrefs.globalAntialiasing;
|
|
char.scrollFactor.set();
|
|
char.alpha = 0.00001;
|
|
add(char);
|
|
|
|
var saveY:Bool = false;
|
|
switch(char.jsonFile.dialogue_pos) {
|
|
case 'center':
|
|
char.x = FlxG.width / 2;
|
|
char.x -= char.width / 2;
|
|
y = char.y;
|
|
char.y = FlxG.height + 50;
|
|
saveY = true;
|
|
case 'right':
|
|
x = FlxG.width - char.width + RIGHT_CHAR_X;
|
|
char.x = x - offsetPos;
|
|
}
|
|
x += char.jsonFile.position[0];
|
|
y += char.jsonFile.position[1];
|
|
char.x += char.jsonFile.position[0];
|
|
char.y += char.jsonFile.position[1];
|
|
char.startingPos = (saveY ? y : x);
|
|
arrayCharacters.push(char);
|
|
}
|
|
}
|
|
|
|
public static var DEFAULT_TEXT_X = 90;
|
|
public static var DEFAULT_TEXT_Y = 430;
|
|
var scrollSpeed = 4500;
|
|
var daText:Alphabet = null;
|
|
var ignoreThisFrame:Bool = true; //First frame is reserved for loading dialogue images
|
|
override function update(elapsed:Float)
|
|
{
|
|
if(ignoreThisFrame) {
|
|
ignoreThisFrame = false;
|
|
super.update(elapsed);
|
|
return;
|
|
}
|
|
|
|
if(!dialogueEnded) {
|
|
bgFade.alpha += 0.5 * elapsed;
|
|
if(bgFade.alpha > 0.5) bgFade.alpha = 0.5;
|
|
|
|
if(PlayerSettings.player1.controls.ACCEPT) {
|
|
if(!daText.finishedText) {
|
|
if(daText != null) {
|
|
daText.killTheTimer();
|
|
daText.kill();
|
|
remove(daText);
|
|
daText.destroy();
|
|
}
|
|
daText = new Alphabet(DEFAULT_TEXT_X, DEFAULT_TEXT_Y, textToType, false, true, 0.0, 0.7);
|
|
add(daText);
|
|
|
|
if(skipDialogueThing != null) {
|
|
skipDialogueThing();
|
|
}
|
|
} else if(currentText >= dialogueList.dialogue.length) {
|
|
dialogueEnded = true;
|
|
for (i in 0...textBoxTypes.length) {
|
|
var checkArray:Array<String> = ['', 'center-'];
|
|
var animName:String = box.animation.curAnim.name;
|
|
for (j in 0...checkArray.length) {
|
|
if(animName == checkArray[j] + textBoxTypes[i] || animName == checkArray[j] + textBoxTypes[i] + 'Open') {
|
|
box.animation.play(checkArray[j] + textBoxTypes[i] + 'Open', true);
|
|
}
|
|
}
|
|
}
|
|
|
|
box.animation.curAnim.curFrame = box.animation.curAnim.frames.length - 1;
|
|
box.animation.curAnim.reverse();
|
|
daText.kill();
|
|
remove(daText);
|
|
daText.destroy();
|
|
daText = null;
|
|
updateBoxOffsets(box);
|
|
FlxG.sound.music.fadeOut(1, 0);
|
|
} else {
|
|
startNextDialog();
|
|
}
|
|
FlxG.sound.play(Paths.sound('dialogueClose'));
|
|
} else if(daText.finishedText) {
|
|
var char:DialogueCharacter = arrayCharacters[lastCharacter];
|
|
if(char != null && char.animation.curAnim != null && char.animationIsLoop() && char.animation.finished) {
|
|
char.playAnim(char.animation.curAnim.name, true);
|
|
}
|
|
} else {
|
|
var char:DialogueCharacter = arrayCharacters[lastCharacter];
|
|
if(char != null && char.animation.curAnim != null && char.animation.finished) {
|
|
char.animation.curAnim.restart();
|
|
}
|
|
}
|
|
|
|
if(box.animation.curAnim.finished) {
|
|
for (i in 0...textBoxTypes.length) {
|
|
var checkArray:Array<String> = ['', 'center-'];
|
|
var animName:String = box.animation.curAnim.name;
|
|
for (j in 0...checkArray.length) {
|
|
if(animName == checkArray[j] + textBoxTypes[i] || animName == checkArray[j] + textBoxTypes[i] + 'Open') {
|
|
box.animation.play(checkArray[j] + textBoxTypes[i], true);
|
|
}
|
|
}
|
|
}
|
|
updateBoxOffsets(box);
|
|
}
|
|
|
|
if(lastCharacter != -1 && arrayCharacters.length > 0) {
|
|
for (i in 0...arrayCharacters.length) {
|
|
var char = arrayCharacters[i];
|
|
if(char != null) {
|
|
if(i != lastCharacter) {
|
|
switch(char.jsonFile.dialogue_pos) {
|
|
case 'left':
|
|
char.x -= scrollSpeed * elapsed;
|
|
if(char.x < char.startingPos + offsetPos) char.x = char.startingPos + offsetPos;
|
|
case 'center':
|
|
char.y += scrollSpeed * elapsed;
|
|
if(char.y > char.startingPos + FlxG.height) char.y = char.startingPos + FlxG.height;
|
|
case 'right':
|
|
char.x += scrollSpeed * elapsed;
|
|
if(char.x > char.startingPos - offsetPos) char.x = char.startingPos - offsetPos;
|
|
}
|
|
char.alpha -= 3 * elapsed;
|
|
if(char.alpha < 0.00001) char.alpha = 0.00001;
|
|
} else {
|
|
switch(char.jsonFile.dialogue_pos) {
|
|
case 'left':
|
|
char.x += scrollSpeed * elapsed;
|
|
if(char.x > char.startingPos) char.x = char.startingPos;
|
|
case 'center':
|
|
char.y -= scrollSpeed * elapsed;
|
|
if(char.y < char.startingPos) char.y = char.startingPos;
|
|
case 'right':
|
|
char.x -= scrollSpeed * elapsed;
|
|
if(char.x < char.startingPos) char.x = char.startingPos;
|
|
}
|
|
char.alpha += 3 * elapsed;
|
|
if(char.alpha > 1) char.alpha = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else { //Dialogue ending
|
|
if(box != null && box.animation.curAnim.curFrame <= 0) {
|
|
box.kill();
|
|
remove(box);
|
|
box.destroy();
|
|
box = null;
|
|
}
|
|
|
|
if(bgFade != null) {
|
|
bgFade.alpha -= 0.5 * elapsed;
|
|
if(bgFade.alpha <= 0) {
|
|
bgFade.kill();
|
|
remove(bgFade);
|
|
bgFade.destroy();
|
|
bgFade = null;
|
|
}
|
|
}
|
|
|
|
for (i in 0...arrayCharacters.length) {
|
|
var leChar:DialogueCharacter = arrayCharacters[i];
|
|
if(leChar != null) {
|
|
switch(arrayCharacters[i].jsonFile.dialogue_pos) {
|
|
case 'left':
|
|
leChar.x -= scrollSpeed * elapsed;
|
|
case 'center':
|
|
leChar.y += scrollSpeed * elapsed;
|
|
case 'right':
|
|
leChar.x += scrollSpeed * elapsed;
|
|
}
|
|
leChar.alpha -= elapsed * 10;
|
|
}
|
|
}
|
|
|
|
if(box == null && bgFade == null) {
|
|
for (i in 0...arrayCharacters.length) {
|
|
var leChar:DialogueCharacter = arrayCharacters[0];
|
|
if(leChar != null) {
|
|
arrayCharacters.remove(leChar);
|
|
leChar.kill();
|
|
remove(leChar);
|
|
leChar.destroy();
|
|
}
|
|
}
|
|
finishThing();
|
|
kill();
|
|
}
|
|
}
|
|
super.update(elapsed);
|
|
}
|
|
|
|
var lastCharacter:Int = -1;
|
|
var lastBoxType:String = '';
|
|
function startNextDialog():Void
|
|
{
|
|
var curDialogue:DialogueLine = null;
|
|
do {
|
|
curDialogue = dialogueList.dialogue[currentText];
|
|
} while(curDialogue == null);
|
|
|
|
if(curDialogue.text == null || curDialogue.text.length < 1) curDialogue.text = ' ';
|
|
if(curDialogue.boxState == null) curDialogue.boxState = 'normal';
|
|
if(curDialogue.speed == null || Math.isNaN(curDialogue.speed)) curDialogue.speed = 0.05;
|
|
|
|
var animName:String = curDialogue.boxState;
|
|
var boxType:String = textBoxTypes[0];
|
|
for (i in 0...textBoxTypes.length) {
|
|
if(textBoxTypes[i] == animName) {
|
|
boxType = animName;
|
|
}
|
|
}
|
|
|
|
var character:Int = 0;
|
|
box.visible = true;
|
|
for (i in 0...arrayCharacters.length) {
|
|
if(arrayCharacters[i].curCharacter == curDialogue.portrait) {
|
|
character = i;
|
|
break;
|
|
}
|
|
}
|
|
var centerPrefix:String = '';
|
|
var lePosition:String = arrayCharacters[character].jsonFile.dialogue_pos;
|
|
if(lePosition == 'center') centerPrefix = 'center-';
|
|
|
|
if(character != lastCharacter) {
|
|
box.animation.play(centerPrefix + boxType + 'Open', true);
|
|
updateBoxOffsets(box);
|
|
box.flipX = (lePosition == 'left');
|
|
} else if(boxType != lastBoxType) {
|
|
box.animation.play(centerPrefix + boxType, true);
|
|
updateBoxOffsets(box);
|
|
}
|
|
lastCharacter = character;
|
|
lastBoxType = boxType;
|
|
|
|
if(daText != null) {
|
|
daText.killTheTimer();
|
|
daText.kill();
|
|
remove(daText);
|
|
daText.destroy();
|
|
}
|
|
|
|
textToType = curDialogue.text;
|
|
daText = new Alphabet(DEFAULT_TEXT_X, DEFAULT_TEXT_Y, textToType, false, true, curDialogue.speed, 0.7);
|
|
add(daText);
|
|
|
|
var char:DialogueCharacter = arrayCharacters[character];
|
|
if(char != null) {
|
|
char.playAnim(curDialogue.expression, daText.finishedText);
|
|
if(char.animation.curAnim != null) {
|
|
var rate:Float = 24 - (((curDialogue.speed - 0.05) / 5) * 480);
|
|
if(rate < 12) rate = 12;
|
|
else if(rate > 48) rate = 48;
|
|
char.animation.curAnim.frameRate = rate;
|
|
}
|
|
}
|
|
currentText++;
|
|
|
|
if(nextDialogueThing != null) {
|
|
nextDialogueThing();
|
|
}
|
|
}
|
|
|
|
public static function parseDialogue(path:String):DialogueFile {
|
|
#if MODS_ALLOWED
|
|
var rawJson = File.getContent(path);
|
|
#else
|
|
var rawJson = Assets.getText(path);
|
|
#end
|
|
return cast Json.parse(rawJson);
|
|
}
|
|
|
|
public static function updateBoxOffsets(box:FlxSprite) { //Had to make it static because of the editors
|
|
box.centerOffsets();
|
|
box.updateHitbox();
|
|
if(box.animation.curAnim.name.startsWith('angry')) {
|
|
box.offset.set(50, 65);
|
|
} else if(box.animation.curAnim.name.startsWith('center-angry')) {
|
|
box.offset.set(50, 30);
|
|
} else {
|
|
box.offset.set(10, 0);
|
|
}
|
|
|
|
if(!box.flipX) box.offset.y += 10;
|
|
}
|
|
}
|