mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-27 01:55:52 -05:00
Merge branch 'master' into feature/chart-editor-importer
This commit is contained in:
commit
85af96d654
35 changed files with 2742 additions and 184 deletions
|
@ -106,7 +106,10 @@
|
||||||
<haxelib name="polymod" /> <!-- Modding framework -->
|
<haxelib name="polymod" /> <!-- Modding framework -->
|
||||||
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
|
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
|
||||||
<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 -->
|
||||||
|
|
||||||
<haxelib name="thx.semver" />
|
<haxelib name="thx.semver" />
|
||||||
<haxelib name="hxcpp-debug-server" if="desktop debug" />
|
<haxelib name="hxcpp-debug-server" if="desktop debug" />
|
||||||
<!--Disable the Flixel core focus lost screen-->
|
<!--Disable the Flixel core focus lost screen-->
|
||||||
|
|
7
hmm.json
7
hmm.json
|
@ -116,6 +116,11 @@
|
||||||
"name": "thx.semver",
|
"name": "thx.semver",
|
||||||
"type": "haxelib",
|
"type": "haxelib",
|
||||||
"version": "0.2.2"
|
"version": "0.2.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tink_json",
|
||||||
|
"type": "haxelib",
|
||||||
|
"version": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,23 +1,18 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
import funkin.play.song.SongData.SongTimeChange;
|
import funkin.util.Constants;
|
||||||
import flixel.util.FlxSignal;
|
import flixel.util.FlxSignal;
|
||||||
|
import flixel.math.FlxMath;
|
||||||
|
import funkin.SongLoad.SwagSong;
|
||||||
import funkin.play.song.Song.SongDifficulty;
|
import funkin.play.song.Song.SongDifficulty;
|
||||||
|
import funkin.play.song.SongData.SongTimeChange;
|
||||||
typedef BPMChangeEvent =
|
|
||||||
{
|
|
||||||
var stepTime:Int;
|
|
||||||
var songTime:Float;
|
|
||||||
var bpm:Float;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A global source of truth for timing information.
|
* A core class which handles musical timing throughout the game,
|
||||||
|
* both in gameplay and in menus.
|
||||||
*/
|
*/
|
||||||
class Conductor
|
class Conductor
|
||||||
{
|
{
|
||||||
static final STEPS_PER_BEAT:Int = 4;
|
|
||||||
|
|
||||||
// onBeatHit is called every quarter note
|
// onBeatHit is called every quarter note
|
||||||
// onStepHit is called every sixteenth note
|
// onStepHit is called every sixteenth note
|
||||||
// 4/4 = 4 beats per measure = 16 steps per measure
|
// 4/4 = 4 beats per measure = 16 steps per measure
|
||||||
|
@ -33,11 +28,22 @@ class Conductor
|
||||||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of time changes in the song.
|
||||||
|
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
||||||
|
*/
|
||||||
|
static var timeChanges:Array<SongTimeChange> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current time change.
|
||||||
|
*/
|
||||||
|
static var currentTimeChange:SongTimeChange;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current position in the song in milliseconds.
|
* The current position in the song in milliseconds.
|
||||||
* Updated every frame based on the audio position.
|
* Updated every frame based on the audio position.
|
||||||
*/
|
*/
|
||||||
public static var songPosition:Float;
|
public static var songPosition:Float = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beats per minute of the current song at the current time.
|
* Beats per minute of the current song at the current time.
|
||||||
|
@ -48,33 +54,17 @@ class Conductor
|
||||||
{
|
{
|
||||||
if (bpmOverride != null) return bpmOverride;
|
if (bpmOverride != null) return bpmOverride;
|
||||||
|
|
||||||
if (currentTimeChange == null) return 100;
|
if (currentTimeChange == null) return Constants.DEFAULT_BPM;
|
||||||
|
|
||||||
return currentTimeChange.bpm;
|
return currentTimeChange.bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value set by `forceBPM`.
|
||||||
|
* If false, BPM is determined by time changes.
|
||||||
|
*/
|
||||||
static var bpmOverride:Null<Float> = null;
|
static var bpmOverride:Null<Float> = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole measures.
|
|
||||||
*/
|
|
||||||
public static var currentMeasure(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole beats.
|
|
||||||
**/
|
|
||||||
public static var currentBeat(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in whole steps.
|
|
||||||
*/
|
|
||||||
public static var currentStep(default, null):Int;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current position in the song, in steps and fractions of a step.
|
|
||||||
*/
|
|
||||||
public static var currentStepTime(default, null):Float;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a measure in milliseconds. Calculated based on bpm.
|
* Duration of a measure in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
|
@ -86,119 +76,99 @@ class Conductor
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
* Duration of a beat in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
public static var beatLengthMs(get, null):Float;
|
public static var beatLengthMs(get, null):Float;
|
||||||
|
|
||||||
static function get_beatLengthMs():Float
|
static function get_beatLengthMs():Float
|
||||||
{
|
{
|
||||||
// Tied directly to BPM.
|
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||||
return ((60 / bpm) * 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Duration of a step (sixteenth) in milliseconds. Calculated based on bpm.
|
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
|
||||||
*/
|
*/
|
||||||
public static var stepLengthMs(get, null):Float;
|
public static var stepLengthMs(get, null):Float;
|
||||||
|
|
||||||
static function get_stepLengthMs():Float
|
static function get_stepLengthMs():Float
|
||||||
{
|
{
|
||||||
return beatLengthMs / STEPS_PER_BEAT;
|
return beatLengthMs / timeSignatureNumerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The numerator of the current time signature (number of notes in a measure)
|
|
||||||
*/
|
|
||||||
public static var timeSignatureNumerator(get, null):Int;
|
public static var timeSignatureNumerator(get, null):Int;
|
||||||
|
|
||||||
static function get_timeSignatureNumerator():Int
|
static function get_timeSignatureNumerator():Int
|
||||||
{
|
{
|
||||||
if (currentTimeChange == null) return 4;
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
||||||
|
|
||||||
return currentTimeChange.timeSignatureNum;
|
return currentTimeChange.timeSignatureNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The numerator of the current time signature (length of notes in a measure)
|
|
||||||
*/
|
|
||||||
public static var timeSignatureDenominator(get, null):Int;
|
public static var timeSignatureDenominator(get, null):Int;
|
||||||
|
|
||||||
static function get_timeSignatureDenominator():Int
|
static function get_timeSignatureDenominator():Int
|
||||||
{
|
{
|
||||||
if (currentTimeChange == null) return 4;
|
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
||||||
|
|
||||||
return currentTimeChange.timeSignatureDen;
|
return currentTimeChange.timeSignatureDen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var offset:Float = 0;
|
/**
|
||||||
|
* Current position in the song, in measures.
|
||||||
// TODO: What's the difference between visualOffset and audioOffset?
|
*/
|
||||||
public static var visualOffset:Float = 0;
|
public static var currentMeasure(default, null):Int;
|
||||||
public static var audioOffset:Float = 0;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Signals
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal that is dispatched every measure.
|
* Current position in the song, in beats.
|
||||||
* At 120 BPM 4/4, this is dispatched every 2 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 1.5 seconds.
|
|
||||||
*/
|
*/
|
||||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
public static var currentBeat(default, null):Int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal that is dispatched every beat.
|
* Current position in the song, in steps.
|
||||||
* At 120 BPM 4/4, this is dispatched every 0.5 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 0.5 seconds.
|
|
||||||
*/
|
*/
|
||||||
|
public static var currentStep(default, null):Int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in measures and fractions of a measure.
|
||||||
|
*/
|
||||||
|
public static var currentMeasureTime(default, null):Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in beats and fractions of a measure.
|
||||||
|
*/
|
||||||
|
public static var currentBeatTime(default, null):Float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position in the song, in steps and fractions of a step.
|
||||||
|
*/
|
||||||
|
public static var currentStepTime(default, null):Float;
|
||||||
|
|
||||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||||
|
|
||||||
/**
|
|
||||||
* Signal that is dispatched when a step is hit.
|
|
||||||
* At 120 BPM 4/4, this is dispatched every 0.125 seconds.
|
|
||||||
* At 120 BPM 3/4, this is dispatched every 0.125 seconds.
|
|
||||||
*/
|
|
||||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||||
|
|
||||||
//
|
|
||||||
// Internal Variables
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of time changes in the song.
|
|
||||||
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
|
||||||
*/
|
|
||||||
static var timeChanges:Array<SongTimeChange> = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current time change.
|
|
||||||
*/
|
|
||||||
static var currentTimeChange:SongTimeChange;
|
|
||||||
|
|
||||||
public static var lastSongPos:Float;
|
public static var lastSongPos:Float;
|
||||||
|
public static var visualOffset:Float = 0;
|
||||||
|
public static var audioOffset:Float = 0;
|
||||||
|
public static var offset:Float = 0;
|
||||||
|
|
||||||
/**
|
public static var beatsPerMeasure(get, null):Float;
|
||||||
* The number of beats (whole notes) in a measure.
|
|
||||||
*/
|
|
||||||
public static var beatsPerMeasure(get, null):Int;
|
|
||||||
|
|
||||||
static function get_beatsPerMeasure():Int
|
static function get_beatsPerMeasure():Float
|
||||||
{
|
{
|
||||||
return timeSignatureNumerator;
|
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||||
|
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of steps (quarter-notes) in a measure.
|
|
||||||
*/
|
|
||||||
public static var stepsPerMeasure(get, null):Int;
|
public static var stepsPerMeasure(get, null):Int;
|
||||||
|
|
||||||
static function get_stepsPerMeasure():Int
|
static function get_stepsPerMeasure():Int
|
||||||
{
|
{
|
||||||
// This is always 4, b
|
// TODO: Is this always an integer?
|
||||||
return timeSignatureNumerator * 4;
|
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function new() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcibly defines the current BPM of the song.
|
* Forcibly defines the current BPM of the song.
|
||||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
||||||
|
@ -208,16 +178,11 @@ class Conductor
|
||||||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||||
* you should have a metadata file for it instead.
|
* you should have a metadata file for it instead.
|
||||||
*/
|
*/
|
||||||
public static function forceBPM(?bpm:Float = null):Void
|
public static function forceBPM(?bpm:Float = null)
|
||||||
{
|
{
|
||||||
if (bpm != null)
|
if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
trace('[CONDUCTOR] Resetting BPM to default');
|
trace('[CONDUCTOR] Resetting BPM to default');
|
||||||
}
|
|
||||||
Conductor.bpmOverride = bpm;
|
Conductor.bpmOverride = bpm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,13 +193,12 @@ class Conductor
|
||||||
* @param songPosition The current position in the song in milliseconds.
|
* @param songPosition The current position in the song in milliseconds.
|
||||||
* Leave blank to use the FlxG.sound.music position.
|
* Leave blank to use the FlxG.sound.music position.
|
||||||
*/
|
*/
|
||||||
public static function update(songPosition:Float = null):Void
|
public static function update(songPosition:Float = null)
|
||||||
{
|
{
|
||||||
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
|
if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
|
||||||
|
|
||||||
var oldMeasure:Int = currentMeasure;
|
var oldBeat = currentBeat;
|
||||||
var oldBeat:Int = currentBeat;
|
var oldStep = currentStep;
|
||||||
var oldStep:Int = currentStep;
|
|
||||||
|
|
||||||
Conductor.songPosition = songPosition;
|
Conductor.songPosition = songPosition;
|
||||||
|
|
||||||
|
@ -252,16 +216,23 @@ class Conductor
|
||||||
}
|
}
|
||||||
else if (currentTimeChange != null)
|
else if (currentTimeChange != null)
|
||||||
{
|
{
|
||||||
currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs;
|
// roundDecimal prevents representing 8 as 7.9999999
|
||||||
|
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||||
|
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||||
|
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||||
currentStep = Math.floor(currentStepTime);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentBeatTime);
|
||||||
|
currentMeasure = Math.floor(currentMeasureTime);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Assume a constant BPM equal to the forced value.
|
// Assume a constant BPM equal to the forced value.
|
||||||
currentStepTime = (songPosition / stepLengthMs);
|
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||||
|
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||||
|
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||||
currentStep = Math.floor(currentStepTime);
|
currentStep = Math.floor(currentStepTime);
|
||||||
currentBeat = Math.floor(currentStep / 4);
|
currentBeat = Math.floor(currentBeatTime);
|
||||||
|
currentMeasure = Math.floor(currentMeasureTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlxSignals are really cool.
|
// FlxSignals are really cool.
|
||||||
|
@ -274,31 +245,52 @@ class Conductor
|
||||||
{
|
{
|
||||||
beatHit.dispatch();
|
beatHit.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMeasure != oldMeasure)
|
|
||||||
{
|
|
||||||
measureHit.dispatch();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>):Void
|
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||||
{
|
{
|
||||||
timeChanges = [];
|
timeChanges = [];
|
||||||
|
|
||||||
for (currentTimeChange in songTimeChanges)
|
for (currentTimeChange in songTimeChanges)
|
||||||
{
|
{
|
||||||
|
// TODO: Maybe handle this different?
|
||||||
|
// Do we care about BPM at negative timestamps?
|
||||||
|
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
|
||||||
|
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
|
||||||
|
|
||||||
|
if (currentTimeChange.beatTime == null)
|
||||||
|
{
|
||||||
|
if (currentTimeChange.timeStamp <= 0.0)
|
||||||
|
{
|
||||||
|
currentTimeChange.beatTime = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Calculate the beat time of this timestamp.
|
||||||
|
currentTimeChange.beatTime = 0.0;
|
||||||
|
|
||||||
|
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||||
|
{
|
||||||
|
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||||
|
currentTimeChange.beatTime = prevTimeChange.beatTime
|
||||||
|
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeChanges.push(currentTimeChange);
|
timeChanges.push(currentTimeChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace('Done mapping time changes: ' + timeChanges);
|
trace('Done mapping time changes: ' + timeChanges);
|
||||||
|
|
||||||
// Done.
|
// Update currentStepTime
|
||||||
|
Conductor.update(Conductor.songPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a time in milliseconds, return a time in steps.
|
* Given a time in milliseconds, return a time in steps.
|
||||||
*/
|
*/
|
||||||
public static function getTimeInSteps(ms:Float):Int
|
public static function getTimeInSteps(ms:Float):Float
|
||||||
{
|
{
|
||||||
if (timeChanges.length == 0)
|
if (timeChanges.length == 0)
|
||||||
{
|
{
|
||||||
|
@ -307,7 +299,7 @@ class Conductor
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var resultStep:Int = 0;
|
var resultStep:Float = 0;
|
||||||
|
|
||||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||||
for (timeChange in timeChanges)
|
for (timeChange in timeChanges)
|
||||||
|
@ -329,4 +321,14 @@ class Conductor
|
||||||
return resultStep;
|
return resultStep;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function reset():Void
|
||||||
|
{
|
||||||
|
beatHit.removeAll();
|
||||||
|
stepHit.removeAll();
|
||||||
|
|
||||||
|
mapTimeChanges([]);
|
||||||
|
forceBPM(null);
|
||||||
|
update(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
import funkin.play.stage.StageData.StageDataParser;
|
|
||||||
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
|
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
||||||
import flixel.addons.transition.TransitionData;
|
import flixel.addons.transition.TransitionData;
|
||||||
import flixel.graphics.FlxGraphic;
|
import flixel.graphics.FlxGraphic;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
|
@ -10,13 +9,17 @@ import flixel.math.FlxRect;
|
||||||
import flixel.system.debug.log.LogStyle;
|
import flixel.system.debug.log.LogStyle;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
import funkin.play.PlayState;
|
|
||||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||||
|
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||||
|
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||||
|
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||||
import funkin.play.event.SongEventData.SongEventParser;
|
import funkin.play.event.SongEventData.SongEventParser;
|
||||||
|
import funkin.play.PlayState;
|
||||||
import funkin.play.song.SongData.SongDataParser;
|
import funkin.play.song.SongData.SongDataParser;
|
||||||
|
import funkin.play.stage.StageData.StageDataParser;
|
||||||
import funkin.ui.PreferencesMenu;
|
import funkin.ui.PreferencesMenu;
|
||||||
import funkin.util.WindowUtil;
|
|
||||||
import funkin.util.macro.MacroUtil;
|
import funkin.util.macro.MacroUtil;
|
||||||
|
import funkin.util.WindowUtil;
|
||||||
import openfl.display.BitmapData;
|
import openfl.display.BitmapData;
|
||||||
#if discord_rpc
|
#if discord_rpc
|
||||||
import Discord.DiscordClient;
|
import Discord.DiscordClient;
|
||||||
|
@ -157,6 +160,9 @@ class InitState extends FlxTransitionableState
|
||||||
|
|
||||||
funkin.data.level.LevelRegistry.instance.loadEntries();
|
funkin.data.level.LevelRegistry.instance.loadEntries();
|
||||||
SongEventParser.loadEventCache();
|
SongEventParser.loadEventCache();
|
||||||
|
ConversationDataParser.loadConversationCache();
|
||||||
|
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||||
|
SpeakerDataParser.loadSpeakerCache();
|
||||||
SongDataParser.loadSongCache();
|
SongDataParser.loadSongCache();
|
||||||
StageDataParser.loadStageCache();
|
StageDataParser.loadStageCache();
|
||||||
CharacterDataParser.loadCharacterCache();
|
CharacterDataParser.loadCharacterCache();
|
||||||
|
@ -216,7 +222,7 @@ class InitState extends FlxTransitionableState
|
||||||
#elseif ANIMATE
|
#elseif ANIMATE
|
||||||
FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest());
|
FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest());
|
||||||
#elseif CHARTING
|
#elseif CHARTING
|
||||||
FlxG.switchState(new ChartingState());
|
FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
|
||||||
#elseif STAGEBUILD
|
#elseif STAGEBUILD
|
||||||
FlxG.switchState(new StageBuilderState());
|
FlxG.switchState(new StageBuilderState());
|
||||||
#elseif FIGHT
|
#elseif FIGHT
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
|
import funkin.modding.IScriptedClass.IEventHandler;
|
||||||
import flixel.FlxState;
|
import flixel.FlxState;
|
||||||
import flixel.FlxSubState;
|
import flixel.FlxSubState;
|
||||||
import flixel.addons.ui.FlxUIState;
|
import flixel.addons.ui.FlxUIState;
|
||||||
|
@ -15,7 +16,7 @@ import funkin.util.SortUtil;
|
||||||
* MusicBeatState actually represents the core utility FlxState of the game.
|
* MusicBeatState actually represents the core utility FlxState of the game.
|
||||||
* It includes functionality for event handling, as well as maintaining BPM-based update events.
|
* It includes functionality for event handling, as well as maintaining BPM-based update events.
|
||||||
*/
|
*/
|
||||||
class MusicBeatState extends FlxUIState
|
class MusicBeatState extends FlxUIState implements IEventHandler
|
||||||
{
|
{
|
||||||
var controls(get, never):Controls;
|
var controls(get, never):Controls;
|
||||||
|
|
||||||
|
@ -66,9 +67,11 @@ class MusicBeatState extends FlxUIState
|
||||||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||||
|
|
||||||
// Display Conductor info in the watch window.
|
// Display Conductor info in the watch window.
|
||||||
FlxG.watch.addQuick("songPos", Conductor.songPosition);
|
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
|
||||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||||
|
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||||
|
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||||
|
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||||
|
|
||||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||||
}
|
}
|
||||||
|
@ -92,7 +95,7 @@ class MusicBeatState extends FlxUIState
|
||||||
add(rightWatermarkText);
|
add(rightWatermarkText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchEvent(event:ScriptEvent)
|
public function dispatchEvent(event:ScriptEvent)
|
||||||
{
|
{
|
||||||
ModuleHandler.callEvent(event);
|
ModuleHandler.callEvent(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package funkin;
|
package funkin;
|
||||||
|
|
||||||
import flixel.FlxSubState;
|
import flixel.FlxSubState;
|
||||||
|
import funkin.modding.IScriptedClass.IEventHandler;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import funkin.Conductor.BPMChangeEvent;
|
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.module.ModuleHandler;
|
import funkin.modding.module.ModuleHandler;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
|
@ -11,7 +11,7 @@ import funkin.modding.PolymodHandler;
|
||||||
/**
|
/**
|
||||||
* MusicBeatSubState reincorporates the functionality of MusicBeatState into an FlxSubState.
|
* MusicBeatSubState reincorporates the functionality of MusicBeatState into an FlxSubState.
|
||||||
*/
|
*/
|
||||||
class MusicBeatSubState extends FlxSubState
|
class MusicBeatSubState extends FlxSubState implements IEventHandler
|
||||||
{
|
{
|
||||||
public var leftWatermarkText:FlxText = null;
|
public var leftWatermarkText:FlxText = null;
|
||||||
public var rightWatermarkText:FlxText = null;
|
public var rightWatermarkText:FlxText = null;
|
||||||
|
@ -99,7 +99,7 @@ class MusicBeatSubState extends FlxSubState
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchEvent(event:ScriptEvent)
|
public function dispatchEvent(event:ScriptEvent)
|
||||||
{
|
{
|
||||||
ModuleHandler.callEvent(event);
|
ModuleHandler.callEvent(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import openfl.utils.Assets as OpenFlAssets;
|
||||||
|
|
||||||
class Paths
|
class Paths
|
||||||
{
|
{
|
||||||
inline public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
|
public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
|
||||||
|
public static var VIDEO_EXT = "mp4";
|
||||||
|
|
||||||
static var currentLevel:String;
|
static var currentLevel:String;
|
||||||
|
|
||||||
|
@ -96,14 +97,19 @@ class Paths
|
||||||
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
|
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static public function voices(song:String, ?suffix:String)
|
inline static public function videos(key:String, ?library:String)
|
||||||
|
{
|
||||||
|
return getPath('videos/$key.$VIDEO_EXT', BINARY, library);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static public function voices(song:String, ?suffix:String = '')
|
||||||
{
|
{
|
||||||
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
|
||||||
|
|
||||||
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
|
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
|
||||||
}
|
}
|
||||||
|
|
||||||
inline static public function inst(song:String, ?suffix:String)
|
inline static public function inst(song:String, ?suffix:String = '')
|
||||||
{
|
{
|
||||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
|
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import funkin.play.PlayStatePlaylist;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||||
import flixel.system.FlxSound;
|
import flixel.sound.FlxSound;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
import flixel.tweens.FlxEase;
|
import flixel.tweens.FlxEase;
|
||||||
import flixel.tweens.FlxTween;
|
import flixel.tweens.FlxTween;
|
||||||
|
|
|
@ -12,6 +12,8 @@ import flixel.util.FlxTimer;
|
||||||
import funkin.audiovis.SpectogramSprite;
|
import funkin.audiovis.SpectogramSprite;
|
||||||
import funkin.shaderslmfao.ColorSwap;
|
import funkin.shaderslmfao.ColorSwap;
|
||||||
import funkin.shaderslmfao.LeftMaskShader;
|
import funkin.shaderslmfao.LeftMaskShader;
|
||||||
|
import funkin.play.song.SongData.SongDataParser;
|
||||||
|
import funkin.play.song.SongData.SongMetadata;
|
||||||
import funkin.shaderslmfao.TitleOutline;
|
import funkin.shaderslmfao.TitleOutline;
|
||||||
import funkin.ui.AtlasText;
|
import funkin.ui.AtlasText;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
|
@ -135,12 +137,7 @@ class TitleState extends MusicBeatState
|
||||||
|
|
||||||
function startIntro()
|
function startIntro()
|
||||||
{
|
{
|
||||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
playMenuMusic();
|
||||||
{
|
|
||||||
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
|
|
||||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
|
||||||
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
|
|
||||||
}
|
|
||||||
|
|
||||||
persistentUpdate = true;
|
persistentUpdate = true;
|
||||||
|
|
||||||
|
@ -234,6 +231,18 @@ class TitleState extends MusicBeatState
|
||||||
if (FlxG.sound.music != null) FlxG.sound.music.onComplete = function() FlxG.switchState(new VideoState());
|
if (FlxG.sound.music != null) FlxG.sound.music.onComplete = function() FlxG.switchState(new VideoState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playMenuMusic():Void
|
||||||
|
{
|
||||||
|
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||||
|
{
|
||||||
|
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||||
|
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||||
|
|
||||||
|
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||||
|
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getIntroTextShit():Array<Array<String>>
|
function getIntroTextShit():Array<Array<String>>
|
||||||
{
|
{
|
||||||
var fullText:String = Assets.getText(Paths.txt('introText'));
|
var fullText:String = Assets.getText(Paths.txt('introText'));
|
||||||
|
|
|
@ -8,7 +8,8 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
|
||||||
// These are great.
|
// These are great.
|
||||||
using Lambda;
|
using Lambda;
|
||||||
using StringTools;
|
using StringTools;
|
||||||
using funkin.util.tools.MapTools;
|
using funkin.util.tools.ArrayTools;
|
||||||
using funkin.util.tools.IteratorTools;
|
using funkin.util.tools.IteratorTools;
|
||||||
|
using funkin.util.tools.MapTools;
|
||||||
using funkin.util.tools.StringTools;
|
using funkin.util.tools.StringTools;
|
||||||
#end
|
#end
|
||||||
|
|
|
@ -16,6 +16,15 @@ interface IScriptedClass
|
||||||
public function onUpdate(event:UpdateScriptEvent):Void;
|
public function onUpdate(event:UpdateScriptEvent):Void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an element which can receive script events.
|
||||||
|
* For example, the PlayState dispatches the event to all its child elements.
|
||||||
|
*/
|
||||||
|
interface IEventHandler
|
||||||
|
{
|
||||||
|
public function dispatchEvent(event:ScriptEvent):Void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a set of callbacks available to scripted classes which can follow the game between states.
|
* Defines a set of callbacks available to scripted classes which can follow the game between states.
|
||||||
*/
|
*/
|
||||||
|
@ -150,3 +159,19 @@ interface IPlayStateScriptedClass extends IScriptedClass
|
||||||
*/
|
*/
|
||||||
public function onCountdownEnd(event:CountdownScriptEvent):Void;
|
public function onCountdownEnd(event:CountdownScriptEvent):Void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a set of callbacks activated during a dialogue conversation.
|
||||||
|
*/
|
||||||
|
interface IDialogueScriptedClass extends IScriptedClass
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Called as the dialogue starts, and before the first dialogue text is displayed.
|
||||||
|
*/
|
||||||
|
public function onDialogueStart(event:DialogueScriptEvent):Void;
|
||||||
|
|
||||||
|
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void;
|
||||||
|
public function onDialogueLine(event:DialogueScriptEvent):Void;
|
||||||
|
public function onDialogueSkip(event:DialogueScriptEvent):Void;
|
||||||
|
public function onDialogueEnd(event:DialogueScriptEvent):Void;
|
||||||
|
}
|
||||||
|
|
|
@ -5,4 +5,4 @@ package funkin.modding.base;
|
||||||
* Create a scripted class that extends FlxSpriteGroup to use this.
|
* Create a scripted class that extends FlxSpriteGroup to use this.
|
||||||
*/
|
*/
|
||||||
@:hscriptClass
|
@:hscriptClass
|
||||||
class ScriptedFlxSpriteGroup extends flixel.group.FlxSpriteGroup implements HScriptedClass {}
|
class ScriptedFlxSpriteGroup extends flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup<flixel.FlxSprite> implements HScriptedClass {}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package funkin.modding.events;
|
||||||
import flixel.FlxState;
|
import flixel.FlxState;
|
||||||
import flixel.FlxSubState;
|
import flixel.FlxSubState;
|
||||||
import funkin.noteStuff.NoteBasic.NoteDir;
|
import funkin.noteStuff.NoteBasic.NoteDir;
|
||||||
|
import funkin.play.cutscene.dialogue.Conversation;
|
||||||
import funkin.play.Countdown.CountdownStep;
|
import funkin.play.Countdown.CountdownStep;
|
||||||
import openfl.events.EventType;
|
import openfl.events.EventType;
|
||||||
import openfl.events.KeyboardEvent;
|
import openfl.events.KeyboardEvent;
|
||||||
|
@ -230,10 +231,42 @@ class ScriptEvent
|
||||||
public static inline final SUBSTATE_CLOSE_END:ScriptEventType = 'SUBSTATE_CLOSE_END';
|
public static inline final SUBSTATE_CLOSE_END:ScriptEventType = 'SUBSTATE_CLOSE_END';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the game is exiting the current FlxState.
|
* Called when the game starts a conversation.
|
||||||
*
|
*
|
||||||
* This event is not cancelable.
|
* This event is not cancelable.
|
||||||
*/
|
*/
|
||||||
|
public static inline final DIALOGUE_START:ScriptEventType = 'DIALOGUE_START';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to display the next line of conversation.
|
||||||
|
*
|
||||||
|
* This event IS cancelable! Canceling this event will prevent the conversation from moving to the next line.
|
||||||
|
* - This event is called when the conversation starts, or when the user presses ACCEPT to advance the conversation.
|
||||||
|
*/
|
||||||
|
public static inline final DIALOGUE_LINE:ScriptEventType = 'DIALOGUE_LINE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to skip scrolling the current line of conversation.
|
||||||
|
*
|
||||||
|
* This event IS cancelable! Canceling this event will prevent the conversation from skipping to the next line.
|
||||||
|
* - This event is called when the user presses ACCEPT to advance the conversation while it is already advancing.
|
||||||
|
*/
|
||||||
|
public static inline final DIALOGUE_COMPLETE_LINE:ScriptEventType = 'DIALOGUE_COMPLETE_LINE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to skip the conversation.
|
||||||
|
*
|
||||||
|
* This event IS cancelable! Canceling this event will prevent the conversation from skipping.
|
||||||
|
*/
|
||||||
|
public static inline final DIALOGUE_SKIP:ScriptEventType = 'DIALOGUE_SKIP';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the game ends a conversation.
|
||||||
|
*
|
||||||
|
* This event is not cancelable.
|
||||||
|
*/
|
||||||
|
public static inline final DIALOGUE_END:ScriptEventType = 'DIALOGUE_END';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, the behavior associated with this event can be prevented.
|
* If true, the behavior associated with this event can be prevented.
|
||||||
* For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
|
* For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
|
||||||
|
@ -489,6 +522,28 @@ class CountdownScriptEvent extends ScriptEvent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event that is fired during a dialogue.
|
||||||
|
*/
|
||||||
|
class DialogueScriptEvent extends ScriptEvent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The dialogue being referenced by the event.
|
||||||
|
*/
|
||||||
|
public var conversation(default, null):Conversation;
|
||||||
|
|
||||||
|
public function new(type:ScriptEventType, conversation:Conversation, cancelable:Bool = true):Void
|
||||||
|
{
|
||||||
|
super(type, cancelable);
|
||||||
|
this.conversation = conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function toString():String
|
||||||
|
{
|
||||||
|
return 'DialogueScriptEvent(type=$type, conversation=$conversation)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event that is fired when the player presses a key.
|
* An event that is fired when the player presses a key.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -45,9 +45,32 @@ class ScriptEventDispatcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Std.isOfType(target, IDialogueScriptedClass))
|
||||||
|
{
|
||||||
|
var t:IDialogueScriptedClass = cast(target, IDialogueScriptedClass);
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case ScriptEvent.DIALOGUE_START:
|
||||||
|
t.onDialogueStart(cast event);
|
||||||
|
return;
|
||||||
|
case ScriptEvent.DIALOGUE_LINE:
|
||||||
|
t.onDialogueLine(cast event);
|
||||||
|
return;
|
||||||
|
case ScriptEvent.DIALOGUE_COMPLETE_LINE:
|
||||||
|
t.onDialogueCompleteLine(cast event);
|
||||||
|
return;
|
||||||
|
case ScriptEvent.DIALOGUE_SKIP:
|
||||||
|
t.onDialogueSkip(cast event);
|
||||||
|
return;
|
||||||
|
case ScriptEvent.DIALOGUE_END:
|
||||||
|
t.onDialogueEnd(cast event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Std.isOfType(target, IPlayStateScriptedClass))
|
if (Std.isOfType(target, IPlayStateScriptedClass))
|
||||||
{
|
{
|
||||||
var t = cast(target, IPlayStateScriptedClass);
|
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
|
||||||
switch (event.type)
|
switch (event.type)
|
||||||
{
|
{
|
||||||
case ScriptEvent.NOTE_HIT:
|
case ScriptEvent.NOTE_HIT:
|
||||||
|
@ -133,7 +156,7 @@ class ScriptEventDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you get a crash on this line, that means ERIC FUCKED UP!
|
// If you get a crash on this line, that means ERIC FUCKED UP!
|
||||||
throw 'No function called for event type: ${event.type}';
|
// throw 'No function called for event type: ${event.type}';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function callEventOnAllTargets(targets:Iterator<IScriptedClass>, event:ScriptEvent):Void
|
public static function callEventOnAllTargets(targets:Iterator<IScriptedClass>, event:ScriptEvent):Void
|
||||||
|
|
|
@ -3,7 +3,7 @@ package funkin.play;
|
||||||
import flixel.FlxG;
|
import flixel.FlxG;
|
||||||
import flixel.FlxObject;
|
import flixel.FlxObject;
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
import flixel.system.FlxSound;
|
import flixel.sound.FlxSound;
|
||||||
import funkin.ui.story.StoryMenuState;
|
import funkin.ui.story.StoryMenuState;
|
||||||
import flixel.util.FlxColor;
|
import flixel.util.FlxColor;
|
||||||
import flixel.util.FlxTimer;
|
import flixel.util.FlxTimer;
|
||||||
|
@ -245,7 +245,7 @@ class GameOverSubState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override function dispatchEvent(event:ScriptEvent)
|
public override function dispatchEvent(event:ScriptEvent)
|
||||||
{
|
{
|
||||||
super.dispatchEvent(event);
|
super.dispatchEvent(event);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package funkin.play;
|
package funkin.play;
|
||||||
|
|
||||||
import flixel.sound.FlxSound;
|
|
||||||
import funkin.ui.story.StoryMenuState;
|
|
||||||
import flixel.addons.display.FlxPieDial;
|
import flixel.addons.display.FlxPieDial;
|
||||||
import flixel.addons.transition.FlxTransitionableState;
|
import flixel.addons.transition.FlxTransitionableState;
|
||||||
import flixel.FlxCamera;
|
import flixel.FlxCamera;
|
||||||
|
@ -14,6 +12,7 @@ import flixel.input.keyboard.FlxKey;
|
||||||
import flixel.math.FlxMath;
|
import flixel.math.FlxMath;
|
||||||
import flixel.math.FlxPoint;
|
import flixel.math.FlxPoint;
|
||||||
import flixel.math.FlxRect;
|
import flixel.math.FlxRect;
|
||||||
|
import flixel.sound.FlxSound;
|
||||||
import flixel.text.FlxText;
|
import flixel.text.FlxText;
|
||||||
import flixel.tweens.FlxEase;
|
import flixel.tweens.FlxEase;
|
||||||
import flixel.tweens.FlxTween;
|
import flixel.tweens.FlxTween;
|
||||||
|
@ -28,6 +27,8 @@ import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.Note;
|
import funkin.Note;
|
||||||
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.ConversationDataParser;
|
||||||
import funkin.play.cutscene.VanillaCutscenes;
|
import funkin.play.cutscene.VanillaCutscenes;
|
||||||
import funkin.play.cutscene.VideoCutscene;
|
import funkin.play.cutscene.VideoCutscene;
|
||||||
import funkin.play.event.SongEventData.SongEventParser;
|
import funkin.play.event.SongEventData.SongEventParser;
|
||||||
|
@ -45,6 +46,7 @@ import funkin.play.Strumline.StrumlineStyle;
|
||||||
import funkin.ui.PopUpStuff;
|
import funkin.ui.PopUpStuff;
|
||||||
import funkin.ui.PreferencesMenu;
|
import funkin.ui.PreferencesMenu;
|
||||||
import funkin.ui.stageBuildShit.StageOffsetSubState;
|
import funkin.ui.stageBuildShit.StageOffsetSubState;
|
||||||
|
import funkin.ui.story.StoryMenuState;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
import funkin.util.SerializerUtil;
|
import funkin.util.SerializerUtil;
|
||||||
import funkin.util.SortUtil;
|
import funkin.util.SortUtil;
|
||||||
|
@ -222,6 +224,11 @@ class PlayState extends MusicBeatState
|
||||||
*/
|
*/
|
||||||
public var disableKeys:Bool = false;
|
public var disableKeys:Bool = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current dialogue.
|
||||||
|
*/
|
||||||
|
public var currentConversation:Conversation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PRIVATE INSTANCE VARIABLES
|
* PRIVATE INSTANCE VARIABLES
|
||||||
* Private instance variables should be used for information that must be reset or dereferenced
|
* Private instance variables should be used for information that must be reset or dereferenced
|
||||||
|
@ -887,7 +894,7 @@ class PlayState extends MusicBeatState
|
||||||
trace('Song difficulty could not be loaded.');
|
trace('Song difficulty could not be loaded.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Conductor.forceBPM(currentChart.getStartingBPM());
|
// Conductor.forceBPM(currentChart.getStartingBPM());
|
||||||
|
|
||||||
vocals = currentChart.buildVocals(currentPlayerId);
|
vocals = currentChart.buildVocals(currentPlayerId);
|
||||||
if (vocals.members.length == 0)
|
if (vocals.members.length == 0)
|
||||||
|
@ -1201,13 +1208,10 @@ class PlayState extends MusicBeatState
|
||||||
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
FlxG.watch.addQuick('beatShit', Conductor.currentBeat);
|
|
||||||
FlxG.watch.addQuick('stepShit', Conductor.currentStep);
|
|
||||||
if (currentStage != null)
|
if (currentStage != null)
|
||||||
{
|
{
|
||||||
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
|
||||||
}
|
}
|
||||||
FlxG.watch.addQuick('songPos', Conductor.songPosition);
|
|
||||||
|
|
||||||
// Handle GF dance speed.
|
// Handle GF dance speed.
|
||||||
// TODO: Add a song event for this.
|
// TODO: Add a song event for this.
|
||||||
|
@ -1425,6 +1429,7 @@ class PlayState extends MusicBeatState
|
||||||
// Handle keybinds.
|
// Handle keybinds.
|
||||||
if (!isInCutscene && !disableKeys) keyShit(true);
|
if (!isInCutscene && !disableKeys) keyShit(true);
|
||||||
if (!isInCutscene && !disableKeys) debugKeyShit();
|
if (!isInCutscene && !disableKeys) debugKeyShit();
|
||||||
|
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
|
||||||
|
|
||||||
// Dispatch the onUpdate event to scripted elements.
|
// Dispatch the onUpdate event to scripted elements.
|
||||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||||
|
@ -1432,6 +1437,36 @@ class PlayState extends MusicBeatState
|
||||||
|
|
||||||
static final CUTSCENE_KEYS:Array<FlxKey> = [SPACE, ESCAPE, ENTER];
|
static final CUTSCENE_KEYS:Array<FlxKey> = [SPACE, ESCAPE, ENTER];
|
||||||
|
|
||||||
|
function handleCutsceneKeys(elapsed:Float):Void
|
||||||
|
{
|
||||||
|
if (currentConversation != null)
|
||||||
|
{
|
||||||
|
if (controls.CUTSCENE_ADVANCE) currentConversation?.advanceConversation();
|
||||||
|
|
||||||
|
if (controls.CUTSCENE_SKIP)
|
||||||
|
{
|
||||||
|
currentConversation?.trySkipConversation(elapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentConversation?.trySkipConversation(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (VideoCutscene.isPlaying())
|
||||||
|
{
|
||||||
|
// This is a video cutscene.
|
||||||
|
|
||||||
|
if (controls.CUTSCENE_SKIP)
|
||||||
|
{
|
||||||
|
trySkipVideoCutscene(elapsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trySkipVideoCutscene(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function trySkipVideoCutscene(elapsed:Float):Void
|
public function trySkipVideoCutscene(elapsed:Float):Void
|
||||||
{
|
{
|
||||||
if (skipTimer == null || skipTimer.animation == null) return;
|
if (skipTimer == null || skipTimer.animation == null) return;
|
||||||
|
@ -2369,9 +2404,9 @@ class PlayState extends MusicBeatState
|
||||||
camHUD.visible = true;
|
camHUD.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override function dispatchEvent(event:ScriptEvent):Void
|
public override function dispatchEvent(event:ScriptEvent):Void
|
||||||
{
|
{
|
||||||
// ORDER: Module, Stage, Character, Song, Note
|
// ORDER: Module, Stage, Character, Song, Conversation, Note
|
||||||
// Modules should get the first chance to cancel the event.
|
// Modules should get the first chance to cancel the event.
|
||||||
|
|
||||||
// super.dispatchEvent(event) dispatches event to module scripts.
|
// super.dispatchEvent(event) dispatches event to module scripts.
|
||||||
|
@ -2383,11 +2418,55 @@ class PlayState extends MusicBeatState
|
||||||
// Dispatch event to character script(s).
|
// Dispatch event to character script(s).
|
||||||
if (currentStage != null) currentStage.dispatchToCharacters(event);
|
if (currentStage != null) currentStage.dispatchToCharacters(event);
|
||||||
|
|
||||||
|
// Dispatch event to song script.
|
||||||
ScriptEventDispatcher.callEvent(currentSong, event);
|
ScriptEventDispatcher.callEvent(currentSong, event);
|
||||||
|
|
||||||
|
// Dispatch event to conversation script.
|
||||||
|
ScriptEventDispatcher.callEvent(currentConversation, event);
|
||||||
|
|
||||||
// TODO: Dispatch event to note scripts
|
// TODO: Dispatch event to note scripts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function startConversation(conversationId:String):Void
|
||||||
|
{
|
||||||
|
isInCutscene = true;
|
||||||
|
|
||||||
|
currentConversation = ConversationDataParser.fetchConversation(conversationId);
|
||||||
|
if (currentConversation == null) return;
|
||||||
|
|
||||||
|
currentConversation.completeCallback = onConversationComplete;
|
||||||
|
currentConversation.cameras = [camCutscene];
|
||||||
|
currentConversation.zIndex = 1000;
|
||||||
|
add(currentConversation);
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false);
|
||||||
|
ScriptEventDispatcher.callEvent(currentConversation, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConversationComplete():Void
|
||||||
|
{
|
||||||
|
isInCutscene = true;
|
||||||
|
remove(currentConversation);
|
||||||
|
currentConversation = null;
|
||||||
|
|
||||||
|
if (startingSong && !isInCountdown)
|
||||||
|
{
|
||||||
|
startCountdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override function destroy():Void
|
||||||
|
{
|
||||||
|
if (currentConversation != null)
|
||||||
|
{
|
||||||
|
remove(currentConversation);
|
||||||
|
currentConversation.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the position and contents of the score display.
|
* Updates the position and contents of the score display.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -44,10 +44,12 @@ class SparrowCharacter extends BaseCharacter
|
||||||
|
|
||||||
if (_data.isPixel)
|
if (_data.isPixel)
|
||||||
{
|
{
|
||||||
|
this.isPixel = true;
|
||||||
this.antialiasing = false;
|
this.antialiasing = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
this.isPixel = false;
|
||||||
this.antialiasing = true;
|
this.antialiasing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,8 @@ class VideoCutscene
|
||||||
|
|
||||||
if (!openfl.Assets.exists(filePath))
|
if (!openfl.Assets.exists(filePath))
|
||||||
{
|
{
|
||||||
trace('ERROR: Video file does not exist: ${filePath}');
|
// Display a popup.
|
||||||
|
lime.app.Application.current.window.alert('Video file does not exist: ${filePath}', 'Error playing video');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
617
source/funkin/play/cutscene/dialogue/Conversation.hx
Normal file
617
source/funkin/play/cutscene/dialogue/Conversation.hx
Normal file
|
@ -0,0 +1,617 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
import flixel.FlxSprite;
|
||||||
|
import flixel.group.FlxSpriteGroup;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
|
import flixel.tweens.FlxTween;
|
||||||
|
import flixel.tweens.FlxEase;
|
||||||
|
import flixel.system.FlxSound;
|
||||||
|
import funkin.util.SortUtil;
|
||||||
|
import flixel.util.FlxSort;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import funkin.modding.IScriptedClass.IEventHandler;
|
||||||
|
import funkin.play.cutscene.dialogue.DialogueBox;
|
||||||
|
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||||
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
|
import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
|
||||||
|
import flixel.addons.display.FlxPieDial;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A high-level handler for dialogue.
|
||||||
|
*
|
||||||
|
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
|
||||||
|
*/
|
||||||
|
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||||
|
{
|
||||||
|
static final CONVERSATION_SKIP_TIMER:Float = 1.5;
|
||||||
|
|
||||||
|
var skipHeldTimer:Float = 0.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DATA
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* The ID of the associated dialogue.
|
||||||
|
*/
|
||||||
|
public final conversationId:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of the conversation.
|
||||||
|
*/
|
||||||
|
var state:ConversationState = ConversationState.Start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data for the associated dialogue.
|
||||||
|
*/
|
||||||
|
var conversationData:ConversationData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current entry in the dialogue.
|
||||||
|
*/
|
||||||
|
var currentDialogueEntry:Int = 0;
|
||||||
|
|
||||||
|
var currentDialogueEntryCount(get, null):Int;
|
||||||
|
|
||||||
|
function get_currentDialogueEntryCount():Int
|
||||||
|
{
|
||||||
|
return conversationData.dialogue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current line in the current entry in the dialogue.
|
||||||
|
* **/
|
||||||
|
var currentDialogueLine:Int = 0;
|
||||||
|
|
||||||
|
var currentDialogueLineCount(get, null):Int;
|
||||||
|
|
||||||
|
function get_currentDialogueLineCount():Int
|
||||||
|
{
|
||||||
|
return currentDialogueEntryData.text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDialogueEntryData(get, null):DialogueEntryData;
|
||||||
|
|
||||||
|
function get_currentDialogueEntryData():DialogueEntryData
|
||||||
|
{
|
||||||
|
if (conversationData == null || conversationData.dialogue == null) return null;
|
||||||
|
if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null;
|
||||||
|
|
||||||
|
return conversationData.dialogue[currentDialogueEntry];
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentDialogueLineString(get, null):String;
|
||||||
|
|
||||||
|
function get_currentDialogueLineString():String
|
||||||
|
{
|
||||||
|
return currentDialogueEntryData?.text[currentDialogueLine];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AUDIO
|
||||||
|
*/
|
||||||
|
var music:FlxSound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GRAPHICS
|
||||||
|
*/
|
||||||
|
var backdrop:FlxSprite;
|
||||||
|
|
||||||
|
var currentSpeaker:Speaker;
|
||||||
|
|
||||||
|
var currentDialogueBox:DialogueBox;
|
||||||
|
|
||||||
|
var skipTimer:FlxPieDial;
|
||||||
|
|
||||||
|
public function new(conversationId:String)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.conversationId = conversationId;
|
||||||
|
this.conversationData = ConversationDataParser.parseConversationData(this.conversationId);
|
||||||
|
|
||||||
|
if (conversationData == null) throw 'Could not load conversation data for conversation ID "$conversationId"';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onCreate(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
// Reset the progress in the dialogue.
|
||||||
|
currentDialogueEntry = 0;
|
||||||
|
this.state = ConversationState.Start;
|
||||||
|
this.alpha = 1.0;
|
||||||
|
|
||||||
|
// Start the dialogue.
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMusic():Void
|
||||||
|
{
|
||||||
|
if (conversationData.music == null) return;
|
||||||
|
|
||||||
|
music = new FlxSound().loadEmbedded(Paths.music(conversationData.music.asset), true, true);
|
||||||
|
music.volume = 0;
|
||||||
|
|
||||||
|
if (conversationData.music.fadeTime > 0.0)
|
||||||
|
{
|
||||||
|
FlxTween.tween(music, {volume: 1.0}, conversationData.music.fadeTime, {ease: FlxEase.linear});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
music.volume = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlxG.sound.list.add(music);
|
||||||
|
music.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupBackdrop():Void
|
||||||
|
{
|
||||||
|
backdrop = new FlxSprite(0, 0);
|
||||||
|
|
||||||
|
if (conversationData.backdrop == null) return;
|
||||||
|
|
||||||
|
// Play intro
|
||||||
|
switch (conversationData?.backdrop.type)
|
||||||
|
{
|
||||||
|
case SOLID:
|
||||||
|
backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color));
|
||||||
|
if (conversationData.backdrop.data.fadeTime > 0.0)
|
||||||
|
{
|
||||||
|
backdrop.alpha = 0.0;
|
||||||
|
FlxTween.tween(backdrop, {alpha: 1.0}, conversationData.backdrop.data.fadeTime, {ease: FlxEase.linear});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backdrop.alpha = 1.0;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop.zIndex = 10;
|
||||||
|
add(backdrop);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSkipTimer():Void
|
||||||
|
{
|
||||||
|
add(skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24));
|
||||||
|
skipTimer.amount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function update(elapsed:Float):Void
|
||||||
|
{
|
||||||
|
super.update(elapsed);
|
||||||
|
|
||||||
|
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCurrentSpeaker():Void
|
||||||
|
{
|
||||||
|
var nextSpeakerId:String = currentDialogueEntryData.speaker;
|
||||||
|
|
||||||
|
// Skip the next steps if the current speaker is already displayed.
|
||||||
|
if (currentSpeaker != null && nextSpeakerId == currentSpeaker.speakerId) return;
|
||||||
|
|
||||||
|
var nextSpeaker:Speaker = SpeakerDataParser.fetchSpeaker(nextSpeakerId);
|
||||||
|
|
||||||
|
if (currentSpeaker != null)
|
||||||
|
{
|
||||||
|
remove(currentSpeaker);
|
||||||
|
currentSpeaker.kill(); // Kill, don't destroy! We want to revive it later.
|
||||||
|
currentSpeaker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSpeaker == null)
|
||||||
|
{
|
||||||
|
if (nextSpeakerId == null)
|
||||||
|
{
|
||||||
|
trace('Dialogue entry has no speaker.');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('Speaker could not be retrieved.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(ScriptEvent.CREATE, true));
|
||||||
|
|
||||||
|
currentSpeaker = nextSpeaker;
|
||||||
|
currentSpeaker.zIndex = 200;
|
||||||
|
add(currentSpeaker);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSpeakerAnimation():Void
|
||||||
|
{
|
||||||
|
var nextSpeakerAnimation:String = currentDialogueEntryData.speakerAnimation;
|
||||||
|
|
||||||
|
if (nextSpeakerAnimation == null) return;
|
||||||
|
|
||||||
|
currentSpeaker.playAnimation(nextSpeakerAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refresh():Void
|
||||||
|
{
|
||||||
|
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCurrentDialogueBox():Void
|
||||||
|
{
|
||||||
|
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
|
||||||
|
|
||||||
|
// Skip the next steps if the current speaker is already displayed.
|
||||||
|
if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.dialogueBoxId) return;
|
||||||
|
|
||||||
|
if (currentDialogueBox != null)
|
||||||
|
{
|
||||||
|
remove(currentDialogueBox);
|
||||||
|
currentDialogueBox.kill(); // Kill, don't destroy! We want to revive it later.
|
||||||
|
currentDialogueBox = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId);
|
||||||
|
|
||||||
|
if (nextDialogueBox == null)
|
||||||
|
{
|
||||||
|
trace('Dialogue box could not be retrieved.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(ScriptEvent.CREATE, true));
|
||||||
|
|
||||||
|
currentDialogueBox = nextDialogueBox;
|
||||||
|
currentDialogueBox.zIndex = 300;
|
||||||
|
|
||||||
|
currentDialogueBox.typingCompleteCallback = this.onTypingComplete;
|
||||||
|
|
||||||
|
add(currentDialogueBox);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playDialogueBoxAnimation():Void
|
||||||
|
{
|
||||||
|
var nextDialogueBoxAnimation:String = currentDialogueEntryData?.boxAnimation;
|
||||||
|
|
||||||
|
if (nextDialogueBoxAnimation == null) return;
|
||||||
|
|
||||||
|
currentDialogueBox.playAnimation(nextDialogueBoxAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTypingComplete():Void
|
||||||
|
{
|
||||||
|
if (this.state == ConversationState.Speaking)
|
||||||
|
{
|
||||||
|
this.state = ConversationState.Idle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('[WARNING] Unexpected state transition from ${this.state}');
|
||||||
|
this.state = ConversationState.Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startConversation():Void
|
||||||
|
{
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an event to attempt to advance the conversation.
|
||||||
|
* This is done once at the start of the conversation, and once whenever the user presses CONFIRM to advance the conversation.
|
||||||
|
*
|
||||||
|
* The broadcast event may be cancelled by modules or ScriptedConversations. This will prevent the conversation from actually advancing.
|
||||||
|
* This is useful if you want to manually play an animation or something.
|
||||||
|
*/
|
||||||
|
public function advanceConversation():Void
|
||||||
|
{
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ConversationState.Start:
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_START, this, true));
|
||||||
|
case ConversationState.Opening:
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_COMPLETE_LINE, this, true));
|
||||||
|
case ConversationState.Speaking:
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_COMPLETE_LINE, this, true));
|
||||||
|
case ConversationState.Idle:
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_LINE, this, true));
|
||||||
|
case ConversationState.Ending:
|
||||||
|
// Skip the outro.
|
||||||
|
endOutro();
|
||||||
|
default:
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatchEvent(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
var currentState:IEventHandler = cast FlxG.state;
|
||||||
|
currentState.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the conversation back to the start.
|
||||||
|
*/
|
||||||
|
public function resetConversation():Void
|
||||||
|
{
|
||||||
|
// Reset the progress in the dialogue.
|
||||||
|
currentDialogueEntry = 0;
|
||||||
|
this.state = ConversationState.Start;
|
||||||
|
|
||||||
|
advanceConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function trySkipConversation(elapsed:Float):Void
|
||||||
|
{
|
||||||
|
if (skipTimer == null || skipTimer.animation == null) return;
|
||||||
|
|
||||||
|
if (elapsed < 0)
|
||||||
|
{
|
||||||
|
skipHeldTimer = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
skipHeldTimer += elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipTimer.visible = skipHeldTimer >= 0.05;
|
||||||
|
skipTimer.amount = Math.min(skipHeldTimer / CONVERSATION_SKIP_TIMER, 1.0);
|
||||||
|
|
||||||
|
if (skipHeldTimer >= CONVERSATION_SKIP_TIMER)
|
||||||
|
{
|
||||||
|
skipConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch an event to attempt to immediately end the conversation.
|
||||||
|
*
|
||||||
|
* The broadcast event may be cancelled by modules or ScriptedConversations. This will prevent the conversation from being cancelled.
|
||||||
|
* This is useful if you want to prevent an animation from being skipped or something.
|
||||||
|
*/
|
||||||
|
public function skipConversation():Void
|
||||||
|
{
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_SKIP, this, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
static var outroTween:FlxTween;
|
||||||
|
|
||||||
|
public function startOutro():Void
|
||||||
|
{
|
||||||
|
switch (conversationData?.outro?.type)
|
||||||
|
{
|
||||||
|
case FADE:
|
||||||
|
var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0;
|
||||||
|
|
||||||
|
outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
|
||||||
|
{
|
||||||
|
type: ONESHOT, // holy shit like the game no way
|
||||||
|
startDelay: 0,
|
||||||
|
onComplete: (_) -> endOutro(),
|
||||||
|
});
|
||||||
|
|
||||||
|
FlxTween.tween(this.music, {volume: 0.0}, fadeTime);
|
||||||
|
case NONE:
|
||||||
|
// Immediately clean up.
|
||||||
|
endOutro();
|
||||||
|
default:
|
||||||
|
// Immediately clean up.
|
||||||
|
endOutro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var completeCallback:Void->Void;
|
||||||
|
|
||||||
|
public function endOutro():Void
|
||||||
|
{
|
||||||
|
outroTween = null;
|
||||||
|
ScriptEventDispatcher.callEvent(this, new ScriptEvent(ScriptEvent.DESTROY, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performed as the conversation starts.
|
||||||
|
*/
|
||||||
|
public function onDialogueStart(event:DialogueScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
|
||||||
|
// Fade in the music and backdrop.
|
||||||
|
setupMusic();
|
||||||
|
setupBackdrop();
|
||||||
|
setupSkipTimer();
|
||||||
|
|
||||||
|
// Advance the conversation.
|
||||||
|
state = ConversationState.Opening;
|
||||||
|
|
||||||
|
showCurrentDialogueBox();
|
||||||
|
playDialogueBoxAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the next line of conversation.
|
||||||
|
*/
|
||||||
|
public function onDialogueLine(event:DialogueScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
if (event.eventCanceled) return;
|
||||||
|
|
||||||
|
// Perform the actual logic to advance the conversation.
|
||||||
|
currentDialogueLine += 1;
|
||||||
|
if (currentDialogueLine >= currentDialogueLineCount)
|
||||||
|
{
|
||||||
|
// Open the next entry.
|
||||||
|
currentDialogueLine = 0;
|
||||||
|
currentDialogueEntry += 1;
|
||||||
|
|
||||||
|
if (currentDialogueEntry >= currentDialogueEntryCount)
|
||||||
|
{
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_END, this, false));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (state == Idle)
|
||||||
|
{
|
||||||
|
showCurrentDialogueBox();
|
||||||
|
playDialogueBoxAnimation();
|
||||||
|
|
||||||
|
state = Opening;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Continue the dialog with more lines.
|
||||||
|
state = Speaking;
|
||||||
|
currentDialogueBox.appendText(currentDialogueLineString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip the scrolling of the next line of conversation.
|
||||||
|
*/
|
||||||
|
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
if (event.eventCanceled) return;
|
||||||
|
|
||||||
|
currentDialogueBox.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip to the end of the conversation, immediately triggering the DIALOGUE_END event.
|
||||||
|
*/
|
||||||
|
public function onDialogueSkip(event:DialogueScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
if (event.eventCanceled) return;
|
||||||
|
|
||||||
|
dispatchEvent(new DialogueScriptEvent(ScriptEvent.DIALOGUE_END, this, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDialogueEnd(event:DialogueScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
|
||||||
|
state = Ending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for events/scripts.
|
||||||
|
|
||||||
|
public function onUpdate(event:UpdateScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
|
||||||
|
if (event.eventCanceled) return;
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ConversationState.Start:
|
||||||
|
// Wait for advance() to be called and DIALOGUE_LINE to be dispatched.
|
||||||
|
return;
|
||||||
|
case ConversationState.Opening:
|
||||||
|
// Backdrop animation should have started.
|
||||||
|
// Box animations should have started.
|
||||||
|
if (currentDialogueBox != null
|
||||||
|
&& (currentDialogueBox.isAnimationFinished()
|
||||||
|
|| currentDialogueBox.getCurrentAnimation() != currentDialogueEntryData?.boxAnimation))
|
||||||
|
{
|
||||||
|
// Box animations have finished.
|
||||||
|
|
||||||
|
// Start playing the speaker animation.
|
||||||
|
state = ConversationState.Speaking;
|
||||||
|
showCurrentSpeaker();
|
||||||
|
playSpeakerAnimation();
|
||||||
|
currentDialogueBox.setText(currentDialogueLineString);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case ConversationState.Speaking:
|
||||||
|
// Speaker animation should be playing.
|
||||||
|
return;
|
||||||
|
case ConversationState.Idle:
|
||||||
|
// Waiting for user input via `advanceConversation()`.
|
||||||
|
return;
|
||||||
|
case ConversationState.Ending:
|
||||||
|
if (outroTween == null) startOutro();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDestroy(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
|
||||||
|
if (outroTween != null) outroTween.cancel(); // Canc
|
||||||
|
outroTween = null;
|
||||||
|
|
||||||
|
this.alpha = 0.0;
|
||||||
|
if (this.music != null) this.music.stop();
|
||||||
|
this.music = null;
|
||||||
|
|
||||||
|
this.skipTimer = null;
|
||||||
|
if (currentSpeaker != null) currentSpeaker.kill();
|
||||||
|
currentSpeaker = null;
|
||||||
|
if (currentDialogueBox != null) currentDialogueBox.kill();
|
||||||
|
currentDialogueBox = null;
|
||||||
|
if (backdrop != null) backdrop.kill();
|
||||||
|
backdrop = null;
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
if (completeCallback != null) completeCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onScriptEvent(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
propagateEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As this event is dispatched to the Conversation, it is also dispatched to the active speaker.
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
function propagateEvent(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
if (this.currentDialogueBox != null)
|
||||||
|
{
|
||||||
|
ScriptEventDispatcher.callEvent(this.currentDialogueBox, event);
|
||||||
|
}
|
||||||
|
if (this.currentSpeaker != null)
|
||||||
|
{
|
||||||
|
ScriptEventDispatcher.callEvent(this.currentSpeaker, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override function toString():String
|
||||||
|
{
|
||||||
|
return 'Conversation($conversationId)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Managing things with a single enum is a lot easier than a multitude of flags.
|
||||||
|
enum ConversationState
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* State hasn't been initialized yet.
|
||||||
|
*/
|
||||||
|
Start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog is animating. If the dialog is static, this may only last for one frame.
|
||||||
|
*/
|
||||||
|
Opening;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text is scrolling and audio is playing. Speaker portrait is probably animating too.
|
||||||
|
*/
|
||||||
|
Speaking;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text is done scrolling and game is waiting for user to open another dialog.
|
||||||
|
*/
|
||||||
|
Idle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fade out and leave conversation.
|
||||||
|
*/
|
||||||
|
Ending;
|
||||||
|
}
|
236
source/funkin/play/cutscene/dialogue/ConversationData.hx
Normal file
236
source/funkin/play/cutscene/dialogue/ConversationData.hx
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
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 looped:Bool;
|
||||||
|
public var fadeTime:Float;
|
||||||
|
|
||||||
|
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:Null<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';
|
||||||
|
}
|
162
source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
Normal file
162
source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
import flixel.FlxState;
|
||||||
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
|
import funkin.Paths;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
377
source/funkin/play/cutscene/dialogue/DialogueBox.hx
Normal file
377
source/funkin/play/cutscene/dialogue/DialogueBox.hx
Normal file
|
@ -0,0 +1,377 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
import flixel.FlxSprite;
|
||||||
|
import flixel.group.FlxSpriteGroup;
|
||||||
|
import flixel.graphics.frames.FlxFramesCollection;
|
||||||
|
import flixel.text.FlxText;
|
||||||
|
import flixel.addons.text.FlxTypeText;
|
||||||
|
import funkin.util.assets.FlxAnimationUtil;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||||
|
import flixel.util.FlxColor;
|
||||||
|
|
||||||
|
class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
|
||||||
|
{
|
||||||
|
public final dialogueBoxId:String;
|
||||||
|
public var dialogueBoxName(get, null):String;
|
||||||
|
|
||||||
|
function get_dialogueBoxName():String
|
||||||
|
{
|
||||||
|
return boxData?.name ?? 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
var boxData:DialogueBoxData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset the speaker's sprite by this much when playing each animation.
|
||||||
|
*/
|
||||||
|
var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current animation offset being used.
|
||||||
|
*/
|
||||||
|
var animOffsets(default, set):Array<Float> = [0, 0];
|
||||||
|
|
||||||
|
function set_animOffsets(value:Array<Float>):Array<Float>
|
||||||
|
{
|
||||||
|
if (animOffsets == null) animOffsets = [0, 0];
|
||||||
|
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
|
||||||
|
|
||||||
|
var xDiff:Float = value[0] - animOffsets[0];
|
||||||
|
var yDiff:Float = value[1] - animOffsets[1];
|
||||||
|
|
||||||
|
this.x += xDiff;
|
||||||
|
this.y += yDiff;
|
||||||
|
|
||||||
|
return animOffsets = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset of the speaker overall.
|
||||||
|
*/
|
||||||
|
public var globalOffsets(default, set):Array<Float> = [0, 0];
|
||||||
|
|
||||||
|
function set_globalOffsets(value:Array<Float>):Array<Float>
|
||||||
|
{
|
||||||
|
if (globalOffsets == null) globalOffsets = [0, 0];
|
||||||
|
if (globalOffsets == value) return value;
|
||||||
|
|
||||||
|
var xDiff:Float = value[0] - globalOffsets[0];
|
||||||
|
var yDiff:Float = value[1] - globalOffsets[1];
|
||||||
|
|
||||||
|
this.x += xDiff;
|
||||||
|
this.y += yDiff;
|
||||||
|
return globalOffsets = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var boxSprite:FlxSprite;
|
||||||
|
var textDisplay:FlxTypeText;
|
||||||
|
|
||||||
|
var text(default, set):String;
|
||||||
|
|
||||||
|
function set_text(value:String):String
|
||||||
|
{
|
||||||
|
this.text = value;
|
||||||
|
|
||||||
|
textDisplay.resetText(this.text);
|
||||||
|
textDisplay.start();
|
||||||
|
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public var speed(default, set):Float;
|
||||||
|
|
||||||
|
function set_speed(value:Float):Float
|
||||||
|
{
|
||||||
|
this.speed = value;
|
||||||
|
textDisplay.delay = this.speed * 0.05; // 1.0 x 0.05
|
||||||
|
return this.speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function new(dialogueBoxId:String)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.dialogueBoxId = dialogueBoxId;
|
||||||
|
this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId);
|
||||||
|
|
||||||
|
if (boxData == null) throw 'Could not load dialogue box data for box ID "$dialogueBoxId"';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onCreate(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
this.globalOffsets = [0, 0];
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.alpha = 1;
|
||||||
|
|
||||||
|
this.boxSprite = new FlxSprite(0, 0);
|
||||||
|
add(this.boxSprite);
|
||||||
|
|
||||||
|
loadSpritesheet();
|
||||||
|
loadAnimations();
|
||||||
|
|
||||||
|
loadText();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSpritesheet():Void
|
||||||
|
{
|
||||||
|
trace('[DIALOGUE BOX] Loading spritesheet ${boxData.assetPath} for ${dialogueBoxId}');
|
||||||
|
|
||||||
|
var tex:FlxFramesCollection = Paths.getSparrowAtlas(boxData.assetPath);
|
||||||
|
if (tex == null)
|
||||||
|
{
|
||||||
|
trace('Could not load Sparrow sprite: ${boxData.assetPath}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boxSprite.frames = tex;
|
||||||
|
|
||||||
|
if (boxData.isPixel)
|
||||||
|
{
|
||||||
|
this.boxSprite.antialiasing = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.boxSprite.antialiasing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flipX = boxData.flipX;
|
||||||
|
this.globalOffsets = boxData.offsets;
|
||||||
|
this.setScale(boxData.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setText(newText:String):Void
|
||||||
|
{
|
||||||
|
textDisplay.prefix = '';
|
||||||
|
textDisplay.resetText(newText);
|
||||||
|
textDisplay.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function appendText(newText:String):Void
|
||||||
|
{
|
||||||
|
textDisplay.prefix = this.textDisplay.text;
|
||||||
|
textDisplay.resetText(newText);
|
||||||
|
textDisplay.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function skip():Void
|
||||||
|
{
|
||||||
|
textDisplay.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reassign this to set a callback.
|
||||||
|
*/
|
||||||
|
function onTypingComplete():Void
|
||||||
|
{
|
||||||
|
// No save navigation? :(
|
||||||
|
if (typingCompleteCallback != null) typingCompleteCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public var typingCompleteCallback:() -> Void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sprite scale to the appropriate value.
|
||||||
|
* @param scale
|
||||||
|
*/
|
||||||
|
public function setScale(scale:Null<Float>):Void
|
||||||
|
{
|
||||||
|
if (scale == null) scale = 1.0;
|
||||||
|
this.boxSprite.scale.x = scale;
|
||||||
|
this.boxSprite.scale.y = scale;
|
||||||
|
this.boxSprite.updateHitbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAnimations():Void
|
||||||
|
{
|
||||||
|
trace('[DIALOGUE BOX] Loading ${boxData.animations.length} animations for ${dialogueBoxId}');
|
||||||
|
|
||||||
|
FlxAnimationUtil.addAtlasAnimations(this.boxSprite, boxData.animations);
|
||||||
|
|
||||||
|
for (anim in boxData.animations)
|
||||||
|
{
|
||||||
|
if (anim.offsets == null)
|
||||||
|
{
|
||||||
|
setAnimationOffsets(anim.name, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var animNames:Array<String> = this.boxSprite?.animation?.getNameList() ?? [];
|
||||||
|
trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${dialogueBoxId}');
|
||||||
|
|
||||||
|
boxSprite.animation.callback = this.onAnimationFrame;
|
||||||
|
boxSprite.animation.finishCallback = this.onAnimationFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an animation finishes.
|
||||||
|
* @param name The name of the animation that just finished.
|
||||||
|
*/
|
||||||
|
function onAnimationFinished(name:String):Void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the current animation's frame changes.
|
||||||
|
* @param name The name of the current animation.
|
||||||
|
* @param frameNumber The number of the current frame.
|
||||||
|
* @param frameIndex The index of the current frame.
|
||||||
|
*
|
||||||
|
* For example, if an animation was defined as having the indexes [3, 0, 1, 2],
|
||||||
|
* then the first callback would have frameNumber = 0 and frameIndex = 3.
|
||||||
|
*/
|
||||||
|
function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1):Void
|
||||||
|
{
|
||||||
|
// Do nothing by default.
|
||||||
|
// This can be overridden by, for example, scripts,
|
||||||
|
// or by calling `animationFrame.add()`.
|
||||||
|
|
||||||
|
// Try not to do anything expensive here, it runs many times a second.
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadText():Void
|
||||||
|
{
|
||||||
|
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
||||||
|
textDisplay.fieldWidth = boxData.text.width;
|
||||||
|
textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
|
||||||
|
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
|
||||||
|
textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
|
||||||
|
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
|
||||||
|
|
||||||
|
textDisplay.completeCallback = onTypingComplete;
|
||||||
|
|
||||||
|
textDisplay.x += boxData.text.offsets[0];
|
||||||
|
textDisplay.y += boxData.text.offsets[1];
|
||||||
|
|
||||||
|
add(textDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the animation to play.
|
||||||
|
* @param restart Whether to restart the animation if it is already playing.
|
||||||
|
* @param reversed If true, play the animation backwards, from the last frame to the first.
|
||||||
|
*/
|
||||||
|
public function playAnimation(name:String, restart:Bool = false, ?reversed:Bool = false):Void
|
||||||
|
{
|
||||||
|
var correctName:String = correctAnimationName(name);
|
||||||
|
if (correctName == null) return;
|
||||||
|
|
||||||
|
this.boxSprite.animation.play(correctName, restart, false, 0);
|
||||||
|
|
||||||
|
applyAnimationOffsets(correctName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a given animation exists before playing it.
|
||||||
|
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
function correctAnimationName(name:String):String
|
||||||
|
{
|
||||||
|
// If the animation exists, we're good.
|
||||||
|
if (hasAnimation(name)) return name;
|
||||||
|
|
||||||
|
trace('[DIALOGUE BOX] Animation "$name" does not exist!');
|
||||||
|
|
||||||
|
// Attempt to strip a `-alt` suffix, if it exists.
|
||||||
|
if (name.lastIndexOf('-') != -1)
|
||||||
|
{
|
||||||
|
var correctName = name.substring(0, name.lastIndexOf('-'));
|
||||||
|
trace('[DIALOGUE BOX] Attempting to fallback to "$correctName"');
|
||||||
|
return correctAnimationName(correctName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (name != 'idle')
|
||||||
|
{
|
||||||
|
trace('[DIALOGUE BOX] Attempting to fallback to "idle"');
|
||||||
|
return correctAnimationName('idle');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('[DIALOGUE BOX] Failing animation playback.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAnimation(id:String):Bool
|
||||||
|
{
|
||||||
|
if (this.boxSprite.animation == null) return false;
|
||||||
|
|
||||||
|
return this.boxSprite.animation.getByName(id) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the animation that is currently playing.
|
||||||
|
* If no animation is playing (usually this means the character is BROKEN!),
|
||||||
|
* returns an empty string to prevent NPEs.
|
||||||
|
*/
|
||||||
|
public function getCurrentAnimation():String
|
||||||
|
{
|
||||||
|
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||||
|
return this.animation.curAnim.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the animation offsets for a specific animation.
|
||||||
|
*/
|
||||||
|
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
||||||
|
{
|
||||||
|
animationOffsets.set(name, [xOffset, yOffset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an apply the animation offsets for a specific animation.
|
||||||
|
*/
|
||||||
|
function applyAnimationOffsets(name:String):Void
|
||||||
|
{
|
||||||
|
var offsets:Array<Float> = animationOffsets.get(name);
|
||||||
|
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
|
||||||
|
{
|
||||||
|
this.animOffsets = offsets;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.animOffsets = [0, 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAnimationFinished():Bool
|
||||||
|
{
|
||||||
|
return this.boxSprite?.animation?.finished ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDialogueStart(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueLine(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueSkip(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueEnd(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onUpdate(event:UpdateScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDestroy(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
if (boxSprite != null) remove(boxSprite);
|
||||||
|
boxSprite = null;
|
||||||
|
if (textDisplay != null) remove(textDisplay);
|
||||||
|
textDisplay = null;
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.globalOffsets = [0, 0];
|
||||||
|
this.alpha = 0;
|
||||||
|
|
||||||
|
this.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||||
|
}
|
123
source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
Normal file
123
source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
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:Null<Int>, size:Null<Int>, color:String, shadowColor:Null<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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
159
source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
Normal file
159
source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
@:hscriptClass
|
||||||
|
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
@:hscriptClass
|
||||||
|
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
|
4
source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx
Normal file
4
source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
@:hscriptClass
|
||||||
|
class ScriptedSpeaker extends Speaker implements polymod.hscript.HScriptedClass {}
|
274
source/funkin/play/cutscene/dialogue/Speaker.hx
Normal file
274
source/funkin/play/cutscene/dialogue/Speaker.hx
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
import flixel.FlxSprite;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import flixel.graphics.frames.FlxFramesCollection;
|
||||||
|
import funkin.util.assets.FlxAnimationUtil;
|
||||||
|
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The character sprite which displays during dialogue.
|
||||||
|
*
|
||||||
|
* Most conversations have two speakers, with one being flipped.
|
||||||
|
*/
|
||||||
|
class Speaker extends FlxSprite implements IDialogueScriptedClass
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The internal ID for this speaker.
|
||||||
|
*/
|
||||||
|
public final speakerId:String;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full data for a speaker.
|
||||||
|
*/
|
||||||
|
var speakerData:SpeakerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A readable name for this speaker.
|
||||||
|
*/
|
||||||
|
public var speakerName(get, null):String;
|
||||||
|
|
||||||
|
function get_speakerName():String
|
||||||
|
{
|
||||||
|
return speakerData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offset the speaker's sprite by this much when playing each animation.
|
||||||
|
*/
|
||||||
|
var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current animation offset being used.
|
||||||
|
*/
|
||||||
|
var animOffsets(default, set):Array<Float> = [0, 0];
|
||||||
|
|
||||||
|
function set_animOffsets(value:Array<Float>):Array<Float>
|
||||||
|
{
|
||||||
|
if (animOffsets == null) animOffsets = [0, 0];
|
||||||
|
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
|
||||||
|
|
||||||
|
var xDiff:Float = value[0] - animOffsets[0];
|
||||||
|
var yDiff:Float = value[1] - animOffsets[1];
|
||||||
|
|
||||||
|
this.x += xDiff;
|
||||||
|
this.y += yDiff;
|
||||||
|
|
||||||
|
return animOffsets = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The offset of the speaker overall.
|
||||||
|
*/
|
||||||
|
public var globalOffsets(default, set):Array<Float> = [0, 0];
|
||||||
|
|
||||||
|
function set_globalOffsets(value:Array<Float>):Array<Float>
|
||||||
|
{
|
||||||
|
if (globalOffsets == null) globalOffsets = [0, 0];
|
||||||
|
if (globalOffsets == value) return value;
|
||||||
|
|
||||||
|
var xDiff:Float = value[0] - globalOffsets[0];
|
||||||
|
var yDiff:Float = value[1] - globalOffsets[1];
|
||||||
|
|
||||||
|
this.x += xDiff;
|
||||||
|
this.y += yDiff;
|
||||||
|
return globalOffsets = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function new(speakerId:String)
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.speakerId = speakerId;
|
||||||
|
this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId);
|
||||||
|
|
||||||
|
if (speakerData == null) throw 'Could not load speaker data for speaker ID "$speakerId"';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when speaker is being created.
|
||||||
|
* @param event The script event.
|
||||||
|
*/
|
||||||
|
public function onCreate(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
this.globalOffsets = [0, 0];
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.alpha = 1;
|
||||||
|
|
||||||
|
loadSpritesheet();
|
||||||
|
loadAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSpritesheet():Void
|
||||||
|
{
|
||||||
|
trace('[SPEAKER] Loading spritesheet ${speakerData.assetPath} for ${speakerId}');
|
||||||
|
|
||||||
|
var tex:FlxFramesCollection = Paths.getSparrowAtlas(speakerData.assetPath);
|
||||||
|
if (tex == null)
|
||||||
|
{
|
||||||
|
trace('Could not load Sparrow sprite: ${speakerData.assetPath}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frames = tex;
|
||||||
|
|
||||||
|
if (speakerData.isPixel)
|
||||||
|
{
|
||||||
|
this.antialiasing = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.antialiasing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flipX = speakerData.flipX;
|
||||||
|
this.globalOffsets = speakerData.offsets;
|
||||||
|
this.setScale(speakerData.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the sprite scale to the appropriate value.
|
||||||
|
* @param scale
|
||||||
|
*/
|
||||||
|
public function setScale(scale:Null<Float>):Void
|
||||||
|
{
|
||||||
|
if (scale == null) scale = 1.0;
|
||||||
|
this.scale.x = scale;
|
||||||
|
this.scale.y = scale;
|
||||||
|
this.updateHitbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAnimations():Void
|
||||||
|
{
|
||||||
|
trace('[SPEAKER] Loading ${speakerData.animations.length} animations for ${speakerId}');
|
||||||
|
|
||||||
|
FlxAnimationUtil.addAtlasAnimations(this, speakerData.animations);
|
||||||
|
|
||||||
|
for (anim in speakerData.animations)
|
||||||
|
{
|
||||||
|
if (anim.offsets == null)
|
||||||
|
{
|
||||||
|
setAnimationOffsets(anim.name, 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var animNames:Array<String> = this.animation.getNameList();
|
||||||
|
trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${speakerId}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param name The name of the animation to play.
|
||||||
|
* @param restart Whether to restart the animation if it is already playing.
|
||||||
|
*/
|
||||||
|
public function playAnimation(name:String, restart:Bool = false):Void
|
||||||
|
{
|
||||||
|
var correctName:String = correctAnimationName(name);
|
||||||
|
if (correctName == null) return;
|
||||||
|
|
||||||
|
this.animation.play(correctName, restart, false, 0);
|
||||||
|
|
||||||
|
applyAnimationOffsets(correctName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentAnimation():String
|
||||||
|
{
|
||||||
|
if (this.animation == null || this.animation.curAnim == null) return "";
|
||||||
|
return this.animation.curAnim.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a given animation exists before playing it.
|
||||||
|
* Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
function correctAnimationName(name:String):String
|
||||||
|
{
|
||||||
|
// If the animation exists, we're good.
|
||||||
|
if (hasAnimation(name)) return name;
|
||||||
|
|
||||||
|
trace('[BOPPER] Animation "$name" does not exist!');
|
||||||
|
|
||||||
|
// Attempt to strip a `-alt` suffix, if it exists.
|
||||||
|
if (name.lastIndexOf('-') != -1)
|
||||||
|
{
|
||||||
|
var correctName = name.substring(0, name.lastIndexOf('-'));
|
||||||
|
trace('[BOPPER] Attempting to fallback to "$correctName"');
|
||||||
|
return correctAnimationName(correctName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (name != 'idle')
|
||||||
|
{
|
||||||
|
trace('[BOPPER] Attempting to fallback to "idle"');
|
||||||
|
return correctAnimationName('idle');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('[BOPPER] Failing animation playback.');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAnimation(id:String):Bool
|
||||||
|
{
|
||||||
|
if (this.animation == null) return false;
|
||||||
|
|
||||||
|
return this.animation.getByName(id) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the animation offsets for a specific animation.
|
||||||
|
*/
|
||||||
|
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
||||||
|
{
|
||||||
|
animationOffsets.set(name, [xOffset, yOffset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an apply the animation offsets for a specific animation.
|
||||||
|
*/
|
||||||
|
function applyAnimationOffsets(name:String):Void
|
||||||
|
{
|
||||||
|
var offsets:Array<Float> = animationOffsets.get(name);
|
||||||
|
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
|
||||||
|
{
|
||||||
|
this.animOffsets = offsets;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.animOffsets = [0, 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onDialogueStart(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueLine(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueSkip(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDialogueEnd(event:DialogueScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onUpdate(event:UpdateScriptEvent):Void {}
|
||||||
|
|
||||||
|
public function onDestroy(event:ScriptEvent):Void
|
||||||
|
{
|
||||||
|
frames = null;
|
||||||
|
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
this.globalOffsets = [0, 0];
|
||||||
|
this.alpha = 0;
|
||||||
|
|
||||||
|
this.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||||
|
}
|
76
source/funkin/play/cutscene/dialogue/SpeakerData.hx
Normal file
76
source/funkin/play/cutscene/dialogue/SpeakerData.hx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package funkin.play.cutscene.dialogue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
159
source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
Normal file
159
source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package funkin.play.song;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import flixel.util.typeLimit.OneOfTwo;
|
import flixel.util.typeLimit.OneOfTwo;
|
||||||
|
import funkin.modding.events.ScriptEvent;
|
||||||
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.song.ScriptedSong;
|
import funkin.play.song.ScriptedSong;
|
||||||
import funkin.util.assets.DataAssets;
|
import funkin.util.assets.DataAssets;
|
||||||
import haxe.DynamicAccess;
|
import haxe.DynamicAccess;
|
||||||
|
@ -22,6 +24,7 @@ class SongDataParser
|
||||||
|
|
||||||
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
|
static final DEFAULT_SONG_ID:String = 'UNKNOWN';
|
||||||
static final SONG_DATA_PATH:String = 'songs/';
|
static final SONG_DATA_PATH:String = 'songs/';
|
||||||
|
static final MUSIC_DATA_PATH:String = 'music/';
|
||||||
static final SONG_DATA_SUFFIX:String = '-metadata.json';
|
static final SONG_DATA_SUFFIX:String = '-metadata.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,6 +187,36 @@ class SongDataParser
|
||||||
return rawJson;
|
return rawJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function parseMusicMetadata(musicId:String):SongMetadata
|
||||||
|
{
|
||||||
|
var rawJson:String = loadMusicMetadataFile(musicId);
|
||||||
|
var jsonData:Dynamic = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jsonData = Json.parse(rawJson);
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
var musicMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, musicId);
|
||||||
|
musicMetadata = SongValidator.validateSongMetadata(musicMetadata, musicId);
|
||||||
|
|
||||||
|
return musicMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function loadMusicMetadataFile(musicPath:String, variation:String = ''):String
|
||||||
|
{
|
||||||
|
var musicMetadataFilePath:String = (variation != '') ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json');
|
||||||
|
|
||||||
|
var rawJson:String = Assets.getText(musicMetadataFilePath).trim();
|
||||||
|
|
||||||
|
while (!rawJson.endsWith("}"))
|
||||||
|
{
|
||||||
|
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawJson;
|
||||||
|
}
|
||||||
|
|
||||||
public static function parseSongChartData(songId:String, variation:String = ''):SongChartData
|
public static function parseSongChartData(songId:String, variation:String = ''):SongChartData
|
||||||
{
|
{
|
||||||
var rawJson:String = loadSongChartDataFile(songId, variation);
|
var rawJson:String = loadSongChartDataFile(songId, variation);
|
||||||
|
@ -376,8 +409,7 @@ abstract SongNoteData(RawSongNoteData)
|
||||||
|
|
||||||
function get_stepTime():Float
|
function get_stepTime():Float
|
||||||
{
|
{
|
||||||
// TODO: Account for changes in BPM.
|
return Conductor.getTimeInSteps(this.t);
|
||||||
return this.t / Conductor.stepLengthMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -562,8 +594,7 @@ abstract SongEventData(RawSongEventData)
|
||||||
|
|
||||||
function get_stepTime():Float
|
function get_stepTime():Float
|
||||||
{
|
{
|
||||||
// TODO: Account for changes in BPM.
|
return Conductor.getTimeInSteps(this.t);
|
||||||
return this.t / Conductor.stepLengthMs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public var event(get, set):String;
|
public var event(get, set):String;
|
||||||
|
@ -805,7 +836,7 @@ typedef RawSongTimeChange =
|
||||||
* Time in beats (int). The game will calculate further beat values based on this one,
|
* Time in beats (int). The game will calculate further beat values based on this one,
|
||||||
* so it can do it in a simple linear fashion.
|
* so it can do it in a simple linear fashion.
|
||||||
*/
|
*/
|
||||||
var b:Int;
|
var b:Null<Float>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
||||||
|
@ -835,9 +866,9 @@ typedef RawSongTimeChange =
|
||||||
* Add aliases to the minimalized property names of the typedef,
|
* Add aliases to the minimalized property names of the typedef,
|
||||||
* to improve readability.
|
* to improve readability.
|
||||||
*/
|
*/
|
||||||
abstract SongTimeChange(RawSongTimeChange)
|
abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange
|
||||||
{
|
{
|
||||||
public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
|
public function new(timeStamp:Float, beatTime:Null<Float>, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
|
||||||
{
|
{
|
||||||
this =
|
this =
|
||||||
{
|
{
|
||||||
|
@ -862,7 +893,7 @@ abstract SongTimeChange(RawSongTimeChange)
|
||||||
return this.t = value;
|
return this.t = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public var beatTime(get, set):Int;
|
public var beatTime(get, set):Null<Float>;
|
||||||
|
|
||||||
function get_beatTime():Int
|
function get_beatTime():Int
|
||||||
{
|
{
|
||||||
|
|
|
@ -2057,7 +2057,7 @@ class ChartEditorState extends HaxeUIState
|
||||||
{
|
{
|
||||||
// Handle extending the note as you drag.
|
// Handle extending the note as you drag.
|
||||||
|
|
||||||
// Since use Math.floor and stepCrochet here, the hold notes will be beat snapped.
|
// Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped.
|
||||||
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
|
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
|
||||||
|
|
||||||
// Without this, the newly placed note feels too short compared to the user's input.
|
// Without this, the newly placed note feels too short compared to the user's input.
|
||||||
|
@ -2770,7 +2770,7 @@ class ChartEditorState extends HaxeUIState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override function dispatchEvent(event:ScriptEvent):Void
|
public override function dispatchEvent(event:ScriptEvent):Void
|
||||||
{
|
{
|
||||||
super.dispatchEvent(event);
|
super.dispatchEvent(event);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.play.PlayState;
|
import funkin.play.PlayState;
|
||||||
import funkin.play.PlayStatePlaylist;
|
import funkin.play.PlayStatePlaylist;
|
||||||
import funkin.play.song.Song;
|
import funkin.play.song.Song;
|
||||||
|
import funkin.play.song.SongData.SongMetadata;
|
||||||
import funkin.play.song.SongData.SongDataParser;
|
import funkin.play.song.SongData.SongDataParser;
|
||||||
import funkin.util.Constants;
|
import funkin.util.Constants;
|
||||||
|
|
||||||
|
@ -115,12 +116,7 @@ class StoryMenuState extends MusicBeatState
|
||||||
transIn = FlxTransitionableState.defaultTransIn;
|
transIn = FlxTransitionableState.defaultTransIn;
|
||||||
transOut = FlxTransitionableState.defaultTransOut;
|
transOut = FlxTransitionableState.defaultTransOut;
|
||||||
|
|
||||||
if (!FlxG.sound.music.playing)
|
playMenuMusic();
|
||||||
{
|
|
||||||
FlxG.sound.playMusic(Paths.music('freakyMenu'));
|
|
||||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
|
||||||
}
|
|
||||||
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
|
|
||||||
|
|
||||||
if (stickerSubState != null)
|
if (stickerSubState != null)
|
||||||
{
|
{
|
||||||
|
@ -129,8 +125,6 @@ class StoryMenuState extends MusicBeatState
|
||||||
|
|
||||||
openSubState(stickerSubState);
|
openSubState(stickerSubState);
|
||||||
stickerSubState.degenStickers();
|
stickerSubState.degenStickers();
|
||||||
|
|
||||||
// resetSubState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
persistentUpdate = persistentDraw = true;
|
persistentUpdate = persistentDraw = true;
|
||||||
|
@ -203,6 +197,18 @@ class StoryMenuState extends MusicBeatState
|
||||||
#end
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playMenuMusic():Void
|
||||||
|
{
|
||||||
|
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||||
|
{
|
||||||
|
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu');
|
||||||
|
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
|
||||||
|
|
||||||
|
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
|
||||||
|
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateData():Void
|
function updateData():Void
|
||||||
{
|
{
|
||||||
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
|
currentLevel = LevelRegistry.instance.fetchEntry(currentLevelId);
|
||||||
|
@ -459,7 +465,7 @@ class StoryMenuState extends MusicBeatState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override function dispatchEvent(event:ScriptEvent):Void
|
public override function dispatchEvent(event:ScriptEvent):Void
|
||||||
{
|
{
|
||||||
// super.dispatchEvent(event) dispatches event to module scripts.
|
// super.dispatchEvent(event) dispatches event to module scripts.
|
||||||
super.dispatchEvent(event);
|
super.dispatchEvent(event);
|
||||||
|
|
|
@ -118,27 +118,72 @@ class Constants
|
||||||
public static final DEFAULT_SONG:String = 'tutorial';
|
public static final DEFAULT_SONG:String = 'tutorial';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OTHER
|
* TIMING
|
||||||
*/
|
*/
|
||||||
// ==============================
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds in a minute.
|
||||||
|
*/
|
||||||
|
public static final SECS_PER_MIN:Int = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final MS_PER_SEC:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of microseconds in a millisecond.
|
||||||
|
*/
|
||||||
|
public static final US_PER_MS:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of microseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a microsecond.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_US:Int = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a millisecond.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of nanoseconds in a second.
|
||||||
|
*/
|
||||||
|
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All MP3 decoders introduce a playback delay of `528` samples,
|
* All MP3 decoders introduce a playback delay of `528` samples,
|
||||||
* which at 44,100 Hz (samples per second) is ~12 ms.
|
* which at 44,100 Hz (samples per second) is ~12 ms.
|
||||||
*/
|
*/
|
||||||
public static final MP3_DELAY_MS:Float = 528 / 44100 * 1000;
|
public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default BPM of the conductor.
|
||||||
|
*/
|
||||||
|
public static final DEFAULT_BPM:Float = 100.0;
|
||||||
|
|
||||||
|
public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
|
||||||
|
|
||||||
|
public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
|
||||||
|
|
||||||
|
public static final STEPS_PER_BEAT:Int = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTHER
|
||||||
|
*/
|
||||||
|
// ==============================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scale factor to use when increasing the size of pixel art graphics.
|
* The scale factor to use when increasing the size of pixel art graphics.
|
||||||
*/
|
*/
|
||||||
public static final PIXEL_ART_SCALE:Float = 6;
|
public static final PIXEL_ART_SCALE:Float = 6;
|
||||||
|
|
||||||
/**
|
|
||||||
* The BPM of the title screen and menu music.
|
|
||||||
* TODO: Move to metadata file.
|
|
||||||
*/
|
|
||||||
public static final FREAKY_MENU_BPM:Float = 102;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The volume at which to play the countdown before the song starts.
|
* The volume at which to play the countdown before the song starts.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue