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; var position:Array; var scale:Float; } typedef DialogueAnimArray = { var anim:String; var loop_name:String; var loop_offsets:Array; var idle_name:String; var idle_offsets:Array; } // Gonna try to kind of make it compatible to Forever Engine, // love u Shubs no homo :flushedh4: typedef DialogueFile = { var dialogue:Array; } typedef DialogueLine = { var portrait:Null; var expression:Null; var text:Null; var boxState:Null; var speed:Null; } 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 = new Map(); #else public var dialogueAnimations:Map = new Map(); #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 = []; 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 = []; var currentText:Int = 0; var offsetPos:Float = -600; var textBoxTypes:Array = ['normal', 'angry']; //var charPositionList:Array = ['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 = new Map(); #else var charsMap:Map = new Map(); #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 = ['', '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 = ['', '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; } }