diff --git a/assets/data/dialogue/boxes/default.xml b/assets/data/dialogue/boxes/default.xml
new file mode 100644
index 00000000..e6fd2f4d
--- /dev/null
+++ b/assets/data/dialogue/boxes/default.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/data/dialogue/characters/boyfriend.xml b/assets/data/dialogue/characters/boyfriend.xml
new file mode 100644
index 00000000..c5774f46
--- /dev/null
+++ b/assets/data/dialogue/characters/boyfriend.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/assets/data/dialogue/characters/dad.xml b/assets/data/dialogue/characters/dad.xml
new file mode 100644
index 00000000..c5774f46
--- /dev/null
+++ b/assets/data/dialogue/characters/dad.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/assets/data/dialogue/characters/niko.xml b/assets/data/dialogue/characters/niko.xml
new file mode 100644
index 00000000..c5774f46
--- /dev/null
+++ b/assets/data/dialogue/characters/niko.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/assets/images/dialogue/speech_bubble_talking.png b/assets/images/dialogue/boxes/default.png
similarity index 100%
rename from assets/images/dialogue/speech_bubble_talking.png
rename to assets/images/dialogue/boxes/default.png
diff --git a/assets/images/dialogue/speech_bubble_talking.xml b/assets/images/dialogue/boxes/default.xml
similarity index 100%
rename from assets/images/dialogue/speech_bubble_talking.xml
rename to assets/images/dialogue/boxes/default.xml
diff --git a/assets/images/dialogue/characters/boyfriend.png b/assets/images/dialogue/characters/boyfriend.png
new file mode 100644
index 00000000..61877299
Binary files /dev/null and b/assets/images/dialogue/characters/boyfriend.png differ
diff --git a/assets/images/dialogue/characters/boyfriend.xml b/assets/images/dialogue/characters/boyfriend.xml
new file mode 100644
index 00000000..1192c2ba
--- /dev/null
+++ b/assets/images/dialogue/characters/boyfriend.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/assets/images/dialogue/characters/dad.png b/assets/images/dialogue/characters/dad.png
new file mode 100644
index 00000000..1580867d
Binary files /dev/null and b/assets/images/dialogue/characters/dad.png differ
diff --git a/assets/images/dialogue/characters/dad.xml b/assets/images/dialogue/characters/dad.xml
new file mode 100644
index 00000000..7a6171ed
--- /dev/null
+++ b/assets/images/dialogue/characters/dad.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/assets/images/dialogue/characters/niko.png b/assets/images/dialogue/characters/niko.png
new file mode 100644
index 00000000..3131de36
Binary files /dev/null and b/assets/images/dialogue/characters/niko.png differ
diff --git a/assets/images/dialogue/characters/niko.xml b/assets/images/dialogue/characters/niko.xml
new file mode 100644
index 00000000..e7892898
--- /dev/null
+++ b/assets/images/dialogue/characters/niko.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/assets/songs/test/dialogue.xml b/assets/songs/test/dialogue.xml
new file mode 100644
index 00000000..b543e9b3
--- /dev/null
+++ b/assets/songs/test/dialogue.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+ yo its me your enemy! how are you
+
+
+ shut up u stupid bitch
+ ur useless af!!! loser!
+
+
+ agreed
+
+
+ shut up! u should respect me! im your dad!
+
+
+ haha no ur not my dad loser im a dream stan i dont have one
+
+
+ HOW COULD YOU
+
+
+ ok shut up and lets rap battle now u blue midget thing or whatever idot!
+
+
+ TF U MEAN U 13' SKYSCRAPER MF BRO CANT EVEN FIT IN HIS CAR TF
+
+
+ loser!
+
+
+ no u
+
+
+ HOW COULD YOU??? THIS IS WAR NOW!
+
+
+ yo its me niko bellic from the video game "Grand Theft Auto IV"
+
+
+ NO FUCKING WAY
+
+
+ NIKO BELLIC? yo ur cousin fat
+
+
+ fuck you dimitri rascalov
+
+
+ HOW DOES HE KNOW MY REAL IDENTITY
+
+
+ ok now stfu and lets rap its getting annoying
+
+
\ No newline at end of file
diff --git a/assets/songs/test/scripts/modchart.hx b/assets/songs/test/scripts/modchart.hx
index 14cf6431..c517ed5a 100644
--- a/assets/songs/test/scripts/modchart.hx
+++ b/assets/songs/test/scripts/modchart.hx
@@ -3,6 +3,7 @@ function create() {
pixelNotesForBF = false;
enablePixelUI = true;
enableCameraHacks = false;
+ playCutscenes = true;
}
function postCreate() {
diff --git a/assets/sounds/pixel/clickText.mp3 b/assets/sounds/dialogue/next-pixel.mp3
similarity index 100%
rename from assets/sounds/pixel/clickText.mp3
rename to assets/sounds/dialogue/next-pixel.mp3
diff --git a/assets/sounds/pixel/clickText.ogg b/assets/sounds/dialogue/next-pixel.ogg
similarity index 100%
rename from assets/sounds/pixel/clickText.ogg
rename to assets/sounds/dialogue/next-pixel.ogg
diff --git a/assets/sounds/dialogue/next.mp3 b/assets/sounds/dialogue/next.mp3
new file mode 100644
index 00000000..9c512593
Binary files /dev/null and b/assets/sounds/dialogue/next.mp3 differ
diff --git a/assets/sounds/dialogue/next.ogg b/assets/sounds/dialogue/next.ogg
new file mode 100644
index 00000000..db5c7d2f
Binary files /dev/null and b/assets/sounds/dialogue/next.ogg differ
diff --git a/assets/sounds/dialogue/text-pixel.mp3 b/assets/sounds/dialogue/text-pixel.mp3
new file mode 100644
index 00000000..981d310f
Binary files /dev/null and b/assets/sounds/dialogue/text-pixel.mp3 differ
diff --git a/assets/sounds/dialogue/text-pixel.ogg b/assets/sounds/dialogue/text-pixel.ogg
new file mode 100644
index 00000000..9746e904
Binary files /dev/null and b/assets/sounds/dialogue/text-pixel.ogg differ
diff --git a/buildnumber.txt b/buildnumber.txt
index 2c9e4a82..6adc804a 100644
--- a/buildnumber.txt
+++ b/buildnumber.txt
@@ -1 +1 @@
-2101
\ No newline at end of file
+2112
\ No newline at end of file
diff --git a/source/funkin/cutscenes/DialogueCutscene.hx b/source/funkin/cutscenes/DialogueCutscene.hx
index 4829f196..64d0fdb1 100644
--- a/source/funkin/cutscenes/DialogueCutscene.hx
+++ b/source/funkin/cutscenes/DialogueCutscene.hx
@@ -1,10 +1,101 @@
package funkin.cutscenes;
+import flixel.system.FlxSound;
+import flixel.FlxG;
+import flixel.FlxCamera;
+import openfl.utils.Assets;
+import funkin.cutscenes.dialogue.*;
+import haxe.xml.Access;
+
class DialogueCutscene extends Cutscene {
public var dialoguePath:String;
+ public var dialogueData:Access;
+
+ public var charMap:Map = [];
+
+ public var dialogueLines:Array = [];
+ public var curLine:DialogueLine = null;
+ public var dialogueBox:DialogueBox;
+
+ public var nextSFX:FlxSound;
public function new(dialoguePath:String, callback:Void->Void) {
super(callback);
this.dialoguePath = dialoguePath;
+ camera = new FlxCamera();
+ camera.bgColor = 0;
+ FlxG.cameras.add(camera, false);
}
+
+ public override function create() {
+ super.create();
+
+ try {
+ dialogueData = new Access(Xml.parse(Assets.getText(dialoguePath)).firstElement());
+
+ // Add characters
+ for(char in dialogueData.nodes.char) {
+ if (!char.has.name) continue;
+ if (charMap.exists(char.att.name))
+ Logs.trace('2 dialogue characters share the same name (${char.att.name}, ${char.att.name}). The old character has been replaced.');
+ add(charMap[char.att.name] = new DialogueCharacter(char.att.name, char.getAtt('position').getDefault('default')));
+ }
+
+ // Add lines
+ for(node in dialogueData.nodes.line) {
+ dialogueLines.push({
+ text: XMLUtil.fixXMLText(node.innerHTML),
+ char: node.getAtt('char').getDefault('boyfriend'),
+ bubble: node.getAtt('bubble').getDefault('normal'),
+ callback: node.getAtt('callback'),
+ speed: node.has.speed ? Std.parseFloat(node.att.speed).getDefault(0.02) : 0.02
+ });
+ }
+
+ // Add dialogue box
+ dialogueBox = new DialogueBox(dialogueData.getAtt("box").getDefault("default"));
+ add(dialogueBox);
+
+ next();
+ } catch(e) {
+ Logs.trace('Error while loading dialogue at ${dialoguePath}', ERROR);
+ close();
+ }
+ }
+
+ public override function update(elapsed:Float) {
+ super.update(elapsed);
+ if (controls.ACCEPT)
+ next();
+ }
+
+ public function next() {
+ if ((curLine = dialogueLines.shift()) == null) {
+ close();
+ return;
+ }
+
+ for(c in charMap) {
+ if (c.visible) {
+ // TODO: remove character smoothly
+ }
+ }
+
+ if (charMap[curLine.char] != null)
+ dialogueBox.popupChar(charMap[curLine.char]);
+ dialogueBox.playBubbleAnim(curLine.bubble);
+ }
+
+ public override function destroy() {
+ super.destroy();
+ FlxG.cameras.remove(camera);
+ }
+}
+
+typedef DialogueLine = {
+ var text:String;
+ var char:String;
+ var bubble:String;
+ var callback:String;
+ var speed:Float;
}
\ No newline at end of file
diff --git a/source/funkin/cutscenes/dialogue/DialogueBox.hx b/source/funkin/cutscenes/dialogue/DialogueBox.hx
new file mode 100644
index 00000000..5f608ed2
--- /dev/null
+++ b/source/funkin/cutscenes/dialogue/DialogueBox.hx
@@ -0,0 +1,76 @@
+package funkin.cutscenes.dialogue;
+
+import openfl.utils.Assets;
+import flixel.FlxG;
+import flixel.math.FlxPoint;
+import haxe.xml.Access;
+
+class DialogueBox extends FunkinSprite {
+ public var dialogueBoxData:Access;
+ public var positions:Map = [];
+
+ public function new(name:String) {
+ super();
+ try {
+ dialogueBoxData = new Access(Xml.parse(Assets.getText(Paths.xml('dialogue/boxes/$name'))).firstElement());
+
+ loadSprite(Paths.image('dialogue/boxes/${dialogueBoxData.getAtt('sprite').getDefault(name)}'));
+
+ for(anim in dialogueBoxData.nodes.anim) {
+ if (!anim.has.name) continue;
+ XMLUtil.addXMLAnimation(this, anim);
+ }
+
+ for(pos in dialogueBoxData.nodes.charpos) {
+ if (!pos.has.name) continue;
+ positions[pos.att.name] = {
+ x: pos.has.x ? Std.parseFloat(pos.att.x).getDefault(0) : 0,
+ y: pos.has.y ? Std.parseFloat(pos.att.y).getDefault(0) : 0,
+ flipBubble: pos.getAtt('flipBubble') == "true"
+ };
+ }
+
+ antialiasing = dialogueBoxData.getAtt("antialiasing").getDefault("true") == "true";
+ screenCenter(X);
+ y = FlxG.height - height;
+ if (dialogueBoxData.has.x) x += Std.parseFloat(dialogueBoxData.att.x).getDefault(0);
+ if (dialogueBoxData.has.y) y += Std.parseFloat(dialogueBoxData.att.y).getDefault(0);
+ visible = false;
+ } catch(e) {
+ active = false;
+ Logs.trace('Couldn\'t load dialogue box "$name": ${e.toString()}', ERROR);
+ }
+ }
+
+ public function popupChar(char:DialogueCharacter) {
+ if (!active) return;
+ var pos = positions[char.positionName];
+ if (pos == null) return;
+
+ if (!char.visible) {
+ char.alpha = 0;
+ char.visible = true;
+ char.setPosition(pos.x, pos.y + 100);
+ char.show(pos.y);
+ }
+ }
+
+ public function playBubbleAnim(bubble:String) {
+ if (hasAnimation('$bubble-open'))
+ playAnim('$bubble-open', true);
+ else
+ playAnim(bubble);
+ visible = true;
+ }
+
+ public override function destroy() {
+ super.destroy();
+ positions = null;
+ }
+}
+
+typedef CharPosDef = {
+ var x:Float;
+ var y:Float;
+ var flipBubble:Bool;
+}
\ No newline at end of file
diff --git a/source/funkin/cutscenes/dialogue/DialogueCharacter.hx b/source/funkin/cutscenes/dialogue/DialogueCharacter.hx
new file mode 100644
index 00000000..e9c32c23
--- /dev/null
+++ b/source/funkin/cutscenes/dialogue/DialogueCharacter.hx
@@ -0,0 +1,38 @@
+package funkin.cutscenes.dialogue;
+
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import openfl.utils.Assets;
+import haxe.xml.Access;
+
+class DialogueCharacter extends FunkinSprite {
+ public var charData:Access;
+ public var curTween:FlxTween;
+ public var positionName:String;
+
+ public function new(name:String, position:String) {
+ super();
+ this.positionName = position;
+ try {
+ charData = new Access(Xml.parse(Assets.getText(Paths.xml('dialogue/characters/$name'))).firstElement());
+ loadSprite(Paths.image('dialogue/characters/${charData.getAtt('sprite').getDefault(name)}'));
+ antialiasing = charData.getAtt('antialiasing').getDefault('true') == 'true';
+ offset.set(
+ charData.has.x ? Std.parseFloat(charData.att.x).getDefault(0) : 0,
+ charData.has.y ? Std.parseFloat(charData.att.y).getDefault(0) : 0);
+ scale.scale(charData.has.scale ? Std.parseFloat(charData.att.scale).getDefault(1) : 1);
+ for(anim in charData.nodes.anim)
+ XMLUtil.addXMLAnimation(this, anim, true);
+ } catch(e) {
+ Logs.trace('Failed to load dialogue character $name: ${e.toString()}', ERROR);
+ }
+ visible = false;
+ }
+
+ public function show(y:Float) {
+ if (curTween != null)
+ curTween.cancel();
+
+ curTween = FlxTween.tween(this, {alpha: 1, y: y}, 0.5, {ease: FlxEase.cubeOut});
+ }
+}
\ No newline at end of file
diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx
index 7682c3f5..6c9106f4 100644
--- a/source/funkin/game/PlayState.hx
+++ b/source/funkin/game/PlayState.hx
@@ -696,18 +696,24 @@ class PlayState extends MusicBeatState
if (playCutscenes) {
var videoCutscene = Paths.video('${PlayState.SONG.song.toLowerCase()}-cutscene');
var videoCutsceneAlt = Paths.file('songs/${PlayState.SONG.song.toLowerCase()}/cutscene.mp4');
+ var dialogue = Paths.file('songs/${PlayState.SONG.song.toLowerCase()}/dialogue.xml');
persistentUpdate = false;
if (cutscene != null) {
openSubState(new ScriptedCutscene(cutscene, function() {
startCountdown();
}));
+ } else if (Assets.exists(dialogue)) {
+ FlxTransitionableState.skipNextTransIn = true;
+ openSubState(new DialogueCutscene(dialogue, function() {
+ startCountdown();
+ }));
} else if (Assets.exists(videoCutsceneAlt)) {
FlxTransitionableState.skipNextTransIn = true;
openSubState(new VideoCutscene(videoCutsceneAlt, function() {
startCountdown();
}));
persistentDraw = false;
- }else if (Assets.exists(videoCutscene)) {
+ } else if (Assets.exists(videoCutscene)) {
FlxTransitionableState.skipNextTransIn = true;
openSubState(new VideoCutscene(videoCutscene, function() {
startCountdown();
diff --git a/source/funkin/utils/XMLUtil.hx b/source/funkin/utils/XMLUtil.hx
index 76092647..9937a458 100644
--- a/source/funkin/utils/XMLUtil.hx
+++ b/source/funkin/utils/XMLUtil.hx
@@ -170,6 +170,11 @@ class XMLUtil {
}
return MISSING_PROPERTY;
}
+
+ public static inline function fixXMLText(text:String) {
+ var v:String;
+ return [for(l in text.split("\n")) if ((v = l.trim()) != "") v].join("\n");
+ }
}
typedef AnimData = {