diff --git a/Project.xml b/Project.xml
index b8f5a0c88..a251ccdbf 100644
--- a/Project.xml
+++ b/Project.xml
@@ -151,16 +151,23 @@
+
+
+
-
-
-
-
+
+
+
+
+
diff --git a/hxformat.json b/hxformat.json
index 3eeb6de92..2a7775dda 100644
--- a/hxformat.json
+++ b/hxformat.json
@@ -2,11 +2,14 @@
"lineEnds": {
"leftCurly": "both",
"rightCurly": "both",
- "emptyCurly": "break",
+ "emptyCurly": "noBreak",
"objectLiteralCurly": {
"leftCurly": "after"
}
},
+ "indentation": {
+ "character": " "
+ },
"sameLine": {
"ifElse": "next",
"doWhile": "next",
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 425ce25ae..8fb057ae9 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -7,297 +7,295 @@ import funkin.play.song.SongData.SongTimeChange;
typedef BPMChangeEvent =
{
- var stepTime:Int;
- var songTime:Float;
- var bpm:Float;
+ var stepTime:Int;
+ var songTime:Float;
+ var bpm:Float;
}
class Conductor
{
- /**
- * 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.
- */
- private static var timeChanges:Array = [];
+ /**
+ * 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.
+ */
+ private static var timeChanges:Array = [];
- /**
- * The current time change.
- */
- private static var currentTimeChange:SongTimeChange;
+ /**
+ * The current time change.
+ */
+ private static var currentTimeChange:SongTimeChange;
- /**
- * The current position in the song in milliseconds.
- * Updated every frame based on the audio position.
- */
- public static var songPosition:Float;
+ /**
+ * The current position in the song in milliseconds.
+ * Updated every frame based on the audio position.
+ */
+ public static var songPosition:Float;
- /**
- * Beats per minute of the current song at the current time.
- */
- public static var bpm(get, null):Float;
+ /**
+ * Beats per minute of the current song at the current time.
+ */
+ public static var bpm(get, null):Float;
- static function get_bpm():Float
- {
- if (bpmOverride != null)
- return bpmOverride;
+ static function get_bpm():Float
+ {
+ if (bpmOverride != null)
+ return bpmOverride;
- if (currentTimeChange == null)
- return 100;
+ if (currentTimeChange == null)
+ return 100;
- return currentTimeChange.bpm;
- }
+ return currentTimeChange.bpm;
+ }
- static var bpmOverride:Null = null;
+ static var bpmOverride:Null = null;
- // OLD, replaced with timeChanges.
- public static var bpmChangeMap:Array = [];
+ // OLD, replaced with timeChanges.
+ public static var bpmChangeMap:Array = [];
- /**
- * Duration of a beat in millisecond. Calculated based on bpm.
- */
- public static var crochet(get, null):Float;
+ /**
+ * Duration of a beat in millisecond. Calculated based on bpm.
+ */
+ public static var crochet(get, null):Float;
- static function get_crochet():Float
- {
- return ((60 / bpm) * 1000);
- }
+ static function get_crochet():Float
+ {
+ return ((60 / bpm) * 1000);
+ }
- /**
- * Duration of a step (quarter) in milliseconds. Calculated based on bpm.
- */
- public static var stepCrochet(get, null):Float;
+ /**
+ * Duration of a step (quarter) in milliseconds. Calculated based on bpm.
+ */
+ public static var stepCrochet(get, null):Float;
- static function get_stepCrochet():Float
- {
- return crochet / timeSignatureNumerator;
- }
+ static function get_stepCrochet():Float
+ {
+ return crochet / timeSignatureNumerator;
+ }
- public static var timeSignatureNumerator(get, null):Int;
+ public static var timeSignatureNumerator(get, null):Int;
- static function get_timeSignatureNumerator():Int
- {
- if (currentTimeChange == null)
- return 4;
+ static function get_timeSignatureNumerator():Int
+ {
+ if (currentTimeChange == null)
+ return 4;
- return currentTimeChange.timeSignatureNum;
- }
+ return currentTimeChange.timeSignatureNum;
+ }
- public static var timeSignatureDenominator(get, null):Int;
+ public static var timeSignatureDenominator(get, null):Int;
- static function get_timeSignatureDenominator():Int
- {
- if (currentTimeChange == null)
- return 4;
+ static function get_timeSignatureDenominator():Int
+ {
+ if (currentTimeChange == null)
+ return 4;
- return currentTimeChange.timeSignatureDen;
- }
+ return currentTimeChange.timeSignatureDen;
+ }
- /**
- * Current position in the song, in beats.
- **/
- public static var currentBeat(default, null):Int;
+ /**
+ * Current position in the song, in beats.
+ **/
+ public static var currentBeat(default, null):Int;
- /**
- * Current position in the song, in steps.
- */
- public static var currentStep(default, null):Int;
+ /**
+ * Current position in the song, in 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;
+ /**
+ * 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 stepHit(default, null):FlxSignal = new FlxSignal();
+ public static var beatHit(default, null):FlxSignal = new FlxSignal();
+ public static var stepHit(default, null):FlxSignal = new FlxSignal();
- 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 lastSongPos:Float;
+ public static var visualOffset:Float = 0;
+ public static var audioOffset:Float = 0;
+ public static var offset:Float = 0;
- // TODO: Add code to update this.
- public static var beatsPerMeasure(get, null):Int;
+ // TODO: Add code to update this.
+ public static var beatsPerMeasure(get, null):Int;
- static function get_beatsPerMeasure():Int
- {
- return timeSignatureNumerator;
- }
+ static function get_beatsPerMeasure():Int
+ {
+ return timeSignatureNumerator;
+ }
- public static var stepsPerMeasure(get, null):Int;
+ public static var stepsPerMeasure(get, null):Int;
- static function get_stepsPerMeasure():Int
- {
- // Is this always x4?
- return timeSignatureNumerator * 4;
- }
+ static function get_stepsPerMeasure():Int
+ {
+ // Is this always x4?
+ return timeSignatureNumerator * 4;
+ }
- private function new()
- {
- }
+ private function new() {}
- public static function getLastBPMChange()
- {
- var lastChange:BPMChangeEvent = {
- stepTime: 0,
- songTime: 0,
- bpm: 0
- }
- for (i in 0...Conductor.bpmChangeMap.length)
- {
- if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime)
- lastChange = Conductor.bpmChangeMap[i];
+ public static function getLastBPMChange()
+ {
+ var lastChange:BPMChangeEvent = {
+ stepTime: 0,
+ songTime: 0,
+ bpm: 0
+ }
+ for (i in 0...Conductor.bpmChangeMap.length)
+ {
+ if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime)
+ lastChange = Conductor.bpmChangeMap[i];
- if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime)
- break;
- }
- return lastChange;
- }
+ if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime)
+ break;
+ }
+ return lastChange;
+ }
- /**
- * Forcibly defines the current BPM of the song.
- * Useful for things like the chart editor that need to manipulate BPM in real time.
- *
- * Set to null to reset to the BPM defined by the timeChanges.
- *
- * WARNING: Avoid this for things like setting the BPM of the title screen music,
- * you should have a metadata file for it instead.
- */
- public static function forceBPM(?bpm:Float = null)
- {
- if (bpm != null)
- trace('[CONDUCTOR] Forcing BPM to ' + bpm);
- else
- trace('[CONDUCTOR] Resetting BPM to default');
- Conductor.bpmOverride = bpm;
- }
+ /**
+ * Forcibly defines the current BPM of the song.
+ * Useful for things like the chart editor that need to manipulate BPM in real time.
+ *
+ * Set to null to reset to the BPM defined by the timeChanges.
+ *
+ * WARNING: Avoid this for things like setting the BPM of the title screen music,
+ * you should have a metadata file for it instead.
+ */
+ public static function forceBPM(?bpm:Float = null)
+ {
+ if (bpm != null)
+ trace('[CONDUCTOR] Forcing BPM to ' + bpm);
+ else
+ trace('[CONDUCTOR] Resetting BPM to default');
+ Conductor.bpmOverride = bpm;
+ }
- /**
- * Update the conductor with the current song position.
- * BPM, current step, etc. will be re-calculated based on the song position.
- *
- * @param songPosition The current position in the song in milliseconds.
- * Leave blank to use the FlxG.sound.music position.
- */
- public static function update(songPosition:Float = null)
- {
- if (songPosition == null)
- songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + Conductor.offset) : 0;
+ /**
+ * Update the conductor with the current song position.
+ * BPM, current step, etc. will be re-calculated based on the song position.
+ *
+ * @param songPosition The current position in the song in milliseconds.
+ * Leave blank to use the FlxG.sound.music position.
+ */
+ public static function update(songPosition:Float = null)
+ {
+ if (songPosition == null)
+ songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0;
- var oldBeat = currentBeat;
- var oldStep = currentStep;
+ var oldBeat = currentBeat;
+ var oldStep = currentStep;
- Conductor.songPosition = songPosition;
- // Conductor.bpm = Conductor.getLastBPMChange().bpm;
+ Conductor.songPosition = songPosition;
+ // Conductor.bpm = Conductor.getLastBPMChange().bpm;
- currentTimeChange = timeChanges[0];
- for (i in 0...timeChanges.length)
- {
- if (songPosition >= timeChanges[i].timeStamp)
- currentTimeChange = timeChanges[i];
+ currentTimeChange = timeChanges[0];
+ for (i in 0...timeChanges.length)
+ {
+ if (songPosition >= timeChanges[i].timeStamp)
+ currentTimeChange = timeChanges[i];
- if (songPosition < timeChanges[i].timeStamp)
- break;
- }
+ if (songPosition < timeChanges[i].timeStamp)
+ break;
+ }
- if (currentTimeChange == null && bpmOverride == null)
- {
- trace('WARNING: Conductor is broken, timeChanges is empty.');
- }
- else if (currentTimeChange != null)
- {
- currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet;
- currentStep = Math.floor(currentStepTime);
- currentBeat = Math.floor(currentStep / 4);
- }
- else
- {
- // Assume a constant BPM equal to the forced value.
- currentStepTime = (songPosition / stepCrochet);
- currentStep = Math.floor(currentStepTime);
- currentBeat = Math.floor(currentStep / 4);
- }
+ if (currentTimeChange == null && bpmOverride == null && FlxG.sound.music != null)
+ {
+ trace('WARNING: Conductor is broken, timeChanges is empty.');
+ }
+ else if (currentTimeChange != null)
+ {
+ currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet;
+ currentStep = Math.floor(currentStepTime);
+ currentBeat = Math.floor(currentStep / 4);
+ }
+ else
+ {
+ // Assume a constant BPM equal to the forced value.
+ currentStepTime = (songPosition / stepCrochet);
+ currentStep = Math.floor(currentStepTime);
+ currentBeat = Math.floor(currentStep / 4);
+ }
- // FlxSignals are really cool.
- if (currentStep != oldStep)
- stepHit.dispatch();
+ // FlxSignals are really cool.
+ if (currentStep != oldStep)
+ stepHit.dispatch();
- if (currentBeat != oldBeat)
- beatHit.dispatch();
- }
+ if (currentBeat != oldBeat)
+ beatHit.dispatch();
+ }
- @:deprecated // Switch to TimeChanges instead.
- public static function mapBPMChanges(song:SwagSong)
- {
- bpmChangeMap = [];
+ @:deprecated // Switch to TimeChanges instead.
+ public static function mapBPMChanges(song:SwagSong)
+ {
+ bpmChangeMap = [];
- var curBPM:Float = song.bpm;
- var totalSteps:Int = 0;
- var totalPos:Float = 0;
- for (i in 0...SongLoad.getSong().length)
- {
- if (SongLoad.getSong()[i].changeBPM && SongLoad.getSong()[i].bpm != curBPM)
- {
- curBPM = SongLoad.getSong()[i].bpm;
- var event:BPMChangeEvent = {
- stepTime: totalSteps,
- songTime: totalPos,
- bpm: curBPM
- };
- bpmChangeMap.push(event);
- }
+ var curBPM:Float = song.bpm;
+ var totalSteps:Int = 0;
+ var totalPos:Float = 0;
+ for (i in 0...SongLoad.getSong().length)
+ {
+ if (SongLoad.getSong()[i].changeBPM && SongLoad.getSong()[i].bpm != curBPM)
+ {
+ curBPM = SongLoad.getSong()[i].bpm;
+ var event:BPMChangeEvent = {
+ stepTime: totalSteps,
+ songTime: totalPos,
+ bpm: curBPM
+ };
+ bpmChangeMap.push(event);
+ }
- var deltaSteps:Int = SongLoad.getSong()[i].lengthInSteps;
- totalSteps += deltaSteps;
- totalPos += ((60 / curBPM) * 1000 / 4) * deltaSteps;
- }
- }
+ var deltaSteps:Int = SongLoad.getSong()[i].lengthInSteps;
+ totalSteps += deltaSteps;
+ totalPos += ((60 / curBPM) * 1000 / 4) * deltaSteps;
+ }
+ }
- public static function mapTimeChanges(songTimeChanges:Array)
- {
- timeChanges = [];
+ public static function mapTimeChanges(songTimeChanges:Array)
+ {
+ timeChanges = [];
- for (currentTimeChange in songTimeChanges)
- {
- timeChanges.push(currentTimeChange);
- }
+ for (currentTimeChange in songTimeChanges)
+ {
+ timeChanges.push(currentTimeChange);
+ }
- trace('Done mapping time changes: ' + timeChanges);
+ trace('Done mapping time changes: ' + timeChanges);
- // Done.
- }
+ // Done.
+ }
- /**
- * Given a time in milliseconds, return a time in steps.
- */
- public static function getTimeInSteps(ms:Float):Int
- {
- if (timeChanges.length == 0)
- {
- // Assume a constant BPM equal to the forced value.
- return Math.floor(ms / stepCrochet);
- }
- else
- {
- var resultStep:Int = 0;
+ /**
+ * Given a time in milliseconds, return a time in steps.
+ */
+ public static function getTimeInSteps(ms:Float):Int
+ {
+ if (timeChanges.length == 0)
+ {
+ // Assume a constant BPM equal to the forced value.
+ return Math.floor(ms / stepCrochet);
+ }
+ else
+ {
+ var resultStep:Int = 0;
- var lastTimeChange:SongTimeChange = timeChanges[0];
- for (timeChange in timeChanges)
- {
- if (ms >= timeChange.timeStamp)
- {
- lastTimeChange = timeChange;
- resultStep = lastTimeChange.beatTime * 4;
- }
- else
- {
- // This time change is after the requested time.
- break;
- }
- }
+ var lastTimeChange:SongTimeChange = timeChanges[0];
+ for (timeChange in timeChanges)
+ {
+ if (ms >= timeChange.timeStamp)
+ {
+ lastTimeChange = timeChange;
+ resultStep = lastTimeChange.beatTime * 4;
+ }
+ else
+ {
+ // This time change is after the requested time.
+ break;
+ }
+ }
- resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet);
+ resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet);
- return resultStep;
- }
- }
+ return resultStep;
+ }
+ }
}
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 60d8f71c6..664613b1c 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -38,905 +38,906 @@ import lime.utils.Assets;
class FreeplayState extends MusicBeatSubstate
{
- var songs:Array = [];
+ var songs:Array = [];
- // var selector:FlxText;
- var curSelected:Int = 0;
- var curDifficulty:Int = 1;
+ // var selector:FlxText;
+ var curSelected:Int = 0;
+ var curDifficulty:Int = 1;
- var fp:FreeplayScore;
- var txtCompletion:FlxText;
- var lerpCompletion:Float = 0;
- var intendedCompletion:Float = 0;
- var lerpScore:Float = 0;
- var intendedScore:Int = 0;
+ var fp:FreeplayScore;
+ var txtCompletion:FlxText;
+ var lerpCompletion:Float = 0;
+ var intendedCompletion:Float = 0;
+ var lerpScore:Float = 0;
+ var intendedScore:Int = 0;
- var grpDifficulties:FlxSpriteGroup;
+ var grpDifficulties:FlxSpriteGroup;
- var coolColors:Array = [
- 0xff9271fd,
- 0xff9271fd,
- 0xff223344,
- 0xFF941653,
- 0xFFfc96d7,
- 0xFFa0d1ff,
- 0xffff78bf,
- 0xfff6b604
- ];
+ var coolColors:Array = [
+ 0xff9271fd,
+ 0xff9271fd,
+ 0xff223344,
+ 0xFF941653,
+ 0xFFfc96d7,
+ 0xFFa0d1ff,
+ 0xffff78bf,
+ 0xfff6b604
+ ];
- private var grpSongs:FlxTypedGroup;
- private var grpCapsules:FlxTypedGroup;
- private var curPlaying:Bool = false;
+ private var grpSongs:FlxTypedGroup;
+ private var grpCapsules:FlxTypedGroup;
+ private var curPlaying:Bool = false;
- private var dj:DJBoyfriend;
+ private var dj:DJBoyfriend;
- private var iconArray:Array = [];
+ private var iconArray:Array = [];
- var typing:FlxInputText;
+ var typing:FlxInputText;
- override function create()
- {
- FlxTransitionableState.skipNextTransIn = true;
+ override function create()
+ {
+ FlxTransitionableState.skipNextTransIn = true;
- #if discord_rpc
- // Updating Discord Rich Presence
- DiscordClient.changePresence("In the Menus", null);
- #end
+ #if discord_rpc
+ // Updating Discord Rich Presence
+ DiscordClient.changePresence("In the Menus", null);
+ #end
- var isDebug:Bool = false;
+ var isDebug:Bool = false;
- #if debug
- isDebug = true;
- addSong('Test', 1, 'bf-pixel');
- addSong('Pyro', 8, 'darnell');
- #end
+ #if debug
+ isDebug = true;
+ addSong('Test', 1, 'bf-pixel');
+ addSong('Pyro', 8, 'darnell');
+ #end
- var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
+ var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
- for (i in 0...initSonglist.length)
- {
- songs.push(new SongMetadata(initSonglist[i], 1, 'gf'));
- }
+ for (i in 0...initSonglist.length)
+ {
+ songs.push(new SongMetadata(initSonglist[i], 1, 'gf'));
+ }
- if (FlxG.sound.music != null)
- {
- if (!FlxG.sound.music.playing)
- FlxG.sound.playMusic(Paths.music('freakyMenu'));
- }
+ if (FlxG.sound.music != null)
+ {
+ if (!FlxG.sound.music.playing)
+ FlxG.sound.playMusic(Paths.music('freakyMenu'));
+ }
- if (StoryMenuState.weekUnlocked[2] || isDebug)
- addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']);
+ if (StoryMenuState.weekUnlocked[2] || isDebug)
+ addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']);
- if (StoryMenuState.weekUnlocked[2] || isDebug)
- addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']);
+ if (StoryMenuState.weekUnlocked[2] || isDebug)
+ addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']);
- if (StoryMenuState.weekUnlocked[3] || isDebug)
- addWeek(['Pico', 'Philly', 'Blammed'], 3, ['pico']);
+ if (StoryMenuState.weekUnlocked[3] || isDebug)
+ addWeek(['Pico', 'Philly', 'Blammed'], 3, ['pico']);
- if (StoryMenuState.weekUnlocked[4] || isDebug)
- addWeek(['Satin-Panties', 'High', 'Milf'], 4, ['mom']);
+ if (StoryMenuState.weekUnlocked[4] || isDebug)
+ addWeek(['Satin-Panties', 'High', 'Milf'], 4, ['mom']);
- if (StoryMenuState.weekUnlocked[5] || isDebug)
- addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 5, ['parents-christmas', 'parents-christmas', 'monster-christmas']);
+ if (StoryMenuState.weekUnlocked[5] || isDebug)
+ addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 5, ['parents-christmas', 'parents-christmas', 'monster-christmas']);
- if (StoryMenuState.weekUnlocked[6] || isDebug)
- addWeek(['Senpai', 'Roses', 'Thorns'], 6, ['senpai', 'senpai', 'spirit']);
+ if (StoryMenuState.weekUnlocked[6] || isDebug)
+ addWeek(['Senpai', 'Roses', 'Thorns'], 6, ['senpai', 'senpai', 'spirit']);
- if (StoryMenuState.weekUnlocked[7] || isDebug)
- addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']);
+ if (StoryMenuState.weekUnlocked[7] || isDebug)
+ addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']);
- addWeek(["Darnell", "lit-up", "2hot", "blazin"], 8, ['darnell']);
+ addWeek(["Darnell", "lit-up", "2hot", "blazin"], 8, ['darnell']);
- // LOAD MUSIC
+ // LOAD MUSIC
- // LOAD CHARACTERS
+ // LOAD CHARACTERS
- trace(FlxG.width);
- trace(FlxG.camera.zoom);
- trace(FlxG.camera.initialZoom);
- trace(FlxCamera.defaultZoom);
+ trace(FlxG.width);
+ trace(FlxG.camera.zoom);
+ trace(FlxG.camera.initialZoom);
+ trace(FlxCamera.defaultZoom);
- var pinkBack:FlxSprite = new FlxSprite().loadGraphic(Paths.image('freeplay/pinkBack'));
- pinkBack.color = 0xFFffd4e9; // sets it to pink!
- pinkBack.x -= pinkBack.width;
+ var pinkBack:FlxSprite = new FlxSprite().loadGraphic(Paths.image('freeplay/pinkBack'));
+ pinkBack.color = 0xFFffd4e9; // sets it to pink!
+ pinkBack.x -= pinkBack.width;
- FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
- add(pinkBack);
+ FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
+ add(pinkBack);
- var orangeBackShit:FlxSprite = new FlxSprite(84, FlxG.height * 0.68).makeGraphic(Std.int(pinkBack.width), 50, 0xFFffd400);
- add(orangeBackShit);
+ var orangeBackShit:FlxSprite = new FlxSprite(84, FlxG.height * 0.68).makeGraphic(Std.int(pinkBack.width), 50, 0xFFffd400);
+ add(orangeBackShit);
- var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400);
- add(alsoOrangeLOL);
+ var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400);
+ add(alsoOrangeLOL);
- FlxSpriteUtil.alphaMaskFlxSprite(orangeBackShit, pinkBack, orangeBackShit);
- orangeBackShit.visible = false;
- alsoOrangeLOL.visible = false;
+ FlxSpriteUtil.alphaMaskFlxSprite(orangeBackShit, pinkBack, orangeBackShit);
+ orangeBackShit.visible = false;
+ alsoOrangeLOL.visible = false;
- var grpTxtScrolls:FlxGroup = new FlxGroup();
- add(grpTxtScrolls);
- grpTxtScrolls.visible = false;
+ var grpTxtScrolls:FlxGroup = new FlxGroup();
+ add(grpTxtScrolls);
+ grpTxtScrolls.visible = false;
- var moreWays:BGScrollingText = new BGScrollingText(0, 200, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
- moreWays.funnyColor = 0xFFfff383;
- moreWays.speed = 4;
- grpTxtScrolls.add(moreWays);
+ var moreWays:BGScrollingText = new BGScrollingText(0, 200, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
+ moreWays.funnyColor = 0xFFfff383;
+ moreWays.speed = 4;
+ grpTxtScrolls.add(moreWays);
- var funnyScroll:BGScrollingText = new BGScrollingText(0, 250, "BOYFRIEND", FlxG.width / 2);
- funnyScroll.funnyColor = 0xFFff9963;
- funnyScroll.speed = -1;
- grpTxtScrolls.add(funnyScroll);
+ var funnyScroll:BGScrollingText = new BGScrollingText(0, 250, "BOYFRIEND", FlxG.width / 2);
+ funnyScroll.funnyColor = 0xFFff9963;
+ funnyScroll.speed = -1;
+ grpTxtScrolls.add(funnyScroll);
- var txtNuts:BGScrollingText = new BGScrollingText(0, 300, "PROTECT YO NUTS", FlxG.width / 2);
- grpTxtScrolls.add(txtNuts);
+ var txtNuts:BGScrollingText = new BGScrollingText(0, 300, "PROTECT YO NUTS", FlxG.width / 2);
+ grpTxtScrolls.add(txtNuts);
- var funnyScroll2:BGScrollingText = new BGScrollingText(0, 340, "BOYFRIEND", FlxG.width / 2);
- funnyScroll2.funnyColor = 0xFFff9963;
- funnyScroll2.speed = -1.2;
- grpTxtScrolls.add(funnyScroll2);
+ var funnyScroll2:BGScrollingText = new BGScrollingText(0, 340, "BOYFRIEND", FlxG.width / 2);
+ funnyScroll2.funnyColor = 0xFFff9963;
+ funnyScroll2.speed = -1.2;
+ grpTxtScrolls.add(funnyScroll2);
- var moreWays2:BGScrollingText = new BGScrollingText(0, 400, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
- moreWays2.funnyColor = 0xFFfff383;
- moreWays2.speed = 4.4;
- grpTxtScrolls.add(moreWays2);
-
- var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y, "BOYFRIEND", FlxG.width / 2);
- funnyScroll3.funnyColor = 0xFFff9963;
- funnyScroll3.speed = -0.8;
- grpTxtScrolls.add(funnyScroll3);
-
- dj = new DJBoyfriend(0, -100);
- add(dj);
-
- var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
- bgDad.setGraphicSize(0, FlxG.height);
- bgDad.updateHitbox();
- bgDad.shader = new AngleMask();
- bgDad.visible = false;
-
- var blackOverlayBullshitLOLXD:FlxSprite = new FlxSprite(FlxG.width).makeGraphic(Std.int(bgDad.width), Std.int(bgDad.height), FlxColor.BLACK);
- add(blackOverlayBullshitLOLXD); // used to mask the text lol!
-
- add(bgDad);
- FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 1, {ease: FlxEase.quintOut});
-
- blackOverlayBullshitLOLXD.shader = bgDad.shader;
-
- grpSongs = new FlxTypedGroup();
- add(grpSongs);
-
- grpCapsules = new FlxTypedGroup();
- add(grpCapsules);
-
- grpDifficulties = new FlxSpriteGroup(-300, 80);
- add(grpDifficulties);
-
- grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayEasy')));
- grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayNorm')));
- grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayHard')));
-
- grpDifficulties.group.forEach(function(spr)
- {
- spr.visible = false;
- });
-
- grpDifficulties.group.members[curDifficulty].visible = true;
-
- var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK);
- overhangStuff.y -= overhangStuff.height;
- add(overhangStuff);
- FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
-
- var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
- fnfFreeplay.font = "VCR OSD Mono";
- fnfFreeplay.visible = false;
- var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
- fnfFreeplay.shader = sillyStroke;
- add(fnfFreeplay);
-
- var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
- fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
- fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
- fnfHighscoreSpr.visible = false;
- fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
- fnfHighscoreSpr.antialiasing = true;
- fnfHighscoreSpr.updateHitbox();
- add(fnfHighscoreSpr);
-
- new FlxTimer().start(FlxG.random.float(12, 50), function(tmr)
- {
- fnfHighscoreSpr.animation.play("highscore");
- tmr.time = FlxG.random.float(20, 60);
- }, 0);
-
- fp = new FreeplayScore(460, 60, 100);
- fp.visible = false;
- add(fp);
-
- txtCompletion = new FlxText(1200, 77, 0, "0", 32);
- txtCompletion.font = "VCR OSD Mono";
- txtCompletion.visible = false;
- add(txtCompletion);
-
- dj.onIntroDone.add(function()
- {
- FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
-
- add(new DifficultySelector(20, grpDifficulties.y - 10, false, controls));
- add(new DifficultySelector(325, grpDifficulties.y - 10, true, controls));
-
- var letterSort:LetterSort = new LetterSort(300, 100);
- add(letterSort);
-
- letterSort.changeSelectionCallback = (str) ->
- {
- switch (str)
- {
- case "fav":
- generateSongList({filterType: FAVORITE}, true);
- case "ALL":
- generateSongList(null, true);
- default:
- generateSongList({filterType: STARTSWITH, filterData: str}, true);
- }
- };
-
- new FlxTimer().start(1 / 24, function(handShit)
- {
- fnfHighscoreSpr.visible = true;
- fnfFreeplay.visible = true;
- fp.visible = true;
- fp.updateScore(0);
-
- txtCompletion.visible = true;
- intendedCompletion = 0;
-
- new FlxTimer().start(1.5 / 24, function(bold)
- {
- sillyStroke.width = 0;
- sillyStroke.height = 0;
- });
- });
-
- pinkBack.color = 0xFFffd863;
- // fnfFreeplay.visible = true;
- bgDad.visible = true;
- orangeBackShit.visible = true;
- alsoOrangeLOL.visible = true;
- grpTxtScrolls.visible = true;
- });
-
- generateSongList();
-
- // FlxG.sound.playMusic(Paths.music('title'), 0);
- // FlxG.sound.music.fadeIn(2, 0, 0.8);
- // selector = new FlxText();
-
- // selector.size = 40;
- // selector.text = ">";
- // add(selector);
-
- var swag:Alphabet = new Alphabet(1, 0, "swag");
-
- // JUST DOIN THIS SHIT FOR TESTING!!!
- /*
- var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md'));
-
- var texFel:TextField = new TextField();
- texFel.width = FlxG.width;
- texFel.height = FlxG.height;
- // texFel.
- texFel.htmlText = md;
-
- FlxG.stage.addChild(texFel);
-
- trace(md);
- */
-
- var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
- funnyCam.bgColor = FlxColor.TRANSPARENT;
- FlxG.cameras.add(funnyCam);
-
- typing = new FlxInputText(100, 100);
- add(typing);
-
- typing.callback = function(txt, action)
- {
- // generateSongList(new EReg(txt.trim(), "ig"));
- trace(action);
- };
-
- forEach(function(bs)
- {
- bs.cameras = [funnyCam];
- });
-
- super.create();
- }
-
- public function generateSongList(?filterStuff:SongFilter, ?force:Bool = false)
- {
- curSelected = 0;
-
- grpCapsules.clear();
-
- // var regexp:EReg = regexp;
- var tempSongs:Array = songs;
-
- if (filterStuff != null)
- {
- switch (filterStuff.filterType)
- {
- case STARTSWITH:
- tempSongs = tempSongs.filter(str ->
- {
- return str.songName.toLowerCase().startsWith(filterStuff.filterData);
- });
- case ALL:
- // no filter!
- case FAVORITE:
- tempSongs = tempSongs.filter(str ->
- {
- return str.isFav;
- });
- default:
- // return all on default
- }
- }
-
- // if (regexp != null)
- // tempSongs = songs.filter(item -> regexp.match(item.songName));
-
- // tempSongs.sort(function(a, b):Int
- // {
- // var tempA = a.songName.toUpperCase();
- // var tempB = b.songName.toUpperCase();
-
- // if (tempA < tempB)
- // return -1;
- // else if (tempA > tempB)
- // return 1;
- // else
- // return 0;
- // });
-
- for (i in 0...tempSongs.length)
- {
- var funnyMenu:SongMenuItem = new SongMenuItem(FlxG.width, (i * 150) + 160, tempSongs[i].songName);
- funnyMenu.targetPos.x = funnyMenu.x;
- funnyMenu.ID = i;
- funnyMenu.alpha = 0.5;
- funnyMenu.songText.visible = false;
- funnyMenu.favIcon.visible = tempSongs[i].isFav;
-
- // fp.updateScore(0);
-
- new FlxTimer().start((1 / 24) * i, function(doShit)
- {
- funnyMenu.doJumpIn = true;
- });
-
- new FlxTimer().start((0.09 * i) + 0.85, function(lerpTmr)
- {
- funnyMenu.doLerp = true;
- });
-
- if (!force)
- {
- new FlxTimer().start(((0.20 * i) / (1 + i)) + 0.75, function(swagShi)
- {
- funnyMenu.songText.visible = true;
- funnyMenu.alpha = 1;
- });
- }
- else
- {
- funnyMenu.songText.visible = true;
- funnyMenu.alpha = 1;
- }
-
- grpCapsules.add(funnyMenu);
-
- var songText:Alphabet = new Alphabet(0, (70 * i) + 30, tempSongs[i].songName, true, false);
- songText.x += 100;
- songText.isMenuItem = true;
- songText.targetY = i;
-
- // grpSongs.add(songText);
-
- var icon:HealthIcon = new HealthIcon(tempSongs[i].songCharacter);
- // icon.sprTracker = songText;
-
- // using a FlxGroup is too much fuss!
- iconArray.push(icon);
- // add(icon);
-
- // songText.x += 40;
- // DONT PUT X IN THE FIRST PARAMETER OF new ALPHABET() !!
- // songText.screenCenter(X);
- }
-
- changeSelection();
- changeDiff();
- }
-
- public function addSong(songName:String, weekNum:Int, songCharacter:String)
- {
- songs.push(new SongMetadata(songName, weekNum, songCharacter));
- }
-
- public function addWeek(songs:Array, weekNum:Int, ?songCharacters:Array)
- {
- if (songCharacters == null)
- songCharacters = ['bf'];
-
- var num:Int = 0;
- for (song in songs)
- {
- addSong(song, weekNum, songCharacters[num]);
-
- if (songCharacters.length != 1)
- num++;
- }
- }
-
- var touchY:Float = 0;
- var touchX:Float = 0;
- var dxTouch:Float = 0;
- var dyTouch:Float = 0;
- var velTouch:Float = 0;
-
- var veloctiyLoopShit:Float = 0;
- var touchTimer:Float = 0;
-
- var initTouchPos:FlxPoint = new FlxPoint();
-
- var spamTimer:Float = 0;
- var spamming:Bool = false;
-
- override function update(elapsed:Float)
- {
- super.update(elapsed);
-
- if (FlxG.keys.justPressed.F)
- {
- var realShit = curSelected;
- songs[curSelected].isFav = !songs[curSelected].isFav;
- if (songs[curSelected].isFav)
- {
- FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, {
- ease: FlxEase.elasticOut,
- onComplete: _ ->
- {
- grpCapsules.members[realShit].favIcon.visible = true;
- grpCapsules.members[realShit].favIcon.animation.play("fav");
- }
- });
- }
- else
- {
- grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
- new FlxTimer().start((1 / 24) * 14, _ ->
- {
- grpCapsules.members[realShit].favIcon.visible = false;
- });
- new FlxTimer().start((1 / 24) * 24, _ ->
- {
- FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
- });
- }
- }
-
- if (FlxG.keys.justPressed.T)
- typing.hasFocus = true;
-
- if (FlxG.sound.music != null)
- {
- if (FlxG.sound.music.volume < 0.7)
- {
- FlxG.sound.music.volume += 0.5 * elapsed;
- }
- }
-
- lerpScore = CoolUtil.coolLerp(lerpScore, intendedScore, 0.2);
- lerpCompletion = CoolUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
-
- fp.updateScore(Std.int(lerpScore));
-
- txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
- trace(Highscore.getCompletion(songs[curSelected].songName, curDifficulty));
-
- // trace(intendedScore);
- // trace(lerpScore);
- // Highscore.getAllScores();
-
- var upP = controls.UI_UP_P;
- var downP = controls.UI_DOWN_P;
- var accepted = controls.ACCEPT;
-
- if (FlxG.onMobile)
- {
- for (touch in FlxG.touches.list)
- {
- if (touch.justPressed)
- {
- initTouchPos.set(touch.screenX, touch.screenY);
- }
- if (touch.pressed)
- {
- var dx = initTouchPos.x - touch.screenX;
- var dy = initTouchPos.y - touch.screenY;
-
- var angle = Math.atan2(dy, dx);
- var length = Math.sqrt(dx * dx + dy * dy);
-
- FlxG.watch.addQuick("LENGTH", length);
- FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
- trace("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
- }
-
- /* switch (inputID)
- {
- case FlxObject.UP:
- return
- case FlxObject.DOWN:
- }
- */
- }
-
- if (FlxG.touches.getFirst() != null)
- {
- if (touchTimer >= 1.5)
- accepted = true;
-
- touchTimer += elapsed;
- var touch:FlxTouch = FlxG.touches.getFirst();
-
- velTouch = Math.abs((touch.screenY - dyTouch)) / 50;
-
- dyTouch = touch.screenY - touchY;
- dxTouch = touch.screenX - touchX;
-
- if (touch.justPressed)
- {
- touchY = touch.screenY;
- dyTouch = 0;
- velTouch = 0;
-
- touchX = touch.screenX;
- dxTouch = 0;
- }
-
- if (Math.abs(dxTouch) >= 100)
- {
- touchX = touch.screenX;
- if (dxTouch != 0)
- dxTouch < 0 ? changeDiff(1) : changeDiff(-1);
- }
-
- if (Math.abs(dyTouch) >= 100)
- {
- touchY = touch.screenY;
-
- if (dyTouch != 0)
- dyTouch < 0 ? changeSelection(1) : changeSelection(-1);
- // changeSelection(1);
- }
- }
- else
- {
- touchTimer = 0;
- }
- }
-
- #if mobile
- for (touch in FlxG.touches.list)
- {
- if (touch.justPressed)
- {
- // accepted = true;
- }
- }
- #end
-
- if (controls.UI_UP || controls.UI_DOWN)
- {
- spamTimer += elapsed;
-
- if (spamming)
- {
- if (spamTimer >= 0.07)
- {
- spamTimer = 0;
-
- if (controls.UI_UP)
- changeSelection(-1);
- else
- changeSelection(1);
- }
- }
- else if (spamTimer >= 0.9)
- spamming = true;
- }
- else
- {
- spamming = false;
- spamTimer = 0;
- }
-
- if (upP)
- {
- dj.resetAFKTimer();
- changeSelection(-1);
- }
- if (downP)
- {
- dj.resetAFKTimer();
- changeSelection(1);
- }
-
- if (FlxG.mouse.wheel != 0)
- {
- dj.resetAFKTimer();
- changeSelection(-Math.round(FlxG.mouse.wheel / 4));
- }
-
- if (controls.UI_LEFT_P)
- {
- dj.resetAFKTimer();
- changeDiff(-1);
- }
- if (controls.UI_RIGHT_P)
- {
- dj.resetAFKTimer();
- changeDiff(1);
- }
-
- if (controls.BACK && !typing.hasFocus)
- {
- FlxG.sound.play(Paths.sound('cancelMenu'));
-
- FlxTransitionableState.skipNextTransIn = true;
- FlxTransitionableState.skipNextTransOut = true;
- FlxG.switchState(new MainMenuState());
- }
-
- if (accepted)
- {
- // if (Assets.exists())
-
- var poop:String = songs[curSelected].songName.toLowerCase();
-
- // does not work properly, always just accidentally sets it to normal anyways!
- /* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop)))
- {
- // defaults to normal if HARD / EASY doesn't exist
- // does not account if NORMAL doesn't exist!
- FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!");
- poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1);
- curDifficulty = 1;
- }*/
-
- PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase());
- PlayState.currentSong_NEW = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase());
- PlayState.isStoryMode = false;
- PlayState.storyDifficulty = curDifficulty;
- PlayState.storyDifficulty_NEW = switch (curDifficulty)
- {
- case 0:
- 'easy';
- case 1:
- 'normal';
- case 2:
- 'hard';
- default: 'normal';
- };
- // SongLoad.curDiff = Highscore.formatSong()
-
- SongLoad.curDiff = PlayState.storyDifficulty_NEW;
-
- PlayState.storyWeek = songs[curSelected].week;
- trace(' CUR WEEK ' + PlayState.storyWeek);
-
- // Visual and audio effects.
- FlxG.sound.play(Paths.sound('confirmMenu'));
- dj.confirm();
-
- new FlxTimer().start(1, function(tmr:FlxTimer)
- {
- LoadingState.loadAndSwitchState(new PlayState(), true);
- });
- }
- }
-
- override function switchTo(nextState:FlxState):Bool
- {
- clearDaCache(songs[curSelected].songName);
- return super.switchTo(nextState);
- }
-
- function changeDiff(change:Int = 0)
- {
- touchTimer = 0;
-
- curDifficulty += change;
-
- if (curDifficulty < 0)
- curDifficulty = 2;
- if (curDifficulty > 2)
- curDifficulty = 0;
-
- // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
- intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
- intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty);
-
- PlayState.storyDifficulty = curDifficulty;
- PlayState.storyDifficulty_NEW = switch (curDifficulty)
- {
- case 0:
- 'easy';
- case 1:
- 'normal';
- case 2:
- 'hard';
- default:
- 'normal';
- };
-
- grpDifficulties.group.forEach(function(spr)
- {
- spr.visible = false;
- });
-
- var curShit:FlxSprite = grpDifficulties.group.members[curDifficulty];
-
- curShit.visible = true;
- curShit.offset.y += 5;
- curShit.alpha = 0.5;
- new FlxTimer().start(1 / 24, function(swag)
- {
- curShit.alpha = 1;
- curShit.updateHitbox();
- });
- }
-
- // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
- function clearDaCache(actualSongTho:String)
- {
- for (song in songs)
- {
- if (song.songName != actualSongTho)
- {
- trace('trying to remove: ' + song.songName);
- // openfl.Assets.cache.clear(Paths.inst(song.songName));
- }
- }
- }
-
- function changeSelection(change:Int = 0)
- {
- // fp.updateScore(12345);
-
- NGio.logEvent('Fresh');
-
- // NGio.logEvent('Fresh');
- FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
-
- curSelected += change;
-
- if (curSelected < 0)
- curSelected = grpCapsules.members.length - 1;
- if (curSelected >= grpCapsules.members.length)
- curSelected = 0;
-
- // selector.y = (70 * curSelected) + 30;
-
- // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
- intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
- intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty);
- // lerpScore = 0;
-
- #if PRELOAD_ALL
- // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0);
- #end
-
- var bullShit:Int = 0;
-
- for (i in 0...iconArray.length)
- {
- iconArray[i].alpha = 0.6;
- }
-
- iconArray[curSelected].alpha = 1;
-
- for (index => capsule in grpCapsules.members)
- {
- capsule.selected = false;
-
- capsule.targetPos.y = ((index - curSelected) * 150) + 160;
- capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected)));
- // capsule.targetPos.x = 320 + (40 * (index - curSelected));
-
- if (index < curSelected)
- capsule.targetPos.y -= 100; // another 100 for good measure
- }
-
- if (grpCapsules.members.length > 0)
- grpCapsules.members[curSelected].selected = true;
- }
+ var moreWays2:BGScrollingText = new BGScrollingText(0, 400, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
+ moreWays2.funnyColor = 0xFFfff383;
+ moreWays2.speed = 4.4;
+ grpTxtScrolls.add(moreWays2);
+
+ var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y, "BOYFRIEND", FlxG.width / 2);
+ funnyScroll3.funnyColor = 0xFFff9963;
+ funnyScroll3.speed = -0.8;
+ grpTxtScrolls.add(funnyScroll3);
+
+ dj = new DJBoyfriend(0, -100);
+ add(dj);
+
+ var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
+ bgDad.setGraphicSize(0, FlxG.height);
+ bgDad.updateHitbox();
+ bgDad.shader = new AngleMask();
+ bgDad.visible = false;
+
+ var blackOverlayBullshitLOLXD:FlxSprite = new FlxSprite(FlxG.width).makeGraphic(Std.int(bgDad.width), Std.int(bgDad.height), FlxColor.BLACK);
+ add(blackOverlayBullshitLOLXD); // used to mask the text lol!
+
+ add(bgDad);
+ FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 1, {ease: FlxEase.quintOut});
+
+ blackOverlayBullshitLOLXD.shader = bgDad.shader;
+
+ grpSongs = new FlxTypedGroup();
+ add(grpSongs);
+
+ grpCapsules = new FlxTypedGroup();
+ add(grpCapsules);
+
+ grpDifficulties = new FlxSpriteGroup(-300, 80);
+ add(grpDifficulties);
+
+ grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayEasy')));
+ grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayNorm')));
+ grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayHard')));
+
+ grpDifficulties.group.forEach(function(spr)
+ {
+ spr.visible = false;
+ });
+
+ grpDifficulties.group.members[curDifficulty].visible = true;
+
+ var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK);
+ overhangStuff.y -= overhangStuff.height;
+ add(overhangStuff);
+ FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
+
+ var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
+ fnfFreeplay.font = "VCR OSD Mono";
+ fnfFreeplay.visible = false;
+ var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
+ fnfFreeplay.shader = sillyStroke;
+ add(fnfFreeplay);
+
+ var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
+ fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
+ fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
+ fnfHighscoreSpr.visible = false;
+ fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
+ fnfHighscoreSpr.antialiasing = true;
+ fnfHighscoreSpr.updateHitbox();
+ add(fnfHighscoreSpr);
+
+ new FlxTimer().start(FlxG.random.float(12, 50), function(tmr)
+ {
+ fnfHighscoreSpr.animation.play("highscore");
+ tmr.time = FlxG.random.float(20, 60);
+ }, 0);
+
+ fp = new FreeplayScore(460, 60, 100);
+ fp.visible = false;
+ add(fp);
+
+ txtCompletion = new FlxText(1200, 77, 0, "0", 32);
+ txtCompletion.font = "VCR OSD Mono";
+ txtCompletion.visible = false;
+ add(txtCompletion);
+
+ dj.onIntroDone.add(function()
+ {
+ FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
+
+ add(new DifficultySelector(20, grpDifficulties.y - 10, false, controls));
+ add(new DifficultySelector(325, grpDifficulties.y - 10, true, controls));
+
+ var letterSort:LetterSort = new LetterSort(300, 100);
+ add(letterSort);
+
+ letterSort.changeSelectionCallback = (str) ->
+ {
+ switch (str)
+ {
+ case "fav":
+ generateSongList({filterType: FAVORITE}, true);
+ case "ALL":
+ generateSongList(null, true);
+ default:
+ generateSongList({filterType: STARTSWITH, filterData: str}, true);
+ }
+ };
+
+ new FlxTimer().start(1 / 24, function(handShit)
+ {
+ fnfHighscoreSpr.visible = true;
+ fnfFreeplay.visible = true;
+ fp.visible = true;
+ fp.updateScore(0);
+
+ txtCompletion.visible = true;
+ intendedCompletion = 0;
+
+ new FlxTimer().start(1.5 / 24, function(bold)
+ {
+ sillyStroke.width = 0;
+ sillyStroke.height = 0;
+ });
+ });
+
+ pinkBack.color = 0xFFffd863;
+ // fnfFreeplay.visible = true;
+ bgDad.visible = true;
+ orangeBackShit.visible = true;
+ alsoOrangeLOL.visible = true;
+ grpTxtScrolls.visible = true;
+ });
+
+ generateSongList();
+
+ // FlxG.sound.playMusic(Paths.music('title'), 0);
+ // FlxG.sound.music.fadeIn(2, 0, 0.8);
+ // selector = new FlxText();
+
+ // selector.size = 40;
+ // selector.text = ">";
+ // add(selector);
+
+ var swag:Alphabet = new Alphabet(1, 0, "swag");
+
+ // JUST DOIN THIS SHIT FOR TESTING!!!
+ /*
+ var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md'));
+
+ var texFel:TextField = new TextField();
+ texFel.width = FlxG.width;
+ texFel.height = FlxG.height;
+ // texFel.
+ texFel.htmlText = md;
+
+ FlxG.stage.addChild(texFel);
+
+ trace(md);
+ */
+
+ var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
+ funnyCam.bgColor = FlxColor.TRANSPARENT;
+ FlxG.cameras.add(funnyCam);
+
+ typing = new FlxInputText(100, 100);
+ add(typing);
+
+ typing.callback = function(txt, action)
+ {
+ // generateSongList(new EReg(txt.trim(), "ig"));
+ trace(action);
+ };
+
+ forEach(function(bs)
+ {
+ bs.cameras = [funnyCam];
+ });
+
+ super.create();
+ }
+
+ public function generateSongList(?filterStuff:SongFilter, ?force:Bool = false)
+ {
+ curSelected = 0;
+
+ grpCapsules.clear();
+
+ // var regexp:EReg = regexp;
+ var tempSongs:Array = songs;
+
+ if (filterStuff != null)
+ {
+ switch (filterStuff.filterType)
+ {
+ case STARTSWITH:
+ tempSongs = tempSongs.filter(str ->
+ {
+ return str.songName.toLowerCase().startsWith(filterStuff.filterData);
+ });
+ case ALL:
+ // no filter!
+ case FAVORITE:
+ tempSongs = tempSongs.filter(str ->
+ {
+ return str.isFav;
+ });
+ default:
+ // return all on default
+ }
+ }
+
+ // if (regexp != null)
+ // tempSongs = songs.filter(item -> regexp.match(item.songName));
+
+ // tempSongs.sort(function(a, b):Int
+ // {
+ // var tempA = a.songName.toUpperCase();
+ // var tempB = b.songName.toUpperCase();
+
+ // if (tempA < tempB)
+ // return -1;
+ // else if (tempA > tempB)
+ // return 1;
+ // else
+ // return 0;
+ // });
+
+ for (i in 0...tempSongs.length)
+ {
+ var funnyMenu:SongMenuItem = new SongMenuItem(FlxG.width, (i * 150) + 160, tempSongs[i].songName);
+ funnyMenu.targetPos.x = funnyMenu.x;
+ funnyMenu.ID = i;
+ funnyMenu.alpha = 0.5;
+ funnyMenu.songText.visible = false;
+ funnyMenu.favIcon.visible = tempSongs[i].isFav;
+
+ // fp.updateScore(0);
+
+ new FlxTimer().start((1 / 24) * i, function(doShit)
+ {
+ funnyMenu.doJumpIn = true;
+ });
+
+ new FlxTimer().start((0.09 * i) + 0.85, function(lerpTmr)
+ {
+ funnyMenu.doLerp = true;
+ });
+
+ if (!force)
+ {
+ new FlxTimer().start(((0.20 * i) / (1 + i)) + 0.75, function(swagShi)
+ {
+ funnyMenu.songText.visible = true;
+ funnyMenu.alpha = 1;
+ });
+ }
+ else
+ {
+ funnyMenu.songText.visible = true;
+ funnyMenu.alpha = 1;
+ }
+
+ grpCapsules.add(funnyMenu);
+
+ var songText:Alphabet = new Alphabet(0, (70 * i) + 30, tempSongs[i].songName, true, false);
+ songText.x += 100;
+ songText.isMenuItem = true;
+ songText.targetY = i;
+
+ // grpSongs.add(songText);
+
+ var icon:HealthIcon = new HealthIcon(tempSongs[i].songCharacter);
+ // icon.sprTracker = songText;
+
+ // using a FlxGroup is too much fuss!
+ iconArray.push(icon);
+ // add(icon);
+
+ // songText.x += 40;
+ // DONT PUT X IN THE FIRST PARAMETER OF new ALPHABET() !!
+ // songText.screenCenter(X);
+ }
+
+ changeSelection();
+ changeDiff();
+ }
+
+ public function addSong(songName:String, weekNum:Int, songCharacter:String)
+ {
+ songs.push(new SongMetadata(songName, weekNum, songCharacter));
+ }
+
+ public function addWeek(songs:Array, weekNum:Int, ?songCharacters:Array)
+ {
+ if (songCharacters == null)
+ songCharacters = ['bf'];
+
+ var num:Int = 0;
+ for (song in songs)
+ {
+ addSong(song, weekNum, songCharacters[num]);
+
+ if (songCharacters.length != 1)
+ num++;
+ }
+ }
+
+ var touchY:Float = 0;
+ var touchX:Float = 0;
+ var dxTouch:Float = 0;
+ var dyTouch:Float = 0;
+ var velTouch:Float = 0;
+
+ var veloctiyLoopShit:Float = 0;
+ var touchTimer:Float = 0;
+
+ var initTouchPos:FlxPoint = new FlxPoint();
+
+ var spamTimer:Float = 0;
+ var spamming:Bool = false;
+
+ override function update(elapsed:Float)
+ {
+ super.update(elapsed);
+
+ if (FlxG.keys.justPressed.F)
+ {
+ var realShit = curSelected;
+ songs[curSelected].isFav = !songs[curSelected].isFav;
+ if (songs[curSelected].isFav)
+ {
+ FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, {
+ ease: FlxEase.elasticOut,
+ onComplete: _ ->
+ {
+ grpCapsules.members[realShit].favIcon.visible = true;
+ grpCapsules.members[realShit].favIcon.animation.play("fav");
+ }
+ });
+ }
+ else
+ {
+ grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
+ new FlxTimer().start((1 / 24) * 14, _ ->
+ {
+ grpCapsules.members[realShit].favIcon.visible = false;
+ });
+ new FlxTimer().start((1 / 24) * 24, _ ->
+ {
+ FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
+ });
+ }
+ }
+
+ if (FlxG.keys.justPressed.T)
+ typing.hasFocus = true;
+
+ if (FlxG.sound.music != null)
+ {
+ if (FlxG.sound.music.volume < 0.7)
+ {
+ FlxG.sound.music.volume += 0.5 * elapsed;
+ }
+ }
+
+ lerpScore = CoolUtil.coolLerp(lerpScore, intendedScore, 0.2);
+ lerpCompletion = CoolUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
+
+ fp.updateScore(Std.int(lerpScore));
+
+ txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
+ // trace(Highscore.getCompletion(songs[curSelected].songName, curDifficulty));
+
+ // trace(intendedScore);
+ // trace(lerpScore);
+ // Highscore.getAllScores();
+
+ var upP = controls.UI_UP_P;
+ var downP = controls.UI_DOWN_P;
+ var accepted = controls.ACCEPT;
+
+ if (FlxG.onMobile)
+ {
+ for (touch in FlxG.touches.list)
+ {
+ if (touch.justPressed)
+ {
+ initTouchPos.set(touch.screenX, touch.screenY);
+ }
+ if (touch.pressed)
+ {
+ var dx = initTouchPos.x - touch.screenX;
+ var dy = initTouchPos.y - touch.screenY;
+
+ var angle = Math.atan2(dy, dx);
+ var length = Math.sqrt(dx * dx + dy * dy);
+
+ FlxG.watch.addQuick("LENGTH", length);
+ FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
+ trace("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
+ }
+
+ /* switch (inputID)
+ {
+ case FlxObject.UP:
+ return
+ case FlxObject.DOWN:
+ }
+ */
+ }
+
+ if (FlxG.touches.getFirst() != null)
+ {
+ if (touchTimer >= 1.5)
+ accepted = true;
+
+ touchTimer += elapsed;
+ var touch:FlxTouch = FlxG.touches.getFirst();
+
+ velTouch = Math.abs((touch.screenY - dyTouch)) / 50;
+
+ dyTouch = touch.screenY - touchY;
+ dxTouch = touch.screenX - touchX;
+
+ if (touch.justPressed)
+ {
+ touchY = touch.screenY;
+ dyTouch = 0;
+ velTouch = 0;
+
+ touchX = touch.screenX;
+ dxTouch = 0;
+ }
+
+ if (Math.abs(dxTouch) >= 100)
+ {
+ touchX = touch.screenX;
+ if (dxTouch != 0)
+ dxTouch < 0 ? changeDiff(1) : changeDiff(-1);
+ }
+
+ if (Math.abs(dyTouch) >= 100)
+ {
+ touchY = touch.screenY;
+
+ if (dyTouch != 0)
+ dyTouch < 0 ? changeSelection(1) : changeSelection(-1);
+ // changeSelection(1);
+ }
+ }
+ else
+ {
+ touchTimer = 0;
+ }
+ }
+
+ #if mobile
+ for (touch in FlxG.touches.list)
+ {
+ if (touch.justPressed)
+ {
+ // accepted = true;
+ }
+ }
+ #end
+
+ if (controls.UI_UP || controls.UI_DOWN)
+ {
+ spamTimer += elapsed;
+
+ if (spamming)
+ {
+ if (spamTimer >= 0.07)
+ {
+ spamTimer = 0;
+
+ if (controls.UI_UP)
+ changeSelection(-1);
+ else
+ changeSelection(1);
+ }
+ }
+ else if (spamTimer >= 0.9)
+ spamming = true;
+ }
+ else
+ {
+ spamming = false;
+ spamTimer = 0;
+ }
+
+ if (upP)
+ {
+ dj.resetAFKTimer();
+ changeSelection(-1);
+ }
+ if (downP)
+ {
+ dj.resetAFKTimer();
+ changeSelection(1);
+ }
+
+ if (FlxG.mouse.wheel != 0)
+ {
+ dj.resetAFKTimer();
+ changeSelection(-Math.round(FlxG.mouse.wheel / 4));
+ }
+
+ if (controls.UI_LEFT_P)
+ {
+ dj.resetAFKTimer();
+ changeDiff(-1);
+ }
+ if (controls.UI_RIGHT_P)
+ {
+ dj.resetAFKTimer();
+ changeDiff(1);
+ }
+
+ if (controls.BACK && !typing.hasFocus)
+ {
+ FlxG.sound.play(Paths.sound('cancelMenu'));
+
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+ FlxG.switchState(new MainMenuState());
+ }
+
+ if (accepted)
+ {
+ // if (Assets.exists())
+
+ var poop:String = songs[curSelected].songName.toLowerCase();
+
+ // does not work properly, always just accidentally sets it to normal anyways!
+ /* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop)))
+ {
+ // defaults to normal if HARD / EASY doesn't exist
+ // does not account if NORMAL doesn't exist!
+ FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!");
+ poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1);
+ curDifficulty = 1;
+ }*/
+
+ // TODO: Deprecate and remove this entirely once all songs are converted to the new format
+ PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase());
+ PlayState.currentSong_NEW = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase());
+ PlayState.isStoryMode = false;
+ PlayState.storyDifficulty = curDifficulty;
+ PlayState.storyDifficulty_NEW = switch (curDifficulty)
+ {
+ case 0:
+ 'easy';
+ case 1:
+ 'normal';
+ case 2:
+ 'hard';
+ default: 'normal';
+ };
+ // SongLoad.curDiff = Highscore.formatSong()
+
+ SongLoad.curDiff = PlayState.storyDifficulty_NEW;
+
+ PlayState.storyWeek = songs[curSelected].week;
+ // trace(' CUR WEEK ' + PlayState.storyWeek);
+
+ // Visual and audio effects.
+ FlxG.sound.play(Paths.sound('confirmMenu'));
+ dj.confirm();
+
+ new FlxTimer().start(1, function(tmr:FlxTimer)
+ {
+ LoadingState.loadAndSwitchState(new PlayState(), true);
+ });
+ }
+ }
+
+ override function switchTo(nextState:FlxState):Bool
+ {
+ clearDaCache(songs[curSelected].songName);
+ return super.switchTo(nextState);
+ }
+
+ function changeDiff(change:Int = 0)
+ {
+ touchTimer = 0;
+
+ curDifficulty += change;
+
+ if (curDifficulty < 0)
+ curDifficulty = 2;
+ if (curDifficulty > 2)
+ curDifficulty = 0;
+
+ // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
+ intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
+ intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty);
+
+ PlayState.storyDifficulty = curDifficulty;
+ PlayState.storyDifficulty_NEW = switch (curDifficulty)
+ {
+ case 0:
+ 'easy';
+ case 1:
+ 'normal';
+ case 2:
+ 'hard';
+ default:
+ 'normal';
+ };
+
+ grpDifficulties.group.forEach(function(spr)
+ {
+ spr.visible = false;
+ });
+
+ var curShit:FlxSprite = grpDifficulties.group.members[curDifficulty];
+
+ curShit.visible = true;
+ curShit.offset.y += 5;
+ curShit.alpha = 0.5;
+ new FlxTimer().start(1 / 24, function(swag)
+ {
+ curShit.alpha = 1;
+ curShit.updateHitbox();
+ });
+ }
+
+ // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
+ function clearDaCache(actualSongTho:String)
+ {
+ for (song in songs)
+ {
+ if (song.songName != actualSongTho)
+ {
+ trace('trying to remove: ' + song.songName);
+ // openfl.Assets.cache.clear(Paths.inst(song.songName));
+ }
+ }
+ }
+
+ function changeSelection(change:Int = 0)
+ {
+ // fp.updateScore(12345);
+
+ NGio.logEvent('Fresh');
+
+ // NGio.logEvent('Fresh');
+ FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
+
+ curSelected += change;
+
+ if (curSelected < 0)
+ curSelected = grpCapsules.members.length - 1;
+ if (curSelected >= grpCapsules.members.length)
+ curSelected = 0;
+
+ // selector.y = (70 * curSelected) + 30;
+
+ // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
+ intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
+ intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty);
+ // lerpScore = 0;
+
+ #if PRELOAD_ALL
+ // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0);
+ #end
+
+ var bullShit:Int = 0;
+
+ for (i in 0...iconArray.length)
+ {
+ iconArray[i].alpha = 0.6;
+ }
+
+ iconArray[curSelected].alpha = 1;
+
+ for (index => capsule in grpCapsules.members)
+ {
+ capsule.selected = false;
+
+ capsule.targetPos.y = ((index - curSelected) * 150) + 160;
+ capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected)));
+ // capsule.targetPos.x = 320 + (40 * (index - curSelected));
+
+ if (index < curSelected)
+ capsule.targetPos.y -= 100; // another 100 for good measure
+ }
+
+ if (grpCapsules.members.length > 0)
+ grpCapsules.members[curSelected].selected = true;
+ }
}
class DifficultySelector extends FlxSprite
{
- var controls:Controls;
- var whiteShader:PureColor;
+ var controls:Controls;
+ var whiteShader:PureColor;
- public function new(x:Float, y:Float, flipped:Bool, controls:Controls)
- {
- super(x, y);
+ public function new(x:Float, y:Float, flipped:Bool, controls:Controls)
+ {
+ super(x, y);
- this.controls = controls;
+ this.controls = controls;
- frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
- animation.addByPrefix('shine', "arrow pointer loop", 24);
- animation.play('shine');
+ frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
+ animation.addByPrefix('shine', "arrow pointer loop", 24);
+ animation.play('shine');
- whiteShader = new PureColor(FlxColor.WHITE);
+ whiteShader = new PureColor(FlxColor.WHITE);
- shader = whiteShader;
+ shader = whiteShader;
- flipX = flipped;
- }
+ flipX = flipped;
+ }
- override function update(elapsed:Float)
- {
- if (flipX && controls.UI_RIGHT_P)
- moveShitDown();
- if (!flipX && controls.UI_LEFT_P)
- moveShitDown();
+ override function update(elapsed:Float)
+ {
+ if (flipX && controls.UI_RIGHT_P)
+ moveShitDown();
+ if (!flipX && controls.UI_LEFT_P)
+ moveShitDown();
- super.update(elapsed);
- }
+ super.update(elapsed);
+ }
- function moveShitDown()
- {
- offset.y -= 5;
+ function moveShitDown()
+ {
+ offset.y -= 5;
- whiteShader.colorSet = true;
+ whiteShader.colorSet = true;
- new FlxTimer().start(2 / 24, function(tmr)
- {
- whiteShader.colorSet = false;
- updateHitbox();
- });
- }
+ new FlxTimer().start(2 / 24, function(tmr)
+ {
+ whiteShader.colorSet = false;
+ updateHitbox();
+ });
+ }
}
typedef SongFilter =
{
- var filterType:FilterType;
- var ?filterData:Dynamic;
+ var filterType:FilterType;
+ var ?filterData:Dynamic;
}
enum abstract FilterType(String)
{
- var STARTSWITH;
- var FAVORITE;
- var ALL;
+ var STARTSWITH;
+ var FAVORITE;
+ var ALL;
}
class SongMetadata
{
- public var songName:String = "";
- public var week:Int = 0;
- public var songCharacter:String = "";
- public var isFav:Bool = false;
+ public var songName:String = "";
+ public var week:Int = 0;
+ public var songCharacter:String = "";
+ public var isFav:Bool = false;
- public function new(song:String, week:Int, songCharacter:String, ?isFav:Bool = false)
- {
- this.songName = song;
- this.week = week;
- this.songCharacter = songCharacter;
- this.isFav = isFav;
- }
+ public function new(song:String, week:Int, songCharacter:String, ?isFav:Bool = false)
+ {
+ this.songName = song;
+ this.week = week;
+ this.songCharacter = songCharacter;
+ this.isFav = isFav;
+ }
}
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 7f192c170..85dc4e4cb 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -11,7 +11,7 @@ import flixel.util.FlxColor;
import funkin.modding.module.ModuleHandler;
import funkin.play.PlayState;
import funkin.play.character.CharacterData.CharacterDataParser;
-import funkin.play.event.SongEvent.SongEventHandler;
+import funkin.play.event.SongEvent.SongEventParser;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.stage.StageData;
import funkin.ui.PreferencesMenu;
@@ -32,196 +32,199 @@ import Discord.DiscordClient;
*/
class InitState extends FlxTransitionableState
{
- override public function create():Void
- {
- trace('This is a debug build, loading InitState...');
- #if android
- FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
- #end
- #if newgrounds
- NGio.init();
- #end
- #if discord_rpc
- DiscordClient.initialize();
+ override public function create():Void
+ {
+ trace('This is a debug build, loading InitState...');
+ #if android
+ FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
+ #end
+ #if newgrounds
+ NGio.init();
+ #end
+ #if discord_rpc
+ DiscordClient.initialize();
- Application.current.onExit.add(function(exitCode)
- {
- DiscordClient.shutdown();
- });
- #end
+ Application.current.onExit.add(function(exitCode)
+ {
+ DiscordClient.shutdown();
+ });
+ #end
- // ==== flixel shit ==== //
+ // ==== flixel shit ==== //
- // This big obnoxious white button is for MOBILE, so that you can press it
- // easily with your finger when debug bullshit pops up during testing lol!
- FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function()
- {
- FlxG.debugger.visible = false;
- });
+ // This big obnoxious white button is for MOBILE, so that you can press it
+ // easily with your finger when debug bullshit pops up during testing lol!
+ FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function()
+ {
+ FlxG.debugger.visible = false;
+ });
- FlxG.sound.muteKeys = [ZERO];
- FlxG.game.focusLostFramerate = 60;
+ FlxG.sound.muteKeys = [ZERO];
+ FlxG.game.focusLostFramerate = 60;
- // FlxG.stage.window.borderless = true;
- // FlxG.stage.window.mouseLock = true;
+ // FlxG.stage.window.borderless = true;
+ // FlxG.stage.window.mouseLock = true;
- var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
- diamond.persist = true;
- diamond.destroyOnNoUse = false;
+ var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
+ diamond.persist = true;
+ diamond.destroyOnNoUse = false;
- FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
- new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
- FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
- new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
+ FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
+ new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
+ FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
+ new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
- // ===== save shit ===== //
+ // ===== save shit ===== //
- FlxG.save.bind('funkin', 'ninjamuffin99');
+ FlxG.save.bind('funkin', 'ninjamuffin99');
- // https://github.com/HaxeFlixel/flixel/pull/2396
- // IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW
- // FlxG.sound.loadSavedPrefs();
+ // https://github.com/HaxeFlixel/flixel/pull/2396
+ // IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW
+ // FlxG.sound.loadSavedPrefs();
- if (FlxG.save.data.volume != null)
- FlxG.sound.volume = FlxG.save.data.volume;
- if (FlxG.save.data.mute != null)
- FlxG.sound.muted = FlxG.save.data.mute;
+ if (FlxG.save.data.volume != null)
+ FlxG.sound.volume = FlxG.save.data.volume;
+ if (FlxG.save.data.mute != null)
+ FlxG.sound.muted = FlxG.save.data.mute;
- // Make errors and warnings less annoying.
- LogStyle.ERROR.openConsole = false;
- LogStyle.ERROR.errorSound = null;
- LogStyle.WARNING.openConsole = false;
- LogStyle.WARNING.errorSound = null;
+ // Make errors and warnings less annoying.
+ LogStyle.ERROR.openConsole = false;
+ LogStyle.ERROR.errorSound = null;
+ LogStyle.WARNING.openConsole = false;
+ LogStyle.WARNING.errorSound = null;
- // FlxG.save.close();
- // FlxG.sound.loadSavedPrefs();
- WindowUtil.initWindowEvents();
+ // FlxG.save.close();
+ // FlxG.sound.loadSavedPrefs();
+ WindowUtil.initWindowEvents();
+ WindowUtil.disableCrashHandler();
- PreferencesMenu.initPrefs();
- PlayerSettings.init();
- Highscore.load();
+ PreferencesMenu.initPrefs();
+ PlayerSettings.init();
+ Highscore.load();
- if (FlxG.save.data.weekUnlocked != null)
- {
- // FIX LATER!!!
- // WEEK UNLOCK PROGRESSION!!
- // StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
+ if (FlxG.save.data.weekUnlocked != null)
+ {
+ // FIX LATER!!!
+ // WEEK UNLOCK PROGRESSION!!
+ // StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
- if (StoryMenuState.weekUnlocked.length < 4)
- StoryMenuState.weekUnlocked.insert(0, true);
+ if (StoryMenuState.weekUnlocked.length < 4)
+ StoryMenuState.weekUnlocked.insert(0, true);
- // QUICK PATCH OOPS!
- if (!StoryMenuState.weekUnlocked[0])
- StoryMenuState.weekUnlocked[0] = true;
- }
+ // QUICK PATCH OOPS!
+ if (!StoryMenuState.weekUnlocked[0])
+ StoryMenuState.weekUnlocked[0] = true;
+ }
- if (FlxG.save.data.seenVideo != null)
- VideoState.seenVideo = FlxG.save.data.seenVideo;
+ if (FlxG.save.data.seenVideo != null)
+ VideoState.seenVideo = FlxG.save.data.seenVideo;
- // ===== fuck outta here ===== //
+ // ===== fuck outta here ===== //
- // FlxTransitionableState.skipNextTransOut = true;
- FlxTransitionableState.skipNextTransIn = true;
+ // FlxTransitionableState.skipNextTransOut = true;
+ FlxTransitionableState.skipNextTransIn = true;
- SongEventHandler.registerBaseEventCallbacks();
- // TODO: Register custom event callbacks here
+ // TODO: Register custom event callbacks here
- SongDataParser.loadSongCache();
- StageDataParser.loadStageCache();
- CharacterDataParser.loadCharacterCache();
- ModuleHandler.buildModuleCallbacks();
- ModuleHandler.loadModuleCache();
+ SongEventParser.loadEventCache();
+ SongDataParser.loadSongCache();
+ StageDataParser.loadStageCache();
+ CharacterDataParser.loadCharacterCache();
+ ModuleHandler.buildModuleCallbacks();
+ ModuleHandler.loadModuleCache();
- FlxG.debugger.toggleKeys = [F2];
+ FlxG.debugger.toggleKeys = [F2];
- #if song
- var song = getSong();
+ ModuleHandler.callOnCreate();
- var weeks = [
- ['bopeebo', 'fresh', 'dadbattle'],
- ['spookeez', 'south', 'monster'],
- ['spooky', 'spooky', 'monster'],
- ['pico', 'philly', 'blammed'],
- ['satin-panties', 'high', 'milf'],
- ['cocoa', 'eggnog', 'winter-horrorland'],
- ['senpai', 'roses', 'thorns'],
- ['ugh', 'guns', 'stress']
- ];
+ #if song
+ var song = getSong();
- var week = 0;
- for (i in 0...weeks.length)
- {
- if (weeks[i].contains(song))
- {
- week = i + 1;
- break;
- }
- }
+ var weeks = [
+ ['bopeebo', 'fresh', 'dadbattle'],
+ ['spookeez', 'south', 'monster'],
+ ['spooky', 'spooky', 'monster'],
+ ['pico', 'philly', 'blammed'],
+ ['satin-panties', 'high', 'milf'],
+ ['cocoa', 'eggnog', 'winter-horrorland'],
+ ['senpai', 'roses', 'thorns'],
+ ['ugh', 'guns', 'stress']
+ ];
- if (week == 0)
- throw 'Invalid -D song=$song';
+ var week = 0;
+ for (i in 0...weeks.length)
+ {
+ if (weeks[i].contains(song))
+ {
+ week = i + 1;
+ break;
+ }
+ }
- startSong(week, song, false);
- #elseif week
- var week = getWeek();
+ if (week == 0)
+ throw 'Invalid -D song=$song';
- var songs = [
- 'bopeebo', 'spookeez', 'spooky', 'pico',
- 'satin-panties', 'cocoa', 'senpai', 'ugh'
- ];
+ startSong(week, song, false);
+ #elseif week
+ var week = getWeek();
- if (week <= 0 || week >= songs.length)
- throw "invalid -D week=" + week;
+ var songs = [
+ 'bopeebo', 'spookeez', 'spooky', 'pico',
+ 'satin-panties', 'cocoa', 'senpai', 'ugh'
+ ];
- startSong(week, songs[week - 1], true);
- #elseif FREEPLAY
- FlxG.switchState(new FreeplayState());
- #elseif ANIMATE
- FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
- #elseif CHARTING
- FlxG.switchState(new ChartingState());
- #elseif STAGEBUILD
- FlxG.switchState(new StageBuilderState());
- #elseif FIGHT
- FlxG.switchState(new PicoFight());
- #elseif ANIMDEBUG
- FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
- #elseif LATENCY
- FlxG.switchState(new LatencyState());
- #elseif NETTEST
- FlxG.switchState(new netTest.NetTest());
- #else
- FlxG.sound.cache(Paths.music('freakyMenu'));
- FlxG.switchState(new TitleState());
- #end
- }
+ if (week <= 0 || week >= songs.length)
+ throw "invalid -D week=" + week;
- function startSong(week, song, isStoryMode)
- {
- var dif = getDif();
+ startSong(week, songs[week - 1], true);
+ #elseif FREEPLAY
+ FlxG.switchState(new FreeplayState());
+ #elseif ANIMATE
+ FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage());
+ #elseif CHARTING
+ FlxG.switchState(new ChartingState());
+ #elseif STAGEBUILD
+ FlxG.switchState(new StageBuilderState());
+ #elseif FIGHT
+ FlxG.switchState(new PicoFight());
+ #elseif ANIMDEBUG
+ FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
+ #elseif LATENCY
+ FlxG.switchState(new LatencyState());
+ #elseif NETTEST
+ FlxG.switchState(new netTest.NetTest());
+ #else
+ FlxG.sound.cache(Paths.music('freakyMenu'));
+ FlxG.switchState(new TitleState());
+ #end
+ }
- PlayState.currentSong = SongLoad.loadFromJson(song, song);
- PlayState.currentSong_NEW = SongDataParser.fetchSong(song);
- PlayState.isStoryMode = isStoryMode;
- PlayState.storyDifficulty = dif;
- PlayState.storyDifficulty_NEW = switch (dif)
- {
- case 0: 'easy';
- case 1: 'normal';
- case 2: 'hard';
- default: 'normal';
- };
- SongLoad.curDiff = PlayState.storyDifficulty_NEW;
- PlayState.storyWeek = week;
- LoadingState.loadAndSwitchState(new PlayState());
- }
+ function startSong(week, song, isStoryMode)
+ {
+ var dif = getDif();
+
+ PlayState.currentSong = SongLoad.loadFromJson(song, song);
+ PlayState.currentSong_NEW = SongDataParser.fetchSong(song);
+ PlayState.isStoryMode = isStoryMode;
+ PlayState.storyDifficulty = dif;
+ PlayState.storyDifficulty_NEW = switch (dif)
+ {
+ case 0: 'easy';
+ case 1: 'normal';
+ case 2: 'hard';
+ default: 'normal';
+ };
+ SongLoad.curDiff = PlayState.storyDifficulty_NEW;
+ PlayState.storyWeek = week;
+ LoadingState.loadAndSwitchState(new PlayState());
+ }
}
function getWeek()
- return Std.parseInt(MacroUtil.getDefine("week"));
+ return Std.parseInt(MacroUtil.getDefine("week"));
function getSong()
- return MacroUtil.getDefine("song");
+ return MacroUtil.getDefine("song");
function getDif()
- return Std.parseInt(MacroUtil.getDefine("dif", "1"));
+ return Std.parseInt(MacroUtil.getDefine("dif", "1"));
diff --git a/source/funkin/freeplayStuff/DJBoyfriend.hx b/source/funkin/freeplayStuff/DJBoyfriend.hx
index 2e0954563..a4afb9753 100644
--- a/source/funkin/freeplayStuff/DJBoyfriend.hx
+++ b/source/funkin/freeplayStuff/DJBoyfriend.hx
@@ -6,178 +6,178 @@ import funkin.util.assets.FlxAnimationUtil;
class DJBoyfriend extends FlxSprite
{
- // Represents the sprite's current status.
- // Without state machines I would have driven myself crazy years ago.
- public var currentState:DJBoyfriendState = Intro;
+ // Represents the sprite's current status.
+ // Without state machines I would have driven myself crazy years ago.
+ public var currentState:DJBoyfriendState = Intro;
- // A callback activated when the intro animation finishes.
- public var onIntroDone:FlxSignal = new FlxSignal();
+ // A callback activated when the intro animation finishes.
+ public var onIntroDone:FlxSignal = new FlxSignal();
- // A callback activated when Boyfriend gets spooked.
- public var onSpook:FlxSignal = new FlxSignal();
+ // A callback activated when Boyfriend gets spooked.
+ public var onSpook:FlxSignal = new FlxSignal();
- // playAnim stolen from Character.hx, cuz im lazy lol!
- // TODO: Switch this class to use SwagSprite instead.
- public var animOffsets:Map>;
+ // playAnim stolen from Character.hx, cuz im lazy lol!
+ // TODO: Switch this class to use SwagSprite instead.
+ public var animOffsets:Map>;
- static final SPOOK_PERIOD:Float = 180.0;
+ static final SPOOK_PERIOD:Float = 180.0;
- // Time since dad last SPOOKED you.
- var timeSinceSpook:Float = 0;
+ // Time since dad last SPOOKED you.
+ var timeSinceSpook:Float = 0;
- public function new(x:Float, y:Float)
- {
- super(x, y);
+ public function new(x:Float, y:Float)
+ {
+ super(x, y);
- animOffsets = new Map>();
+ animOffsets = new Map>();
- setupAnimations();
+ setupAnimations();
- animation.finishCallback = onFinishAnim;
- }
+ animation.finishCallback = onFinishAnim;
+ }
- public override function update(elapsed:Float):Void
- {
- super.update(elapsed);
+ public override function update(elapsed:Float):Void
+ {
+ super.update(elapsed);
- if (FlxG.keys.justPressed.LEFT)
- {
- animOffsets["confirm"] = [animOffsets["confirm"][0] + 1, animOffsets["confirm"][1]];
- applyAnimOffset();
- }
- else if (FlxG.keys.justPressed.RIGHT)
- {
- animOffsets["confirm"] = [animOffsets["confirm"][0] - 1, animOffsets["confirm"][1]];
- applyAnimOffset();
- }
- else if (FlxG.keys.justPressed.UP)
- {
- animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] + 1];
- applyAnimOffset();
- }
- else if (FlxG.keys.justPressed.DOWN)
- {
- animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] - 1];
- applyAnimOffset();
- }
+ if (FlxG.keys.justPressed.LEFT)
+ {
+ animOffsets["confirm"] = [animOffsets["confirm"][0] + 1, animOffsets["confirm"][1]];
+ applyAnimOffset();
+ }
+ else if (FlxG.keys.justPressed.RIGHT)
+ {
+ animOffsets["confirm"] = [animOffsets["confirm"][0] - 1, animOffsets["confirm"][1]];
+ applyAnimOffset();
+ }
+ else if (FlxG.keys.justPressed.UP)
+ {
+ animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] + 1];
+ applyAnimOffset();
+ }
+ else if (FlxG.keys.justPressed.DOWN)
+ {
+ animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] - 1];
+ applyAnimOffset();
+ }
- switch (currentState)
- {
- case Intro:
- // Play the intro animation then leave this state immediately.
- if (getCurrentAnimation() != 'intro')
- playAnimation('intro', true);
- timeSinceSpook = 0;
- case Idle:
- // We are in this state the majority of the time.
- if (getCurrentAnimation() != 'idle' || animation.finished)
- {
- if (timeSinceSpook > SPOOK_PERIOD)
- {
- currentState = Spook;
- }
- else
- {
- playAnimation('idle', false);
- }
- }
- timeSinceSpook += elapsed;
- case Confirm:
- if (getCurrentAnimation() != 'confirm')
- playAnimation('confirm', false);
- timeSinceSpook = 0;
- case Spook:
- if (getCurrentAnimation() != 'spook')
- {
- onSpook.dispatch();
- playAnimation('spook', false);
- }
- timeSinceSpook = 0;
- default:
- // I shit myself.
- }
- }
+ switch (currentState)
+ {
+ case Intro:
+ // Play the intro animation then leave this state immediately.
+ if (getCurrentAnimation() != 'intro')
+ playAnimation('intro', true);
+ timeSinceSpook = 0;
+ case Idle:
+ // We are in this state the majority of the time.
+ if (getCurrentAnimation() != 'idle' || animation.finished)
+ {
+ if (timeSinceSpook > SPOOK_PERIOD)
+ {
+ currentState = Spook;
+ }
+ else
+ {
+ playAnimation('idle', false);
+ }
+ }
+ timeSinceSpook += elapsed;
+ case Confirm:
+ if (getCurrentAnimation() != 'confirm')
+ playAnimation('confirm', false);
+ timeSinceSpook = 0;
+ case Spook:
+ if (getCurrentAnimation() != 'spook')
+ {
+ onSpook.dispatch();
+ playAnimation('spook', false);
+ }
+ timeSinceSpook = 0;
+ default:
+ // I shit myself.
+ }
+ }
- function onFinishAnim(name:String):Void
- {
- switch (name)
- {
- case "intro":
- trace('Finished intro');
- currentState = Idle;
- onIntroDone.dispatch();
- case "idle":
- trace('Finished idle');
- case "spook":
- trace('Finished spook');
- currentState = Idle;
- case "confirm":
- trace('Finished confirm');
- }
- }
+ function onFinishAnim(name:String):Void
+ {
+ switch (name)
+ {
+ case "intro":
+ // trace('Finished intro');
+ currentState = Idle;
+ onIntroDone.dispatch();
+ case "idle":
+ // trace('Finished idle');
+ case "spook":
+ // trace('Finished spook');
+ currentState = Idle;
+ case "confirm":
+ // trace('Finished confirm');
+ }
+ }
- public function resetAFKTimer():Void
- {
- timeSinceSpook = 0;
- }
+ public function resetAFKTimer():Void
+ {
+ timeSinceSpook = 0;
+ }
- function setupAnimations():Void
- {
- frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
+ function setupAnimations():Void
+ {
+ frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
- animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
- addOffset('intro', 0, 0);
+ animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
+ addOffset('intro', 0, 0);
- animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
- addOffset('idle', -4, -426);
+ animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
+ addOffset('idle', -4, -426);
- animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
- addOffset('confirm', 40, -451);
+ animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
+ addOffset('confirm', 40, -451);
- animation.addByPrefix('spook', "bf dj afk0", 24, false);
- addOffset('spook', -3, -272);
- }
+ animation.addByPrefix('spook', "bf dj afk0", 24, false);
+ addOffset('spook', -3, -272);
+ }
- public function confirm():Void
- {
- currentState = Confirm;
- }
+ public function confirm():Void
+ {
+ currentState = Confirm;
+ }
- public inline function addOffset(name:String, x:Float = 0, y:Float = 0)
- {
- animOffsets[name] = [x, y];
- }
+ public inline function addOffset(name:String, x:Float = 0, y:Float = 0)
+ {
+ animOffsets[name] = [x, y];
+ }
- public function getCurrentAnimation():String
- {
- if (this.animation == null || this.animation.curAnim == null)
- return "";
- return this.animation.curAnim.name;
- }
+ public function getCurrentAnimation():String
+ {
+ if (this.animation == null || this.animation.curAnim == null)
+ return "";
+ return this.animation.curAnim.name;
+ }
- public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
- {
- animation.play(AnimName, Force, Reversed, Frame);
- applyAnimOffset();
- }
+ public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
+ {
+ animation.play(AnimName, Force, Reversed, Frame);
+ applyAnimOffset();
+ }
- function applyAnimOffset()
- {
- var AnimName = getCurrentAnimation();
- var daOffset = animOffsets.get(AnimName);
- if (animOffsets.exists(AnimName))
- {
- offset.set(daOffset[0], daOffset[1]);
- }
- else
- offset.set(0, 0);
- }
+ function applyAnimOffset()
+ {
+ var AnimName = getCurrentAnimation();
+ var daOffset = animOffsets.get(AnimName);
+ if (animOffsets.exists(AnimName))
+ {
+ offset.set(daOffset[0], daOffset[1]);
+ }
+ else
+ offset.set(0, 0);
+ }
}
enum DJBoyfriendState
{
- Intro;
- Idle;
- Confirm;
- Spook;
+ Intro;
+ Idle;
+ Confirm;
+ Spook;
}
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 35cd7b869..56b6aa4d3 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -6,8 +6,7 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
// These are great.
using Lambda;
using StringTools;
-
+using funkin.util.tools.MapTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.StringTools;
-
#end
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index b6eca7e68..fe2e983b3 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -60,6 +60,7 @@ interface IPlayStateScriptedClass extends IScriptedClass
* and can be cancelled by scripts.
*/
public function onPause(event:PauseScriptEvent):Void;
+
/**
* Called when the game is unpaused.
*/
@@ -70,18 +71,22 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Use this to mutate the chart.
*/
public function onSongLoaded(event:SongLoadScriptEvent):Void;
+
/**
* Called when the song starts (conductor time is 0 seconds).
*/
public function onSongStart(event:ScriptEvent):Void;
+
/**
* Called when the song ends and the song is about to be unloaded.
*/
public function onSongEnd(event:ScriptEvent):Void;
+
/**
* Called as the player runs out of health just before the game over substate is entered.
*/
public function onGameOver(event:ScriptEvent):Void;
+
/**
* Called when the player restarts the song, either via pause menu or restarting after a game over.
*/
@@ -92,19 +97,27 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Query the note attached to the event to determine if it was hit by the player or CPU.
*/
public function onNoteHit(event:NoteScriptEvent):Void;
+
/**
* Called when EITHER player (usually the player) misses a note.
*/
public function onNoteMiss(event:NoteScriptEvent):Void;
+
/**
* Called when the player presses a key when no note is on the strumline.
*/
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void;
+ /**
+ * Called when the song reaches an event.
+ */
+ public function onSongEvent(event:SongEventScriptEvent):Void;
+
/**
* Called once every step of the song.
*/
public function onStepHit(event:SongTimeScriptEvent):Void;
+
/**
* Called once every beat of the song.
*/
@@ -114,10 +127,12 @@ interface IPlayStateScriptedClass extends IScriptedClass
* Called when the countdown of the song starts.
*/
public function onCountdownStart(event:CountdownScriptEvent):Void;
+
/**
* Called when the a part of the countdown happens.
*/
public function onCountdownStep(event:CountdownScriptEvent):Void;
+
/**
* Called when the countdown of the song ends.
*/
diff --git a/source/funkin/modding/PolymodErrorHandler.hx b/source/funkin/modding/PolymodErrorHandler.hx
index c0616dfb5..70a716821 100644
--- a/source/funkin/modding/PolymodErrorHandler.hx
+++ b/source/funkin/modding/PolymodErrorHandler.hx
@@ -4,68 +4,88 @@ import polymod.Polymod;
class PolymodErrorHandler
{
- /**
- * Show a popup with the given text.
- * This displays a system popup, it WILL interrupt the game.
- * Make sure to only use this when it's important, like when there's a script error.
- *
- * @param name The name at the top of the popup.
- * @param desc The body text of the popup.
- */
- public static function showAlert(name:String, desc:String):Void
- {
- lime.app.Application.current.window.alert(desc, name);
- }
+ /**
+ * Show a popup with the given text.
+ * This displays a system popup, it WILL interrupt the game.
+ * Make sure to only use this when it's important, like when there's a script error.
+ *
+ * @param name The name at the top of the popup.
+ * @param desc The body text of the popup.
+ */
+ public static function showAlert(name:String, desc:String):Void
+ {
+ lime.app.Application.current.window.alert(desc, name);
+ }
- public static function onPolymodError(error:PolymodError):Void
- {
- // Perform an action based on the error code.
- switch (error.code)
- {
- case MOD_LOAD_PREPARE:
- logInfo('[POLYMOD]: ${error.message}');
- case MOD_LOAD_DONE:
- logInfo('[POLYMOD]: ${error.message}');
- case MISSING_ICON:
- logWarn('[POLYMOD]: A mod is missing an icon. Please add one.');
- case SCRIPT_PARSE_ERROR:
- // A syntax error when parsing a script.
- logError('[POLYMOD]: ${error.message}');
- showAlert('Polymod Script Parsing Error', error.message);
- case SCRIPT_EXCEPTION:
- // A runtime error when running a script.
- logError('[POLYMOD]: ${error.message}');
- showAlert('Polymod Script Execution Error', error.message);
- case SCRIPT_CLASS_NOT_FOUND:
- // A scripted class tried to reference an unknown superclass.
- logError('[POLYMOD]: ${error.message}');
- showAlert('Polymod Script Parsing Error', error.message);
- default:
- // Log the message based on its severity.
- switch (error.severity)
- {
- case NOTICE:
- logInfo('[POLYMOD]: ${error.message}');
- case WARNING:
- logWarn('[POLYMOD]: ${error.message}');
- case ERROR:
- logError('[POLYMOD]: ${error.message}');
- }
- }
- }
+ public static function onPolymodError(error:PolymodError):Void
+ {
+ // Perform an action based on the error code.
+ switch (error.code)
+ {
+ case FRAMEWORK_INIT, FRAMEWORK_AUTODETECT, SCRIPT_PARSING:
+ // Unimportant.
+ return;
- static function logInfo(message:String):Void
- {
- trace('[INFO ] ${message}');
- }
+ case MOD_LOAD_PREPARE, MOD_LOAD_DONE:
+ logInfo('LOADING MOD - ${error.message}');
- static function logError(message:String):Void
- {
- trace('[ERROR] ${message}');
- }
+ case MISSING_ICON:
+ logWarn('A mod is missing an icon. Please add one.');
- static function logWarn(message:String):Void
- {
- trace('[WARN ] ${message}');
- }
+ case SCRIPT_PARSE_ERROR:
+ // A syntax error when parsing a script.
+ logError(error.message);
+ // Notify the user via popup.
+ showAlert('Polymod Script Parsing Error', error.message);
+ case SCRIPT_RUNTIME_EXCEPTION:
+ // A runtime error when running a script.
+ logError(error.message);
+ // Notify the user via popup.
+ showAlert('Polymod Script Exception', error.message);
+ case SCRIPT_CLASS_MODULE_NOT_FOUND:
+ // A scripted class tried to reference an unknown class or module.
+ logError(error.message);
+
+ // Last word is the class name.
+ var className:String = error.message.split(' ').pop();
+ var msg:String = 'Import error in ${error.origin}';
+ msg += '\nCould not import unknown class ${className}';
+ msg += '\nCheck to ensure the class exists and is spelled correctly.';
+
+ // Notify the user via popup.
+ showAlert('Polymod Script Import Error', msg);
+ case SCRIPT_CLASS_MODULE_BLACKLISTED:
+ // A scripted class tried to reference a blacklisted class or module.
+ logError(error.message);
+ // Notify the user via popup.
+ showAlert('Polymod Script Blacklist Violation', error.message);
+
+ default:
+ // Log the message based on its severity.
+ switch (error.severity)
+ {
+ case NOTICE:
+ logInfo(error.message);
+ case WARNING:
+ logWarn(error.message);
+ case ERROR:
+ logError(error.message);
+ }
+ }
+ }
+
+ static function logInfo(message:String):Void
+ {
+ trace('[INFO-] ${message}');
+ }
+
+ static function logError(message:String):Void
+ {
+ trace('[ERROR] ${message}');
+ }
+
+ static function logWarn(message:String):Void
+ {
+ trace('[WARN-] ${message}');
+ }
}
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 9ec5a968f..9586d6f46 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -1,5 +1,6 @@
package funkin.modding;
+import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.SongData;
@@ -11,251 +12,271 @@ import funkin.util.FileUtil;
class PolymodHandler
{
- /**
- * The API version that mods should comply with.
- * Format this with Semantic Versioning; ...
- * Bug fixes increment the patch version, new features increment the minor version.
- * Changes that break old mods increment the major version.
- */
- static final API_VERSION = "0.1.0";
+ /**
+ * The API version that mods should comply with.
+ * Format this with Semantic Versioning; ...
+ * Bug fixes increment the patch version, new features increment the minor version.
+ * Changes that break old mods increment the major version.
+ */
+ static final API_VERSION = "0.1.0";
- /**
- * Where relative to the executable that mods are located.
- */
- static final MOD_FOLDER = "mods";
+ /**
+ * Where relative to the executable that mods are located.
+ */
+ static final MOD_FOLDER = "mods";
- public static function createModRoot()
- {
- FileUtil.createDirIfNotExists(MOD_FOLDER);
- }
+ public static function createModRoot()
+ {
+ FileUtil.createDirIfNotExists(MOD_FOLDER);
+ }
- /**
- * Loads the game with ALL mods enabled with Polymod.
- */
- public static function loadAllMods()
- {
- // Create the mod root if it doesn't exist.
- createModRoot();
- trace("Initializing Polymod (using all mods)...");
- loadModsById(getAllModIds());
- }
+ /**
+ * Loads the game with ALL mods enabled with Polymod.
+ */
+ public static function loadAllMods()
+ {
+ // Create the mod root if it doesn't exist.
+ createModRoot();
+ trace("Initializing Polymod (using all mods)...");
+ loadModsById(getAllModIds());
+ }
- /**
- * Loads the game with configured mods enabled with Polymod.
- */
- public static function loadEnabledMods()
- {
- // Create the mod root if it doesn't exist.
- createModRoot();
+ /**
+ * Loads the game with configured mods enabled with Polymod.
+ */
+ public static function loadEnabledMods()
+ {
+ // Create the mod root if it doesn't exist.
+ createModRoot();
- trace("Initializing Polymod (using configured mods)...");
- loadModsById(getEnabledModIds());
- }
+ trace("Initializing Polymod (using configured mods)...");
+ loadModsById(getEnabledModIds());
+ }
- /**
- * Loads the game without any mods enabled with Polymod.
- */
- public static function loadNoMods()
- {
- // Create the mod root if it doesn't exist.
- createModRoot();
+ /**
+ * Loads the game without any mods enabled with Polymod.
+ */
+ public static function loadNoMods()
+ {
+ // Create the mod root if it doesn't exist.
+ createModRoot();
- // We still need to configure the debug print calls etc.
- trace("Initializing Polymod (using no mods)...");
- loadModsById([]);
- }
+ // We still need to configure the debug print calls etc.
+ trace("Initializing Polymod (using no mods)...");
+ loadModsById([]);
+ }
- public static function loadModsById(ids:Array)
- {
- if (ids.length == 0)
- {
- trace('You attempted to load zero mods.');
- }
- else
- {
- trace('Attempting to load ${ids.length} mods...');
- }
- var loadedModList = polymod.Polymod.init({
- // Root directory for all mods.
- modRoot: MOD_FOLDER,
- // The directories for one or more mods to load.
- dirs: ids,
- // Framework being used to load assets.
- framework: OPENFL,
- // The current version of our API.
- apiVersionRule: API_VERSION,
- // Call this function any time an error occurs.
- errorCallback: PolymodErrorHandler.onPolymodError,
- // Enforce semantic version patterns for each mod.
- // modVersions: null,
- // A map telling Polymod what the asset type is for unfamiliar file extensions.
- // extensionMap: [],
+ public static function loadModsById(ids:Array)
+ {
+ if (ids.length == 0)
+ {
+ trace('You attempted to load zero mods.');
+ }
+ else
+ {
+ trace('Attempting to load ${ids.length} mods...');
+ }
- frameworkParams: buildFrameworkParams(),
+ buildImports();
- // List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
- ignoredFiles: Polymod.getDefaultIgnoreList(),
+ var loadedModList = polymod.Polymod.init({
+ // Root directory for all mods.
+ modRoot: MOD_FOLDER,
+ // The directories for one or more mods to load.
+ dirs: ids,
+ // Framework being used to load assets.
+ framework: OPENFL,
+ // The current version of our API.
+ apiVersionRule: API_VERSION,
+ // Call this function any time an error occurs.
+ errorCallback: PolymodErrorHandler.onPolymodError,
+ // Enforce semantic version patterns for each mod.
+ // modVersions: null,
+ // A map telling Polymod what the asset type is for unfamiliar file extensions.
+ // extensionMap: [],
- // Parsing rules for various data formats.
- parseRules: buildParseRules(),
+ frameworkParams: buildFrameworkParams(),
- // Parse hxc files and register the scripted classes in them.
- useScriptedClasses: true,
- });
+ // List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
+ ignoredFiles: Polymod.getDefaultIgnoreList(),
- if (loadedModList == null)
- {
- trace('[POLYMOD] An error occurred! Failed when loading mods!');
- }
- else
- {
- if (loadedModList.length == 0)
- {
- trace('[POLYMOD] Mod loading complete. We loaded no mods / ${ids.length} mods.');
- }
- else
- {
- trace('[POLYMOD] Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
- }
- }
+ // Parsing rules for various data formats.
+ parseRules: buildParseRules(),
- for (mod in loadedModList)
- {
- trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
- }
+ // Parse hxc files and register the scripted classes in them.
+ useScriptedClasses: true,
+ });
- #if debug
- var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
- trace('[POLYMOD] Installed mods have replaced ${fileList.length} images.');
- for (item in fileList)
- trace(' * $item');
+ if (loadedModList == null)
+ {
+ trace('An error occurred! Failed when loading mods!');
+ }
+ else
+ {
+ if (loadedModList.length == 0)
+ {
+ trace('Mod loading complete. We loaded no mods / ${ids.length} mods.');
+ }
+ else
+ {
+ trace('Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
+ }
+ }
- fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
- trace('[POLYMOD] Installed mods have replaced ${fileList.length} text files.');
- for (item in fileList)
- trace(' * $item');
+ for (mod in loadedModList)
+ {
+ trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
+ }
- fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
- trace('[POLYMOD] Installed mods have replaced ${fileList.length} music files.');
- for (item in fileList)
- trace(' * $item');
+ #if debug
+ var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
+ trace('Installed mods have replaced ${fileList.length} images.');
+ for (item in fileList)
+ trace(' * $item');
- fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
- trace('[POLYMOD] Installed mods have replaced ${fileList.length} sound files.');
- for (item in fileList)
- trace(' * $item');
+ fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
+ trace('Installed mods have added/replaced ${fileList.length} text files.');
+ for (item in fileList)
+ trace(' * $item');
- fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
- trace('[POLYMOD] Installed mods have replaced ${fileList.length} generic audio files.');
- for (item in fileList)
- trace(' * $item');
- #end
- }
+ fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
+ trace('Installed mods have replaced ${fileList.length} music files.');
+ for (item in fileList)
+ trace(' * $item');
- static function buildParseRules():polymod.format.ParseRules
- {
- var output = polymod.format.ParseRules.getDefault();
- // Ensure TXT files have merge support.
- output.addType("txt", TextFileFormat.LINES);
- // Ensure script files have merge support.
- output.addType("hscript", TextFileFormat.PLAINTEXT);
- output.addType("hxs", TextFileFormat.PLAINTEXT);
- output.addType("hxc", TextFileFormat.PLAINTEXT);
- output.addType("hx", TextFileFormat.PLAINTEXT);
+ fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
+ trace('Installed mods have replaced ${fileList.length} sound files.');
+ for (item in fileList)
+ trace(' * $item');
- // You can specify the format of a specific file, with file extension.
- // output.addFile("data/introText.txt", TextFileFormat.LINES)
- return output;
- }
+ fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
+ trace('Installed mods have replaced ${fileList.length} generic audio files.');
+ for (item in fileList)
+ trace(' * $item');
+ #end
+ }
- static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams
- {
- return {
- assetLibraryPaths: [
- "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
- "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
- ]
- }
- }
+ static function buildImports():Void
+ {
+ // Add default imports for common classes.
- public static function getAllMods():Array
- {
- trace('Scanning the mods folder...');
- var modMetadata = Polymod.scan({
- modRoot: MOD_FOLDER,
- apiVersionRule: API_VERSION,
- errorCallback: PolymodErrorHandler.onPolymodError
- });
- trace('Found ${modMetadata.length} mods when scanning.');
- return modMetadata;
- }
+ // Add import aliases for certain classes.
+ // NOTE: Scripted classes are automatically aliased to their parent class.
+ Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
- public static function getAllModIds():Array
- {
- var modIds = [for (i in getAllMods()) i.id];
- return modIds;
- }
+ // Add blacklisting for prohibited classes and packages.
+ // `polymod.*`
+ for (cls in ClassMacro.listClassesInPackage('polymod'))
+ {
+ var className = Type.getClassName(cls);
+ Polymod.blacklistImport(className);
+ }
+ }
- public static function setEnabledMods(newModList:Array):Void
- {
- FlxG.save.data.enabledMods = newModList;
- // Make sure to COMMIT the changes.
- FlxG.save.flush();
- }
+ static function buildParseRules():polymod.format.ParseRules
+ {
+ var output = polymod.format.ParseRules.getDefault();
+ // Ensure TXT files have merge support.
+ output.addType("txt", TextFileFormat.LINES);
+ // Ensure script files have merge support.
+ output.addType("hscript", TextFileFormat.PLAINTEXT);
+ output.addType("hxs", TextFileFormat.PLAINTEXT);
+ output.addType("hxc", TextFileFormat.PLAINTEXT);
+ output.addType("hx", TextFileFormat.PLAINTEXT);
- /**
- * Returns the list of enabled mods.
- * @return Array
- */
- public static function getEnabledModIds():Array
- {
- if (FlxG.save.data.enabledMods == null)
- {
- // NOTE: If the value is null, the enabled mod list is unconfigured.
- // Currently, we default to disabling newly installed mods.
- // If we want to auto-enable new mods, but otherwise leave the configured list in place,
- // we will need some custom logic.
- FlxG.save.data.enabledMods = [];
- }
- return FlxG.save.data.enabledMods;
- }
+ // You can specify the format of a specific file, with file extension.
+ // output.addFile("data/introText.txt", TextFileFormat.LINES)
+ return output;
+ }
- public static function getEnabledMods():Array
- {
- var modIds = getEnabledModIds();
- var modMetadata = getAllMods();
- var enabledMods = [];
- for (item in modMetadata)
- {
- if (modIds.indexOf(item.id) != -1)
- {
- enabledMods.push(item);
- }
- }
- return enabledMods;
- }
+ static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams
+ {
+ return {
+ assetLibraryPaths: [
+ "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2",
+ "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
+ ]
+ }
+ }
- public static function forceReloadAssets()
- {
- // Forcibly clear scripts so that scripts can be edited.
- ModuleHandler.clearModuleCache();
- Polymod.clearScripts();
+ public static function getAllMods():Array
+ {
+ trace('Scanning the mods folder...');
+ var modMetadata = Polymod.scan({
+ modRoot: MOD_FOLDER,
+ apiVersionRule: API_VERSION,
+ errorCallback: PolymodErrorHandler.onPolymodError
+ });
+ trace('Found ${modMetadata.length} mods when scanning.');
+ return modMetadata;
+ }
- // Forcibly reload Polymod so it finds any new files.
- // TODO: Replace this with loadEnabledMods().
- funkin.modding.PolymodHandler.loadAllMods();
+ public static function getAllModIds():Array
+ {
+ var modIds = [for (i in getAllMods()) i.id];
+ return modIds;
+ }
- // Reload scripted classes so stages and modules will update.
- Polymod.registerAllScriptClasses();
+ public static function setEnabledMods(newModList:Array):Void
+ {
+ FlxG.save.data.enabledMods = newModList;
+ // Make sure to COMMIT the changes.
+ FlxG.save.flush();
+ }
- // Reload everything that is cached.
- // Currently this freezes the game for a second but I guess that's tolerable?
+ /**
+ * Returns the list of enabled mods.
+ * @return Array
+ */
+ public static function getEnabledModIds():Array
+ {
+ if (FlxG.save.data.enabledMods == null)
+ {
+ // NOTE: If the value is null, the enabled mod list is unconfigured.
+ // Currently, we default to disabling newly installed mods.
+ // If we want to auto-enable new mods, but otherwise leave the configured list in place,
+ // we will need some custom logic.
+ FlxG.save.data.enabledMods = [];
+ }
+ return FlxG.save.data.enabledMods;
+ }
- // TODO: Reload event callbacks
+ public static function getEnabledMods():Array
+ {
+ var modIds = getEnabledModIds();
+ var modMetadata = getAllMods();
+ var enabledMods = [];
+ for (item in modMetadata)
+ {
+ if (modIds.indexOf(item.id) != -1)
+ {
+ enabledMods.push(item);
+ }
+ }
+ return enabledMods;
+ }
- SongDataParser.loadSongCache();
- StageDataParser.loadStageCache();
- CharacterDataParser.loadCharacterCache();
- ModuleHandler.loadModuleCache();
- }
+ public static function forceReloadAssets()
+ {
+ // Forcibly clear scripts so that scripts can be edited.
+ ModuleHandler.clearModuleCache();
+ Polymod.clearScripts();
+
+ // Forcibly reload Polymod so it finds any new files.
+ // TODO: Replace this with loadEnabledMods().
+ funkin.modding.PolymodHandler.loadAllMods();
+
+ // Reload scripted classes so stages and modules will update.
+ Polymod.registerAllScriptClasses();
+
+ // Reload everything that is cached.
+ // Currently this freezes the game for a second but I guess that's tolerable?
+
+ // TODO: Reload event callbacks
+
+ SongDataParser.loadSongCache();
+ StageDataParser.loadStageCache();
+ CharacterDataParser.loadCharacterCache();
+ ModuleHandler.loadModuleCache();
+ }
}
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 3e4249063..9e31a3032 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -15,269 +15,277 @@ typedef ScriptEventType = EventType;
*/
class ScriptEvent
{
- /**
- * Called when the relevant object is created.
- * Keep in mind that the constructor may be called before the object is needed,
- * for the purposes of caching data or otherwise.
- *
- * This event is not cancelable.
- */
- public static inline final CREATE:ScriptEventType = "CREATE";
+ /**
+ * Called when the relevant object is created.
+ * Keep in mind that the constructor may be called before the object is needed,
+ * for the purposes of caching data or otherwise.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final CREATE:ScriptEventType = "CREATE";
- /**
- * Called when the relevant object is destroyed.
- * This should perform relevant cleanup to ensure good performance.
- *
- * This event is not cancelable.
- */
- public static inline final DESTROY:ScriptEventType = "DESTROY";
+ /**
+ * Called when the relevant object is destroyed.
+ * This should perform relevant cleanup to ensure good performance.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final DESTROY:ScriptEventType = "DESTROY";
- /**
- * Called during the update function.
- * This is called every frame, so be careful!
- *
- * This event is not cancelable.
- */
- public static inline final UPDATE:ScriptEventType = "UPDATE";
+ /**
+ * Called during the update function.
+ * This is called every frame, so be careful!
+ *
+ * This event is not cancelable.
+ */
+ public static inline final UPDATE:ScriptEventType = "UPDATE";
- /**
- * Called when the player moves to pause the game.
- *
- * This event IS cancelable! Canceling the event will prevent the game from pausing.
- */
- public static inline final PAUSE:ScriptEventType = "PAUSE";
+ /**
+ * Called when the player moves to pause the game.
+ *
+ * This event IS cancelable! Canceling the event will prevent the game from pausing.
+ */
+ public static inline final PAUSE:ScriptEventType = "PAUSE";
- /**
- * Called when the player moves to unpause the game while paused.
- *
- * This event IS cancelable! Canceling the event will prevent the game from resuming.
- */
- public static inline final RESUME:ScriptEventType = "RESUME";
+ /**
+ * Called when the player moves to unpause the game while paused.
+ *
+ * This event IS cancelable! Canceling the event will prevent the game from resuming.
+ */
+ public static inline final RESUME:ScriptEventType = "RESUME";
- /**
- * Called once per step in the song. This happens 4 times per measure.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT";
+ /**
+ * Called once per step in the song. This happens 4 times per measure.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SONG_BEAT_HIT:ScriptEventType = "BEAT_HIT";
- /**
- * Called once per step in the song. This happens 16 times per measure.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT";
+ /**
+ * Called once per step in the song. This happens 16 times per measure.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SONG_STEP_HIT:ScriptEventType = "STEP_HIT";
- /**
- * Called when a character hits a note.
- * Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
- *
- * This event IS cancelable! Canceling this event prevents the note from being hit,
- * and will likely result in a miss later.
- */
- public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT";
+ /**
+ * Called when a character hits a note.
+ * Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
+ *
+ * This event IS cancelable! Canceling this event prevents the note from being hit,
+ * and will likely result in a miss later.
+ */
+ public static inline final NOTE_HIT:ScriptEventType = "NOTE_HIT";
- /**
- * Called when a character misses a note.
- * Important information such as note data, player/opponent, etc. are all provided.
- *
- * This event IS cancelable! Canceling this event prevents the note from being considered missed,
- * avoiding a combo break and lost health.
- */
- public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS";
+ /**
+ * Called when a character misses a note.
+ * Important information such as note data, player/opponent, etc. are all provided.
+ *
+ * This event IS cancelable! Canceling this event prevents the note from being considered missed,
+ * avoiding a combo break and lost health.
+ */
+ public static inline final NOTE_MISS:ScriptEventType = "NOTE_MISS";
- /**
- * Called when a character presses a note when there was none there, causing them to lose health.
- * Important information such as direction pressed, etc. are all provided.
- *
- * This event IS cancelable! Canceling this event prevents the note from being considered missed,
- * avoiding lost health/score and preventing the miss animation.
- */
- public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS";
+ /**
+ * Called when a character presses a note when there was none there, causing them to lose health.
+ * Important information such as direction pressed, etc. are all provided.
+ *
+ * This event IS cancelable! Canceling this event prevents the note from being considered missed,
+ * avoiding lost health/score and preventing the miss animation.
+ */
+ public static inline final NOTE_GHOST_MISS:ScriptEventType = "NOTE_GHOST_MISS";
- /**
- * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_START:ScriptEventType = "SONG_START";
+ /**
+ * Called when a song event is reached in the chart.
+ *
+ * This event IS cancelable! Cancelling this event prevents the event from being triggered,
+ * thus blocking its normal functionality.
+ */
+ public static inline final SONG_EVENT:ScriptEventType = "SONG_EVENT";
- /**
- * Called when the song ends. This happens as the instrumental and vocals end.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_END:ScriptEventType = "SONG_END";
+ /**
+ * Called when the song starts. This occurs as the countdown ends and the instrumental and vocals begin.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SONG_START:ScriptEventType = "SONG_START";
- /**
- * Called when the countdown begins. This occurs before the song starts.
- *
- * This event IS cancelable! Canceling this event will prevent the countdown from starting.
- * - The song will not start until you call Countdown.performCountdown() later.
- * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it.
- */
- public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START";
+ /**
+ * Called when the song ends. This happens as the instrumental and vocals end.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SONG_END:ScriptEventType = "SONG_END";
- /**
- * Called when a step of the countdown happens.
- * Includes information about what step of the countdown was hit.
- *
- * This event IS cancelable! Canceling this event will pause the countdown.
- * - The countdown will not resume until you call PlayState.resumeCountdown().
- */
- public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP";
+ /**
+ * Called when the countdown begins. This occurs before the song starts.
+ *
+ * This event IS cancelable! Canceling this event will prevent the countdown from starting.
+ * - The song will not start until you call Countdown.performCountdown() later.
+ * - Note that calling performCountdown() will trigger this event again, so be sure to add logic to ignore it.
+ */
+ public static inline final COUNTDOWN_START:ScriptEventType = "COUNTDOWN_START";
- /**
- * Called when the countdown is done but just before the song starts.
- *
- * This event is not cancelable.
- */
- public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
+ /**
+ * Called when a step of the countdown happens.
+ * Includes information about what step of the countdown was hit.
+ *
+ * This event IS cancelable! Canceling this event will pause the countdown.
+ * - The countdown will not resume until you call PlayState.resumeCountdown().
+ */
+ public static inline final COUNTDOWN_STEP:ScriptEventType = "COUNTDOWN_STEP";
- /**
- * Called before the game over screen triggers and the death animation plays.
- *
- * This event is not cancelable.
- */
- public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
+ /**
+ * Called when the countdown is done but just before the song starts.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final COUNTDOWN_END:ScriptEventType = "COUNTDOWN_END";
- /**
- * Called after the player presses a key to restart the game.
- * This can happen from the pause menu or the game over screen.
- *
- * This event IS cancelable! Canceling this event will prevent the game from restarting.
- */
- public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
+ /**
+ * Called before the game over screen triggers and the death animation plays.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final GAME_OVER:ScriptEventType = "GAME_OVER";
- /**
- * Called when the player pushes down any key on the keyboard.
- *
- * This event is not cancelable.
- */
- public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN";
+ /**
+ * Called after the player presses a key to restart the game.
+ * This can happen from the pause menu or the game over screen.
+ *
+ * This event IS cancelable! Canceling this event will prevent the game from restarting.
+ */
+ public static inline final SONG_RETRY:ScriptEventType = "SONG_RETRY";
- /**
- * Called when the player releases a key on the keyboard.
- *
- * This event is not cancelable.
- */
- public static inline final KEY_UP:ScriptEventType = "KEY_UP";
+ /**
+ * Called when the player pushes down any key on the keyboard.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final KEY_DOWN:ScriptEventType = "KEY_DOWN";
- /**
- * Called when the game has finished loading the notes from JSON.
- * This allows modders to mutate the notes before they are used in the song.
- *
- * This event is not cancelable.
- */
- public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
+ /**
+ * Called when the player releases a key on the keyboard.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final KEY_UP:ScriptEventType = "KEY_UP";
- /**
- * Called when the game is about to switch the current FlxState.
- *
- * This event is not cancelable.
- */
- public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN";
+ /**
+ * Called when the game has finished loading the notes from JSON.
+ * This allows modders to mutate the notes before they are used in the song.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED";
- /**
- * Called when the game has finished switching the current FlxState.
- *
- * This event is not cancelable.
- */
- public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END";
+ /**
+ * Called when the game is about to switch the current FlxState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final STATE_CHANGE_BEGIN:ScriptEventType = "STATE_CHANGE_BEGIN";
- /**
- * Called when the game is about to open a new FlxSubState.
- *
- * This event is not cancelable.
- */
- public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN";
+ /**
+ * Called when the game has finished switching the current FlxState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final STATE_CHANGE_END:ScriptEventType = "STATE_CHANGE_END";
- /**
- * Called when the game has finished opening a new FlxSubState.
- *
- * This event is not cancelable.
- */
- public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END";
+ /**
+ * Called when the game is about to open a new FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_OPEN_BEGIN:ScriptEventType = "SUBSTATE_OPEN_BEGIN";
- /**
- * Called when the game is about to close the current FlxSubState.
- *
- * This event is not cancelable.
- */
- public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN";
+ /**
+ * Called when the game has finished opening a new FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_OPEN_END:ScriptEventType = "SUBSTATE_OPEN_END";
- /**
- * Called when the game has finished closing the current FlxSubState.
- *
- * This event is not cancelable.
- */
- public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END";
+ /**
+ * Called when the game is about to close the current FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_CLOSE_BEGIN:ScriptEventType = "SUBSTATE_CLOSE_BEGIN";
- /**
- * Called when the game is exiting the current FlxState.
- *
- * This event is not cancelable.
- */
- /**
- * If true, the behavior associated with this event can be prevented.
- * For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
- * until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
- */
- public var cancelable(default, null):Bool;
+ /**
+ * Called when the game has finished closing the current FlxSubState.
+ *
+ * This event is not cancelable.
+ */
+ public static inline final SUBSTATE_CLOSE_END:ScriptEventType = "SUBSTATE_CLOSE_END";
- /**
- * The type associated with the event.
- */
- public var type(default, null):ScriptEventType;
+ /**
+ * Called when the game is exiting the current FlxState.
+ *
+ * This event is not cancelable.
+ */
+ /**
+ * If true, the behavior associated with this event can be prevented.
+ * For example, cancelling COUNTDOWN_START should prevent the countdown from starting,
+ * until another script restarts it, or cancelling NOTE_HIT should cause the note to be missed.
+ */
+ public var cancelable(default, null):Bool;
- /**
- * Whether the event should continue to be triggered on additional targets.
- */
- public var shouldPropagate(default, null):Bool;
+ /**
+ * The type associated with the event.
+ */
+ public var type(default, null):ScriptEventType;
- /**
- * Whether the event has been canceled by one of the scripts that received it.
- */
- public var eventCanceled(default, null):Bool;
+ /**
+ * Whether the event should continue to be triggered on additional targets.
+ */
+ public var shouldPropagate(default, null):Bool;
- public function new(type:ScriptEventType, cancelable:Bool = false):Void
- {
- this.type = type;
- this.cancelable = cancelable;
- this.eventCanceled = false;
- this.shouldPropagate = true;
- }
+ /**
+ * Whether the event has been canceled by one of the scripts that received it.
+ */
+ public var eventCanceled(default, null):Bool;
- /**
- * Call this function on a cancelable event to cancel the associated behavior.
- * For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
- */
- public function cancelEvent():Void
- {
- if (cancelable)
- {
- eventCanceled = true;
- }
- }
+ public function new(type:ScriptEventType, cancelable:Bool = false):Void
+ {
+ this.type = type;
+ this.cancelable = cancelable;
+ this.eventCanceled = false;
+ this.shouldPropagate = true;
+ }
- public function cancel():Void
- {
- // This typo happens enough that I just added this.
- cancelEvent();
- }
+ /**
+ * Call this function on a cancelable event to cancel the associated behavior.
+ * For example, cancelling COUNTDOWN_START will prevent the countdown from starting.
+ */
+ public function cancelEvent():Void
+ {
+ if (cancelable)
+ {
+ eventCanceled = true;
+ }
+ }
- /**
- * Call this function to stop any other Scripteds from receiving the event.
- */
- public function stopPropagation():Void
- {
- shouldPropagate = false;
- }
+ public function cancel():Void
+ {
+ // This typo happens enough that I just added this.
+ cancelEvent();
+ }
- public function toString():String
- {
- return 'ScriptEvent(type=$type, cancelable=$cancelable)';
- }
+ /**
+ * Call this function to stop any other Scripteds from receiving the event.
+ */
+ public function stopPropagation():Void
+ {
+ shouldPropagate = false;
+ }
+
+ public function toString():String
+ {
+ return 'ScriptEvent(type=$type, cancelable=$cancelable)';
+ }
}
/**
@@ -288,29 +296,29 @@ class ScriptEvent
*/
class NoteScriptEvent extends ScriptEvent
{
- /**
- * The note associated with this event.
- * You cannot replace it, but you can edit it.
- */
- public var note(default, null):Note;
+ /**
+ * The note associated with this event.
+ * You cannot replace it, but you can edit it.
+ */
+ public var note(default, null):Note;
- /**
- * The combo count as it is with this event.
- * Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled).
- */
- public var comboCount(default, null):Int;
+ /**
+ * The combo count as it is with this event.
+ * Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled).
+ */
+ public var comboCount(default, null):Int;
- public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
- {
- super(type, cancelable);
- this.note = note;
- this.comboCount = comboCount;
- }
+ public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
+ {
+ super(type, cancelable);
+ this.note = note;
+ this.comboCount = comboCount;
+ }
- public override function toString():String
- {
- return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ', comboCount=' + comboCount + ')';
- }
+ public override function toString():String
+ {
+ return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ', comboCount=' + comboCount + ')';
+ }
}
/**
@@ -318,52 +326,81 @@ class NoteScriptEvent extends ScriptEvent
*/
class GhostMissNoteScriptEvent extends ScriptEvent
{
- /**
- * The direction that was mistakenly pressed.
- */
- public var dir(default, null):NoteDir;
+ /**
+ * The direction that was mistakenly pressed.
+ */
+ public var dir(default, null):NoteDir;
- /**
- * Whether there was a note within judgement range when this ghost note was pressed.
- */
- public var hasPossibleNotes(default, null):Bool;
+ /**
+ * Whether there was a note within judgement range when this ghost note was pressed.
+ */
+ public var hasPossibleNotes(default, null):Bool;
- /**
- * How much health should be lost when this ghost note is pressed.
- * Remember that max health is 2.00.
- */
- public var healthChange(default, default):Float;
+ /**
+ * How much health should be lost when this ghost note is pressed.
+ * Remember that max health is 2.00.
+ */
+ public var healthChange(default, default):Float;
- /**
- * How much score should be lost when this ghost note is pressed.
- */
- public var scoreChange(default, default):Int;
+ /**
+ * How much score should be lost when this ghost note is pressed.
+ */
+ public var scoreChange(default, default):Int;
- /**
- * Whether to play the record scratch sound.
- */
- public var playSound(default, default):Bool;
+ /**
+ * Whether to play the record scratch sound.
+ */
+ public var playSound(default, default):Bool;
- /**
- * Whether to play the miss animation on the player.
- */
- public var playAnim(default, default):Bool;
+ /**
+ * Whether to play the miss animation on the player.
+ */
+ public var playAnim(default, default):Bool;
- public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
- {
- super(ScriptEvent.NOTE_GHOST_MISS, true);
- this.dir = dir;
- this.hasPossibleNotes = hasPossibleNotes;
- this.healthChange = healthChange;
- this.scoreChange = scoreChange;
- this.playSound = true;
- this.playAnim = true;
- }
+ public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
+ {
+ super(ScriptEvent.NOTE_GHOST_MISS, true);
+ this.dir = dir;
+ this.hasPossibleNotes = hasPossibleNotes;
+ this.healthChange = healthChange;
+ this.scoreChange = scoreChange;
+ this.playSound = true;
+ this.playAnim = true;
+ }
- public override function toString():String
- {
- return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')';
- }
+ public override function toString():String
+ {
+ return 'GhostMissNoteScriptEvent(dir=' + dir + ', hasPossibleNotes=' + hasPossibleNotes + ')';
+ }
+}
+
+/**
+ * An event that is fired when the song reaches an event.
+ */
+class SongEventScriptEvent extends ScriptEvent
+{
+ /**
+ * The note associated with this event.
+ * You cannot replace it, but you can edit it.
+ */
+ public var event(default, null):funkin.play.song.SongData.SongEventData;
+
+ /**
+ * The combo count as it is with this event.
+ * Will be (combo) on miss events and (combo + 1) on hit events (the stored combo count won't update if the event is cancelled).
+ */
+ public var comboCount(default, null):Int;
+
+ public function new(event:funkin.play.song.SongData.SongEventData):Void
+ {
+ super(ScriptEvent.SONG_EVENT, true);
+ this.event = event;
+ }
+
+ public override function toString():String
+ {
+ return 'SongEventScriptEvent(event=' + event + ')';
+ }
}
/**
@@ -371,22 +408,22 @@ class GhostMissNoteScriptEvent extends ScriptEvent
*/
class UpdateScriptEvent extends ScriptEvent
{
- /**
- * The note associated with this event.
- * You cannot replace it, but you can edit it.
- */
- public var elapsed(default, null):Float;
+ /**
+ * The note associated with this event.
+ * You cannot replace it, but you can edit it.
+ */
+ public var elapsed(default, null):Float;
- public function new(elapsed:Float):Void
- {
- super(ScriptEvent.UPDATE, false);
- this.elapsed = elapsed;
- }
+ public function new(elapsed:Float):Void
+ {
+ super(ScriptEvent.UPDATE, false);
+ this.elapsed = elapsed;
+ }
- public override function toString():String
- {
- return 'UpdateScriptEvent(elapsed=$elapsed)';
- }
+ public override function toString():String
+ {
+ return 'UpdateScriptEvent(elapsed=$elapsed)';
+ }
}
/**
@@ -395,27 +432,27 @@ class UpdateScriptEvent extends ScriptEvent
*/
class SongTimeScriptEvent extends ScriptEvent
{
- /**
- * The current beat of the song.
- */
- public var beat(default, null):Int;
+ /**
+ * The current beat of the song.
+ */
+ public var beat(default, null):Int;
- /**
- * The current step of the song.
- */
- public var step(default, null):Int;
+ /**
+ * The current step of the song.
+ */
+ public var step(default, null):Int;
- public function new(type:ScriptEventType, beat:Int, step:Int):Void
- {
- super(type, true);
- this.beat = beat;
- this.step = step;
- }
+ public function new(type:ScriptEventType, beat:Int, step:Int):Void
+ {
+ super(type, true);
+ this.beat = beat;
+ this.step = step;
+ }
- public override function toString():String
- {
- return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')';
- }
+ public override function toString():String
+ {
+ return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')';
+ }
}
/**
@@ -424,21 +461,21 @@ class SongTimeScriptEvent extends ScriptEvent
*/
class CountdownScriptEvent extends ScriptEvent
{
- /**
- * The current step of the countdown.
- */
- public var step(default, null):CountdownStep;
+ /**
+ * The current step of the countdown.
+ */
+ public var step(default, null):CountdownStep;
- public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void
- {
- super(type, cancelable);
- this.step = step;
- }
+ public function new(type:ScriptEventType, step:CountdownStep, cancelable = true):Void
+ {
+ super(type, cancelable);
+ this.step = step;
+ }
- public override function toString():String
- {
- return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')';
- }
+ public override function toString():String
+ {
+ return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')';
+ }
}
/**
@@ -446,21 +483,21 @@ class CountdownScriptEvent extends ScriptEvent
*/
class KeyboardInputScriptEvent extends ScriptEvent
{
- /**
- * The associated keyboard event.
- */
- public var event(default, null):KeyboardEvent;
+ /**
+ * The associated keyboard event.
+ */
+ public var event(default, null):KeyboardEvent;
- public function new(type:ScriptEventType, event:KeyboardEvent):Void
- {
- super(type, false);
- this.event = event;
- }
+ public function new(type:ScriptEventType, event:KeyboardEvent):Void
+ {
+ super(type, false);
+ this.event = event;
+ }
- public override function toString():String
- {
- return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')';
- }
+ public override function toString():String
+ {
+ return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')';
+ }
}
/**
@@ -468,35 +505,35 @@ class KeyboardInputScriptEvent extends ScriptEvent
*/
class SongLoadScriptEvent extends ScriptEvent
{
- /**
- * The note associated with this event.
- * You cannot replace it, but you can edit it.
- */
- public var notes(default, set):Array;
+ /**
+ * The note associated with this event.
+ * You cannot replace it, but you can edit it.
+ */
+ public var notes(default, set):Array;
- public var id(default, null):String;
+ public var id(default, null):String;
- public var difficulty(default, null):String;
+ public var difficulty(default, null):String;
- function set_notes(notes:Array):Array
- {
- this.notes = notes;
- return this.notes;
- }
+ function set_notes(notes:Array):Array
+ {
+ this.notes = notes;
+ return this.notes;
+ }
- public function new(id:String, difficulty:String, notes:Array):Void
- {
- super(ScriptEvent.SONG_LOADED, false);
- this.id = id;
- this.difficulty = difficulty;
- this.notes = notes;
- }
+ public function new(id:String, difficulty:String, notes:Array):Void
+ {
+ super(ScriptEvent.SONG_LOADED, false);
+ this.id = id;
+ this.difficulty = difficulty;
+ this.notes = notes;
+ }
- public override function toString():String
- {
- var noteStr = notes == null ? 'null' : 'Array(' + notes.length + ')';
- return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)';
- }
+ public override function toString():String
+ {
+ var noteStr = notes == null ? 'null' : 'Array(' + notes.length + ')';
+ return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)';
+ }
}
/**
@@ -504,21 +541,21 @@ class SongLoadScriptEvent extends ScriptEvent
*/
class StateChangeScriptEvent extends ScriptEvent
{
- /**
- * The state the game is moving into.
- */
- public var targetState(default, null):FlxState;
+ /**
+ * The state the game is moving into.
+ */
+ public var targetState(default, null):FlxState;
- public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void
- {
- super(type, cancelable);
- this.targetState = targetState;
- }
+ public function new(type:ScriptEventType, targetState:FlxState, cancelable:Bool = false):Void
+ {
+ super(type, cancelable);
+ this.targetState = targetState;
+ }
- public override function toString():String
- {
- return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')';
- }
+ public override function toString():String
+ {
+ return 'StateChangeScriptEvent(type=' + type + ', targetState=' + targetState + ')';
+ }
}
/**
@@ -526,21 +563,21 @@ class StateChangeScriptEvent extends ScriptEvent
*/
class SubStateScriptEvent extends ScriptEvent
{
- /**
- * The state the game is moving into.
- */
- public var targetState(default, null):FlxSubState;
+ /**
+ * The state the game is moving into.
+ */
+ public var targetState(default, null):FlxSubState;
- public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void
- {
- super(type, cancelable);
- this.targetState = targetState;
- }
+ public function new(type:ScriptEventType, targetState:FlxSubState, cancelable:Bool = false):Void
+ {
+ super(type, cancelable);
+ this.targetState = targetState;
+ }
- public override function toString():String
- {
- return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
- }
+ public override function toString():String
+ {
+ return 'SubStateScriptEvent(type=' + type + ', targetState=' + targetState + ')';
+ }
}
/**
@@ -548,14 +585,14 @@ class SubStateScriptEvent extends ScriptEvent
*/
class PauseScriptEvent extends ScriptEvent
{
- /**
- * Whether to use the Gitaroo Man pause.
- */
- public var gitaroo(default, default):Bool;
+ /**
+ * Whether to use the Gitaroo Man pause.
+ */
+ public var gitaroo(default, default):Bool;
- public function new(gitaroo:Bool):Void
- {
- super(ScriptEvent.PAUSE, true);
- this.gitaroo = gitaroo;
- }
+ public function new(gitaroo:Bool):Void
+ {
+ super(ScriptEvent.PAUSE, true);
+ this.gitaroo = gitaroo;
+ }
}
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index d337ad719..0d76c2c14 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -10,109 +10,111 @@ import funkin.modding.events.ScriptEvent;
*/
class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass
{
- /**
- * Whether the module is currently active.
- */
- public var active(default, set):Bool = true;
+ /**
+ * Whether the module is currently active.
+ */
+ public var active(default, set):Bool = true;
- function set_active(value:Bool):Bool
- {
- this.active = value;
- return value;
- }
+ function set_active(value:Bool):Bool
+ {
+ this.active = value;
+ return value;
+ }
- public var moduleId(default, null):String = 'UNKNOWN';
+ public var moduleId(default, null):String = 'UNKNOWN';
- /**
- * Determines the order in which modules receive events.
- * You can modify this to change the order in which a given module receives events.
- *
- * Priority 1 is processed before Priority 1000, etc.
- */
- public var priority(default, set):Int;
+ /**
+ * Determines the order in which modules receive events.
+ * You can modify this to change the order in which a given module receives events.
+ *
+ * Priority 1 is processed before Priority 1000, etc.
+ */
+ public var priority(default, set):Int;
- function set_priority(value:Int):Int
- {
- this.priority = value;
- @:privateAccess
- ModuleHandler.reorderModuleCache();
- return value;
- }
+ function set_priority(value:Int):Int
+ {
+ this.priority = value;
+ @:privateAccess
+ ModuleHandler.reorderModuleCache();
+ return value;
+ }
- /**
- * Called when the module is initialized.
- * It may not be safe to reference other modules here since they may not be loaded yet.
- *
- * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
- */
- public function new(moduleId:String, priority:Int = 1000):Void
- {
- this.moduleId = moduleId;
- this.priority = priority;
- }
+ /**
+ * Called when the module is initialized.
+ * It may not be safe to reference other modules here since they may not be loaded yet.
+ *
+ * NOTE: To make the module start inactive, call `this.active = false` in the constructor.
+ */
+ public function new(moduleId:String, priority:Int = 1000):Void
+ {
+ this.moduleId = moduleId;
+ this.priority = priority;
+ }
- public function toString()
- {
- return 'Module(' + this.moduleId + ')';
- }
+ public function toString()
+ {
+ return 'Module(' + this.moduleId + ')';
+ }
- // TODO: Half of these aren't actually being called!!!!!!!
+ // TODO: Half of these aren't actually being called!!!!!!!
- public function onScriptEvent(event:ScriptEvent) {}
+ public function onScriptEvent(event:ScriptEvent) {}
- /**
- * Called when the module is first created.
- * This happens before the title screen appears!
- */
- public function onCreate(event:ScriptEvent) {}
+ /**
+ * Called when the module is first created.
+ * This happens before the title screen appears!
+ */
+ public function onCreate(event:ScriptEvent) {}
- /**
- * Called when a module is destroyed.
- * This currently only happens when reloading modules with F5.
- */
- public function onDestroy(event:ScriptEvent) {}
+ /**
+ * Called when a module is destroyed.
+ * This currently only happens when reloading modules with F5.
+ */
+ public function onDestroy(event:ScriptEvent) {}
- public function onUpdate(event:UpdateScriptEvent) {}
+ public function onUpdate(event:UpdateScriptEvent) {}
- public function onPause(event:PauseScriptEvent) {}
+ public function onPause(event:PauseScriptEvent) {}
- public function onResume(event:ScriptEvent) {}
+ public function onResume(event:ScriptEvent) {}
- public function onSongStart(event:ScriptEvent) {}
+ public function onSongStart(event:ScriptEvent) {}
- public function onSongEnd(event:ScriptEvent) {}
+ public function onSongEnd(event:ScriptEvent) {}
- public function onGameOver(event:ScriptEvent) {}
+ public function onGameOver(event:ScriptEvent) {}
- public function onNoteHit(event:NoteScriptEvent) {}
+ public function onNoteHit(event:NoteScriptEvent) {}
- public function onNoteMiss(event:NoteScriptEvent) {}
+ public function onNoteMiss(event:NoteScriptEvent) {}
- public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
+ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent) {}
- public function onStepHit(event:SongTimeScriptEvent) {}
+ public function onStepHit(event:SongTimeScriptEvent) {}
- public function onBeatHit(event:SongTimeScriptEvent) {}
+ public function onBeatHit(event:SongTimeScriptEvent) {}
- public function onCountdownStart(event:CountdownScriptEvent) {}
+ public function onSongEvent(event:SongEventScriptEvent) {}
- public function onCountdownStep(event:CountdownScriptEvent) {}
+ public function onCountdownStart(event:CountdownScriptEvent) {}
- public function onCountdownEnd(event:CountdownScriptEvent) {}
+ public function onCountdownStep(event:CountdownScriptEvent) {}
- public function onSongLoaded(event:SongLoadScriptEvent) {}
+ public function onCountdownEnd(event:CountdownScriptEvent) {}
- public function onStateChangeBegin(event:StateChangeScriptEvent) {}
+ public function onSongLoaded(event:SongLoadScriptEvent) {}
- public function onStateChangeEnd(event:StateChangeScriptEvent) {}
+ public function onStateChangeBegin(event:StateChangeScriptEvent) {}
- public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
+ public function onStateChangeEnd(event:StateChangeScriptEvent) {}
- public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
+ public function onSubstateOpenBegin(event:SubStateScriptEvent) {}
- public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
+ public function onSubstateOpenEnd(event:SubStateScriptEvent) {}
- public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
+ public function onSubstateCloseBegin(event:SubStateScriptEvent) {}
- public function onSongRetry(event:ScriptEvent) {}
+ public function onSubstateCloseEnd(event:SubStateScriptEvent) {}
+
+ public function onSongRetry(event:ScriptEvent) {}
}
diff --git a/source/funkin/modding/module/ModuleHandler.hx b/source/funkin/modding/module/ModuleHandler.hx
index 93ac7bc66..51d723d0d 100644
--- a/source/funkin/modding/module/ModuleHandler.hx
+++ b/source/funkin/modding/module/ModuleHandler.hx
@@ -11,132 +11,137 @@ import funkin.modding.module.ScriptedModule;
*/
class ModuleHandler
{
- static final moduleCache:Map = new Map();
- static var modulePriorityOrder:Array = [];
+ static final moduleCache:Map = new Map();
+ static var modulePriorityOrder:Array = [];
- /**
- * Parses and preloads the game's stage data and scripts when the game starts.
- *
- * If you want to force stages to be reloaded, you can just call this function again.
- */
- public static function loadModuleCache():Void
- {
- // Clear any stages that are cached if there were any.
- clearModuleCache();
- trace("[MODULEHANDLER] Loading module cache...");
+ /**
+ * Parses and preloads the game's stage data and scripts when the game starts.
+ *
+ * If you want to force stages to be reloaded, you can just call this function again.
+ */
+ public static function loadModuleCache():Void
+ {
+ // Clear any stages that are cached if there were any.
+ clearModuleCache();
+ trace("[MODULEHANDLER] Loading module cache...");
- var scriptedModuleClassNames:Array = ScriptedModule.listScriptClasses();
- trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
- for (moduleCls in scriptedModuleClassNames)
- {
- var module:Module = ScriptedModule.init(moduleCls, moduleCls);
- if (module != null)
- {
- trace(' Loaded module: ${moduleCls}');
+ var scriptedModuleClassNames:Array = ScriptedModule.listScriptClasses();
+ trace(' Instantiating ${scriptedModuleClassNames.length} modules...');
+ for (moduleCls in scriptedModuleClassNames)
+ {
+ var module:Module = ScriptedModule.init(moduleCls, moduleCls);
+ if (module != null)
+ {
+ trace(' Loaded module: ${moduleCls}');
- // Then store it.
- addToModuleCache(module);
- }
- else
- {
- trace(' Failed to instantiate module: ${moduleCls}');
- }
- }
- reorderModuleCache();
+ // Then store it.
+ addToModuleCache(module);
+ }
+ else
+ {
+ trace(' Failed to instantiate module: ${moduleCls}');
+ }
+ }
+ reorderModuleCache();
- trace("[MODULEHANDLER] Module cache loaded.");
- }
+ trace("[MODULEHANDLER] Module cache loaded.");
+ }
- public static function buildModuleCallbacks():Void
- {
- FlxG.signals.postStateSwitch.add(onStateSwitchComplete);
- }
+ public static function buildModuleCallbacks():Void
+ {
+ FlxG.signals.postStateSwitch.add(onStateSwitchComplete);
+ }
- static function onStateSwitchComplete():Void
- {
- callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true));
- }
+ static function onStateSwitchComplete():Void
+ {
+ callEvent(new StateChangeScriptEvent(ScriptEvent.STATE_CHANGE_END, FlxG.state, true));
+ }
- static function addToModuleCache(module:Module):Void
- {
- moduleCache.set(module.moduleId, module);
- }
+ static function addToModuleCache(module:Module):Void
+ {
+ moduleCache.set(module.moduleId, module);
+ }
- static function reorderModuleCache():Void
- {
- modulePriorityOrder = moduleCache.keys().array();
+ static function reorderModuleCache():Void
+ {
+ modulePriorityOrder = moduleCache.keys().array();
- modulePriorityOrder.sort(function(a:String, b:String):Int
- {
- var aModule:Module = moduleCache.get(a);
- var bModule:Module = moduleCache.get(b);
+ modulePriorityOrder.sort(function(a:String, b:String):Int
+ {
+ var aModule:Module = moduleCache.get(a);
+ var bModule:Module = moduleCache.get(b);
- if (aModule.priority != bModule.priority)
- {
- return aModule.priority - bModule.priority;
- }
- else
- {
- // Sort alphabetically. Yes that's how this works.
- return a > b ? 1 : -1;
- }
- });
- }
+ if (aModule.priority != bModule.priority)
+ {
+ return aModule.priority - bModule.priority;
+ }
+ else
+ {
+ // Sort alphabetically. Yes that's how this works.
+ return a > b ? 1 : -1;
+ }
+ });
+ }
- public static function getModule(moduleId:String):Module
- {
- return moduleCache.get(moduleId);
- }
+ public static function getModule(moduleId:String):Module
+ {
+ return moduleCache.get(moduleId);
+ }
- public static function activateModule(moduleId:String):Void
- {
- var module:Module = getModule(moduleId);
- if (module != null)
- {
- module.active = true;
- }
- }
+ public static function activateModule(moduleId:String):Void
+ {
+ var module:Module = getModule(moduleId);
+ if (module != null)
+ {
+ module.active = true;
+ }
+ }
- public static function deactivateModule(moduleId:String):Void
- {
- var module:Module = getModule(moduleId);
- if (module != null)
- {
- module.active = false;
- }
- }
+ public static function deactivateModule(moduleId:String):Void
+ {
+ var module:Module = getModule(moduleId);
+ if (module != null)
+ {
+ module.active = false;
+ }
+ }
- /**
- * Clear the module cache, forcing all modules to call shutdown events.
- */
- public static function clearModuleCache():Void
- {
- if (moduleCache != null)
- {
- var event = new ScriptEvent(ScriptEvent.DESTROY, false);
+ /**
+ * Clear the module cache, forcing all modules to call shutdown events.
+ */
+ public static function clearModuleCache():Void
+ {
+ if (moduleCache != null)
+ {
+ var event = new ScriptEvent(ScriptEvent.DESTROY, false);
- // Note: Ignore stopPropagation()
- for (key => value in moduleCache)
- {
- ScriptEventDispatcher.callEvent(value, event);
- moduleCache.remove(key);
- }
+ // Note: Ignore stopPropagation()
+ for (key => value in moduleCache)
+ {
+ ScriptEventDispatcher.callEvent(value, event);
+ moduleCache.remove(key);
+ }
- moduleCache.clear();
- modulePriorityOrder = [];
- }
- }
+ moduleCache.clear();
+ modulePriorityOrder = [];
+ }
+ }
- public static function callEvent(event:ScriptEvent):Void
- {
- for (moduleId in modulePriorityOrder)
- {
- var module:Module = moduleCache.get(moduleId);
- // The module needs to be active to receive events.
- if (module != null && module.active)
- {
- ScriptEventDispatcher.callEvent(module, event);
- }
- }
- }
+ public static function callEvent(event:ScriptEvent):Void
+ {
+ for (moduleId in modulePriorityOrder)
+ {
+ var module:Module = moduleCache.get(moduleId);
+ // The module needs to be active to receive events.
+ if (module != null && module.active)
+ {
+ ScriptEventDispatcher.callEvent(module, event);
+ }
+ }
+ }
+
+ public static inline function callOnCreate():Void
+ {
+ callEvent(new ScriptEvent(ScriptEvent.CREATE, false));
+ }
}
diff --git a/source/funkin/noteStuff/NoteUtil.hx b/source/funkin/noteStuff/NoteUtil.hx
index 054ec2fef..c4d552b8b 100644
--- a/source/funkin/noteStuff/NoteUtil.hx
+++ b/source/funkin/noteStuff/NoteUtil.hx
@@ -13,87 +13,87 @@ import openfl.Assets;
*/
class NoteUtil
{
- /**
- * IDK THING FOR BOTH LOL! DIS SHIT HACK-Y
- * @param jsonPath
- * @return Map>
- */
- public static function loadSongEvents(jsonPath:String):Map>
- {
- return parseSongEvents(loadSongEventFromJson(jsonPath));
- }
+ /**
+ * IDK THING FOR BOTH LOL! DIS SHIT HACK-Y
+ * @param jsonPath
+ * @return Map>
+ */
+ public static function loadSongEvents(jsonPath:String):Map>
+ {
+ return parseSongEvents(loadSongEventFromJson(jsonPath));
+ }
- public static function loadSongEventFromJson(jsonPath:String):Array
- {
- var daEvents:Array;
- daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE??
- trace('GET JSON SONG EVENTS:');
- trace(daEvents);
- return daEvents;
- }
+ public static function loadSongEventFromJson(jsonPath:String):Array
+ {
+ var daEvents:Array;
+ daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE??
+ trace('GET JSON SONG EVENTS:');
+ trace(daEvents);
+ return daEvents;
+ }
- /**
- * Parses song event json stuff into a neater lil map grouping?
- * @param songEvents
- */
- public static function parseSongEvents(songEvents:Array):Map>
- {
- var songData:Map> = new Map();
+ /**
+ * Parses song event json stuff into a neater lil map grouping?
+ * @param songEvents
+ */
+ public static function parseSongEvents(songEvents:Array):Map>
+ {
+ var songData:Map> = new Map();
- for (songEvent in songEvents)
- {
- trace(songEvent);
- if (songData[songEvent.t] == null)
- songData[songEvent.t] = [];
+ for (songEvent in songEvents)
+ {
+ trace(songEvent);
+ if (songData[songEvent.t] == null)
+ songData[songEvent.t] = [];
- songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false});
- }
+ songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false});
+ }
- trace("FINISH SONG EVENTS!");
- trace(songData);
+ trace("FINISH SONG EVENTS!");
+ trace(songData);
- return songData;
- }
+ return songData;
+ }
- public static function checkSongEvents(songData:Map>, time:Float)
- {
- for (eventGrp in songData.keys())
- {
- if (time >= eventGrp)
- {
- for (events in songData[eventGrp])
- {
- if (!events.activated)
- {
- // TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!!
- trace(events.value);
- trace(eventGrp);
- trace(Conductor.songPosition);
- events.activated = true;
- }
- }
- }
- }
- }
+ public static function checkSongEvents(songData:Map>, time:Float)
+ {
+ for (eventGrp in songData.keys())
+ {
+ if (time >= eventGrp)
+ {
+ for (events in songData[eventGrp])
+ {
+ if (!events.activated)
+ {
+ // TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!!
+ trace(events.value);
+ trace(eventGrp);
+ trace(Conductor.songPosition);
+ events.activated = true;
+ }
+ }
+ }
+ }
+ }
}
typedef SongEventInfo =
{
- var songEventType:SongEventType;
- var value:Dynamic;
- var activated:Bool;
+ var songEventType:SongEventType;
+ var value:Dynamic;
+ var activated:Bool;
}
typedef SongEvent =
{
- var t:Int;
- var e:SongEventType;
- var v:Dynamic;
+ var t:Int;
+ var e:SongEventType;
+ var v:Dynamic;
}
enum abstract SongEventType(String)
{
- var FocusCamera;
- var PlayCharAnim;
- var Trace;
+ var FocusCamera;
+ var PlayCharAnim;
+ var Trace;
}
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index f5fff3cf5..5c166d43d 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,5 +1,7 @@
package funkin.play;
+import funkin.play.song.SongData.SongEventData;
+import funkin.play.event.SongEvent.SongEventParser;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
@@ -29,7 +31,6 @@ import funkin.play.Strumline.StrumlineArrow;
import funkin.play.Strumline.StrumlineStyle;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
-import funkin.play.event.SongEvent;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongNoteData;
@@ -49,2612 +50,2608 @@ import Discord.DiscordClient;
class PlayState extends MusicBeatState
{
- /**
- * STATIC VARIABLES
- * Static variables should be used for information that must be persisted between states or between resets,
- * such as the active song or song playlist.
- */
- /**
- * The currently active PlayState.
- * Since there is only one PlayState in existance at a time, we can use a singleton.
- */
- public static var instance:PlayState = null;
-
- /**
- * The currently active song. Includes data about what stage should be used, what characters,
- * and the notes to be played.
- */
- public static var currentSong:SwagSong = null;
-
- public static var currentSong_NEW:Song = null;
-
- /**
- * Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
- */
- public static var isStoryMode:Bool = false;
-
- /**
- * Whether the game is currently in Practice Mode.
- * If true, player will not lose gain or lose score from notes.
- */
- public static var isPracticeMode:Bool = false;
-
- /**
- * Whether the game is currently in a cutscene, and gameplay should be stopped.
- */
- public static var isInCutscene:Bool = false;
-
- /**
- * Whether the game is currently in the countdown before the song resumes.
- */
- public static var isInCountdown:Bool = false;
-
- /**
- * Gets set to true when the PlayState needs to reset (player opted to restart or died).
- * Gets disabled once resetting happens.
- */
- public static var needsReset:Bool = false;
-
- /**
- * The current "Blueball Counter" to display in the pause menu.
- * Resets when you beat a song or go back to the main menu.
- */
- public static var deathCounter:Int = 0;
-
- /**
- * The default camera zoom level. The camera lerps back to this after zooming in.
- * Defaults to 1.05 but may be larger or smaller depending on the current stage.
- */
- public static var defaultCameraZoom:Float = 1.05;
-
- /**
- * Used to persist the position of the `cameraFollowPosition` between resets.
- */
- private static var previousCameraFollowPoint:FlxObject = null;
-
- /**
- * PUBLIC INSTANCE VARIABLES
- * Public instance variables should be used for information that must be reset or dereferenced
- * every time the state is reset, such as the currently active stage, but may need to be accessed externally.
- */
- /**
- * The currently active Stage. This is the object containing all the props.
- */
- public var currentStage:Stage = null;
-
- public var currentChart(get, null):SongDifficulty;
-
- /**
- * The internal ID of the currently active Stage.
- * Used to retrieve the data required to build the `currentStage`.
- */
- public var currentStageId:String = '';
-
- /**
- * The player's current health.
- * The default maximum health is 2.0, and the default starting health is 1.0.
- */
- public var health:Float = 1;
-
- /**
- * The player's current score.
- */
- public var songScore:Int = 0;
-
- /**
- * An empty FlxObject contained in the scene.
- * The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
- *
- * This is an FlxSprite for two reasons:
- * 1. It needs to be an object in the scene for the camera to be configured to follow it.
- * 2. It needs to be an FlxSprite to allow a graphic (optionally, for debug purposes) to be drawn on it.
- */
- public var cameraFollowPoint:FlxSprite = new FlxSprite(0, 0);
-
- /**
- * PRIVATE INSTANCE VARIABLES
- * Private instance variables should be used for information that must be reset or dereferenced
- * every time the state is reset, but should not be accessed externally.
- */
- /**
- * The Array containing the notes that are not currently on the screen.
- * The `update()` function regularly shifts these out to add new notes to the screen.
- */
- private var inactiveNotes:Array;
-
- private var songEvents:Array;
-
- /**
- * If true, the player is allowed to pause the game.
- * Disabled during the ending of a song.
- */
- private var mayPauseGame:Bool = true;
-
- /**
- * The displayed value of the player's health.
- * Used to provide smooth animations based on linear interpolation of the player's health.
- */
- private var healthLerp:Float = 1;
-
- /**
- * Forcibly disables all update logic while the game moves back to the Menu state.
- * This is used only when a critical error occurs and the game cannot continue.
- */
- private var criticalFailure:Bool = false;
-
- /**
- * RENDER OBJECTS
- */
- /**
- * The SpriteGroup containing the notes that are currently on the screen or are about to be on the screen.
- */
- private var activeNotes:FlxTypedGroup = null;
-
- /**
- * The FlxText which displays the current score.
- */
- private var scoreText:FlxText;
-
- /**
- * The bar which displays the player's health.
- * Dynamically updated based on the value of `healthLerp` (which is based on `health`).
- */
- public var healthBar:FlxBar;
-
- /**
- * The background image used for the health bar.
- * Emma says the image is slightly skewed so I'm leaving it as an image instead of a `createGraphic`.
- */
- public var healthBarBG:FlxSprite;
-
- /**
- * The health icon representing the player.
- */
- public var iconP1:HealthIcon;
-
- /**
- * The health icon representing the opponent.
- */
- public var iconP2:HealthIcon;
-
- /**
- * The sprite group containing active player's strumline notes.
- */
- public var playerStrumline:Strumline;
-
- /**
- * The sprite group containing opponent's strumline notes.
- */
- public var enemyStrumline:Strumline;
-
- /**
- * The camera which contains, and controls visibility of, the user interface elements.
- */
- public var camHUD:FlxCamera;
-
- /**
- * The camera which contains, and controls visibility of, the stage and characters.
- */
- public var camGame:FlxCamera;
-
- /**
- * PROPERTIES
- */
- /**
- * If a substate is rendering over the PlayState, it is paused and normal update logic is skipped.
- * Examples include:
- * - The Pause screen is open.
- * - The Game Over screen is open.
- * - The Chart Editor screen is open.
- */
- private var isGamePaused(get, never):Bool;
-
- function get_isGamePaused():Bool
- {
- // Note: If there is a substate which requires the game to act unpaused,
- // this should be changed to include something like `&& Std.isOfType()`
- return this.subState != null;
- }
-
- // TODO: Reorganize these variables (maybe there should be a separate class like Conductor just to hold them?)
- public static var storyWeek:Int = 0;
- public static var storyPlaylist:Array = [];
- public static var storyDifficulty:Int = 1;
- public static var storyDifficulty_NEW:String = "normal";
- public static var seenCutscene:Bool = false;
- public static var campaignScore:Int = 0;
-
- private var vocals:VoicesGroup;
- private var vocalsFinished:Bool = false;
-
- private var camZooming:Bool = false;
- private var gfSpeed:Int = 1;
- // private var combo:Int = 0;
- private var generatedMusic:Bool = false;
- private var startingSong:Bool = false;
-
- var dialogue:Array;
- var talking:Bool = true;
- var doof:DialogueBox;
- var grpNoteSplashes:FlxTypedGroup;
- var comboPopUps:PopUpStuff;
- var perfectMode:Bool = false;
- var previousFrameTime:Int = 0;
- var songTime:Float = 0;
-
- #if discord_rpc
- // Discord RPC variables
- var storyDifficultyText:String = "";
- var iconRPC:String = "";
- var songLength:Float = 0;
- var detailsText:String = "";
- var detailsPausedText:String = "";
- #end
-
- override public function create()
- {
- super.create();
-
- if (currentSong == null && currentSong_NEW == null)
- {
- criticalFailure = true;
-
- lime.app.Application.current.window.alert("There was a critical error while accessing the selected song. Click OK to return to the main menu.",
- "Error loading PlayState");
- FlxG.switchState(new MainMenuState());
- return;
- }
-
- instance = this;
-
- if (currentSong_NEW != null)
- {
- // TODO: Do this in the loading state.
- currentSong_NEW.cacheCharts(true);
- }
-
- // Displays the camera follow point as a sprite for debug purposes.
- // TODO: Put this on a toggle?
- cameraFollowPoint.makeGraphic(8, 8, 0xFF00FF00);
- cameraFollowPoint.visible = false;
- cameraFollowPoint.zIndex = 1000000;
-
- // Reduce physics accuracy (who cares!!!) to improve animation quality.
- FlxG.fixedTimestep = false;
-
- // This state receives update() even when a substate is active.
- this.persistentUpdate = true;
- // This state receives draw calls even when a substate is active.
- this.persistentDraw = true;
-
- // Stop any pre-existing music.
- if (FlxG.sound.music != null)
- FlxG.sound.music.stop();
-
- // Prepare the current song to be played.
- if (currentChart != null)
- {
- currentChart.cacheInst();
- currentChart.cacheVocals();
- }
- else
- {
- FlxG.sound.cache(Paths.inst(currentSong.song));
- FlxG.sound.cache(Paths.voices(currentSong.song));
- }
-
- // Initialize stage stuff.
- initCameras();
-
- if (currentSong == null && currentSong_NEW == null)
- {
- currentSong = SongLoad.loadFromJson('tutorial');
- }
-
- if (currentSong_NEW != null)
- {
- Conductor.mapTimeChanges(currentChart.timeChanges);
- // Conductor.bpm = currentChart.getStartingBPM();
-
- // TODO: Support for dialog.
- }
- else
- {
- Conductor.mapBPMChanges(currentSong);
- // Conductor.bpm = currentSong.bpm;
-
- switch (currentSong.song.toLowerCase())
- {
- case 'senpai':
- dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue'));
- case 'roses':
- dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue'));
- case 'thorns':
- dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue'));
- }
- }
-
- Conductor.update(-5000);
-
- if (dialogue != null)
- {
- doof = new DialogueBox(false, dialogue);
- doof.scrollFactor.set();
- doof.finishThing = startCountdown;
- doof.cameras = [camHUD];
- }
-
- // Once the song is loaded, we can continue and initialize the stage.
-
- var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
- healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
- healthBarBG.screenCenter(X);
- healthBarBG.scrollFactor.set(0, 0);
- add(healthBarBG);
-
- healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
- 'healthLerp', 0, 2);
- healthBar.scrollFactor.set();
- healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN);
- add(healthBar);
-
- initStage();
- initCharacters();
- #if discord_rpc
- initDiscord();
- #end
-
- // Configure camera follow point.
- if (previousCameraFollowPoint != null)
- {
- cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
- previousCameraFollowPoint = null;
- }
- add(cameraFollowPoint);
-
- comboPopUps = new PopUpStuff();
- comboPopUps.cameras = [camHUD];
- add(comboPopUps);
-
- grpNoteSplashes = new FlxTypedGroup();
-
- var noteSplash:NoteSplash = new NoteSplash(100, 100, 0);
- grpNoteSplashes.add(noteSplash);
- noteSplash.alpha = 0.1;
-
- add(grpNoteSplashes);
-
- if (currentSong_NEW != null)
- {
- generateSong_NEW();
- }
- else
- {
- generateSong();
- }
-
- resetCamera();
-
- FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
-
- scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, "", 20);
- scoreText.setFormat(Paths.font("vcr.ttf"), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
- scoreText.scrollFactor.set();
- add(scoreText);
-
- // Attach the groups to the HUD camera so they are rendered independent of the stage.
- grpNoteSplashes.cameras = [camHUD];
- activeNotes.cameras = [camHUD];
- healthBar.cameras = [camHUD];
- healthBarBG.cameras = [camHUD];
- iconP1.cameras = [camHUD];
- iconP2.cameras = [camHUD];
- scoreText.cameras = [camHUD];
- leftWatermarkText.cameras = [camHUD];
- rightWatermarkText.cameras = [camHUD];
-
- // if (SONG.song == 'South')
- // FlxG.camera.alpha = 0.7;
- // UI_camera.zoom = 1;
-
- // cameras = [FlxG.cameras.list[1]];
- startingSong = true;
-
- if (isStoryMode && !seenCutscene)
- {
- seenCutscene = true;
-
- switch (currentSong.song.toLowerCase())
- {
- case "winter-horrorland":
- VanillaCutscenes.playHorrorStartCutscene();
- case 'senpai' | 'roses' | 'thorns':
- schoolIntro(doof); // doof is assumed to be non-null, lol!
- case 'ugh':
- VanillaCutscenes.playUghCutscene();
- case 'stress':
- VanillaCutscenes.playStressCutscene();
- case 'guns':
- VanillaCutscenes.playGunsCutscene();
- default:
- // VanillaCutscenes will call startCountdown later.
- // TODO: Alternatively: make a song script that allows startCountdown to be called,
- // then cancels the countdown, hides the strumline, plays the cutscene,
- // then calls Countdown.performCountdown()
- startCountdown();
- }
- }
- else
- {
- startCountdown();
- }
-
- #if debug
- this.rightWatermarkText.text = Constants.VERSION;
- #end
- }
-
- function get_currentChart():SongDifficulty
- {
- if (currentSong_NEW == null || storyDifficulty_NEW == null)
- return null;
- return currentSong_NEW.getDifficulty(storyDifficulty_NEW);
- }
-
- /**
- * Initializes the game and HUD cameras.
- */
- function initCameras()
- {
- // Configure the default camera zoom level.
- defaultCameraZoom = FlxCamera.defaultZoom * 1.05;
-
- camGame = new SwagCamera();
- camHUD = new FlxCamera();
- camHUD.bgColor.alpha = 0;
-
- FlxG.cameras.reset(camGame);
- FlxG.cameras.add(camHUD, false);
- }
-
- function initStage()
- {
- if (currentSong_NEW != null)
- {
- initStage_NEW();
- return;
- }
-
- // TODO: Move stageId to the song file.
- switch (currentSong.song.toLowerCase())
- {
- case 'spookeez' | 'monster' | 'south':
- currentStageId = "spookyMansion";
- case 'pico' | 'blammed' | 'philly':
- currentStageId = 'phillyTrain';
- case "milf" | 'satin-panties' | 'high':
- currentStageId = 'limoRide';
- case "cocoa" | 'eggnog':
- currentStageId = 'mallXmas';
- case 'winter-horrorland':
- currentStageId = 'mallEvil';
- case 'senpai' | 'roses':
- currentStageId = 'school';
- case "darnell" | "lit-up" | "2hot":
- currentStageId = 'phillyStreets';
- // currentStageId = 'pyro';
- case "blazin":
- currentStageId = 'phillyBlazin';
- // currentStageId = 'pyro';
- case 'pyro':
- currentStageId = 'pyro';
- case 'thorns':
- currentStageId = 'schoolEvil';
- case 'guns' | 'stress' | 'ugh':
- currentStageId = 'tankmanBattlefield';
- default:
- currentStageId = "mainStage";
- }
- // Loads the relevant stage based on its ID.
- loadStage(currentStageId);
- }
-
- function initStage_NEW()
- {
- if (currentChart == null)
- {
- trace('Song difficulty could not be loaded.');
- }
-
- if (currentChart.stage != null && currentChart.stage != '')
- {
- currentStageId = currentChart.stage;
- }
- else
- {
- currentStageId = SongValidator.DEFAULT_STAGE;
- }
-
- loadStage(currentStageId);
- }
-
- function initCharacters()
- {
- if (currentSong_NEW != null)
- {
- initCharacters_NEW();
- return;
- }
-
- iconP1 = new HealthIcon(currentSong.player1, 0);
- iconP1.y = healthBar.y - (iconP1.height / 2);
- add(iconP1);
-
- iconP2 = new HealthIcon(currentSong.player2, 1);
- iconP2.y = healthBar.y - (iconP2.height / 2);
- add(iconP2);
-
- //
- // GIRLFRIEND
- //
-
- // TODO: Tie the GF version to the song data, not the stage ID or the current player.
- var gfVersion:String = 'gf';
-
- switch (currentStageId)
- {
- case 'pyro' | 'phillyStreets':
- gfVersion = 'nene';
- case 'blazin':
- gfVersion = '';
- case 'limoRide':
- gfVersion = 'gf-car';
- case 'mallXmas' | 'mallEvil':
- gfVersion = 'gf-christmas';
- case 'school' | 'schoolEvil':
- gfVersion = 'gf-pixel';
- case 'tankmanBattlefield':
- gfVersion = 'gf-tankmen';
- }
-
- if (currentSong.player1 == "pico")
- gfVersion = "nene";
-
- if (currentSong.song.toLowerCase() == 'stress')
- gfVersion = 'pico-speaker';
-
- if (currentSong.song.toLowerCase() == 'tutorial')
- gfVersion = '';
-
- //
- // GIRLFRIEND
- //
- var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(gfVersion);
-
- if (girlfriend != null)
- {
- girlfriend.characterType = CharacterType.GF;
- girlfriend.scrollFactor.set(0.95, 0.95);
- if (gfVersion == 'pico-speaker')
- {
- girlfriend.x -= 50;
- girlfriend.y -= 200;
- }
- }
- else if (gfVersion != '')
- {
- trace('WARNING: Could not load girlfriend character with ID ${gfVersion}, skipping...');
- }
-
- //
- // DAD
- //
- var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player2);
-
- if (dad != null)
- {
- dad.characterType = CharacterType.DAD;
- }
-
- switch (currentSong.player2)
- {
- case 'gf':
- if (isStoryMode)
- {
- cameraFollowPoint.x += 600;
- tweenCamIn();
- }
- }
-
- //
- // BOYFRIEND
- //
- var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player1);
-
- if (boyfriend != null)
- {
- boyfriend.characterType = CharacterType.BF;
- }
-
- if (currentStage != null)
- {
- // We're using Eric's stage handler.
- // Characters get added to the stage, not the main scene.
- if (girlfriend != null)
- {
- currentStage.addCharacter(girlfriend, GF);
- }
-
- if (boyfriend != null)
- {
- currentStage.addCharacter(boyfriend, BF);
- }
-
- if (dad != null)
- {
- currentStage.addCharacter(dad, DAD);
- // Camera starts at dad.
- cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
- }
-
- // Redo z-indexes.
- currentStage.refresh();
- }
- }
-
- function initCharacters_NEW()
- {
- if (currentSong_NEW == null || currentChart == null)
- {
- trace('Song difficulty could not be loaded.');
- }
-
- // TODO: Switch playable character by manipulating this value.
- // TODO: How to choose which one to use for story mode?
-
- var playableChars = currentChart.getPlayableChars();
- var currentPlayer = 'bf';
-
- if (playableChars.length == 0)
- {
- trace('WARNING: No playable characters found for this song.');
- }
- else if (playableChars.indexOf(currentPlayer) == -1)
- {
- currentPlayer = playableChars[0];
- }
-
- var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayer);
-
- //
- // GIRLFRIEND
- //
- var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend);
-
- if (girlfriend != null)
- {
- girlfriend.characterType = CharacterType.GF;
- }
- else if (currentCharData.girlfriend != '')
- {
- trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...');
- }
- else
- {
- // Chosen GF was '' so we don't load one.
- }
-
- //
- // DAD
- //
- var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent);
-
- if (dad != null)
- {
- dad.characterType = CharacterType.DAD;
- }
-
- // TODO: Cut out this code/make it generic.
- switch (currentCharData.opponent)
- {
- case 'gf':
- if (isStoryMode)
- {
- cameraFollowPoint.x += 600;
- tweenCamIn();
- }
- }
-
- //
- // OPPONENT HEALTH ICON
- //
- iconP2 = new HealthIcon(currentCharData.opponent, 1);
- iconP2.y = healthBar.y - (iconP2.height / 2);
- add(iconP2);
-
- //
- // BOYFRIEND
- //
- var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayer);
-
- if (boyfriend != null)
- {
- boyfriend.characterType = CharacterType.BF;
- }
-
- //
- // PLAYER HEALTH ICON
- //
- iconP1 = new HealthIcon(currentPlayer, 0);
- iconP1.y = healthBar.y - (iconP1.height / 2);
- add(iconP1);
-
- //
- // ADD CHARACTERS TO SCENE
- //
-
- if (currentStage != null)
- {
- // Characters get added to the stage, not the main scene.
- if (girlfriend != null)
- {
- currentStage.addCharacter(girlfriend, GF);
- }
-
- if (boyfriend != null)
- {
- currentStage.addCharacter(boyfriend, BF);
- }
-
- if (dad != null)
- {
- currentStage.addCharacter(dad, DAD);
- // Camera starts at dad.
- cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
- }
-
- // Rearrange by z-indexes.
- currentStage.refresh();
- }
- }
-
- /**
- * Removes any references to the current stage, then clears the stage cache,
- * then reloads all the stages.
- *
- * This is useful for when you want to edit a stage without reloading the whole game.
- * Reloading works on both the JSON and the HXC, if applicable.
- *
- * Call this by pressing F5 on a debug build.
- */
- override function debug_refreshModules()
- {
- // Remove the current stage. If the stage gets deleted while it's still in use,
- // it'll probably crash the game or something.
- if (this.currentStage != null)
- {
- remove(currentStage);
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false);
- ScriptEventDispatcher.callEvent(currentStage, event);
- currentStage = null;
- }
-
- super.debug_refreshModules();
- }
-
- /**
- * Pauses music and vocals easily.
- */
- public function pauseMusic()
- {
- FlxG.sound.music.pause();
- vocals.pause();
- }
-
- /**
- * Loads stage data from cache, assembles the props,
- * and adds it to the state.
- * @param id
- */
- function loadStage(id:String)
- {
- currentStage = StageDataParser.fetchStage(id);
-
- if (currentStage != null)
- {
- // Actually create and position the sprites.
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false);
- ScriptEventDispatcher.callEvent(currentStage, event);
-
- // Apply camera zoom.
- defaultCameraZoom = currentStage.camZoom;
-
- // Add the stage to the scene.
- this.add(currentStage);
- }
- }
-
- function initDiscord():Void
- {
- #if discord_rpc
- storyDifficultyText = difficultyString();
- iconRPC = currentSong.player2;
-
- // To avoid having duplicate images in Discord assets
- switch (iconRPC)
- {
- case 'senpai-angry':
- iconRPC = 'senpai';
- case 'monster-christmas':
- iconRPC = 'monster';
- case 'mom-car':
- iconRPC = 'mom';
- }
-
- // String that contains the mode defined here so it isn't necessary to call changePresence for each mode
- detailsText = isStoryMode ? "Story Mode: Week " + storyWeek : "Freeplay";
- detailsPausedText = "Paused - " + detailsText;
-
- // Updating Discord Rich Presence.
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- #end
- }
-
- function schoolIntro(?dialogueBox:DialogueBox):Void
- {
- var black:FlxSprite = new FlxSprite(-100, -100).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
- black.scrollFactor.set();
- add(black);
-
- var red:FlxSprite = new FlxSprite(-100, -100).makeGraphic(FlxG.width * 2, FlxG.height * 2, 0xFFff1b31);
- red.scrollFactor.set();
-
- var senpaiEvil:FlxSprite = new FlxSprite();
- senpaiEvil.frames = Paths.getSparrowAtlas('weeb/senpaiCrazy');
- senpaiEvil.animation.addByPrefix('idle', 'Senpai Pre Explosion', 24, false);
- senpaiEvil.setGraphicSize(Std.int(senpaiEvil.width * Constants.PIXEL_ART_SCALE));
- senpaiEvil.scrollFactor.set();
- senpaiEvil.updateHitbox();
- senpaiEvil.screenCenter();
- senpaiEvil.x += senpaiEvil.width / 5;
-
- if (currentSong.song.toLowerCase() == 'roses' || currentSong.song.toLowerCase() == 'thorns')
- {
- remove(black);
-
- if (currentSong.song.toLowerCase() == 'thorns')
- {
- add(red);
- camHUD.visible = false;
- }
- else
- FlxG.sound.play(Paths.sound('ANGRY'));
- // moved senpai angry noise in here to clean up cutscene switch case lol
- }
-
- new FlxTimer().start(0.3, function(tmr:FlxTimer)
- {
- black.alpha -= 0.15;
-
- if (black.alpha > 0)
- tmr.reset(0.3);
- else
- {
- if (dialogueBox != null)
- {
- isInCutscene = true;
-
- if (currentSong.song.toLowerCase() == 'thorns')
- {
- add(senpaiEvil);
- senpaiEvil.alpha = 0;
- new FlxTimer().start(0.3, function(swagTimer:FlxTimer)
- {
- senpaiEvil.alpha += 0.15;
- if (senpaiEvil.alpha < 1)
- swagTimer.reset();
- else
- {
- senpaiEvil.animation.play('idle');
- FlxG.sound.play(Paths.sound('Senpai_Dies'), 1, false, null, true, function()
- {
- remove(senpaiEvil);
- remove(red);
- FlxG.camera.fade(FlxColor.WHITE, 0.01, true, function()
- {
- add(dialogueBox);
- camHUD.visible = true;
- }, true);
- });
- new FlxTimer().start(3.2, function(deadTime:FlxTimer)
- {
- FlxG.camera.fade(FlxColor.WHITE, 1.6, false);
- });
- }
- });
- }
- else
- add(dialogueBox);
- }
- else
- startCountdown();
-
- remove(black);
- }
- });
- }
-
- function startSong():Void
- {
- dispatchEvent(new ScriptEvent(ScriptEvent.SONG_START));
-
- startingSong = false;
-
- previousFrameTime = FlxG.game.ticks;
-
- if (!isGamePaused)
- {
- // if (FlxG.sound.music != null)
- // FlxG.sound.music.play(true);
- // else
- if (currentChart != null)
- {
- currentChart.playInst(1.0, false);
- }
- else
- {
- FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false);
- }
- }
-
- FlxG.sound.music.onComplete = endSong;
- trace('Playing vocals...');
- vocals.play();
-
- #if discord_rpc
- // Song duration in a float, useful for the time left feature
- songLength = FlxG.sound.music.length;
-
- // Updating Discord Rich Presence (with Time Left)
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength);
- #end
- }
-
- private function generateSong():Void
- {
- // FlxG.log.add(ChartParser.parse());
-
- Conductor.forceBPM(currentSong.bpm);
-
- currentSong.song = currentSong.song;
-
- if (currentSong.needsVoices)
- vocals = VoicesGroup.build(currentSong.song, currentSong.voiceList);
- else
- vocals = VoicesGroup.build(currentSong.song, null);
-
- vocals.members[0].onComplete = function()
- {
- vocalsFinished = true;
- };
-
- trace(vocals);
-
- activeNotes = new FlxTypedGroup();
- activeNotes.zIndex = 1000;
- add(activeNotes);
-
- regenNoteData();
-
- generatedMusic = true;
- }
-
- private function generateSong_NEW():Void
- {
- if (currentChart == null)
- {
- trace('Song difficulty could not be loaded.');
- }
-
- Conductor.forceBPM(currentChart.getStartingBPM());
-
- // TODO: Fix grouped vocals
- vocals = currentChart.buildVocals();
- vocals.members[0].onComplete = function()
- {
- vocalsFinished = true;
- }
-
- // Create the rendered note group.
- activeNotes = new FlxTypedGroup();
- activeNotes.zIndex = 1000;
- add(activeNotes);
-
- regenNoteData_NEW();
-
- generatedMusic = true;
- }
-
- function regenNoteData():Void
- {
- // resets combo, should prob put somewhere else!
- Highscore.tallies.combo = 0;
- Highscore.tallies = new Tallies();
- // make unspawn notes shit def empty
- inactiveNotes = [];
-
- activeNotes.forEach(function(nt)
- {
- nt.followsTime = false;
- FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, {
- ease: FlxEase.expoIn,
- onComplete: function(twn)
- {
- nt.kill();
- activeNotes.remove(nt, true);
- nt.destroy();
- }
- });
- });
-
- var noteData:Array;
-
- // NEW SHIT
- noteData = SongLoad.getSong();
-
- for (section in noteData)
- {
- for (songNotes in section.sectionNotes)
- {
- var daStrumTime:Float = songNotes.strumTime;
- // TODO: Replace 4 with strumlineSize
- var daNoteData:Int = Std.int(songNotes.noteData % 4);
- var gottaHitNote:Bool = section.mustHitSection;
-
- if (songNotes.highStakes) // noteData > 3
- gottaHitNote = !section.mustHitSection;
-
- var oldNote:Note;
- if (inactiveNotes.length > 0)
- oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)];
- else
- oldNote = null;
-
- var strumlineStyle:StrumlineStyle = NORMAL;
-
- // TODO: Put this in the chart or something?
- switch (currentStageId)
- {
- case 'school':
- strumlineStyle = PIXEL;
- case 'schoolEvil':
- strumlineStyle = PIXEL;
- }
-
- var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote, false, strumlineStyle);
- // swagNote.data = songNotes;
- swagNote.data.sustainLength = songNotes.sustainLength;
- swagNote.data.noteKind = songNotes.noteKind;
- swagNote.scrollFactor.set(0, 0);
-
- var susLength:Float = swagNote.data.sustainLength;
-
- susLength = susLength / Conductor.stepCrochet;
- inactiveNotes.push(swagNote);
-
- for (susNote in 0...Math.round(susLength))
- {
- oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)];
-
- var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true,
- strumlineStyle);
- sustainNote.data.noteKind = songNotes.noteKind;
- sustainNote.scrollFactor.set();
- inactiveNotes.push(sustainNote);
-
- sustainNote.mustPress = gottaHitNote;
-
- if (sustainNote.mustPress)
- sustainNote.x += FlxG.width / 2; // general offset
- }
-
- // TODO: Replace 4 with strumlineSize
- swagNote.mustPress = gottaHitNote;
-
- if (swagNote.mustPress)
- {
- if (playerStrumline != null)
- {
- swagNote.x = playerStrumline.getArrow(swagNote.data.noteData).x;
- }
- else
- {
- swagNote.x += FlxG.width / 2; // general offset
- }
- }
- else
- {
- if (enemyStrumline != null)
- {
- swagNote.x = enemyStrumline.getArrow(swagNote.data.noteData).x;
- }
- else
- {
- // swagNote.x += FlxG.width / 2; // general offset
- }
- }
- }
- }
-
- inactiveNotes.sort(function(a:Note, b:Note):Int
- {
- return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
- });
- }
-
- function regenNoteData_NEW():Void
- {
- Highscore.tallies.combo = 0;
- Highscore.tallies = new Tallies();
-
- // Reset song events.
- songEvents = currentChart.getEvents();
- SongEventHandler.resetEvents(songEvents);
-
- // Destroy inactive notes.
- inactiveNotes = [];
-
- // Destroy active notes.
- activeNotes.forEach(function(nt)
- {
- nt.followsTime = false;
- FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, {
- ease: FlxEase.expoIn,
- onComplete: function(twn)
- {
- nt.kill();
- activeNotes.remove(nt, true);
- nt.destroy();
- }
- });
- });
-
- var noteData:Array = currentChart.notes;
-
- var oldNote:Note = null;
- for (songNote in noteData)
- {
- var mustHitNote:Bool = songNote.getMustHitNote();
-
- // TODO: Put this in the chart or something?
- var strumlineStyle:StrumlineStyle = null;
- switch (currentStageId)
- {
- case 'school':
- strumlineStyle = PIXEL;
- case 'schoolEvil':
- strumlineStyle = PIXEL;
- default:
- strumlineStyle = NORMAL;
- }
-
- var newNote:Note = new Note(songNote.time, songNote.data, oldNote, false, strumlineStyle);
- newNote.mustPress = mustHitNote;
- newNote.data.sustainLength = songNote.length;
- newNote.data.noteKind = songNote.kind;
- newNote.scrollFactor.set(0, 0);
-
- // Note positioning.
- // TODO: Make this more robust.
- if (newNote.mustPress)
- {
- if (playerStrumline != null)
- {
- // Align with the strumline arrow.
- newNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
- }
- else
- {
- // Assume strumline position.
- newNote.x += FlxG.width / 2;
- }
- }
- else
- {
- if (enemyStrumline != null)
- {
- newNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
- }
- else
- {
- // newNote.x += 0;
- }
- }
-
- inactiveNotes.push(newNote);
-
- oldNote = newNote;
-
- // Generate X sustain notes.
- var sustainSections = Math.round(songNote.length / Conductor.stepCrochet);
- for (noteIndex in 0...sustainSections)
- {
- var noteTimeOffset:Float = Conductor.stepCrochet + (Conductor.stepCrochet * noteIndex);
- var sustainNote:Note = new Note(songNote.time + noteTimeOffset, songNote.data, oldNote, true, strumlineStyle);
- sustainNote.mustPress = mustHitNote;
- sustainNote.data.noteKind = songNote.kind;
- sustainNote.scrollFactor.set(0, 0);
-
- if (sustainNote.mustPress)
- {
- if (playerStrumline != null)
- {
- // Align with the strumline arrow.
- sustainNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
- }
- else
- {
- // Assume strumline position.
- sustainNote.x += FlxG.width / 2;
- }
- }
- else
- {
- if (enemyStrumline != null)
- {
- sustainNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
- }
- else
- {
- // newNote.x += 0;
- }
- }
-
- inactiveNotes.push(sustainNote);
-
- oldNote = sustainNote;
- }
- }
-
- // Sorting is an expensive operation.
- // Assume it was done in the chart file.
- /**
- inactiveNotes.sort(function(a:Note, b:Note):Int
- {
- return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
- });
- **/
- }
-
- function tweenCamIn():Void
- {
- FlxTween.tween(FlxG.camera, {zoom: 1.3 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
- }
-
- #if discord_rpc
- override public function onFocus():Void
- {
- if (health > 0 && !paused && FlxG.autoPause)
- {
- if (Conductor.songPosition > 0.0)
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true,
- songLength - Conductor.songPosition);
- else
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- }
-
- super.onFocus();
- }
-
- override public function onFocusLost():Void
- {
- if (health > 0 && !paused && FlxG.autoPause)
- DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
-
- super.onFocusLost();
- }
- #end
-
- function resyncVocals():Void
- {
- if (_exiting || vocals == null)
- return;
-
- vocals.pause();
-
- FlxG.sound.music.play();
- Conductor.update(FlxG.sound.music.time + Conductor.offset);
-
- if (vocalsFinished)
- return;
-
- vocals.time = FlxG.sound.music.time;
- vocals.play();
- }
-
- override public function update(elapsed:Float)
- {
- super.update(elapsed);
-
- if (criticalFailure)
- return;
-
- if (FlxG.keys.justPressed.U)
- {
- // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
- persistentUpdate = false;
- openSubState(new StageOffsetSubstate());
- }
-
- updateHealthBar();
- updateScoreText();
-
- if (needsReset)
- {
- dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
-
- resetCamera();
-
- persistentUpdate = true;
- persistentDraw = true;
-
- startingSong = true;
-
- FlxG.sound.music.pause();
- vocals.pause();
-
- FlxG.sound.music.time = 0;
-
- currentStage.resetStage();
-
- // Delete all notes and reset the arrays.
- if (currentChart != null)
- {
- regenNoteData_NEW();
- }
- else
- {
- regenNoteData();
- }
-
- health = 1;
- songScore = 0;
- Highscore.tallies.combo = 0;
- Countdown.performCountdown(currentStageId.startsWith('school'));
-
- needsReset = false;
- }
-
- #if !debug
- perfectMode = false;
- #else
- if (FlxG.keys.justPressed.H)
- camHUD.visible = !camHUD.visible;
- #end
-
- // do this BEFORE super.update() so songPosition is accurate
- if (startingSong)
- {
- if (isInCountdown)
- {
- Conductor.songPosition += elapsed * 1000;
- if (Conductor.songPosition >= 0)
- startSong();
- }
- }
- else
- {
- if (Paths.SOUND_EXT == 'mp3')
- Conductor.offset = -13; // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
-
- Conductor.update(FlxG.sound.music.time + Conductor.offset);
-
- if (!isGamePaused)
- {
- songTime += FlxG.game.ticks - previousFrameTime;
- previousFrameTime = FlxG.game.ticks;
-
- // Interpolation type beat
- if (Conductor.lastSongPos != Conductor.songPosition)
- {
- songTime = (songTime + Conductor.songPosition) / 2;
- Conductor.lastSongPos = Conductor.songPosition;
- }
- }
- }
-
- var androidPause:Bool = false;
-
- #if android
- androidPause = FlxG.android.justPressed.BACK;
- #end
-
- if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
- {
- var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
-
- dispatchEvent(event);
-
- if (!event.eventCanceled)
- {
- // Pause updates while the substate is open, preventing the game state from advancing.
- persistentUpdate = false;
- // Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
- persistentDraw = true;
-
- // There is a 1/1000 change to use a special pause menu.
- // This prevents the player from resuming, but that's the point.
- // It's a reference to Gitaroo Man, which doesn't let you pause the game.
- if (event.gitaroo)
- {
- FlxG.switchState(new GitarooPause());
- }
- else
- {
- var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
- var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
- openSubState(pauseSubState);
- pauseSubState.camera = camHUD;
- boyfriendPos.put();
- }
-
- #if discord_rpc
- DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- #end
- }
- }
-
- #if debug
- // 1: End the song immediately.
- if (FlxG.keys.justPressed.ONE)
- endSong();
-
- // 2: Gain 10% health.
- if (FlxG.keys.justPressed.TWO)
- health += 0.1 * 2.0;
-
- // 3: Lose 5% health.
- if (FlxG.keys.justPressed.THREE)
- health -= 0.05 * 2.0;
- #end
-
- // 7: Move to the charter.
- if (FlxG.keys.justPressed.SEVEN)
- {
- FlxG.switchState(new ChartingState());
-
- #if discord_rpc
- DiscordClient.changePresence("Chart Editor", null, null, true);
- #end
- }
-
- // 8: Move to the offset editor.
- if (FlxG.keys.justPressed.EIGHT)
- FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
-
- // 9: Toggle the old icon.
- if (FlxG.keys.justPressed.NINE)
- iconP1.toggleOldIcon();
-
- #if debug
- // PAGEUP: Skip forward one section.
- // SHIFT+PAGEUP: Skip forward ten sections.
- if (FlxG.keys.justPressed.PAGEUP)
- changeSection(FlxG.keys.pressed.SHIFT ? 10 : 1);
- // PAGEDOWN: Skip backward one section. Doesn't replace notes.
- // SHIFT+PAGEDOWN: Skip backward ten sections.
- if (FlxG.keys.justPressed.PAGEDOWN)
- changeSection(FlxG.keys.pressed.SHIFT ? -10 : -1);
- #end
-
- if (health > 2.0)
- health = 2.0;
- if (health < 0.0)
- health = 0.0;
-
- if (camZooming && subState == null)
- {
- FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
- camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95);
- }
-
- FlxG.watch.addQuick("beatShit", Conductor.currentBeat);
- FlxG.watch.addQuick("stepShit", Conductor.currentStep);
- if (currentStage != null)
- {
- FlxG.watch.addQuick("bfAnim", currentStage.getBoyfriend().getCurrentAnimation());
- }
- FlxG.watch.addQuick("songPos", Conductor.songPosition);
-
- if (currentSong != null && currentSong.song == 'Fresh')
- {
- switch (Conductor.currentBeat)
- {
- case 16:
- camZooming = true;
- gfSpeed = 2;
- case 48:
- gfSpeed = 1;
- case 80:
- gfSpeed = 2;
- case 112:
- gfSpeed = 1;
- }
- }
-
- if (!isInCutscene && !_exiting)
- {
- // RESET = Quick Game Over Screen
- if (controls.RESET)
- {
- health = 0;
- trace("RESET = True");
- }
-
- #if CAN_CHEAT // brandon's a pussy
- if (controls.CHEAT)
- {
- health += 1;
- trace("User is cheating!");
- }
- #end
-
- if (health <= 0 && !isPracticeMode)
- {
- vocals.pause();
- FlxG.sound.music.pause();
-
- deathCounter += 1;
-
- dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER));
-
- // Disable updates, preventing animations in the background from playing.
- persistentUpdate = false;
- #if debug
- if (FlxG.keys.pressed.THREE)
- {
- // TODO: Change the key or delete this?
- // In debug builds, pressing 3 to kill the player makes the background transparent.
- persistentDraw = true;
- }
- else
- {
- #end
- persistentDraw = false;
- #if debug
- }
- #end
-
- var gameOverSubstate = new GameOverSubstate();
- openSubState(gameOverSubstate);
-
- #if discord_rpc
- // Game Over doesn't get his own variable because it's only used here
- DiscordClient.changePresence("Game Over - " + detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- #end
- }
- }
-
- while (inactiveNotes[0] != null && inactiveNotes[0].data.strumTime - Conductor.songPosition < 1800 / SongLoad.getSpeed())
- {
- var dunceNote:Note = inactiveNotes[0];
-
- if (dunceNote.mustPress && !dunceNote.isSustainNote)
- Highscore.tallies.totalNotes++;
-
- activeNotes.add(dunceNote);
-
- inactiveNotes.shift();
- }
-
- if (generatedMusic && playerStrumline != null)
- {
- activeNotes.forEachAlive(function(daNote:Note)
- {
- if ((PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height)
- || (!PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height))
- {
- daNote.active = false;
- daNote.visible = false;
- }
- else
- {
- daNote.visible = true;
- daNote.active = true;
- }
-
- var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
-
- if (daNote.followsTime)
- daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(SongLoad.getSpeed(),
- 2) * daNote.noteSpeedMulti);
-
- if (PreferencesMenu.getPref('downscroll'))
- {
- daNote.y += playerStrumline.y;
- if (daNote.isSustainNote)
- {
- if (daNote.animation.curAnim.name.endsWith("end") && daNote.prevNote != null)
- daNote.y += daNote.prevNote.height;
- else
- daNote.y += daNote.height / 2;
-
- if ((!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit)))
- && daNote.y - daNote.offset.y * daNote.scale.y + daNote.height >= strumLineMid)
- {
- applyClipRect(daNote);
- }
- }
- }
- else
- {
- if (daNote.followsTime)
- daNote.y = playerStrumline.y - daNote.y;
- if (daNote.isSustainNote
- && (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit)))
- && daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid)
- {
- applyClipRect(daNote);
- }
- }
-
- if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
- {
- if (currentSong != null && currentSong.song != 'Tutorial')
- camZooming = true;
-
- var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, Highscore.tallies.combo, true);
- dispatchEvent(event);
-
- // Calling event.cancelEvent() in a module should force the CPU to miss the note.
- // This is useful for cool shit, including but not limited to:
- // - Making the AI ignore notes which are hazardous.
- // - Making the AI miss notes on purpose for aesthetic reasons.
- if (event.eventCanceled)
- {
- daNote.tooLate = true;
- }
- else
- {
- // Volume of DAD.
- if (currentSong != null && currentSong.needsVoices)
- vocals.volume = 1;
- }
- }
-
- // WIP interpolation shit? Need to fix the pause issue
- // daNote.y = (strumLine.y - (songTime - daNote.strumTime) * (0.45 * SONG.speed[SongLoad.curDiff]));
-
- // removing this so whether the note misses or not is entirely up to Note class
- // var noteMiss:Bool = daNote.y < -daNote.height;
-
- // if (PreferencesMenu.getPref('downscroll'))
- // noteMiss = daNote.y > FlxG.height;
-
- if (daNote.isSustainNote && daNote.wasGoodHit)
- {
- if ((!PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height)
- || (PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height))
- {
- daNote.active = false;
- daNote.visible = false;
-
- daNote.kill();
- activeNotes.remove(daNote, true);
- daNote.destroy();
- }
- }
- if (daNote.wasGoodHit)
- {
- daNote.active = false;
- daNote.visible = false;
-
- daNote.kill();
- activeNotes.remove(daNote, true);
- daNote.destroy();
- }
-
- if (daNote.tooLate)
- {
- noteMiss(daNote);
- }
- });
- }
-
- if (songEvents != null && songEvents.length > 0)
- {
- var songEventsToActivate:Array = SongEventHandler.queryEvents(songEvents, Conductor.songPosition);
-
- if (songEventsToActivate.length > 0)
- trace('[EVENTS] Found ${songEventsToActivate.length} event(s) to activate.');
-
- SongEventHandler.activateEvents(songEventsToActivate);
- }
-
- if (!isInCutscene)
- keyShit(true);
- }
-
- function applyClipRect(daNote:Note):Void
- {
- // clipRect is applied to graphic itself so use frame Heights
- var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight);
- var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
-
- if (PreferencesMenu.getPref('downscroll'))
- {
- swagRect.height = (strumLineMid - daNote.y) / daNote.scale.y;
- swagRect.y = daNote.frameHeight - swagRect.height;
- }
- else
- {
- swagRect.y = (strumLineMid - daNote.y) / daNote.scale.y;
- swagRect.height -= swagRect.y;
- }
-
- daNote.clipRect = swagRect;
- }
-
- function killCombo():Void
- {
- // Girlfriend gets sad if you combo break after hitting 5 notes.
- if (currentStage != null && currentStage.getGirlfriend() != null)
- if (Highscore.tallies.combo > 5 && currentStage.getGirlfriend().hasAnimation('sad'))
- currentStage.getGirlfriend().playAnimation('sad');
-
- if (Highscore.tallies.combo != 0)
- {
- Highscore.tallies.combo = comboPopUps.displayCombo(0);
- }
- }
-
- #if debug
- /**
- * Jumps forward or backward a number of sections in the song.
- * Accounts for BPM changes, does not prevent death from skipped notes.
- * @param sec
- */
- function changeSection(sec:Int):Void
- {
- FlxG.sound.music.pause();
-
- var daBPM:Float = currentSong.bpm;
- var daPos:Float = 0;
- for (i in 0...(Std.int(curStep / 16 + sec)))
- {
- var section = SongLoad.getSong()[i];
- if (section == null)
- continue;
- if (section.changeBPM)
- {
- daBPM = SongLoad.getSong()[i].bpm;
- }
- daPos += 4 * (1000 * 60 / daBPM);
- }
- Conductor.songPosition = FlxG.sound.music.time = daPos;
- Conductor.songPosition += Conductor.offset;
- updateCurStep();
- resyncVocals();
- }
- #end
-
- function endSong():Void
- {
- dispatchEvent(new ScriptEvent(ScriptEvent.SONG_END));
-
- seenCutscene = false;
- deathCounter = 0;
- mayPauseGame = false;
- FlxG.sound.music.volume = 0;
- vocals.volume = 0;
- if (currentSong != null && currentSong.validScore)
- {
- // crackhead double thingie, sets whether was new highscore, AND saves the song!
- Highscore.tallies.isNewHighscore = Highscore.saveScore(currentSong.song, songScore, storyDifficulty);
-
- Highscore.saveCompletion(currentSong.song, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, storyDifficulty);
- }
-
- if (isStoryMode)
- {
- campaignScore += songScore;
-
- storyPlaylist.remove(storyPlaylist[0]);
-
- if (storyPlaylist.length <= 0)
- {
- FlxG.sound.playMusic(Paths.music('freakyMenu'));
-
- transIn = FlxTransitionableState.defaultTransIn;
- transOut = FlxTransitionableState.defaultTransOut;
-
- switch (storyWeek)
- {
- case 7:
- FlxG.switchState(new VideoState());
- default:
- FlxG.switchState(new StoryMenuState());
- }
-
- // if ()
- StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
-
- if (currentSong.validScore)
- {
- NGio.unlockMedal(60961);
- Highscore.saveWeekScore(storyWeek, campaignScore, storyDifficulty);
- }
-
- FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
- FlxG.save.flush();
- }
- else
- {
- var difficulty:String = "";
-
- if (storyDifficulty == 0)
- difficulty = '-easy';
-
- if (storyDifficulty == 2)
- difficulty = '-hard';
-
- trace('LOADING NEXT SONG');
- trace(storyPlaylist[0].toLowerCase() + difficulty);
-
- FlxTransitionableState.skipNextTransIn = true;
- FlxTransitionableState.skipNextTransOut = true;
-
- FlxG.sound.music.stop();
- vocals.stop();
-
- if (currentSong.song.toLowerCase() == 'eggnog')
- {
- var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
- -FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
- blackShit.scrollFactor.set();
- add(blackShit);
- camHUD.visible = false;
- isInCutscene = true;
-
- FlxG.sound.play(Paths.sound('Lights_Shut_off'), function()
- {
- // no camFollow so it centers on horror tree
- currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]);
- LoadingState.loadAndSwitchState(new PlayState());
- });
- }
- else
- {
- previousCameraFollowPoint = cameraFollowPoint;
-
- currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]);
- LoadingState.loadAndSwitchState(new PlayState());
- }
- }
- }
- else
- {
- trace('WENT TO RESULTS SCREEN!');
- // unloadAssets();
-
- camZooming = false;
-
- FlxG.camera.follow(PlayState.instance.currentStage.getGirlfriend(), null, 0.05);
- FlxG.camera.targetOffset.y -= 350;
- FlxG.camera.targetOffset.x += 20;
-
- FlxTween.tween(camHUD, {alpha: 0}, 0.6);
-
- new FlxTimer().start(0.8, _ ->
- {
- currentStage.getGirlfriend().animation.play("cheer");
-
- FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1, {
- ease: FlxEase.expoIn,
- onComplete: _ ->
- {
- persistentUpdate = false;
- vocals.stop();
- camHUD.alpha = 1;
- var res:ResultState = new ResultState();
- res.camera = camHUD;
- openSubState(res);
- }
- });
- });
- // FlxG.switchState(new FreeplayState());
- }
- }
-
- // gives score and pops up rating
- private function popUpScore(strumtime:Float, daNote:Note):Void
- {
- var noteDiff:Float = Math.abs(strumtime - Conductor.songPosition);
- // boyfriend.playAnimation('hey');
- vocals.volume = 1;
-
- var isSick:Bool = false;
- var score = Scoring.scoreNote(noteDiff, PBOT1);
- var daRating = Scoring.judgeNote(noteDiff, PBOT1);
- var healthMulti:Float = daNote.lowStakes ? 0.002 : 0.033;
-
- if (noteDiff > Note.HIT_WINDOW * Note.BAD_THRESHOLD)
- {
- healthMulti *= 0; // no health on shit note
- daRating = 'shit';
- Highscore.tallies.shit += 1;
- score = 50;
- }
- else if (noteDiff > Note.HIT_WINDOW * Note.GOOD_THRESHOLD)
- {
- healthMulti *= 0.2;
- daRating = 'bad';
- Highscore.tallies.bad += 1;
- }
- else if (noteDiff > Note.HIT_WINDOW * Note.SICK_THRESHOLD)
- {
- healthMulti *= 0.78;
- daRating = 'good';
- Highscore.tallies.good += 1;
- score = 200;
- }
- else
- {
- isSick = true;
- }
-
- health += healthMulti;
- if (isSick)
- {
- Highscore.tallies.sick += 1;
- var noteSplash:NoteSplash = grpNoteSplashes.recycle(NoteSplash);
- noteSplash.setupNoteSplash(daNote.x, daNote.y, daNote.data.noteData);
- // new NoteSplash(daNote.x, daNote.y, daNote.noteData);
- grpNoteSplashes.add(noteSplash);
- }
- // Only add the score if you're not on practice mode
- if (!isPracticeMode)
- songScore += score;
- comboPopUps.displayRating(daRating);
- if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0)
- comboPopUps.displayCombo(Highscore.tallies.combo);
- }
-
- /*
- function controlCamera()
- {
- if (currentStage == null)
- return;
-
- switch (cameraFocusCharacter)
- {
- default: // null = No change
- break;
- case 0: // Boyfriend
- var isFocusedOnBF = cameraFollowPoint.x == currentStage.getBoyfriend().cameraFocusPoint.x;
- if (!isFocusedOnBF)
- {
- // Focus the camera on the player.
- cameraFollowPoint.setPosition(currentStage.getBoyfriend().cameraFocusPoint.x, currentStage.getBoyfriend().cameraFocusPoint.y);
- }
- case 1: // Dad
- var isFocusedOnDad = cameraFollowPoint.x == currentStage.getDad().cameraFocusPoint.x;
- if (!isFocusedOnDad)
- {
- cameraFollowPoint.setPosition(currentStage.getDad().cameraFocusPoint.x, currentStage.getDad().cameraFocusPoint.y);
- }
- case 2: // Girlfriend
- var isFocusedOnGF = cameraFollowPoint.x == currentStage.getGirlfriend().cameraFocusPoint.x;
- if (!isFocusedOnGF)
- {
- cameraFollowPoint.setPosition(currentStage.getGirlfriend().cameraFocusPoint.x, currentStage.getGirlfriend().cameraFocusPoint.y);
- }
- }
-
- /*
- if (cameraRightSide && !isFocusedOnBF)
- {
- // Focus the camera on the player.
- cameraFollowPoint.setPosition(currentStage.getBoyfriend().cameraFocusPoint.x, currentStage.getBoyfriend().cameraFocusPoint.y);
-
- // TODO: Un-hardcode this.
- if (currentSong.song.toLowerCase() == 'tutorial')
- FlxTween.tween(FlxG.camera, {zoom: 1 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
- }
- else if (!cameraRightSide && !isFocusedOnDad)
- {
- // Focus the camera on the opponent.
- cameraFollowPoint.setPosition(currentStage.getDad().cameraFocusPoint.x, currentStage.getDad().cameraFocusPoint.y);
-
- // TODO: Un-hardcode this stuff.
- if (currentStage.getDad().characterId == 'mom')
- {
- vocals.volume = 1;
- }
-
- if (currentSong.song.toLowerCase() == 'tutorial')
- tweenCamIn();
- }
- */
- // }
-
- public function keyShit(test:Bool):Void
- {
- if (PlayState.instance == null)
- return;
-
- // control arrays, order L D R U
- var holdArray:Array = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
- var pressArray:Array = [
- controls.NOTE_LEFT_P,
- controls.NOTE_DOWN_P,
- controls.NOTE_UP_P,
- controls.NOTE_RIGHT_P
- ];
- var releaseArray:Array = [
- controls.NOTE_LEFT_R,
- controls.NOTE_DOWN_R,
- controls.NOTE_UP_R,
- controls.NOTE_RIGHT_R
- ];
- // HOLDS, check for sustain notes
- if (holdArray.contains(true) && PlayState.instance.generatedMusic)
- {
- PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
- {
- if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData])
- PlayState.instance.goodNoteHit(daNote);
- });
- }
-
- // PRESSES, check for note hits
- if (pressArray.contains(true) && PlayState.instance.generatedMusic)
- {
- Haptic.vibrate(100, 100);
-
- PlayState.instance.currentStage.getBoyfriend().holdTimer = 0;
-
- var possibleNotes:Array = []; // notes that can be hit
- var directionList:Array = []; // directions that can be hit
- var dumbNotes:Array = []; // notes to kill later
-
- PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
- {
- if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit)
- {
- if (directionList.contains(daNote.data.noteData))
- {
- for (coolNote in possibleNotes)
- {
- if (coolNote.data.noteData == daNote.data.noteData
- && Math.abs(daNote.data.strumTime - coolNote.data.strumTime) < 10)
- { // if it's the same note twice at < 10ms distance, just delete it
- // EXCEPT u cant delete it in this loop cuz it fucks with the collection lol
- dumbNotes.push(daNote);
- break;
- }
- else if (coolNote.data.noteData == daNote.data.noteData && daNote.data.strumTime < coolNote.data.strumTime)
- { // if daNote is earlier than existing note (coolNote), replace
- possibleNotes.remove(coolNote);
- possibleNotes.push(daNote);
- break;
- }
- }
- }
- else
- {
- possibleNotes.push(daNote);
- directionList.push(daNote.data.noteData);
- }
- }
- });
-
- for (note in dumbNotes)
- {
- FlxG.log.add("killing dumb ass note at " + note.data.strumTime);
- note.kill();
- PlayState.instance.activeNotes.remove(note, true);
- note.destroy();
- }
-
- possibleNotes.sort((a, b) -> Std.int(a.data.strumTime - b.data.strumTime));
-
- if (PlayState.instance.perfectMode)
- PlayState.instance.goodNoteHit(possibleNotes[0]);
- else if (possibleNotes.length > 0)
- {
- for (shit in 0...pressArray.length)
- { // if a direction is hit that shouldn't be
- if (pressArray[shit] && !directionList.contains(shit))
- PlayState.instance.ghostNoteMiss(shit);
- }
- for (coolNote in possibleNotes)
- {
- if (pressArray[coolNote.data.noteData])
- PlayState.instance.goodNoteHit(coolNote);
- }
- }
- else
- {
- // HNGGG I really want to add an option for ghost tapping
- // L + ratio
- for (shit in 0...pressArray.length)
- if (pressArray[shit])
- PlayState.instance.ghostNoteMiss(shit, false);
- }
- }
-
- if (PlayState.instance == null || PlayState.instance.currentStage == null)
- return;
-
- for (keyId => isPressed in pressArray)
- {
- if (playerStrumline == null)
- continue;
- var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId);
-
- if (isPressed && arrow.animation.curAnim.name != 'confirm')
- {
- arrow.playAnimation('pressed');
- }
- if (!holdArray[keyId])
- {
- arrow.playAnimation('static');
- }
- }
- }
-
- /**
- * Called when a player presses a key with no note present.
- * Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
- * or even cancel the event entirely.
- *
- * @param direction
- * @param hasPossibleNotes
- */
- function ghostNoteMiss(direction:funkin.noteStuff.NoteBasic.NoteType = 1, hasPossibleNotes:Bool = true):Void
- {
- var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
- hasPossibleNotes, // Whether there was a note you could have hit.
- - 0.035 * 2, // How much health to add (negative).
- - 10 // Amount of score to add (negative).
- );
- dispatchEvent(event);
-
- // Calling event.cancelEvent() skips animations and penalties. Neat!
- if (event.eventCanceled)
- return;
-
- health += event.healthChange;
-
- if (!isPracticeMode)
- songScore += event.scoreChange;
-
- if (event.playSound)
- {
- vocals.volume = 0;
- FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
- }
- }
-
- function noteMiss(note:Note):Void
- {
- // a MISS is when you let a note scroll past you!!
- Highscore.tallies.missed++;
-
- var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, Highscore.tallies.combo, true);
- dispatchEvent(event);
- // Calling event.cancelEvent() skips all the other logic! Neat!
- if (event.eventCanceled)
- return;
-
- health -= 0.0775;
- if (!isPracticeMode)
- songScore -= 10;
- vocals.volume = 0;
-
- if (Highscore.tallies.combo != 0)
- {
- Highscore.tallies.combo = comboPopUps.displayCombo(0);
- }
-
- note.active = false;
- note.visible = false;
-
- note.kill();
- activeNotes.remove(note, true);
- note.destroy();
- }
-
- function goodNoteHit(note:Note):Void
- {
- if (!note.wasGoodHit)
- {
- var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true);
- dispatchEvent(event);
-
- // Calling event.cancelEvent() skips all the other logic! Neat!
- if (event.eventCanceled)
- return;
-
- if (!note.isSustainNote)
- {
- Highscore.tallies.combo++;
- Highscore.tallies.totalNotesHit++;
-
- if (Highscore.tallies.combo > Highscore.tallies.maxCombo)
- Highscore.tallies.maxCombo = Highscore.tallies.combo;
-
- popUpScore(note.data.strumTime, note);
- }
-
- playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
-
- note.wasGoodHit = true;
- vocals.volume = 1;
-
- if (!note.isSustainNote)
- {
- note.kill();
- activeNotes.remove(note, true);
- note.destroy();
- }
- }
- }
-
- override function stepHit():Bool
- {
- if (SongLoad.songData == null)
- return false;
-
- // super.stepHit() returns false if a module cancelled the event.
- if (!super.stepHit())
- return false;
-
- if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
- || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 20)
- {
- resyncVocals();
- }
-
- if (iconP1 != null)
- iconP1.onStepHit(Std.int(Conductor.currentStep));
- if (iconP2 != null)
- iconP2.onStepHit(Std.int(Conductor.currentStep));
-
- return true;
- }
-
- override function beatHit():Bool
- {
- // super.beatHit() returns false if a module cancelled the event.
- if (!super.beatHit())
- return false;
-
- if (generatedMusic)
- {
- // TODO: Sort more efficiently, or less often, to improve performance.
- activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
- }
-
- // Moving this code into the `beatHit` function allows for scripts and modules to control the camera better.
- if (currentSong != null)
- {
- if (generatedMusic && SongLoad.getSong()[Std.int(Conductor.currentStep / 16)] != null)
- {
- // cameraRightSide = SongLoad.getSong()[Std.int(Conductor.currentStep / 16)].mustHitSection;
- }
-
- if (SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)] != null)
- {
- if (SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)].changeBPM)
- {
- Conductor.forceBPM(SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)].bpm);
- FlxG.log.add('CHANGED BPM!');
- }
- }
- }
-
- // Manage the camera focus, if necessary.
- // controlCamera();
-
- // HARDCODING FOR MILF ZOOMS!
-
- if (PreferencesMenu.getPref('camera-zoom'))
- {
- if (currentSong != null
- && currentSong.song.toLowerCase() == 'milf'
- && Conductor.currentBeat >= 168
- && Conductor.currentBeat < 200
- && camZooming
- && FlxG.camera.zoom < 1.35)
- {
- FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
- camHUD.zoom += 0.03;
- }
-
- if (camZooming && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && Conductor.currentBeat % 4 == 0)
- {
- FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
- camHUD.zoom += 0.03;
- }
- }
-
- // That combo counter that got spoiled that one time.
- // Comes with NEAT visual and audio effects.
-
- // bruh this var is bonkers i thot it was a function lmfaooo
-
- // Break up into individual lines to aid debugging.
-
- var shouldShowComboText:Bool = false;
- if (currentSong != null)
- {
- shouldShowComboText = (Conductor.currentBeat % 8 == 7);
- var daSection = SongLoad.getSong()[Std.int(Conductor.currentBeat / 16)];
- shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
- shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
-
- var daNextSection = SongLoad.getSong()[Std.int(Conductor.currentBeat / 16) + 1];
- var isEndOfSong = SongLoad.getSong().length < Std.int(Conductor.currentBeat / 16);
- shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
- }
-
- if (shouldShowComboText)
- {
- var animShit:ComboCounter = new ComboCounter(-100, 300, Highscore.tallies.combo);
- animShit.scrollFactor.set(0.6, 0.6);
- animShit.cameras = [camHUD];
- add(animShit);
-
- var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
-
- new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
- {
- animShit.forceFinish();
- });
- }
-
- // Make the characters dance on the beat
- danceOnBeat();
-
- return true;
- }
-
- /**
- * Handles characters dancing to the beat of the current song.
- *
- * TODO: Move some of this logic into `Bopper.hx`
- */
- public function danceOnBeat()
- {
- if (currentStage == null)
- return;
-
- // TODO: Move this to a song event.
- if (Conductor.currentBeat % 16 == 15 // && currentSong.song == 'Tutorial'
- && currentStage.getDad().characterId == 'gf'
- && Conductor.currentBeat > 16
- && Conductor.currentBeat < 48)
- {
- currentStage.getBoyfriend().playAnimation('hey', true);
- currentStage.getDad().playAnimation('cheer', true);
- }
- }
-
- /**
- * Constructs the strumlines for each player.
- */
- function buildStrumlines():Void
- {
- var strumlineStyle:StrumlineStyle = NORMAL;
-
- // TODO: Put this in the chart or something?
- switch (currentStageId)
- {
- case 'school':
- strumlineStyle = PIXEL;
- case 'schoolEvil':
- strumlineStyle = PIXEL;
- }
-
- var strumlineYPos = Strumline.getYPos();
-
- playerStrumline = new Strumline(0, strumlineStyle, 4);
- playerStrumline.x = 50 + FlxG.width / 2;
- playerStrumline.y = strumlineYPos;
- // Set the z-index so they don't appear in front of notes.
- playerStrumline.zIndex = 100;
- add(playerStrumline);
- playerStrumline.cameras = [camHUD];
-
- if (!isStoryMode)
- {
- playerStrumline.fadeInArrows();
- }
-
- enemyStrumline = new Strumline(1, strumlineStyle, 4);
- enemyStrumline.x = 50;
- enemyStrumline.y = strumlineYPos;
- // Set the z-index so they don't appear in front of notes.
- enemyStrumline.zIndex = 100;
- add(enemyStrumline);
- enemyStrumline.cameras = [camHUD];
-
- if (!isStoryMode)
- {
- enemyStrumline.fadeInArrows();
- }
-
- this.refresh();
- }
-
- /**
- * Function called before opening a new substate.
- * @param subState The substate to open.
- */
- override function openSubState(subState:FlxSubState)
- {
- // If there is a substate which requires the game to continue,
- // then make this a condition.
- var shouldPause = true;
-
- if (shouldPause)
- {
- // Pause the music.
- if (FlxG.sound.music != null)
- {
- FlxG.sound.music.pause();
- if (vocals != null)
- vocals.pause();
- }
-
- // Pause the countdown.
- Countdown.pauseCountdown();
- }
-
- super.openSubState(subState);
- }
-
- /**
- * Function called before closing the current substate.
- * @param subState
- */
- override function closeSubState()
- {
- if (isGamePaused)
- {
- var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
-
- dispatchEvent(event);
-
- if (event.eventCanceled)
- return;
-
- if (FlxG.sound.music != null && !startingSong && !isInCutscene)
- resyncVocals();
-
- // Resume the countdown.
- Countdown.resumeCountdown();
-
- #if discord_rpc
- if (startTimer.finished)
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true,
- songLength - Conductor.songPosition);
- else
- DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
- #end
- }
-
- super.closeSubState();
- }
-
- /**
- * Prepares to start the countdown.
- * Ends any running cutscenes, creates the strumlines, and starts the countdown.
- */
- function startCountdown():Void
- {
- var result = Countdown.performCountdown(currentStageId.startsWith('school'));
- if (!result)
- return;
-
- isInCutscene = false;
- camHUD.visible = true;
- talking = false;
-
- buildStrumlines();
- }
-
- override function dispatchEvent(event:ScriptEvent):Void
- {
- // ORDER: Module, Stage, Character, Song, Note
- // Modules should get the first chance to cancel the event.
-
- // super.dispatchEvent(event) dispatches event to module scripts.
- super.dispatchEvent(event);
-
- // Dispatch event to stage script.
- ScriptEventDispatcher.callEvent(currentStage, event);
-
- // Dispatch event to character script(s).
- if (currentStage != null)
- currentStage.dispatchToCharacters(event);
-
- // TODO: Dispatch event to song script
- }
-
- /**
- * Updates the position and contents of the score display.
- */
- function updateScoreText():Void
- {
- // TODO: Add functionality for modules to update the score text.
- scoreText.text = "Score:" + songScore;
- }
-
- /**
- * Updates the values of the health bar.
- */
- function updateHealthBar():Void
- {
- healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
- }
-
- /**
- * Resets the camera's zoom level and focus point.
- */
- public function resetCamera():Void
- {
- FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
- FlxG.camera.targetOffset.set();
- FlxG.camera.zoom = defaultCameraZoom;
- FlxG.camera.focusOn(cameraFollowPoint.getPosition());
- }
-
- /**
- * Perform necessary cleanup before leaving the PlayState.
- */
- function performCleanup()
- {
- // Uncache the song.
- if (currentChart != null)
- {
- }
- else if (currentSong != null)
- {
- openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song));
- openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song));
- }
-
- // Remove reference to stage and remove sprites from it to save memory.
- if (currentStage != null)
- {
- remove(currentStage);
- currentStage.kill();
- dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false));
- currentStage = null;
- }
-
- GameOverSubstate.reset();
-
- // Clear the static reference to this state.
- instance = null;
- }
-
- /**
- * This function is called whenever Flixel switches switching to a new FlxState.
- * @return Whether to actually switch to the new state.
- */
- override function switchTo(nextState:FlxState):Bool
- {
- var result = super.switchTo(nextState);
-
- if (result)
- {
- performCleanup();
- }
-
- return result;
- }
+ /**
+ * STATIC VARIABLES
+ * Static variables should be used for information that must be persisted between states or between resets,
+ * such as the active song or song playlist.
+ */
+ /**
+ * The currently active PlayState.
+ * Since there is only one PlayState in existance at a time, we can use a singleton.
+ */
+ public static var instance:PlayState = null;
+
+ /**
+ * The currently active song. Includes data about what stage should be used, what characters,
+ * and the notes to be played.
+ */
+ public static var currentSong:SwagSong = null;
+
+ public static var currentSong_NEW:Song = null;
+
+ /**
+ * Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
+ */
+ public static var isStoryMode:Bool = false;
+
+ /**
+ * Whether the game is currently in Practice Mode.
+ * If true, player will not lose gain or lose score from notes.
+ */
+ public static var isPracticeMode:Bool = false;
+
+ /**
+ * Whether the game is currently in a cutscene, and gameplay should be stopped.
+ */
+ public static var isInCutscene:Bool = false;
+
+ /**
+ * Whether the game is currently in the countdown before the song resumes.
+ */
+ public static var isInCountdown:Bool = false;
+
+ /**
+ * Gets set to true when the PlayState needs to reset (player opted to restart or died).
+ * Gets disabled once resetting happens.
+ */
+ public static var needsReset:Bool = false;
+
+ /**
+ * The current "Blueball Counter" to display in the pause menu.
+ * Resets when you beat a song or go back to the main menu.
+ */
+ public static var deathCounter:Int = 0;
+
+ /**
+ * The default camera zoom level. The camera lerps back to this after zooming in.
+ * Defaults to 1.05 but may be larger or smaller depending on the current stage.
+ */
+ public static var defaultCameraZoom:Float = 1.05;
+
+ /**
+ * Used to persist the position of the `cameraFollowPosition` between resets.
+ */
+ private static var previousCameraFollowPoint:FlxObject = null;
+
+ /**
+ * PUBLIC INSTANCE VARIABLES
+ * Public instance variables should be used for information that must be reset or dereferenced
+ * every time the state is reset, such as the currently active stage, but may need to be accessed externally.
+ */
+ /**
+ * The currently active Stage. This is the object containing all the props.
+ */
+ public var currentStage:Stage = null;
+
+ public var currentChart(get, null):SongDifficulty;
+
+ /**
+ * The internal ID of the currently active Stage.
+ * Used to retrieve the data required to build the `currentStage`.
+ */
+ public var currentStageId:String = '';
+
+ /**
+ * The player's current health.
+ * The default maximum health is 2.0, and the default starting health is 1.0.
+ */
+ public var health:Float = 1;
+
+ /**
+ * The player's current score.
+ */
+ public var songScore:Int = 0;
+
+ /**
+ * An empty FlxObject contained in the scene.
+ * The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly.
+ *
+ * This is an FlxSprite for two reasons:
+ * 1. It needs to be an object in the scene for the camera to be configured to follow it.
+ * 2. It needs to be an FlxSprite to allow a graphic (optionally, for debug purposes) to be drawn on it.
+ */
+ public var cameraFollowPoint:FlxSprite = new FlxSprite(0, 0);
+
+ /**
+ * PRIVATE INSTANCE VARIABLES
+ * Private instance variables should be used for information that must be reset or dereferenced
+ * every time the state is reset, but should not be accessed externally.
+ */
+ /**
+ * The Array containing the notes that are not currently on the screen.
+ * The `update()` function regularly shifts these out to add new notes to the screen.
+ */
+ private var inactiveNotes:Array;
+
+ private var songEvents:Array;
+
+ /**
+ * If true, the player is allowed to pause the game.
+ * Disabled during the ending of a song.
+ */
+ private var mayPauseGame:Bool = true;
+
+ /**
+ * The displayed value of the player's health.
+ * Used to provide smooth animations based on linear interpolation of the player's health.
+ */
+ private var healthLerp:Float = 1;
+
+ /**
+ * Forcibly disables all update logic while the game moves back to the Menu state.
+ * This is used only when a critical error occurs and the game cannot continue.
+ */
+ private var criticalFailure:Bool = false;
+
+ /**
+ * RENDER OBJECTS
+ */
+ /**
+ * The SpriteGroup containing the notes that are currently on the screen or are about to be on the screen.
+ */
+ private var activeNotes:FlxTypedGroup = null;
+
+ /**
+ * The FlxText which displays the current score.
+ */
+ private var scoreText:FlxText;
+
+ /**
+ * The bar which displays the player's health.
+ * Dynamically updated based on the value of `healthLerp` (which is based on `health`).
+ */
+ public var healthBar:FlxBar;
+
+ /**
+ * The background image used for the health bar.
+ * Emma says the image is slightly skewed so I'm leaving it as an image instead of a `createGraphic`.
+ */
+ public var healthBarBG:FlxSprite;
+
+ /**
+ * The health icon representing the player.
+ */
+ public var iconP1:HealthIcon;
+
+ /**
+ * The health icon representing the opponent.
+ */
+ public var iconP2:HealthIcon;
+
+ /**
+ * The sprite group containing active player's strumline notes.
+ */
+ public var playerStrumline:Strumline;
+
+ /**
+ * The sprite group containing opponent's strumline notes.
+ */
+ public var enemyStrumline:Strumline;
+
+ /**
+ * The camera which contains, and controls visibility of, the user interface elements.
+ */
+ public var camHUD:FlxCamera;
+
+ /**
+ * The camera which contains, and controls visibility of, the stage and characters.
+ */
+ public var camGame:FlxCamera;
+
+ /**
+ * PROPERTIES
+ */
+ /**
+ * If a substate is rendering over the PlayState, it is paused and normal update logic is skipped.
+ * Examples include:
+ * - The Pause screen is open.
+ * - The Game Over screen is open.
+ * - The Chart Editor screen is open.
+ */
+ private var isGamePaused(get, never):Bool;
+
+ function get_isGamePaused():Bool
+ {
+ // Note: If there is a substate which requires the game to act unpaused,
+ // this should be changed to include something like `&& Std.isOfType()`
+ return this.subState != null;
+ }
+
+ // TODO: Reorganize these variables (maybe there should be a separate class like Conductor just to hold them?)
+ public static var storyWeek:Int = 0;
+ public static var storyPlaylist:Array = [];
+ public static var storyDifficulty:Int = 1;
+ public static var storyDifficulty_NEW:String = "normal";
+ public static var seenCutscene:Bool = false;
+ public static var campaignScore:Int = 0;
+
+ private var vocals:VoicesGroup;
+ private var vocalsFinished:Bool = false;
+
+ private var camZooming:Bool = false;
+ private var gfSpeed:Int = 1;
+ // private var combo:Int = 0;
+ private var generatedMusic:Bool = false;
+ private var startingSong:Bool = false;
+
+ var dialogue:Array;
+ var talking:Bool = true;
+ var doof:DialogueBox;
+ var grpNoteSplashes:FlxTypedGroup;
+ var comboPopUps:PopUpStuff;
+ var perfectMode:Bool = false;
+ var previousFrameTime:Int = 0;
+ var songTime:Float = 0;
+
+ #if discord_rpc
+ // Discord RPC variables
+ var storyDifficultyText:String = "";
+ var iconRPC:String = "";
+ var songLength:Float = 0;
+ var detailsText:String = "";
+ var detailsPausedText:String = "";
+ #end
+
+ override public function create()
+ {
+ super.create();
+
+ if (currentSong == null && currentSong_NEW == null)
+ {
+ criticalFailure = true;
+
+ lime.app.Application.current.window.alert("There was a critical error while accessing the selected song. Click OK to return to the main menu.",
+ "Error loading PlayState");
+ FlxG.switchState(new MainMenuState());
+ return;
+ }
+
+ instance = this;
+
+ if (currentSong_NEW != null)
+ {
+ // TODO: Do this in the loading state.
+ currentSong_NEW.cacheCharts(true);
+ }
+
+ // Displays the camera follow point as a sprite for debug purposes.
+ // TODO: Put this on a toggle?
+ cameraFollowPoint.makeGraphic(8, 8, 0xFF00FF00);
+ cameraFollowPoint.visible = false;
+ cameraFollowPoint.zIndex = 1000000;
+
+ // Reduce physics accuracy (who cares!!!) to improve animation quality.
+ FlxG.fixedTimestep = false;
+
+ // This state receives update() even when a substate is active.
+ this.persistentUpdate = true;
+ // This state receives draw calls even when a substate is active.
+ this.persistentDraw = true;
+
+ // Stop any pre-existing music.
+ if (FlxG.sound.music != null)
+ FlxG.sound.music.stop();
+
+ // Prepare the current song to be played.
+ if (currentChart != null)
+ {
+ currentChart.cacheInst();
+ currentChart.cacheVocals();
+ }
+ else
+ {
+ FlxG.sound.cache(Paths.inst(currentSong.song));
+ FlxG.sound.cache(Paths.voices(currentSong.song));
+ }
+
+ // Initialize stage stuff.
+ initCameras();
+
+ if (currentSong == null && currentSong_NEW == null)
+ {
+ currentSong = SongLoad.loadFromJson('tutorial');
+ }
+
+ if (currentSong_NEW != null)
+ {
+ Conductor.mapTimeChanges(currentChart.timeChanges);
+ // Conductor.bpm = currentChart.getStartingBPM();
+
+ // TODO: Support for dialog.
+ }
+ else
+ {
+ Conductor.mapBPMChanges(currentSong);
+ // Conductor.bpm = currentSong.bpm;
+
+ switch (currentSong.song.toLowerCase())
+ {
+ case 'senpai':
+ dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue'));
+ case 'roses':
+ dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue'));
+ case 'thorns':
+ dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue'));
+ }
+ }
+
+ Conductor.update(-5000);
+
+ if (dialogue != null)
+ {
+ doof = new DialogueBox(false, dialogue);
+ doof.scrollFactor.set();
+ doof.finishThing = startCountdown;
+ doof.cameras = [camHUD];
+ }
+
+ // Once the song is loaded, we can continue and initialize the stage.
+
+ var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
+ healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
+ healthBarBG.screenCenter(X);
+ healthBarBG.scrollFactor.set(0, 0);
+ add(healthBarBG);
+
+ healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
+ 'healthLerp', 0, 2);
+ healthBar.scrollFactor.set();
+ healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN);
+ add(healthBar);
+
+ initStage();
+ initCharacters();
+ #if discord_rpc
+ initDiscord();
+ #end
+
+ // Configure camera follow point.
+ if (previousCameraFollowPoint != null)
+ {
+ cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y);
+ previousCameraFollowPoint = null;
+ }
+ add(cameraFollowPoint);
+
+ comboPopUps = new PopUpStuff();
+ comboPopUps.cameras = [camHUD];
+ add(comboPopUps);
+
+ grpNoteSplashes = new FlxTypedGroup();
+
+ var noteSplash:NoteSplash = new NoteSplash(100, 100, 0);
+ grpNoteSplashes.add(noteSplash);
+ noteSplash.alpha = 0.1;
+
+ add(grpNoteSplashes);
+
+ if (currentSong_NEW != null)
+ {
+ generateSong_NEW();
+ }
+ else
+ {
+ generateSong();
+ }
+
+ resetCamera();
+
+ FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
+
+ scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, "", 20);
+ scoreText.setFormat(Paths.font("vcr.ttf"), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
+ scoreText.scrollFactor.set();
+ add(scoreText);
+
+ // Attach the groups to the HUD camera so they are rendered independent of the stage.
+ grpNoteSplashes.cameras = [camHUD];
+ activeNotes.cameras = [camHUD];
+ healthBar.cameras = [camHUD];
+ healthBarBG.cameras = [camHUD];
+ iconP1.cameras = [camHUD];
+ iconP2.cameras = [camHUD];
+ scoreText.cameras = [camHUD];
+ leftWatermarkText.cameras = [camHUD];
+ rightWatermarkText.cameras = [camHUD];
+
+ // if (SONG.song == 'South')
+ // FlxG.camera.alpha = 0.7;
+ // UI_camera.zoom = 1;
+
+ // cameras = [FlxG.cameras.list[1]];
+ startingSong = true;
+
+ if (isStoryMode && !seenCutscene)
+ {
+ seenCutscene = true;
+
+ switch (currentSong.song.toLowerCase())
+ {
+ case "winter-horrorland":
+ VanillaCutscenes.playHorrorStartCutscene();
+ case 'senpai' | 'roses' | 'thorns':
+ schoolIntro(doof); // doof is assumed to be non-null, lol!
+ case 'ugh':
+ VanillaCutscenes.playUghCutscene();
+ case 'stress':
+ VanillaCutscenes.playStressCutscene();
+ case 'guns':
+ VanillaCutscenes.playGunsCutscene();
+ default:
+ // VanillaCutscenes will call startCountdown later.
+ // TODO: Alternatively: make a song script that allows startCountdown to be called,
+ // then cancels the countdown, hides the strumline, plays the cutscene,
+ // then calls Countdown.performCountdown()
+ startCountdown();
+ }
+ }
+ else
+ {
+ startCountdown();
+ }
+
+ #if debug
+ this.rightWatermarkText.text = Constants.VERSION;
+ #end
+ }
+
+ function get_currentChart():SongDifficulty
+ {
+ if (currentSong_NEW == null || storyDifficulty_NEW == null)
+ return null;
+ return currentSong_NEW.getDifficulty(storyDifficulty_NEW);
+ }
+
+ /**
+ * Initializes the game and HUD cameras.
+ */
+ function initCameras()
+ {
+ // Configure the default camera zoom level.
+ defaultCameraZoom = FlxCamera.defaultZoom * 1.05;
+
+ camGame = new SwagCamera();
+ camHUD = new FlxCamera();
+ camHUD.bgColor.alpha = 0;
+
+ FlxG.cameras.reset(camGame);
+ FlxG.cameras.add(camHUD, false);
+ }
+
+ function initStage()
+ {
+ if (currentSong_NEW != null)
+ {
+ initStage_NEW();
+ return;
+ }
+
+ // TODO: Move stageId to the song file.
+ switch (currentSong.song.toLowerCase())
+ {
+ case 'spookeez' | 'monster' | 'south':
+ currentStageId = "spookyMansion";
+ case 'pico' | 'blammed' | 'philly':
+ currentStageId = 'phillyTrain';
+ case "milf" | 'satin-panties' | 'high':
+ currentStageId = 'limoRide';
+ case "cocoa" | 'eggnog':
+ currentStageId = 'mallXmas';
+ case 'winter-horrorland':
+ currentStageId = 'mallEvil';
+ case 'senpai' | 'roses':
+ currentStageId = 'school';
+ case "darnell" | "lit-up" | "2hot":
+ currentStageId = 'phillyStreets';
+ // currentStageId = 'pyro';
+ case "blazin":
+ currentStageId = 'phillyBlazin';
+ // currentStageId = 'pyro';
+ case 'pyro':
+ currentStageId = 'pyro';
+ case 'thorns':
+ currentStageId = 'schoolEvil';
+ case 'guns' | 'stress' | 'ugh':
+ currentStageId = 'tankmanBattlefield';
+ default:
+ currentStageId = "mainStage";
+ }
+ // Loads the relevant stage based on its ID.
+ loadStage(currentStageId);
+ }
+
+ function initStage_NEW()
+ {
+ if (currentChart == null)
+ {
+ trace('Song difficulty could not be loaded.');
+ }
+
+ if (currentChart.stage != null && currentChart.stage != '')
+ {
+ currentStageId = currentChart.stage;
+ }
+ else
+ {
+ currentStageId = SongValidator.DEFAULT_STAGE;
+ }
+
+ loadStage(currentStageId);
+ }
+
+ function initCharacters()
+ {
+ if (currentSong_NEW != null)
+ {
+ initCharacters_NEW();
+ return;
+ }
+
+ iconP1 = new HealthIcon(currentSong.player1, 0);
+ iconP1.y = healthBar.y - (iconP1.height / 2);
+ add(iconP1);
+
+ iconP2 = new HealthIcon(currentSong.player2, 1);
+ iconP2.y = healthBar.y - (iconP2.height / 2);
+ add(iconP2);
+
+ //
+ // GIRLFRIEND
+ //
+
+ // TODO: Tie the GF version to the song data, not the stage ID or the current player.
+ var gfVersion:String = 'gf';
+
+ switch (currentStageId)
+ {
+ case 'pyro' | 'phillyStreets':
+ gfVersion = 'nene';
+ case 'blazin':
+ gfVersion = '';
+ case 'limoRide':
+ gfVersion = 'gf-car';
+ case 'mallXmas' | 'mallEvil':
+ gfVersion = 'gf-christmas';
+ case 'school' | 'schoolEvil':
+ gfVersion = 'gf-pixel';
+ case 'tankmanBattlefield':
+ gfVersion = 'gf-tankmen';
+ }
+
+ if (currentSong.player1 == "pico")
+ gfVersion = "nene";
+
+ if (currentSong.song.toLowerCase() == 'stress')
+ gfVersion = 'pico-speaker';
+
+ if (currentSong.song.toLowerCase() == 'tutorial')
+ gfVersion = '';
+
+ //
+ // GIRLFRIEND
+ //
+ var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(gfVersion);
+
+ if (girlfriend != null)
+ {
+ girlfriend.characterType = CharacterType.GF;
+ girlfriend.scrollFactor.set(0.95, 0.95);
+ if (gfVersion == 'pico-speaker')
+ {
+ girlfriend.x -= 50;
+ girlfriend.y -= 200;
+ }
+ }
+ else if (gfVersion != '')
+ {
+ trace('WARNING: Could not load girlfriend character with ID ${gfVersion}, skipping...');
+ }
+
+ //
+ // DAD
+ //
+ var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player2);
+
+ if (dad != null)
+ {
+ dad.characterType = CharacterType.DAD;
+ }
+
+ switch (currentSong.player2)
+ {
+ case 'gf':
+ if (isStoryMode)
+ {
+ cameraFollowPoint.x += 600;
+ tweenCamIn();
+ }
+ }
+
+ //
+ // BOYFRIEND
+ //
+ var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentSong.player1);
+
+ if (boyfriend != null)
+ {
+ boyfriend.characterType = CharacterType.BF;
+ }
+
+ if (currentStage != null)
+ {
+ // We're using Eric's stage handler.
+ // Characters get added to the stage, not the main scene.
+ if (girlfriend != null)
+ {
+ currentStage.addCharacter(girlfriend, GF);
+ }
+
+ if (boyfriend != null)
+ {
+ currentStage.addCharacter(boyfriend, BF);
+ }
+
+ if (dad != null)
+ {
+ currentStage.addCharacter(dad, DAD);
+ // Camera starts at dad.
+ cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
+ }
+
+ // Redo z-indexes.
+ currentStage.refresh();
+ }
+ }
+
+ function initCharacters_NEW()
+ {
+ if (currentSong_NEW == null || currentChart == null)
+ {
+ trace('Song difficulty could not be loaded.');
+ }
+
+ // TODO: Switch playable character by manipulating this value.
+ // TODO: How to choose which one to use for story mode?
+
+ var playableChars = currentChart.getPlayableChars();
+ var currentPlayer = 'bf';
+
+ if (playableChars.length == 0)
+ {
+ trace('WARNING: No playable characters found for this song.');
+ }
+ else if (playableChars.indexOf(currentPlayer) == -1)
+ {
+ currentPlayer = playableChars[0];
+ }
+
+ var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayer);
+
+ //
+ // GIRLFRIEND
+ //
+ var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend);
+
+ if (girlfriend != null)
+ {
+ girlfriend.characterType = CharacterType.GF;
+ }
+ else if (currentCharData.girlfriend != '')
+ {
+ trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...');
+ }
+ else
+ {
+ // Chosen GF was '' so we don't load one.
+ }
+
+ //
+ // DAD
+ //
+ var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent);
+
+ if (dad != null)
+ {
+ dad.characterType = CharacterType.DAD;
+ }
+
+ // TODO: Cut out this code/make it generic.
+ switch (currentCharData.opponent)
+ {
+ case 'gf':
+ if (isStoryMode)
+ {
+ cameraFollowPoint.x += 600;
+ tweenCamIn();
+ }
+ }
+
+ //
+ // OPPONENT HEALTH ICON
+ //
+ iconP2 = new HealthIcon(currentCharData.opponent, 1);
+ iconP2.y = healthBar.y - (iconP2.height / 2);
+ add(iconP2);
+
+ //
+ // BOYFRIEND
+ //
+ var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayer);
+
+ if (boyfriend != null)
+ {
+ boyfriend.characterType = CharacterType.BF;
+ }
+
+ //
+ // PLAYER HEALTH ICON
+ //
+ iconP1 = new HealthIcon(currentPlayer, 0);
+ iconP1.y = healthBar.y - (iconP1.height / 2);
+ add(iconP1);
+
+ //
+ // ADD CHARACTERS TO SCENE
+ //
+
+ if (currentStage != null)
+ {
+ // Characters get added to the stage, not the main scene.
+ if (girlfriend != null)
+ {
+ currentStage.addCharacter(girlfriend, GF);
+ }
+
+ if (boyfriend != null)
+ {
+ currentStage.addCharacter(boyfriend, BF);
+ }
+
+ if (dad != null)
+ {
+ currentStage.addCharacter(dad, DAD);
+ // Camera starts at dad.
+ cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
+ }
+
+ // Rearrange by z-indexes.
+ currentStage.refresh();
+ }
+ }
+
+ /**
+ * Removes any references to the current stage, then clears the stage cache,
+ * then reloads all the stages.
+ *
+ * This is useful for when you want to edit a stage without reloading the whole game.
+ * Reloading works on both the JSON and the HXC, if applicable.
+ *
+ * Call this by pressing F5 on a debug build.
+ */
+ override function debug_refreshModules()
+ {
+ // Remove the current stage. If the stage gets deleted while it's still in use,
+ // it'll probably crash the game or something.
+ if (this.currentStage != null)
+ {
+ remove(currentStage);
+ var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false);
+ ScriptEventDispatcher.callEvent(currentStage, event);
+ currentStage = null;
+ }
+
+ super.debug_refreshModules();
+ }
+
+ /**
+ * Pauses music and vocals easily.
+ */
+ public function pauseMusic()
+ {
+ FlxG.sound.music.pause();
+ vocals.pause();
+ }
+
+ /**
+ * Loads stage data from cache, assembles the props,
+ * and adds it to the state.
+ * @param id
+ */
+ function loadStage(id:String)
+ {
+ currentStage = StageDataParser.fetchStage(id);
+
+ if (currentStage != null)
+ {
+ // Actually create and position the sprites.
+ var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false);
+ ScriptEventDispatcher.callEvent(currentStage, event);
+
+ // Apply camera zoom.
+ defaultCameraZoom = currentStage.camZoom;
+
+ // Add the stage to the scene.
+ this.add(currentStage);
+ }
+ }
+
+ function initDiscord():Void
+ {
+ #if discord_rpc
+ storyDifficultyText = difficultyString();
+ iconRPC = currentSong.player2;
+
+ // To avoid having duplicate images in Discord assets
+ switch (iconRPC)
+ {
+ case 'senpai-angry':
+ iconRPC = 'senpai';
+ case 'monster-christmas':
+ iconRPC = 'monster';
+ case 'mom-car':
+ iconRPC = 'mom';
+ }
+
+ // String that contains the mode defined here so it isn't necessary to call changePresence for each mode
+ detailsText = isStoryMode ? "Story Mode: Week " + storyWeek : "Freeplay";
+ detailsPausedText = "Paused - " + detailsText;
+
+ // Updating Discord Rich Presence.
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ #end
+ }
+
+ function schoolIntro(?dialogueBox:DialogueBox):Void
+ {
+ var black:FlxSprite = new FlxSprite(-100, -100).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
+ black.scrollFactor.set();
+ add(black);
+
+ var red:FlxSprite = new FlxSprite(-100, -100).makeGraphic(FlxG.width * 2, FlxG.height * 2, 0xFFff1b31);
+ red.scrollFactor.set();
+
+ var senpaiEvil:FlxSprite = new FlxSprite();
+ senpaiEvil.frames = Paths.getSparrowAtlas('weeb/senpaiCrazy');
+ senpaiEvil.animation.addByPrefix('idle', 'Senpai Pre Explosion', 24, false);
+ senpaiEvil.setGraphicSize(Std.int(senpaiEvil.width * Constants.PIXEL_ART_SCALE));
+ senpaiEvil.scrollFactor.set();
+ senpaiEvil.updateHitbox();
+ senpaiEvil.screenCenter();
+ senpaiEvil.x += senpaiEvil.width / 5;
+
+ if (currentSong.song.toLowerCase() == 'roses' || currentSong.song.toLowerCase() == 'thorns')
+ {
+ remove(black);
+
+ if (currentSong.song.toLowerCase() == 'thorns')
+ {
+ add(red);
+ camHUD.visible = false;
+ }
+ else
+ FlxG.sound.play(Paths.sound('ANGRY'));
+ // moved senpai angry noise in here to clean up cutscene switch case lol
+ }
+
+ new FlxTimer().start(0.3, function(tmr:FlxTimer)
+ {
+ black.alpha -= 0.15;
+
+ if (black.alpha > 0)
+ tmr.reset(0.3);
+ else
+ {
+ if (dialogueBox != null)
+ {
+ isInCutscene = true;
+
+ if (currentSong.song.toLowerCase() == 'thorns')
+ {
+ add(senpaiEvil);
+ senpaiEvil.alpha = 0;
+ new FlxTimer().start(0.3, function(swagTimer:FlxTimer)
+ {
+ senpaiEvil.alpha += 0.15;
+ if (senpaiEvil.alpha < 1)
+ swagTimer.reset();
+ else
+ {
+ senpaiEvil.animation.play('idle');
+ FlxG.sound.play(Paths.sound('Senpai_Dies'), 1, false, null, true, function()
+ {
+ remove(senpaiEvil);
+ remove(red);
+ FlxG.camera.fade(FlxColor.WHITE, 0.01, true, function()
+ {
+ add(dialogueBox);
+ camHUD.visible = true;
+ }, true);
+ });
+ new FlxTimer().start(3.2, function(deadTime:FlxTimer)
+ {
+ FlxG.camera.fade(FlxColor.WHITE, 1.6, false);
+ });
+ }
+ });
+ }
+ else
+ add(dialogueBox);
+ }
+ else
+ startCountdown();
+
+ remove(black);
+ }
+ });
+ }
+
+ function startSong():Void
+ {
+ dispatchEvent(new ScriptEvent(ScriptEvent.SONG_START));
+
+ startingSong = false;
+
+ previousFrameTime = FlxG.game.ticks;
+
+ if (!isGamePaused)
+ {
+ // if (FlxG.sound.music != null)
+ // FlxG.sound.music.play(true);
+ // else
+ if (currentChart != null)
+ {
+ currentChart.playInst(1.0, false);
+ }
+ else
+ {
+ FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false);
+ }
+ }
+
+ FlxG.sound.music.onComplete = endSong;
+ trace('Playing vocals...');
+ vocals.play();
+
+ #if discord_rpc
+ // Song duration in a float, useful for the time left feature
+ songLength = FlxG.sound.music.length;
+
+ // Updating Discord Rich Presence (with Time Left)
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength);
+ #end
+ }
+
+ private function generateSong():Void
+ {
+ // FlxG.log.add(ChartParser.parse());
+
+ Conductor.forceBPM(currentSong.bpm);
+
+ currentSong.song = currentSong.song;
+
+ if (currentSong.needsVoices)
+ vocals = VoicesGroup.build(currentSong.song, currentSong.voiceList);
+ else
+ vocals = VoicesGroup.build(currentSong.song, null);
+
+ vocals.members[0].onComplete = function()
+ {
+ vocalsFinished = true;
+ };
+
+ trace(vocals);
+
+ activeNotes = new FlxTypedGroup();
+ activeNotes.zIndex = 1000;
+ add(activeNotes);
+
+ regenNoteData();
+
+ generatedMusic = true;
+ }
+
+ private function generateSong_NEW():Void
+ {
+ if (currentChart == null)
+ {
+ trace('Song difficulty could not be loaded.');
+ }
+
+ Conductor.forceBPM(currentChart.getStartingBPM());
+
+ // TODO: Fix grouped vocals
+ vocals = currentChart.buildVocals();
+ vocals.members[0].onComplete = function()
+ {
+ vocalsFinished = true;
+ }
+
+ // Create the rendered note group.
+ activeNotes = new FlxTypedGroup();
+ activeNotes.zIndex = 1000;
+ add(activeNotes);
+
+ regenNoteData_NEW();
+
+ generatedMusic = true;
+ }
+
+ function regenNoteData():Void
+ {
+ // resets combo, should prob put somewhere else!
+ Highscore.tallies.combo = 0;
+ Highscore.tallies = new Tallies();
+ // make unspawn notes shit def empty
+ inactiveNotes = [];
+
+ activeNotes.forEach(function(nt)
+ {
+ nt.followsTime = false;
+ FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, {
+ ease: FlxEase.expoIn,
+ onComplete: function(twn)
+ {
+ nt.kill();
+ activeNotes.remove(nt, true);
+ nt.destroy();
+ }
+ });
+ });
+
+ var noteData:Array;
+
+ // NEW SHIT
+ noteData = SongLoad.getSong();
+
+ for (section in noteData)
+ {
+ for (songNotes in section.sectionNotes)
+ {
+ var daStrumTime:Float = songNotes.strumTime;
+ // TODO: Replace 4 with strumlineSize
+ var daNoteData:Int = Std.int(songNotes.noteData % 4);
+ var gottaHitNote:Bool = section.mustHitSection;
+
+ if (songNotes.highStakes) // noteData > 3
+ gottaHitNote = !section.mustHitSection;
+
+ var oldNote:Note;
+ if (inactiveNotes.length > 0)
+ oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)];
+ else
+ oldNote = null;
+
+ var strumlineStyle:StrumlineStyle = NORMAL;
+
+ // TODO: Put this in the chart or something?
+ switch (currentStageId)
+ {
+ case 'school':
+ strumlineStyle = PIXEL;
+ case 'schoolEvil':
+ strumlineStyle = PIXEL;
+ }
+
+ var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote, false, strumlineStyle);
+ // swagNote.data = songNotes;
+ swagNote.data.sustainLength = songNotes.sustainLength;
+ swagNote.data.noteKind = songNotes.noteKind;
+ swagNote.scrollFactor.set(0, 0);
+
+ var susLength:Float = swagNote.data.sustainLength;
+
+ susLength = susLength / Conductor.stepCrochet;
+ inactiveNotes.push(swagNote);
+
+ for (susNote in 0...Math.round(susLength))
+ {
+ oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)];
+
+ var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true, strumlineStyle);
+ sustainNote.data.noteKind = songNotes.noteKind;
+ sustainNote.scrollFactor.set();
+ inactiveNotes.push(sustainNote);
+
+ sustainNote.mustPress = gottaHitNote;
+
+ if (sustainNote.mustPress)
+ sustainNote.x += FlxG.width / 2; // general offset
+ }
+
+ // TODO: Replace 4 with strumlineSize
+ swagNote.mustPress = gottaHitNote;
+
+ if (swagNote.mustPress)
+ {
+ if (playerStrumline != null)
+ {
+ swagNote.x = playerStrumline.getArrow(swagNote.data.noteData).x;
+ }
+ else
+ {
+ swagNote.x += FlxG.width / 2; // general offset
+ }
+ }
+ else
+ {
+ if (enemyStrumline != null)
+ {
+ swagNote.x = enemyStrumline.getArrow(swagNote.data.noteData).x;
+ }
+ else
+ {
+ // swagNote.x += FlxG.width / 2; // general offset
+ }
+ }
+ }
+ }
+
+ inactiveNotes.sort(function(a:Note, b:Note):Int
+ {
+ return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
+ });
+ }
+
+ function regenNoteData_NEW():Void
+ {
+ Highscore.tallies.combo = 0;
+ Highscore.tallies = new Tallies();
+
+ // Reset song events.
+ songEvents = currentChart.getEvents();
+ SongEventParser.resetEvents(songEvents);
+
+ // Destroy inactive notes.
+ inactiveNotes = [];
+
+ // Destroy active notes.
+ activeNotes.forEach(function(nt)
+ {
+ nt.followsTime = false;
+ FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, {
+ ease: FlxEase.expoIn,
+ onComplete: function(twn)
+ {
+ nt.kill();
+ activeNotes.remove(nt, true);
+ nt.destroy();
+ }
+ });
+ });
+
+ var noteData:Array = currentChart.notes;
+
+ var oldNote:Note = null;
+ for (songNote in noteData)
+ {
+ var mustHitNote:Bool = songNote.getMustHitNote();
+
+ // TODO: Put this in the chart or something?
+ var strumlineStyle:StrumlineStyle = null;
+ switch (currentStageId)
+ {
+ case 'school':
+ strumlineStyle = PIXEL;
+ case 'schoolEvil':
+ strumlineStyle = PIXEL;
+ default:
+ strumlineStyle = NORMAL;
+ }
+
+ var newNote:Note = new Note(songNote.time, songNote.data, oldNote, false, strumlineStyle);
+ newNote.mustPress = mustHitNote;
+ newNote.data.sustainLength = songNote.length;
+ newNote.data.noteKind = songNote.kind;
+ newNote.scrollFactor.set(0, 0);
+
+ // Note positioning.
+ // TODO: Make this more robust.
+ if (newNote.mustPress)
+ {
+ if (playerStrumline != null)
+ {
+ // Align with the strumline arrow.
+ newNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
+ }
+ else
+ {
+ // Assume strumline position.
+ newNote.x += FlxG.width / 2;
+ }
+ }
+ else
+ {
+ if (enemyStrumline != null)
+ {
+ newNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
+ }
+ else
+ {
+ // newNote.x += 0;
+ }
+ }
+
+ inactiveNotes.push(newNote);
+
+ oldNote = newNote;
+
+ // Generate X sustain notes.
+ var sustainSections = Math.round(songNote.length / Conductor.stepCrochet);
+ for (noteIndex in 0...sustainSections)
+ {
+ var noteTimeOffset:Float = Conductor.stepCrochet + (Conductor.stepCrochet * noteIndex);
+ var sustainNote:Note = new Note(songNote.time + noteTimeOffset, songNote.data, oldNote, true, strumlineStyle);
+ sustainNote.mustPress = mustHitNote;
+ sustainNote.data.noteKind = songNote.kind;
+ sustainNote.scrollFactor.set(0, 0);
+
+ if (sustainNote.mustPress)
+ {
+ if (playerStrumline != null)
+ {
+ // Align with the strumline arrow.
+ sustainNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
+ }
+ else
+ {
+ // Assume strumline position.
+ sustainNote.x += FlxG.width / 2;
+ }
+ }
+ else
+ {
+ if (enemyStrumline != null)
+ {
+ sustainNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
+ }
+ else
+ {
+ // newNote.x += 0;
+ }
+ }
+
+ inactiveNotes.push(sustainNote);
+
+ oldNote = sustainNote;
+ }
+ }
+
+ // Sorting is an expensive operation.
+ // Assume it was done in the chart file.
+ /**
+ inactiveNotes.sort(function(a:Note, b:Note):Int
+ {
+ return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
+ });
+ **/
+ }
+
+ function tweenCamIn():Void
+ {
+ FlxTween.tween(FlxG.camera, {zoom: 1.3 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
+ }
+
+ #if discord_rpc
+ override public function onFocus():Void
+ {
+ if (health > 0 && !paused && FlxG.autoPause)
+ {
+ if (Conductor.songPosition > 0.0)
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength - Conductor.songPosition);
+ else
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ }
+
+ super.onFocus();
+ }
+
+ override public function onFocusLost():Void
+ {
+ if (health > 0 && !paused && FlxG.autoPause)
+ DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+
+ super.onFocusLost();
+ }
+ #end
+
+ function resyncVocals():Void
+ {
+ if (_exiting || vocals == null)
+ return;
+
+ vocals.pause();
+
+ FlxG.sound.music.play();
+ Conductor.update(FlxG.sound.music.time + Conductor.offset);
+
+ if (vocalsFinished)
+ return;
+
+ vocals.time = FlxG.sound.music.time;
+ vocals.play();
+ }
+
+ override public function update(elapsed:Float)
+ {
+ super.update(elapsed);
+
+ if (criticalFailure)
+ return;
+
+ if (FlxG.keys.justPressed.U)
+ {
+ // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
+ persistentUpdate = false;
+ openSubState(new StageOffsetSubstate());
+ }
+
+ updateHealthBar();
+ updateScoreText();
+
+ if (needsReset)
+ {
+ dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
+
+ resetCamera();
+
+ persistentUpdate = true;
+ persistentDraw = true;
+
+ startingSong = true;
+
+ FlxG.sound.music.pause();
+ vocals.pause();
+
+ FlxG.sound.music.time = 0;
+
+ currentStage.resetStage();
+
+ // Delete all notes and reset the arrays.
+ if (currentChart != null)
+ {
+ regenNoteData_NEW();
+ }
+ else
+ {
+ regenNoteData();
+ }
+
+ health = 1;
+ songScore = 0;
+ Highscore.tallies.combo = 0;
+ Countdown.performCountdown(currentStageId.startsWith('school'));
+
+ needsReset = false;
+ }
+
+ #if !debug
+ perfectMode = false;
+ #else
+ if (FlxG.keys.justPressed.H)
+ camHUD.visible = !camHUD.visible;
+ #end
+
+ // do this BEFORE super.update() so songPosition is accurate
+ if (startingSong)
+ {
+ if (isInCountdown)
+ {
+ Conductor.songPosition += elapsed * 1000;
+ if (Conductor.songPosition >= 0)
+ startSong();
+ }
+ }
+ else
+ {
+ if (Paths.SOUND_EXT == 'mp3')
+ Conductor.offset = -13; // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
+
+ Conductor.update(FlxG.sound.music.time + Conductor.offset);
+
+ if (!isGamePaused)
+ {
+ songTime += FlxG.game.ticks - previousFrameTime;
+ previousFrameTime = FlxG.game.ticks;
+
+ // Interpolation type beat
+ if (Conductor.lastSongPos != Conductor.songPosition)
+ {
+ songTime = (songTime + Conductor.songPosition) / 2;
+ Conductor.lastSongPos = Conductor.songPosition;
+ }
+ }
+ }
+
+ var androidPause:Bool = false;
+
+ #if android
+ androidPause = FlxG.android.justPressed.BACK;
+ #end
+
+ if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
+ {
+ var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
+
+ dispatchEvent(event);
+
+ if (!event.eventCanceled)
+ {
+ // Pause updates while the substate is open, preventing the game state from advancing.
+ persistentUpdate = false;
+ // Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
+ persistentDraw = true;
+
+ // There is a 1/1000 change to use a special pause menu.
+ // This prevents the player from resuming, but that's the point.
+ // It's a reference to Gitaroo Man, which doesn't let you pause the game.
+ if (event.gitaroo)
+ {
+ FlxG.switchState(new GitarooPause());
+ }
+ else
+ {
+ var boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
+ var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y);
+ openSubState(pauseSubState);
+ pauseSubState.camera = camHUD;
+ boyfriendPos.put();
+ }
+
+ #if discord_rpc
+ DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ #end
+ }
+ }
+
+ #if debug
+ // 1: End the song immediately.
+ if (FlxG.keys.justPressed.ONE)
+ endSong();
+
+ // 2: Gain 10% health.
+ if (FlxG.keys.justPressed.TWO)
+ health += 0.1 * 2.0;
+
+ // 3: Lose 5% health.
+ if (FlxG.keys.justPressed.THREE)
+ health -= 0.05 * 2.0;
+ #end
+
+ // 7: Move to the charter.
+ if (FlxG.keys.justPressed.SEVEN)
+ {
+ FlxG.switchState(new ChartingState());
+
+ #if discord_rpc
+ DiscordClient.changePresence("Chart Editor", null, null, true);
+ #end
+ }
+
+ // 8: Move to the offset editor.
+ if (FlxG.keys.justPressed.EIGHT)
+ FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
+
+ // 9: Toggle the old icon.
+ if (FlxG.keys.justPressed.NINE)
+ iconP1.toggleOldIcon();
+
+ #if debug
+ // PAGEUP: Skip forward one section.
+ // SHIFT+PAGEUP: Skip forward ten sections.
+ if (FlxG.keys.justPressed.PAGEUP)
+ changeSection(FlxG.keys.pressed.SHIFT ? 10 : 1);
+ // PAGEDOWN: Skip backward one section. Doesn't replace notes.
+ // SHIFT+PAGEDOWN: Skip backward ten sections.
+ if (FlxG.keys.justPressed.PAGEDOWN)
+ changeSection(FlxG.keys.pressed.SHIFT ? -10 : -1);
+ #end
+
+ if (health > 2.0)
+ health = 2.0;
+ if (health < 0.0)
+ health = 0.0;
+
+ if (camZooming && subState == null)
+ {
+ FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
+ camHUD.zoom = FlxMath.lerp(1 * FlxCamera.defaultZoom, camHUD.zoom, 0.95);
+ }
+
+ FlxG.watch.addQuick("beatShit", Conductor.currentBeat);
+ FlxG.watch.addQuick("stepShit", Conductor.currentStep);
+ if (currentStage != null)
+ {
+ FlxG.watch.addQuick("bfAnim", currentStage.getBoyfriend().getCurrentAnimation());
+ }
+ FlxG.watch.addQuick("songPos", Conductor.songPosition);
+
+ if (currentSong != null && currentSong.song == 'Fresh')
+ {
+ switch (Conductor.currentBeat)
+ {
+ case 16:
+ camZooming = true;
+ gfSpeed = 2;
+ case 48:
+ gfSpeed = 1;
+ case 80:
+ gfSpeed = 2;
+ case 112:
+ gfSpeed = 1;
+ }
+ }
+
+ if (!isInCutscene && !_exiting)
+ {
+ // RESET = Quick Game Over Screen
+ if (controls.RESET)
+ {
+ health = 0;
+ trace("RESET = True");
+ }
+
+ #if CAN_CHEAT // brandon's a pussy
+ if (controls.CHEAT)
+ {
+ health += 1;
+ trace("User is cheating!");
+ }
+ #end
+
+ if (health <= 0 && !isPracticeMode)
+ {
+ vocals.pause();
+ FlxG.sound.music.pause();
+
+ deathCounter += 1;
+
+ dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER));
+
+ // Disable updates, preventing animations in the background from playing.
+ persistentUpdate = false;
+ #if debug
+ if (FlxG.keys.pressed.THREE)
+ {
+ // TODO: Change the key or delete this?
+ // In debug builds, pressing 3 to kill the player makes the background transparent.
+ persistentDraw = true;
+ }
+ else
+ {
+ #end
+ persistentDraw = false;
+ #if debug
+ }
+ #end
+
+ var gameOverSubstate = new GameOverSubstate();
+ openSubState(gameOverSubstate);
+
+ #if discord_rpc
+ // Game Over doesn't get his own variable because it's only used here
+ DiscordClient.changePresence("Game Over - " + detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ #end
+ }
+ }
+
+ while (inactiveNotes[0] != null && inactiveNotes[0].data.strumTime - Conductor.songPosition < 1800 / SongLoad.getSpeed())
+ {
+ var dunceNote:Note = inactiveNotes[0];
+
+ if (dunceNote.mustPress && !dunceNote.isSustainNote)
+ Highscore.tallies.totalNotes++;
+
+ activeNotes.add(dunceNote);
+
+ inactiveNotes.shift();
+ }
+
+ if (generatedMusic && playerStrumline != null)
+ {
+ activeNotes.forEachAlive(function(daNote:Note)
+ {
+ if ((PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height)
+ || (!PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height))
+ {
+ daNote.active = false;
+ daNote.visible = false;
+ }
+ else
+ {
+ daNote.visible = true;
+ daNote.active = true;
+ }
+
+ var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
+
+ if (daNote.followsTime)
+ daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(SongLoad.getSpeed(), 2) * daNote.noteSpeedMulti);
+
+ if (PreferencesMenu.getPref('downscroll'))
+ {
+ daNote.y += playerStrumline.y;
+ if (daNote.isSustainNote)
+ {
+ if (daNote.animation.curAnim.name.endsWith("end") && daNote.prevNote != null)
+ daNote.y += daNote.prevNote.height;
+ else
+ daNote.y += daNote.height / 2;
+
+ if ((!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit)))
+ && daNote.y - daNote.offset.y * daNote.scale.y + daNote.height >= strumLineMid)
+ {
+ applyClipRect(daNote);
+ }
+ }
+ }
+ else
+ {
+ if (daNote.followsTime)
+ daNote.y = playerStrumline.y - daNote.y;
+ if (daNote.isSustainNote
+ && (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit)))
+ && daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid)
+ {
+ applyClipRect(daNote);
+ }
+ }
+
+ if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
+ {
+ if (currentSong != null && currentSong.song != 'Tutorial')
+ camZooming = true;
+
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, Highscore.tallies.combo, true);
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() in a module should force the CPU to miss the note.
+ // This is useful for cool shit, including but not limited to:
+ // - Making the AI ignore notes which are hazardous.
+ // - Making the AI miss notes on purpose for aesthetic reasons.
+ if (event.eventCanceled)
+ {
+ daNote.tooLate = true;
+ }
+ else
+ {
+ // Volume of DAD.
+ if (currentSong != null && currentSong.needsVoices)
+ vocals.volume = 1;
+ }
+ }
+
+ // WIP interpolation shit? Need to fix the pause issue
+ // daNote.y = (strumLine.y - (songTime - daNote.strumTime) * (0.45 * SONG.speed[SongLoad.curDiff]));
+
+ // removing this so whether the note misses or not is entirely up to Note class
+ // var noteMiss:Bool = daNote.y < -daNote.height;
+
+ // if (PreferencesMenu.getPref('downscroll'))
+ // noteMiss = daNote.y > FlxG.height;
+
+ if (daNote.isSustainNote && daNote.wasGoodHit)
+ {
+ if ((!PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height)
+ || (PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height))
+ {
+ daNote.active = false;
+ daNote.visible = false;
+
+ daNote.kill();
+ activeNotes.remove(daNote, true);
+ daNote.destroy();
+ }
+ }
+ if (daNote.wasGoodHit)
+ {
+ daNote.active = false;
+ daNote.visible = false;
+
+ daNote.kill();
+ activeNotes.remove(daNote, true);
+ daNote.destroy();
+ }
+
+ if (daNote.tooLate)
+ {
+ noteMiss(daNote);
+ }
+ });
+ }
+
+ if (songEvents != null && songEvents.length > 0)
+ {
+ var songEventsToActivate:Array = SongEventParser.queryEvents(songEvents, Conductor.songPosition);
+
+ if (songEventsToActivate.length > 0)
+ {
+ trace('Found ${songEventsToActivate.length} event(s) to activate.');
+ for (event in songEventsToActivate)
+ {
+ SongEventParser.handleEvent(event);
+ }
+ }
+ }
+
+ if (!isInCutscene)
+ keyShit(true);
+ }
+
+ function applyClipRect(daNote:Note):Void
+ {
+ // clipRect is applied to graphic itself so use frame Heights
+ var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight);
+ var strumLineMid = playerStrumline.y + Note.swagWidth / 2;
+
+ if (PreferencesMenu.getPref('downscroll'))
+ {
+ swagRect.height = (strumLineMid - daNote.y) / daNote.scale.y;
+ swagRect.y = daNote.frameHeight - swagRect.height;
+ }
+ else
+ {
+ swagRect.y = (strumLineMid - daNote.y) / daNote.scale.y;
+ swagRect.height -= swagRect.y;
+ }
+
+ daNote.clipRect = swagRect;
+ }
+
+ function killCombo():Void
+ {
+ // Girlfriend gets sad if you combo break after hitting 5 notes.
+ if (currentStage != null && currentStage.getGirlfriend() != null)
+ if (Highscore.tallies.combo > 5 && currentStage.getGirlfriend().hasAnimation('sad'))
+ currentStage.getGirlfriend().playAnimation('sad');
+
+ if (Highscore.tallies.combo != 0)
+ {
+ Highscore.tallies.combo = comboPopUps.displayCombo(0);
+ }
+ }
+
+ #if debug
+ /**
+ * Jumps forward or backward a number of sections in the song.
+ * Accounts for BPM changes, does not prevent death from skipped notes.
+ * @param sec
+ */
+ function changeSection(sec:Int):Void
+ {
+ FlxG.sound.music.pause();
+
+ var daBPM:Float = currentSong.bpm;
+ var daPos:Float = 0;
+ for (i in 0...(Std.int(Conductor.currentStep / 16 + sec)))
+ {
+ var section = SongLoad.getSong()[i];
+ if (section == null)
+ continue;
+ if (section.changeBPM)
+ {
+ daBPM = SongLoad.getSong()[i].bpm;
+ }
+ daPos += 4 * (1000 * 60 / daBPM);
+ }
+ Conductor.songPosition = FlxG.sound.music.time = daPos;
+ Conductor.songPosition += Conductor.offset;
+ resyncVocals();
+ }
+ #end
+
+ function endSong():Void
+ {
+ dispatchEvent(new ScriptEvent(ScriptEvent.SONG_END));
+
+ seenCutscene = false;
+ deathCounter = 0;
+ mayPauseGame = false;
+ FlxG.sound.music.volume = 0;
+ vocals.volume = 0;
+ if (currentSong != null && currentSong.validScore)
+ {
+ // crackhead double thingie, sets whether was new highscore, AND saves the song!
+ Highscore.tallies.isNewHighscore = Highscore.saveScore(currentSong.song, songScore, storyDifficulty);
+
+ Highscore.saveCompletion(currentSong.song, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, storyDifficulty);
+ }
+
+ if (isStoryMode)
+ {
+ campaignScore += songScore;
+
+ storyPlaylist.remove(storyPlaylist[0]);
+
+ if (storyPlaylist.length <= 0)
+ {
+ FlxG.sound.playMusic(Paths.music('freakyMenu'));
+
+ transIn = FlxTransitionableState.defaultTransIn;
+ transOut = FlxTransitionableState.defaultTransOut;
+
+ switch (storyWeek)
+ {
+ case 7:
+ FlxG.switchState(new VideoState());
+ default:
+ FlxG.switchState(new StoryMenuState());
+ }
+
+ // if ()
+ StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true;
+
+ if (currentSong.validScore)
+ {
+ NGio.unlockMedal(60961);
+ Highscore.saveWeekScore(storyWeek, campaignScore, storyDifficulty);
+ }
+
+ FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
+ FlxG.save.flush();
+ }
+ else
+ {
+ var difficulty:String = "";
+
+ if (storyDifficulty == 0)
+ difficulty = '-easy';
+
+ if (storyDifficulty == 2)
+ difficulty = '-hard';
+
+ trace('LOADING NEXT SONG');
+ trace(storyPlaylist[0].toLowerCase() + difficulty);
+
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+
+ FlxG.sound.music.stop();
+ vocals.stop();
+
+ if (currentSong.song.toLowerCase() == 'eggnog')
+ {
+ var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
+ -FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
+ blackShit.scrollFactor.set();
+ add(blackShit);
+ camHUD.visible = false;
+ isInCutscene = true;
+
+ FlxG.sound.play(Paths.sound('Lights_Shut_off'), function()
+ {
+ // no camFollow so it centers on horror tree
+ currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]);
+ LoadingState.loadAndSwitchState(new PlayState());
+ });
+ }
+ else
+ {
+ previousCameraFollowPoint = cameraFollowPoint;
+
+ currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]);
+ LoadingState.loadAndSwitchState(new PlayState());
+ }
+ }
+ }
+ else
+ {
+ trace('WENT TO RESULTS SCREEN!');
+ // unloadAssets();
+
+ camZooming = false;
+
+ FlxG.camera.follow(PlayState.instance.currentStage.getGirlfriend(), null, 0.05);
+ FlxG.camera.targetOffset.y -= 350;
+ FlxG.camera.targetOffset.x += 20;
+
+ FlxTween.tween(camHUD, {alpha: 0}, 0.6);
+
+ new FlxTimer().start(0.8, _ ->
+ {
+ currentStage.getGirlfriend().animation.play("cheer");
+
+ FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1, {
+ ease: FlxEase.expoIn,
+ onComplete: _ ->
+ {
+ persistentUpdate = false;
+ vocals.stop();
+ camHUD.alpha = 1;
+ var res:ResultState = new ResultState();
+ res.camera = camHUD;
+ openSubState(res);
+ }
+ });
+ });
+ // FlxG.switchState(new FreeplayState());
+ }
+ }
+
+ // gives score and pops up rating
+ private function popUpScore(strumtime:Float, daNote:Note):Void
+ {
+ var noteDiff:Float = Math.abs(strumtime - Conductor.songPosition);
+ // boyfriend.playAnimation('hey');
+ vocals.volume = 1;
+
+ var isSick:Bool = false;
+ var score = Scoring.scoreNote(noteDiff, PBOT1);
+ var daRating = Scoring.judgeNote(noteDiff, PBOT1);
+ var healthMulti:Float = daNote.lowStakes ? 0.002 : 0.033;
+
+ if (noteDiff > Note.HIT_WINDOW * Note.BAD_THRESHOLD)
+ {
+ healthMulti *= 0; // no health on shit note
+ daRating = 'shit';
+ Highscore.tallies.shit += 1;
+ score = 50;
+ }
+ else if (noteDiff > Note.HIT_WINDOW * Note.GOOD_THRESHOLD)
+ {
+ healthMulti *= 0.2;
+ daRating = 'bad';
+ Highscore.tallies.bad += 1;
+ }
+ else if (noteDiff > Note.HIT_WINDOW * Note.SICK_THRESHOLD)
+ {
+ healthMulti *= 0.78;
+ daRating = 'good';
+ Highscore.tallies.good += 1;
+ score = 200;
+ }
+ else
+ {
+ isSick = true;
+ }
+
+ health += healthMulti;
+ if (isSick)
+ {
+ Highscore.tallies.sick += 1;
+ var noteSplash:NoteSplash = grpNoteSplashes.recycle(NoteSplash);
+ noteSplash.setupNoteSplash(daNote.x, daNote.y, daNote.data.noteData);
+ // new NoteSplash(daNote.x, daNote.y, daNote.noteData);
+ grpNoteSplashes.add(noteSplash);
+ }
+ // Only add the score if you're not on practice mode
+ if (!isPracticeMode)
+ songScore += score;
+ comboPopUps.displayRating(daRating);
+ if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0)
+ comboPopUps.displayCombo(Highscore.tallies.combo);
+ }
+
+ /*
+ function controlCamera()
+ {
+ if (currentStage == null)
+ return;
+
+ switch (cameraFocusCharacter)
+ {
+ default: // null = No change
+ break;
+ case 0: // Boyfriend
+ var isFocusedOnBF = cameraFollowPoint.x == currentStage.getBoyfriend().cameraFocusPoint.x;
+ if (!isFocusedOnBF)
+ {
+ // Focus the camera on the player.
+ cameraFollowPoint.setPosition(currentStage.getBoyfriend().cameraFocusPoint.x, currentStage.getBoyfriend().cameraFocusPoint.y);
+ }
+ case 1: // Dad
+ var isFocusedOnDad = cameraFollowPoint.x == currentStage.getDad().cameraFocusPoint.x;
+ if (!isFocusedOnDad)
+ {
+ cameraFollowPoint.setPosition(currentStage.getDad().cameraFocusPoint.x, currentStage.getDad().cameraFocusPoint.y);
+ }
+ case 2: // Girlfriend
+ var isFocusedOnGF = cameraFollowPoint.x == currentStage.getGirlfriend().cameraFocusPoint.x;
+ if (!isFocusedOnGF)
+ {
+ cameraFollowPoint.setPosition(currentStage.getGirlfriend().cameraFocusPoint.x, currentStage.getGirlfriend().cameraFocusPoint.y);
+ }
+ }
+
+ /*
+ if (cameraRightSide && !isFocusedOnBF)
+ {
+ // Focus the camera on the player.
+ cameraFollowPoint.setPosition(currentStage.getBoyfriend().cameraFocusPoint.x, currentStage.getBoyfriend().cameraFocusPoint.y);
+
+ // TODO: Un-hardcode this.
+ if (currentSong.song.toLowerCase() == 'tutorial')
+ FlxTween.tween(FlxG.camera, {zoom: 1 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
+ }
+ else if (!cameraRightSide && !isFocusedOnDad)
+ {
+ // Focus the camera on the opponent.
+ cameraFollowPoint.setPosition(currentStage.getDad().cameraFocusPoint.x, currentStage.getDad().cameraFocusPoint.y);
+
+ // TODO: Un-hardcode this stuff.
+ if (currentStage.getDad().characterId == 'mom')
+ {
+ vocals.volume = 1;
+ }
+
+ if (currentSong.song.toLowerCase() == 'tutorial')
+ tweenCamIn();
+ }
+ */
+ // }
+
+ public function keyShit(test:Bool):Void
+ {
+ if (PlayState.instance == null)
+ return;
+
+ // control arrays, order L D R U
+ var holdArray:Array = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT];
+ var pressArray:Array = [
+ controls.NOTE_LEFT_P,
+ controls.NOTE_DOWN_P,
+ controls.NOTE_UP_P,
+ controls.NOTE_RIGHT_P
+ ];
+ var releaseArray:Array = [
+ controls.NOTE_LEFT_R,
+ controls.NOTE_DOWN_R,
+ controls.NOTE_UP_R,
+ controls.NOTE_RIGHT_R
+ ];
+ // HOLDS, check for sustain notes
+ if (holdArray.contains(true) && PlayState.instance.generatedMusic)
+ {
+ PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
+ {
+ if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData])
+ PlayState.instance.goodNoteHit(daNote);
+ });
+ }
+
+ // PRESSES, check for note hits
+ if (pressArray.contains(true) && PlayState.instance.generatedMusic)
+ {
+ Haptic.vibrate(100, 100);
+
+ PlayState.instance.currentStage.getBoyfriend().holdTimer = 0;
+
+ var possibleNotes:Array = []; // notes that can be hit
+ var directionList:Array = []; // directions that can be hit
+ var dumbNotes:Array = []; // notes to kill later
+
+ PlayState.instance.activeNotes.forEachAlive(function(daNote:Note)
+ {
+ if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit)
+ {
+ if (directionList.contains(daNote.data.noteData))
+ {
+ for (coolNote in possibleNotes)
+ {
+ if (coolNote.data.noteData == daNote.data.noteData && Math.abs(daNote.data.strumTime - coolNote.data.strumTime) < 10)
+ { // if it's the same note twice at < 10ms distance, just delete it
+ // EXCEPT u cant delete it in this loop cuz it fucks with the collection lol
+ dumbNotes.push(daNote);
+ break;
+ }
+ else if (coolNote.data.noteData == daNote.data.noteData && daNote.data.strumTime < coolNote.data.strumTime)
+ { // if daNote is earlier than existing note (coolNote), replace
+ possibleNotes.remove(coolNote);
+ possibleNotes.push(daNote);
+ break;
+ }
+ }
+ }
+ else
+ {
+ possibleNotes.push(daNote);
+ directionList.push(daNote.data.noteData);
+ }
+ }
+ });
+
+ for (note in dumbNotes)
+ {
+ FlxG.log.add("killing dumb ass note at " + note.data.strumTime);
+ note.kill();
+ PlayState.instance.activeNotes.remove(note, true);
+ note.destroy();
+ }
+
+ possibleNotes.sort((a, b) -> Std.int(a.data.strumTime - b.data.strumTime));
+
+ if (PlayState.instance.perfectMode)
+ PlayState.instance.goodNoteHit(possibleNotes[0]);
+ else if (possibleNotes.length > 0)
+ {
+ for (shit in 0...pressArray.length)
+ { // if a direction is hit that shouldn't be
+ if (pressArray[shit] && !directionList.contains(shit))
+ PlayState.instance.ghostNoteMiss(shit);
+ }
+ for (coolNote in possibleNotes)
+ {
+ if (pressArray[coolNote.data.noteData])
+ PlayState.instance.goodNoteHit(coolNote);
+ }
+ }
+ else
+ {
+ // HNGGG I really want to add an option for ghost tapping
+ // L + ratio
+ for (shit in 0...pressArray.length)
+ if (pressArray[shit])
+ PlayState.instance.ghostNoteMiss(shit, false);
+ }
+ }
+
+ if (PlayState.instance == null || PlayState.instance.currentStage == null)
+ return;
+
+ for (keyId => isPressed in pressArray)
+ {
+ if (playerStrumline == null)
+ continue;
+ var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId);
+
+ if (isPressed && arrow.animation.curAnim.name != 'confirm')
+ {
+ arrow.playAnimation('pressed');
+ }
+ if (!holdArray[keyId])
+ {
+ arrow.playAnimation('static');
+ }
+ }
+ }
+
+ /**
+ * Called when a player presses a key with no note present.
+ * Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
+ * or even cancel the event entirely.
+ *
+ * @param direction
+ * @param hasPossibleNotes
+ */
+ function ghostNoteMiss(direction:funkin.noteStuff.NoteBasic.NoteType = 1, hasPossibleNotes:Bool = true):Void
+ {
+ var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
+ hasPossibleNotes, // Whether there was a note you could have hit.
+ - 0.035 * 2, // How much health to add (negative).
+ - 10 // Amount of score to add (negative).
+ );
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() skips animations and penalties. Neat!
+ if (event.eventCanceled)
+ return;
+
+ health += event.healthChange;
+
+ if (!isPracticeMode)
+ songScore += event.scoreChange;
+
+ if (event.playSound)
+ {
+ vocals.volume = 0;
+ FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
+ }
+ }
+
+ function noteMiss(note:Note):Void
+ {
+ // a MISS is when you let a note scroll past you!!
+ Highscore.tallies.missed++;
+
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, Highscore.tallies.combo, true);
+ dispatchEvent(event);
+ // Calling event.cancelEvent() skips all the other logic! Neat!
+ if (event.eventCanceled)
+ return;
+
+ health -= 0.0775;
+ if (!isPracticeMode)
+ songScore -= 10;
+ vocals.volume = 0;
+
+ if (Highscore.tallies.combo != 0)
+ {
+ Highscore.tallies.combo = comboPopUps.displayCombo(0);
+ }
+
+ note.active = false;
+ note.visible = false;
+
+ note.kill();
+ activeNotes.remove(note, true);
+ note.destroy();
+ }
+
+ function goodNoteHit(note:Note):Void
+ {
+ if (!note.wasGoodHit)
+ {
+ var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true);
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() skips all the other logic! Neat!
+ if (event.eventCanceled)
+ return;
+
+ if (!note.isSustainNote)
+ {
+ Highscore.tallies.combo++;
+ Highscore.tallies.totalNotesHit++;
+
+ if (Highscore.tallies.combo > Highscore.tallies.maxCombo)
+ Highscore.tallies.maxCombo = Highscore.tallies.combo;
+
+ popUpScore(note.data.strumTime, note);
+ }
+
+ playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true);
+
+ note.wasGoodHit = true;
+ vocals.volume = 1;
+
+ if (!note.isSustainNote)
+ {
+ note.kill();
+ activeNotes.remove(note, true);
+ note.destroy();
+ }
+ }
+ }
+
+ override function stepHit():Bool
+ {
+ if (SongLoad.songData == null)
+ return false;
+
+ // super.stepHit() returns false if a module cancelled the event.
+ if (!super.stepHit())
+ return false;
+
+ if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20
+ || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 20)
+ {
+ resyncVocals();
+ }
+
+ if (iconP1 != null)
+ iconP1.onStepHit(Std.int(Conductor.currentStep));
+ if (iconP2 != null)
+ iconP2.onStepHit(Std.int(Conductor.currentStep));
+
+ return true;
+ }
+
+ override function beatHit():Bool
+ {
+ // super.beatHit() returns false if a module cancelled the event.
+ if (!super.beatHit())
+ return false;
+
+ if (generatedMusic)
+ {
+ // TODO: Sort more efficiently, or less often, to improve performance.
+ activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
+ }
+
+ // Moving this code into the `beatHit` function allows for scripts and modules to control the camera better.
+ if (currentSong != null)
+ {
+ if (generatedMusic && SongLoad.getSong()[Std.int(Conductor.currentStep / 16)] != null)
+ {
+ // cameraRightSide = SongLoad.getSong()[Std.int(Conductor.currentStep / 16)].mustHitSection;
+ }
+
+ if (SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)] != null)
+ {
+ if (SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)].changeBPM)
+ {
+ Conductor.forceBPM(SongLoad.getSong()[Math.floor(Conductor.currentStep / 16)].bpm);
+ FlxG.log.add('CHANGED BPM!');
+ }
+ }
+ }
+
+ // Manage the camera focus, if necessary.
+ // controlCamera();
+
+ // HARDCODING FOR MILF ZOOMS!
+
+ if (PreferencesMenu.getPref('camera-zoom'))
+ {
+ if (currentSong != null
+ && currentSong.song.toLowerCase() == 'milf'
+ && Conductor.currentBeat >= 168
+ && Conductor.currentBeat < 200
+ && camZooming
+ && FlxG.camera.zoom < 1.35)
+ {
+ FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
+ camHUD.zoom += 0.03;
+ }
+
+ if (camZooming && FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom) && Conductor.currentBeat % 4 == 0)
+ {
+ FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom;
+ camHUD.zoom += 0.03;
+ }
+ }
+
+ // That combo counter that got spoiled that one time.
+ // Comes with NEAT visual and audio effects.
+
+ // bruh this var is bonkers i thot it was a function lmfaooo
+
+ // Break up into individual lines to aid debugging.
+
+ var shouldShowComboText:Bool = false;
+ if (currentSong != null)
+ {
+ shouldShowComboText = (Conductor.currentBeat % 8 == 7);
+ var daSection = SongLoad.getSong()[Std.int(Conductor.currentBeat / 16)];
+ shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
+ shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
+
+ var daNextSection = SongLoad.getSong()[Std.int(Conductor.currentBeat / 16) + 1];
+ var isEndOfSong = SongLoad.getSong().length < Std.int(Conductor.currentBeat / 16);
+ shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
+ }
+
+ if (shouldShowComboText)
+ {
+ var animShit:ComboCounter = new ComboCounter(-100, 300, Highscore.tallies.combo);
+ animShit.scrollFactor.set(0.6, 0.6);
+ animShit.cameras = [camHUD];
+ add(animShit);
+
+ var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
+
+ new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr)
+ {
+ animShit.forceFinish();
+ });
+ }
+
+ // Make the characters dance on the beat
+ danceOnBeat();
+
+ return true;
+ }
+
+ /**
+ * Handles characters dancing to the beat of the current song.
+ *
+ * TODO: Move some of this logic into `Bopper.hx`
+ */
+ public function danceOnBeat()
+ {
+ if (currentStage == null)
+ return;
+
+ // TODO: Move this to a song event.
+ if (Conductor.currentBeat % 16 == 15 // && currentSong.song == 'Tutorial'
+ && currentStage.getDad().characterId == 'gf'
+ && Conductor.currentBeat > 16
+ && Conductor.currentBeat < 48)
+ {
+ currentStage.getBoyfriend().playAnimation('hey', true);
+ currentStage.getDad().playAnimation('cheer', true);
+ }
+ }
+
+ /**
+ * Constructs the strumlines for each player.
+ */
+ function buildStrumlines():Void
+ {
+ var strumlineStyle:StrumlineStyle = NORMAL;
+
+ // TODO: Put this in the chart or something?
+ switch (currentStageId)
+ {
+ case 'school':
+ strumlineStyle = PIXEL;
+ case 'schoolEvil':
+ strumlineStyle = PIXEL;
+ }
+
+ var strumlineYPos = Strumline.getYPos();
+
+ playerStrumline = new Strumline(0, strumlineStyle, 4);
+ playerStrumline.x = 50 + FlxG.width / 2;
+ playerStrumline.y = strumlineYPos;
+ // Set the z-index so they don't appear in front of notes.
+ playerStrumline.zIndex = 100;
+ add(playerStrumline);
+ playerStrumline.cameras = [camHUD];
+
+ if (!isStoryMode)
+ {
+ playerStrumline.fadeInArrows();
+ }
+
+ enemyStrumline = new Strumline(1, strumlineStyle, 4);
+ enemyStrumline.x = 50;
+ enemyStrumline.y = strumlineYPos;
+ // Set the z-index so they don't appear in front of notes.
+ enemyStrumline.zIndex = 100;
+ add(enemyStrumline);
+ enemyStrumline.cameras = [camHUD];
+
+ if (!isStoryMode)
+ {
+ enemyStrumline.fadeInArrows();
+ }
+
+ this.refresh();
+ }
+
+ /**
+ * Function called before opening a new substate.
+ * @param subState The substate to open.
+ */
+ override function openSubState(subState:FlxSubState)
+ {
+ // If there is a substate which requires the game to continue,
+ // then make this a condition.
+ var shouldPause = true;
+
+ if (shouldPause)
+ {
+ // Pause the music.
+ if (FlxG.sound.music != null)
+ {
+ FlxG.sound.music.pause();
+ if (vocals != null)
+ vocals.pause();
+ }
+
+ // Pause the countdown.
+ Countdown.pauseCountdown();
+ }
+
+ super.openSubState(subState);
+ }
+
+ /**
+ * Function called before closing the current substate.
+ * @param subState
+ */
+ override function closeSubState()
+ {
+ if (isGamePaused)
+ {
+ var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true);
+
+ dispatchEvent(event);
+
+ if (event.eventCanceled)
+ return;
+
+ if (FlxG.sound.music != null && !startingSong && !isInCutscene)
+ resyncVocals();
+
+ // Resume the countdown.
+ Countdown.resumeCountdown();
+
+ #if discord_rpc
+ if (startTimer.finished)
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength - Conductor.songPosition);
+ else
+ DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC);
+ #end
+ }
+
+ super.closeSubState();
+ }
+
+ /**
+ * Prepares to start the countdown.
+ * Ends any running cutscenes, creates the strumlines, and starts the countdown.
+ */
+ function startCountdown():Void
+ {
+ var result = Countdown.performCountdown(currentStageId.startsWith('school'));
+ if (!result)
+ return;
+
+ isInCutscene = false;
+ camHUD.visible = true;
+ talking = false;
+
+ buildStrumlines();
+ }
+
+ override function dispatchEvent(event:ScriptEvent):Void
+ {
+ // ORDER: Module, Stage, Character, Song, Note
+ // Modules should get the first chance to cancel the event.
+
+ // super.dispatchEvent(event) dispatches event to module scripts.
+ super.dispatchEvent(event);
+
+ // Dispatch event to stage script.
+ ScriptEventDispatcher.callEvent(currentStage, event);
+
+ // Dispatch event to character script(s).
+ if (currentStage != null)
+ currentStage.dispatchToCharacters(event);
+
+ // TODO: Dispatch event to song script
+ }
+
+ /**
+ * Updates the position and contents of the score display.
+ */
+ function updateScoreText():Void
+ {
+ // TODO: Add functionality for modules to update the score text.
+ scoreText.text = "Score:" + songScore;
+ }
+
+ /**
+ * Updates the values of the health bar.
+ */
+ function updateHealthBar():Void
+ {
+ healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+ }
+
+ /**
+ * Resets the camera's zoom level and focus point.
+ */
+ public function resetCamera():Void
+ {
+ FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
+ FlxG.camera.targetOffset.set();
+ FlxG.camera.zoom = defaultCameraZoom;
+ FlxG.camera.focusOn(cameraFollowPoint.getPosition());
+ }
+
+ /**
+ * Perform necessary cleanup before leaving the PlayState.
+ */
+ function performCleanup()
+ {
+ // Uncache the song.
+ if (currentChart != null) {}
+ else if (currentSong != null)
+ {
+ openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song));
+ openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song));
+ }
+
+ // Remove reference to stage and remove sprites from it to save memory.
+ if (currentStage != null)
+ {
+ remove(currentStage);
+ currentStage.kill();
+ dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false));
+ currentStage = null;
+ }
+
+ GameOverSubstate.reset();
+
+ // Clear the static reference to this state.
+ instance = null;
+ }
+
+ /**
+ * This function is called whenever Flixel switches switching to a new FlxState.
+ * @return Whether to actually switch to the new state.
+ */
+ override function switchTo(nextState:FlxState):Bool
+ {
+ var result = super.switchTo(nextState);
+
+ if (result)
+ {
+ performCleanup();
+ }
+
+ return result;
+ }
}
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 2eaf4f944..7dcbf9cf6 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -18,589 +18,589 @@ import openfl.utils.Assets;
class CharacterDataParser
{
- /**
- * The current version string for the stage data format.
- * Handle breaking changes by incrementing this value
- * and adding migration to the `migrateStageData()` function.
- */
- public static final CHARACTER_DATA_VERSION:String = "1.0.0";
+ /**
+ * The current version string for the stage data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateStageData()` function.
+ */
+ public static final CHARACTER_DATA_VERSION:String = "1.0.0";
- /**
- * The current version rule check for the stage data format.
- */
- public static final CHARACTER_DATA_VERSION_RULE:String = "1.0.x";
+ /**
+ * The current version rule check for the stage data format.
+ */
+ public static final CHARACTER_DATA_VERSION_RULE:String = "1.0.x";
- static final characterCache:Map = new Map();
- static final characterScriptedClass:Map = new Map();
+ static final characterCache:Map = new Map();
+ static final characterScriptedClass:Map = new Map();
- static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
+ static final DEFAULT_CHAR_ID:String = 'UNKNOWN';
- /**
- * Parses and preloads the game's stage data and scripts when the game starts.
- *
- * If you want to force stages to be reloaded, you can just call this function again.
- */
- public static function loadCharacterCache():Void
- {
- // Clear any stages that are cached if there were any.
- clearCharacterCache();
- trace("[CHARDATA] Loading character cache...");
+ /**
+ * Parses and preloads the game's stage data and scripts when the game starts.
+ *
+ * If you want to force stages to be reloaded, you can just call this function again.
+ */
+ public static function loadCharacterCache():Void
+ {
+ // Clear any stages that are cached if there were any.
+ clearCharacterCache();
+ trace("Loading character cache...");
- //
- // UNSCRIPTED CHARACTERS
- //
- var charIdList:Array = DataAssets.listDataFilesInPath('characters/');
- var unscriptedCharIds:Array = charIdList.filter(function(charId:String):Bool
- {
- return !characterCache.exists(charId);
- });
- trace(' Fetching data for ${unscriptedCharIds.length} characters...');
- for (charId in unscriptedCharIds)
- {
- try
- {
- var charData:CharacterData = parseCharacterData(charId);
- if (charData != null)
- {
- trace(' Loaded character data: ${charId}');
- characterCache.set(charId, charData);
- }
- }
- catch (e)
- {
- // Assume error was already logged.
- continue;
- }
- }
+ //
+ // UNSCRIPTED CHARACTERS
+ //
+ var charIdList:Array = DataAssets.listDataFilesInPath('characters/');
+ var unscriptedCharIds:Array = charIdList.filter(function(charId:String):Bool
+ {
+ return !characterCache.exists(charId);
+ });
+ trace(' Fetching data for ${unscriptedCharIds.length} characters...');
+ for (charId in unscriptedCharIds)
+ {
+ try
+ {
+ var charData:CharacterData = parseCharacterData(charId);
+ if (charData != null)
+ {
+ trace(' Loaded character data: ${charId}');
+ characterCache.set(charId, charData);
+ }
+ }
+ catch (e)
+ {
+ // Assume error was already logged.
+ continue;
+ }
+ }
- //
- // SCRIPTED CHARACTERS
- //
+ //
+ // SCRIPTED CHARACTERS
+ //
- // Fuck I wish scripted classes supported static functions.
+ // Fuck I wish scripted classes supported static functions.
- var scriptedCharClassNames1:Array = ScriptedSparrowCharacter.listScriptClasses();
- if (scriptedCharClassNames1.length > 0)
- {
- trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
- for (charCls in scriptedCharClassNames1)
- {
- var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
- characterScriptedClass.set(character.characterId, charCls);
- }
- }
+ var scriptedCharClassNames1:Array = ScriptedSparrowCharacter.listScriptClasses();
+ if (scriptedCharClassNames1.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames1.length} (Sparrow) scripted characters...');
+ for (charCls in scriptedCharClassNames1)
+ {
+ var character = ScriptedSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
- var scriptedCharClassNames2:Array = ScriptedPackerCharacter.listScriptClasses();
- if (scriptedCharClassNames2.length > 0)
- {
- trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
- for (charCls in scriptedCharClassNames2)
- {
- var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
- characterScriptedClass.set(character.characterId, charCls);
- }
- }
+ var scriptedCharClassNames2:Array = ScriptedPackerCharacter.listScriptClasses();
+ if (scriptedCharClassNames2.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames2.length} (Packer) scripted characters...');
+ for (charCls in scriptedCharClassNames2)
+ {
+ var character = ScriptedPackerCharacter.init(charCls, DEFAULT_CHAR_ID);
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
- var scriptedCharClassNames3:Array = ScriptedMultiSparrowCharacter.listScriptClasses();
- if (scriptedCharClassNames3.length > 0)
- {
- trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
- for (charCls in scriptedCharClassNames3)
- {
- var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
- if (character == null)
- {
- trace(' Failed to instantiate scripted character: ${charCls}');
- continue;
- }
- characterScriptedClass.set(character.characterId, charCls);
- }
- }
+ var scriptedCharClassNames3:Array = ScriptedMultiSparrowCharacter.listScriptClasses();
+ if (scriptedCharClassNames3.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...');
+ for (charCls in scriptedCharClassNames3)
+ {
+ var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID);
+ if (character == null)
+ {
+ trace(' Failed to instantiate scripted character: ${charCls}');
+ continue;
+ }
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
- // NOTE: Only instantiate the ones not populated above.
- // ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
- var scriptedCharClassNames:Array = ScriptedBaseCharacter.listScriptClasses();
- scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool
- {
- return !(scriptedCharClassNames1.contains(charCls)
- || scriptedCharClassNames2.contains(charCls)
- || scriptedCharClassNames3.contains(charCls));
- });
+ // NOTE: Only instantiate the ones not populated above.
+ // ScriptedBaseCharacter.listScriptClasses() will pick up scripts extending the other classes.
+ var scriptedCharClassNames:Array = ScriptedBaseCharacter.listScriptClasses();
+ scriptedCharClassNames = scriptedCharClassNames.filter(function(charCls:String):Bool
+ {
+ return !(scriptedCharClassNames1.contains(charCls)
+ || scriptedCharClassNames2.contains(charCls)
+ || scriptedCharClassNames3.contains(charCls));
+ });
- if (scriptedCharClassNames.length > 0)
- {
- trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
- for (charCls in scriptedCharClassNames)
- {
- var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
- if (character == null)
- {
- trace(' Failed to instantiate scripted character: ${charCls}');
- continue;
- }
- else
- {
- trace(' Successfully instantiated scripted character: ${charCls}');
- characterScriptedClass.set(character.characterId, charCls);
- }
- }
- }
+ if (scriptedCharClassNames.length > 0)
+ {
+ trace(' Instantiating ${scriptedCharClassNames.length} (Base) scripted characters...');
+ for (charCls in scriptedCharClassNames)
+ {
+ var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID);
+ if (character == null)
+ {
+ trace(' Failed to instantiate scripted character: ${charCls}');
+ continue;
+ }
+ else
+ {
+ trace(' Successfully instantiated scripted character: ${charCls}');
+ characterScriptedClass.set(character.characterId, charCls);
+ }
+ }
+ }
- trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
- }
+ trace(' Successfully loaded ${Lambda.count(characterCache)} stages.');
+ }
- public static function fetchCharacter(charId:String):Null
- {
- if (charId == null || charId == '')
- {
- // Gracefully handle songs that don't use this character.
- return null;
- }
+ public static function fetchCharacter(charId:String):Null
+ {
+ if (charId == null || charId == '')
+ {
+ // Gracefully handle songs that don't use this character.
+ return null;
+ }
- if (characterCache.exists(charId))
- {
- var charData:CharacterData = characterCache.get(charId);
- var charScriptClass:String = characterScriptedClass.get(charId);
+ if (characterCache.exists(charId))
+ {
+ var charData:CharacterData = characterCache.get(charId);
+ var charScriptClass:String = characterScriptedClass.get(charId);
- var char:BaseCharacter;
+ var char:BaseCharacter;
- if (charScriptClass != null)
- {
- switch (charData.renderType)
- {
- case CharacterRenderType.MULTISPARROW:
- char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
- case CharacterRenderType.SPARROW:
- char = ScriptedSparrowCharacter.init(charScriptClass, charId);
- case CharacterRenderType.PACKER:
- char = ScriptedPackerCharacter.init(charScriptClass, charId);
- default:
- // We're going to assume that the script class does the rendering.
- char = ScriptedBaseCharacter.init(charScriptClass, charId);
- }
- }
- else
- {
- switch (charData.renderType)
- {
- case CharacterRenderType.MULTISPARROW:
- char = new MultiSparrowCharacter(charId);
- case CharacterRenderType.SPARROW:
- char = new SparrowCharacter(charId);
- case CharacterRenderType.PACKER:
- char = new PackerCharacter(charId);
- default:
- trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
- char = new BaseCharacter(charId);
- }
- }
+ if (charScriptClass != null)
+ {
+ switch (charData.renderType)
+ {
+ case CharacterRenderType.MULTISPARROW:
+ char = ScriptedMultiSparrowCharacter.init(charScriptClass, charId);
+ case CharacterRenderType.SPARROW:
+ char = ScriptedSparrowCharacter.init(charScriptClass, charId);
+ case CharacterRenderType.PACKER:
+ char = ScriptedPackerCharacter.init(charScriptClass, charId);
+ default:
+ // We're going to assume that the script class does the rendering.
+ char = ScriptedBaseCharacter.init(charScriptClass, charId);
+ }
+ }
+ else
+ {
+ switch (charData.renderType)
+ {
+ case CharacterRenderType.MULTISPARROW:
+ char = new MultiSparrowCharacter(charId);
+ case CharacterRenderType.SPARROW:
+ char = new SparrowCharacter(charId);
+ case CharacterRenderType.PACKER:
+ char = new PackerCharacter(charId);
+ default:
+ trace('[WARN] Creating character with undefined renderType ${charData.renderType}');
+ char = new BaseCharacter(charId);
+ }
+ }
- trace('[CHARDATA] Successfully instantiated character: ${charId}');
+ trace('Successfully instantiated character: ${charId}');
- // Call onCreate only in the fetchCharacter() function, not at application initialization.
- ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE));
+ // Call onCreate only in the fetchCharacter() function, not at application initialization.
+ ScriptEventDispatcher.callEvent(char, new ScriptEvent(ScriptEvent.CREATE));
- return char;
- }
- else
- {
- trace('[CHARDATA] Failed to build character, not found in cache: ${charId}');
- return null;
- }
- }
+ return char;
+ }
+ else
+ {
+ trace('Failed to build character, not found in cache: ${charId}');
+ return null;
+ }
+ }
- public static function fetchCharacterData(charId:String):Null
- {
- if (characterCache.exists(charId))
- {
- return characterCache.get(charId);
- }
- else
- {
- return null;
- }
- }
+ public static function fetchCharacterData(charId:String):Null
+ {
+ if (characterCache.exists(charId))
+ {
+ return characterCache.get(charId);
+ }
+ else
+ {
+ return null;
+ }
+ }
- public static function listCharacterIds():Array
- {
- return characterCache.keys().array();
- }
+ public static function listCharacterIds():Array
+ {
+ return characterCache.keys().array();
+ }
- static function clearCharacterCache():Void
- {
- if (characterCache != null)
- {
- characterCache.clear();
- }
- if (characterScriptedClass != null)
- {
- characterScriptedClass.clear();
- }
- }
+ static function clearCharacterCache():Void
+ {
+ if (characterCache != null)
+ {
+ characterCache.clear();
+ }
+ if (characterScriptedClass != null)
+ {
+ characterScriptedClass.clear();
+ }
+ }
- /**
- * Load a character's JSON file, parse its data, and return it.
- *
- * @param charId The character to load.
- * @return The character data, or null if validation failed.
- */
- public static function parseCharacterData(charId:String):Null
- {
- var rawJson:String = loadCharacterFile(charId);
+ /**
+ * Load a character's JSON file, parse its data, and return it.
+ *
+ * @param charId The character to load.
+ * @return The character data, or null if validation failed.
+ */
+ public static function parseCharacterData(charId:String):Null
+ {
+ var rawJson:String = loadCharacterFile(charId);
- var charData:CharacterData = migrateCharacterData(rawJson, charId);
+ var charData:CharacterData = migrateCharacterData(rawJson, charId);
- return validateCharacterData(charId, charData);
- }
+ return validateCharacterData(charId, charData);
+ }
- static function loadCharacterFile(charPath:String):String
- {
- var charFilePath:String = Paths.json('characters/${charPath}');
- var rawJson = Assets.getText(charFilePath).trim();
+ static function loadCharacterFile(charPath:String):String
+ {
+ var charFilePath:String = Paths.json('characters/${charPath}');
+ var rawJson = Assets.getText(charFilePath).trim();
- while (!StringTools.endsWith(rawJson, "}"))
- {
- rawJson = rawJson.substr(0, rawJson.length - 1);
- }
+ while (!StringTools.endsWith(rawJson, "}"))
+ {
+ rawJson = rawJson.substr(0, rawJson.length - 1);
+ }
- return rawJson;
- }
+ return rawJson;
+ }
- static function migrateCharacterData(rawJson:String, charId:String)
- {
- // If you update the character data format in a breaking way,
- // handle migration here by checking the `version` value.
+ static function migrateCharacterData(rawJson:String, charId:String)
+ {
+ // If you update the character data format in a breaking way,
+ // handle migration here by checking the `version` value.
- try
- {
- var charData:CharacterData = cast Json.parse(rawJson);
- return charData;
- }
- catch (e)
- {
- trace(' Error parsing data for character: ${charId}');
- trace(' ${e}');
- return null;
- }
- }
+ try
+ {
+ var charData:CharacterData = cast Json.parse(rawJson);
+ return charData;
+ }
+ catch (e)
+ {
+ trace(' Error parsing data for character: ${charId}');
+ trace(' ${e}');
+ return null;
+ }
+ }
- /**
- * The default time the character should sing for, in beats.
- * Values that are too low will cause the character to stop singing between notes.
- * Originally, this value was set to 1, but it was changed to 2 because that became
- * too low after some other code changes.
- */
- static final DEFAULT_SINGTIME:Float = 2.0;
+ /**
+ * The default time the character should sing for, in beats.
+ * Values that are too low will cause the character to stop singing between notes.
+ * Originally, this value was set to 1, but it was changed to 2 because that became
+ * too low after some other code changes.
+ */
+ static final DEFAULT_SINGTIME:Float = 2.0;
- static final DEFAULT_DANCEEVERY:Int = 1;
- static final DEFAULT_FLIPX:Bool = false;
- static final DEFAULT_FLIPY:Bool = false;
- static final DEFAULT_FRAMERATE:Int = 24;
- static final DEFAULT_ISPIXEL:Bool = false;
- static final DEFAULT_LOOP:Bool = false;
- static final DEFAULT_NAME:String = "Untitled Character";
- static final DEFAULT_OFFSETS:Array = [0, 0];
- static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25];
- static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
- static final DEFAULT_SCALE:Float = 1;
- static final DEFAULT_SCROLL:Array = [0, 0];
- static final DEFAULT_STARTINGANIM:String = "idle";
+ static final DEFAULT_DANCEEVERY:Int = 1;
+ static final DEFAULT_FLIPX:Bool = false;
+ static final DEFAULT_FLIPY:Bool = false;
+ static final DEFAULT_FRAMERATE:Int = 24;
+ static final DEFAULT_ISPIXEL:Bool = false;
+ static final DEFAULT_LOOP:Bool = false;
+ static final DEFAULT_NAME:String = "Untitled Character";
+ static final DEFAULT_OFFSETS:Array = [0, 0];
+ static final DEFAULT_HEALTHICON_OFFSETS:Array = [0, 25];
+ static final DEFAULT_RENDERTYPE:CharacterRenderType = CharacterRenderType.SPARROW;
+ static final DEFAULT_SCALE:Float = 1;
+ static final DEFAULT_SCROLL:Array = [0, 0];
+ static final DEFAULT_STARTINGANIM:String = "idle";
- /**
- * Set unspecified parameters to their defaults.
- * If the parameter is mandatory, print an error message.
- * @param id
- * @param input
- * @return The validated character data
- */
- static function validateCharacterData(id:String, input:CharacterData):Null
- {
- if (input == null)
- {
- // trace('[CHARDATA] ERROR: Could not parse character data for "${id}".');
- return null;
- }
+ /**
+ * Set unspecified parameters to their defaults.
+ * If the parameter is mandatory, print an error message.
+ * @param id
+ * @param input
+ * @return The validated character data
+ */
+ static function validateCharacterData(id:String, input:CharacterData):Null
+ {
+ if (input == null)
+ {
+ // trace('ERROR: Could not parse character data for "${id}".');
+ return null;
+ }
- if (input.version == null)
- {
- trace('[CHARDATA] WARN: No semantic version specified for character data file "$id", assuming ${CHARACTER_DATA_VERSION}');
- input.version = CHARACTER_DATA_VERSION;
- }
+ if (input.version == null)
+ {
+ trace('WARN: No semantic version specified for character data file "$id", assuming ${CHARACTER_DATA_VERSION}');
+ input.version = CHARACTER_DATA_VERSION;
+ }
- if (!VersionUtil.validateVersion(input.version, CHARACTER_DATA_VERSION_RULE))
- {
- trace('[CHARDATA] ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
- return null;
- }
+ if (!VersionUtil.validateVersion(input.version, CHARACTER_DATA_VERSION_RULE))
+ {
+ trace('ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
+ return null;
+ }
- if (input.name == null)
- {
- trace('[CHARDATA] WARN: Character data for "$id" missing name');
- input.name = DEFAULT_NAME;
- }
+ if (input.name == null)
+ {
+ trace('WARN: Character data for "$id" missing name');
+ input.name = DEFAULT_NAME;
+ }
- if (input.renderType == null)
- {
- input.renderType = DEFAULT_RENDERTYPE;
- }
+ if (input.renderType == null)
+ {
+ input.renderType = DEFAULT_RENDERTYPE;
+ }
- if (input.assetPath == null)
- {
- trace('[CHARDATA] ERROR: Could not load character data for "$id": missing assetPath');
- return null;
- }
+ if (input.assetPath == null)
+ {
+ trace('ERROR: Could not load character data for "$id": missing assetPath');
+ return null;
+ }
- if (input.offsets == null)
- {
- input.offsets = DEFAULT_OFFSETS;
- }
+ if (input.offsets == null)
+ {
+ input.offsets = DEFAULT_OFFSETS;
+ }
- if (input.cameraOffsets == null)
- {
- input.cameraOffsets = DEFAULT_OFFSETS;
- }
+ if (input.cameraOffsets == null)
+ {
+ input.cameraOffsets = DEFAULT_OFFSETS;
+ }
- if (input.healthIcon == null)
- {
- input.healthIcon = {
- id: null,
- scale: null,
- flipX: null,
- offsets: null
- };
- }
+ if (input.healthIcon == null)
+ {
+ input.healthIcon = {
+ id: null,
+ scale: null,
+ flipX: null,
+ offsets: null
+ };
+ }
- if (input.healthIcon.id == null)
- {
- input.healthIcon.id = id;
- }
+ if (input.healthIcon.id == null)
+ {
+ input.healthIcon.id = id;
+ }
- if (input.healthIcon.scale == null)
- {
- input.healthIcon.scale = DEFAULT_SCALE;
- }
+ if (input.healthIcon.scale == null)
+ {
+ input.healthIcon.scale = DEFAULT_SCALE;
+ }
- if (input.healthIcon.flipX == null)
- {
- input.healthIcon.flipX = DEFAULT_FLIPX;
- }
+ if (input.healthIcon.flipX == null)
+ {
+ input.healthIcon.flipX = DEFAULT_FLIPX;
+ }
- if (input.healthIcon.offsets == null)
- {
- input.healthIcon.offsets = DEFAULT_OFFSETS;
- }
+ if (input.healthIcon.offsets == null)
+ {
+ input.healthIcon.offsets = DEFAULT_OFFSETS;
+ }
- if (input.startingAnimation == null)
- {
- input.startingAnimation = DEFAULT_STARTINGANIM;
- }
+ if (input.startingAnimation == null)
+ {
+ input.startingAnimation = DEFAULT_STARTINGANIM;
+ }
- if (input.scale == null)
- {
- input.scale = DEFAULT_SCALE;
- }
+ if (input.scale == null)
+ {
+ input.scale = DEFAULT_SCALE;
+ }
- if (input.isPixel == null)
- {
- input.isPixel = DEFAULT_ISPIXEL;
- }
+ if (input.isPixel == null)
+ {
+ input.isPixel = DEFAULT_ISPIXEL;
+ }
- if (input.danceEvery == null)
- {
- input.danceEvery = DEFAULT_DANCEEVERY;
- }
+ if (input.danceEvery == null)
+ {
+ input.danceEvery = DEFAULT_DANCEEVERY;
+ }
- if (input.singTime == null)
- {
- input.singTime = DEFAULT_SINGTIME;
- }
+ if (input.singTime == null)
+ {
+ input.singTime = DEFAULT_SINGTIME;
+ }
- if (input.animations == null || input.animations.length == 0)
- {
- trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animations');
- input.animations = [];
- }
+ if (input.animations == null || input.animations.length == 0)
+ {
+ trace('ERROR: Could not load character data for "$id": missing animations');
+ input.animations = [];
+ }
- if (input.flipX == null)
- {
- input.flipX = DEFAULT_FLIPX;
- }
+ if (input.flipX == null)
+ {
+ input.flipX = DEFAULT_FLIPX;
+ }
- if (input.animations.length == 0 && input.startingAnimation != null)
- {
- return null;
- }
+ if (input.animations.length == 0 && input.startingAnimation != null)
+ {
+ return null;
+ }
- for (inputAnimation in input.animations)
- {
- if (inputAnimation.name == null)
- {
- trace('[CHARDATA] ERROR: Could not load character data for "$id": missing animation name for prop "${input.name}"');
- return null;
- }
+ for (inputAnimation in input.animations)
+ {
+ if (inputAnimation.name == null)
+ {
+ trace('ERROR: Could not load character data for "$id": missing animation name for prop "${input.name}"');
+ return null;
+ }
- if (inputAnimation.frameRate == null)
- {
- inputAnimation.frameRate = DEFAULT_FRAMERATE;
- }
+ if (inputAnimation.frameRate == null)
+ {
+ inputAnimation.frameRate = DEFAULT_FRAMERATE;
+ }
- if (inputAnimation.offsets == null)
- {
- inputAnimation.offsets = DEFAULT_OFFSETS;
- }
+ if (inputAnimation.offsets == null)
+ {
+ inputAnimation.offsets = DEFAULT_OFFSETS;
+ }
- if (inputAnimation.looped == null)
- {
- inputAnimation.looped = DEFAULT_LOOP;
- }
+ if (inputAnimation.looped == null)
+ {
+ inputAnimation.looped = DEFAULT_LOOP;
+ }
- if (inputAnimation.flipX == null)
- {
- inputAnimation.flipX = DEFAULT_FLIPX;
- }
+ if (inputAnimation.flipX == null)
+ {
+ inputAnimation.flipX = DEFAULT_FLIPX;
+ }
- if (inputAnimation.flipY == null)
- {
- inputAnimation.flipY = DEFAULT_FLIPY;
- }
- }
+ if (inputAnimation.flipY == null)
+ {
+ inputAnimation.flipY = DEFAULT_FLIPY;
+ }
+ }
- // All good!
- return input;
- }
+ // All good!
+ return input;
+ }
}
enum abstract CharacterRenderType(String) from String to String
{
- var SPARROW = 'sparrow';
- var PACKER = 'packer';
- var MULTISPARROW = 'multisparrow';
- // TODO: FlxSpine?
- // https://api.haxeflixel.com/flixel/addons/editors/spine/FlxSpine.html
- // TODO: Aseprite?
- // https://lib.haxe.org/p/openfl-aseprite/
- // TODO: Animate?
- // https://lib.haxe.org/p/flxanimate
- // TODO: REDACTED
+ var SPARROW = 'sparrow';
+ var PACKER = 'packer';
+ var MULTISPARROW = 'multisparrow';
+ // TODO: FlxSpine?
+ // https://api.haxeflixel.com/flixel/addons/editors/spine/FlxSpine.html
+ // TODO: Aseprite?
+ // https://lib.haxe.org/p/openfl-aseprite/
+ // TODO: Animate?
+ // https://lib.haxe.org/p/flxanimate
+ // TODO: REDACTED
}
typedef CharacterData =
{
- /**
- * The sematic version number of the character data JSON format.
- */
- var version:String;
+ /**
+ * The sematic version number of the character data JSON format.
+ */
+ var version:String;
- /**
- * The readable name of the character.
- */
- var name:String;
+ /**
+ * The readable name of the character.
+ */
+ var name:String;
- /**
- * The type of rendering system to use for the character.
- * @default sparrow
- */
- var renderType:CharacterRenderType;
+ /**
+ * The type of rendering system to use for the character.
+ * @default sparrow
+ */
+ var renderType:CharacterRenderType;
- /**
- * Behavior varies by render type:
- * - SPARROW: Path to retrieve both the spritesheet and the XML data from.
- * - PACKER: Path to retrieve both the spritsheet and the TXT data from.
- */
- var assetPath:String;
+ /**
+ * Behavior varies by render type:
+ * - SPARROW: Path to retrieve both the spritesheet and the XML data from.
+ * - PACKER: Path to retrieve both the spritsheet and the TXT data from.
+ */
+ var assetPath:String;
- /**
- * The scale of the graphic as a float.
- * Pro tip: On pixel-art levels, save the sprites small and set this value to 6 or so to save memory.
- * @default 1
- */
- var scale:Null;
+ /**
+ * The scale of the graphic as a float.
+ * Pro tip: On pixel-art levels, save the sprites small and set this value to 6 or so to save memory.
+ * @default 1
+ */
+ var scale:Null;
- /**
- * Optional data about the health icon for the character.
- */
- var healthIcon:Null;
+ /**
+ * Optional data about the health icon for the character.
+ */
+ var healthIcon:Null;
- /**
- * The global offset to the character's position, in pixels.
- * @default [0, 0]
- */
- var offsets:Null>;
+ /**
+ * The global offset to the character's position, in pixels.
+ * @default [0, 0]
+ */
+ var offsets:Null>;
- /**
- * The amount to offset the camera by while focusing on this character.
- * Default value focuses on the character directly.
- * @default [0, 0]
- */
- var cameraOffsets:Array;
+ /**
+ * The amount to offset the camera by while focusing on this character.
+ * Default value focuses on the character directly.
+ * @default [0, 0]
+ */
+ var cameraOffsets:Array;
- /**
- * Setting this to true disables anti-aliasing for the character.
- * @default false
- */
- var isPixel:Null;
+ /**
+ * Setting this to true disables anti-aliasing for the character.
+ * @default false
+ */
+ var isPixel:Null;
- /**
- * The frequency at which the character will play its idle animation, in beats.
- * Increasing this number will make the character dance less often.
- *
- * @default 1
- */
- var danceEvery:Null;
+ /**
+ * The frequency at which the character will play its idle animation, in beats.
+ * Increasing this number will make the character dance less often.
+ *
+ * @default 1
+ */
+ var danceEvery:Null;
- /**
- * The minimum duration that a character will play a note animation for, in beats.
- * If this number is too low, you may see the character start playing the idle animation between notes.
- * If this number is too high, you may see the the character play the sing animation for too long after the notes are gone.
- *
- * Examples:
- * - Daddy Dearest uses a value of `1.525`.
- * @default 1.0
- */
- var singTime:Null;
+ /**
+ * The minimum duration that a character will play a note animation for, in beats.
+ * If this number is too low, you may see the character start playing the idle animation between notes.
+ * If this number is too high, you may see the the character play the sing animation for too long after the notes are gone.
+ *
+ * Examples:
+ * - Daddy Dearest uses a value of `1.525`.
+ * @default 1.0
+ */
+ var singTime:Null;
- /**
- * An optional array of animations which the character can play.
- */
- var animations:Array;
+ /**
+ * An optional array of animations which the character can play.
+ */
+ var animations:Array;
- /**
- * If animations are used, this is the name of the animation to play first.
- * @default idle
- */
- var startingAnimation:Null;
+ /**
+ * If animations are used, this is the name of the animation to play first.
+ * @default idle
+ */
+ var startingAnimation:Null;
- /**
- * Whether or not the whole ass sprite is flipped by default.
- * Useful for characters that could also be played (Pico)
- *
- * @default false
- */
- var flipX:Null;
+ /**
+ * Whether or not the whole ass sprite is flipped by default.
+ * Useful for characters that could also be played (Pico)
+ *
+ * @default false
+ */
+ var flipX:Null;
};
typedef HealthIconData =
{
- /**
- * The ID to use for the health icon.
- * @default The character's ID
- */
- var id:Null;
+ /**
+ * The ID to use for the health icon.
+ * @default The character's ID
+ */
+ var id:Null;
- /**
- * The scale of the health icon.
- */
- var scale:Null;
+ /**
+ * The scale of the health icon.
+ */
+ var scale:Null;
- /**
- * Whether to flip the health icon horizontally.
- * @default false
- */
- var flipX:Null;
+ /**
+ * Whether to flip the health icon horizontally.
+ * @default false
+ */
+ var flipX:Null;
- /**
- * The offset of the health icon, in pixels.
- * @default [0, 25]
- */
- var offsets:Null>;
+ /**
+ * The offset of the health icon, in pixels.
+ * @default [0, 25]
+ */
+ var offsets:Null>;
}
diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx
index 132845832..2b539d6e3 100644
--- a/source/funkin/play/character/MultiSparrowCharacter.hx
+++ b/source/funkin/play/character/MultiSparrowCharacter.hx
@@ -20,199 +20,199 @@ import funkin.util.assets.FlxAnimationUtil;
*/
class MultiSparrowCharacter extends BaseCharacter
{
- /**
- * The actual group which holds all spritesheets this character uses.
- */
- private var members:Map = new Map();
+ /**
+ * The actual group which holds all spritesheets this character uses.
+ */
+ private var members:Map = new Map();
- /**
- * A map between animation names and what frame collection the animation should use.
- */
- private var animAssetPath:Map = new Map();
+ /**
+ * A map between animation names and what frame collection the animation should use.
+ */
+ private var animAssetPath:Map = new Map();
- /**
- * The current frame collection being used.
- */
- private var activeMember:String;
+ /**
+ * The current frame collection being used.
+ */
+ private var activeMember:String;
- public function new(id:String)
- {
- super(id);
- }
+ public function new(id:String)
+ {
+ super(id);
+ }
- override function onCreate(event:ScriptEvent):Void
- {
- trace('Creating MULTI SPARROW CHARACTER: ' + this.characterId);
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating Multi-Sparrow character: ' + this.characterId);
- buildSprites();
- super.onCreate(event);
- }
+ buildSprites();
+ super.onCreate(event);
+ }
- function buildSprites()
- {
- buildSpritesheets();
- buildAnimations();
+ function buildSprites()
+ {
+ buildSpritesheets();
+ buildAnimations();
- if (_data.isPixel)
- {
- this.antialiasing = false;
- }
- else
- {
- this.antialiasing = true;
- }
- }
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
+ }
- function buildSpritesheets()
- {
- // Build the list of asset paths to use.
- // Ignore nulls and duplicates.
- var assetList = [_data.assetPath];
- for (anim in _data.animations)
- {
- if (anim.assetPath != null && !assetList.contains(anim.assetPath))
- {
- assetList.push(anim.assetPath);
- }
- animAssetPath.set(anim.name, anim.assetPath);
- }
+ function buildSpritesheets()
+ {
+ // Build the list of asset paths to use.
+ // Ignore nulls and duplicates.
+ var assetList = [_data.assetPath];
+ for (anim in _data.animations)
+ {
+ if (anim.assetPath != null && !assetList.contains(anim.assetPath))
+ {
+ assetList.push(anim.assetPath);
+ }
+ animAssetPath.set(anim.name, anim.assetPath);
+ }
- // Load the Sparrow atlas for each path and store them in the members map.
- for (asset in assetList)
- {
- var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
- // If we don't do this, the unused textures will be removed as soon as they're loaded.
+ // Load the Sparrow atlas for each path and store them in the members map.
+ for (asset in assetList)
+ {
+ var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
+ // If we don't do this, the unused textures will be removed as soon as they're loaded.
- if (texture == null)
- {
- trace('Multi-Sparrow atlas could not load texture: ${asset}');
- }
- else
- {
- trace('Adding multi-sparrow atlas: ${asset}');
- texture.parent.destroyOnNoUse = false;
- members.set(asset, texture);
- }
- }
+ if (texture == null)
+ {
+ trace('Multi-Sparrow atlas could not load texture: ${asset}');
+ }
+ else
+ {
+ trace('Adding multi-sparrow atlas: ${asset}');
+ texture.parent.destroyOnNoUse = false;
+ members.set(asset, texture);
+ }
+ }
- // Use the default frame collection to start.
- loadFramesByAssetPath(_data.assetPath);
- }
+ // Use the default frame collection to start.
+ loadFramesByAssetPath(_data.assetPath);
+ }
- /**
- * Replace this sprite's animation frames with the ones at this asset path.
- */
- function loadFramesByAssetPath(assetPath:String):Void
- {
- if (_data.assetPath == null)
- {
- trace('[ERROR] Multi-Sparrow character has no default asset path!');
- return;
- }
- if (assetPath == null)
- {
- // trace('Asset path is null, falling back to default. This is normal!');
- loadFramesByAssetPath(_data.assetPath);
- return;
- }
+ /**
+ * Replace this sprite's animation frames with the ones at this asset path.
+ */
+ function loadFramesByAssetPath(assetPath:String):Void
+ {
+ if (_data.assetPath == null)
+ {
+ trace('[ERROR] Multi-Sparrow character has no default asset path!');
+ return;
+ }
+ if (assetPath == null)
+ {
+ // trace('Asset path is null, falling back to default. This is normal!');
+ loadFramesByAssetPath(_data.assetPath);
+ return;
+ }
- if (this.activeMember == assetPath)
- {
- // trace('Already using this asset path: ${assetPath}');
- return;
- }
+ if (this.activeMember == assetPath)
+ {
+ // trace('Already using this asset path: ${assetPath}');
+ return;
+ }
- if (members.exists(assetPath))
- {
- // Switch to a new set of sprites.
- // trace('Loading frames from asset path: ${assetPath}');
- this.frames = members.get(assetPath);
- this.activeMember = assetPath;
- this.setScale(_data.scale);
- }
- else
- {
- trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
- }
- }
+ if (members.exists(assetPath))
+ {
+ // Switch to a new set of sprites.
+ // trace('Loading frames from asset path: ${assetPath}');
+ this.frames = members.get(assetPath);
+ this.activeMember = assetPath;
+ this.setScale(_data.scale);
+ }
+ else
+ {
+ trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
+ }
+ }
- /**
- * Replace this sprite's animation frames with the ones needed to play this animation.
- */
- function loadFramesByAnimName(animName)
- {
- if (animAssetPath.exists(animName))
- {
- loadFramesByAssetPath(animAssetPath.get(animName));
- }
- else
- {
- trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
- }
- }
+ /**
+ * Replace this sprite's animation frames with the ones needed to play this animation.
+ */
+ function loadFramesByAnimName(animName)
+ {
+ if (animAssetPath.exists(animName))
+ {
+ loadFramesByAssetPath(animAssetPath.get(animName));
+ }
+ else
+ {
+ trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
+ }
+ }
- function buildAnimations()
- {
- trace('[MULTISPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+ function buildAnimations()
+ {
+ trace('[MULTISPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
- // We need to swap to the proper frame collection before adding the animations, I think?
- for (anim in _data.animations)
- {
- loadFramesByAnimName(anim.name);
- FlxAnimationUtil.addAtlasAnimation(this, anim);
+ // We need to swap to the proper frame collection before adding the animations, I think?
+ for (anim in _data.animations)
+ {
+ loadFramesByAnimName(anim.name);
+ FlxAnimationUtil.addAtlasAnimation(this, anim);
- if (anim.offsets == null)
- {
- setAnimationOffsets(anim.name, 0, 0);
- }
- else
- {
- setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
- }
- }
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
- var animNames = this.animation.getNameList();
- trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
- }
+ var animNames = this.animation.getNameList();
+ trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
- public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
- {
- // Make sure we ignore other animations if we're currently playing a forced one,
- // unless we're forcing a new animation.
- if (!this.canPlayOtherAnims && !ignoreOther)
- return;
+ public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void
+ {
+ // Make sure we ignore other animations if we're currently playing a forced one,
+ // unless we're forcing a new animation.
+ if (!this.canPlayOtherAnims && !ignoreOther)
+ return;
- loadFramesByAnimName(name);
- super.playAnimation(name, restart, ignoreOther);
- }
+ loadFramesByAnimName(name);
+ super.playAnimation(name, restart, ignoreOther);
+ }
- override function set_frames(value:FlxFramesCollection):FlxFramesCollection
- {
- // DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
- // WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
- // if (animation != null)
- // {
- // animation.destroyAnimations();
- // }
+ override function set_frames(value:FlxFramesCollection):FlxFramesCollection
+ {
+ // DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
+ // WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
+ // if (animation != null)
+ // {
+ // animation.destroyAnimations();
+ // }
- if (value != null)
- {
- graphic = value.parent;
- this.frames = value;
- this.frame = value.getByIndex(0);
- this.numFrames = value.numFrames;
- resetHelpers();
- this.bakedRotationAngle = 0;
- this.animation.frameIndex = 0;
- graphicLoaded();
- }
- else
- {
- this.frames = null;
- this.frame = null;
- this.graphic = null;
- }
+ if (value != null)
+ {
+ graphic = value.parent;
+ this.frames = value;
+ this.frame = value.getByIndex(0);
+ this.numFrames = value.numFrames;
+ resetHelpers();
+ this.bakedRotationAngle = 0;
+ this.animation.frameIndex = 0;
+ graphicLoaded();
+ }
+ else
+ {
+ this.frames = null;
+ this.frame = null;
+ this.graphic = null;
+ }
- return this.frames;
- }
+ return this.frames;
+ }
}
diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx
index 91e44e9f2..00469964f 100644
--- a/source/funkin/play/character/PackerCharacter.hx
+++ b/source/funkin/play/character/PackerCharacter.hx
@@ -11,65 +11,65 @@ import funkin.play.character.BaseCharacter.CharacterType;
*/
class PackerCharacter extends BaseCharacter
{
- public function new(id:String)
- {
- super(id);
- }
+ public function new(id:String)
+ {
+ super(id);
+ }
- override function onCreate(event:ScriptEvent):Void
- {
- trace('Creating PACKER CHARACTER: ' + this.characterId);
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating Packer character: ' + this.characterId);
- loadSpritesheet();
- loadAnimations();
+ loadSpritesheet();
+ loadAnimations();
- super.onCreate(event);
- }
+ super.onCreate(event);
+ }
- function loadSpritesheet()
- {
- trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
+ function loadSpritesheet()
+ {
+ trace('[PACKERCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
- var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
- if (tex == null)
- {
- trace('Could not load Packer sprite: ${_data.assetPath}');
- return;
- }
+ var tex:FlxFramesCollection = Paths.getPackerAtlas(_data.assetPath, 'shared');
+ if (tex == null)
+ {
+ trace('Could not load Packer sprite: ${_data.assetPath}');
+ return;
+ }
- this.frames = tex;
+ this.frames = tex;
- if (_data.isPixel)
- {
- this.antialiasing = false;
- }
- else
- {
- this.antialiasing = true;
- }
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
- this.setScale(_data.scale);
- }
+ this.setScale(_data.scale);
+ }
- function loadAnimations()
- {
- trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+ function loadAnimations()
+ {
+ trace('[PACKERCHAR] Loading ${_data.animations.length} animations for ${characterId}');
- FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
+ FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
- for (anim in _data.animations)
- {
- if (anim.offsets == null)
- {
- setAnimationOffsets(anim.name, 0, 0);
- }
- else
- {
- setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
- }
- }
+ for (anim in _data.animations)
+ {
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
- var animNames = this.animation.getNameList();
- trace('[PACKERCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
- }
+ var animNames = this.animation.getNameList();
+ trace('[PACKERCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
}
diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx
index 927a4c764..e9e9cf423 100644
--- a/source/funkin/play/character/SparrowCharacter.hx
+++ b/source/funkin/play/character/SparrowCharacter.hx
@@ -13,65 +13,65 @@ import flixel.graphics.frames.FlxFramesCollection;
*/
class SparrowCharacter extends BaseCharacter
{
- public function new(id:String)
- {
- super(id);
- }
+ public function new(id:String)
+ {
+ super(id);
+ }
- override function onCreate(event:ScriptEvent):Void
- {
- trace('Creating SPARROW CHARACTER: ' + this.characterId);
+ override function onCreate(event:ScriptEvent):Void
+ {
+ trace('Creating Sparrow character: ' + this.characterId);
- loadSpritesheet();
- loadAnimations();
+ loadSpritesheet();
+ loadAnimations();
- super.onCreate(event);
- }
+ super.onCreate(event);
+ }
- function loadSpritesheet()
- {
- trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
+ function loadSpritesheet()
+ {
+ trace('[SPARROWCHAR] Loading spritesheet ${_data.assetPath} for ${characterId}');
- var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
- if (tex == null)
- {
- trace('Could not load Sparrow sprite: ${_data.assetPath}');
- return;
- }
+ var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath, 'shared');
+ if (tex == null)
+ {
+ trace('Could not load Sparrow sprite: ${_data.assetPath}');
+ return;
+ }
- this.frames = tex;
+ this.frames = tex;
- if (_data.isPixel)
- {
- this.antialiasing = false;
- }
- else
- {
- this.antialiasing = true;
- }
+ if (_data.isPixel)
+ {
+ this.antialiasing = false;
+ }
+ else
+ {
+ this.antialiasing = true;
+ }
- this.setScale(_data.scale);
- }
+ this.setScale(_data.scale);
+ }
- function loadAnimations()
- {
- trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+ function loadAnimations()
+ {
+ trace('[SPARROWCHAR] Loading ${_data.animations.length} animations for ${characterId}');
- FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
+ FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
- for (anim in _data.animations)
- {
- if (anim.offsets == null)
- {
- setAnimationOffsets(anim.name, 0, 0);
- }
- else
- {
- setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
- }
- }
+ for (anim in _data.animations)
+ {
+ if (anim.offsets == null)
+ {
+ setAnimationOffsets(anim.name, 0, 0);
+ }
+ else
+ {
+ setAnimationOffsets(anim.name, anim.offsets[0], anim.offsets[1]);
+ }
+ }
- var animNames = this.animation.getNameList();
- trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
- }
+ var animNames = this.animation.getNameList();
+ trace('[SPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}');
+ }
}
diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx
new file mode 100644
index 000000000..cc8f52164
--- /dev/null
+++ b/source/funkin/play/event/FocusCameraSongEvent.hx
@@ -0,0 +1,142 @@
+package funkin.play.event;
+
+import funkin.play.event.SongEvent;
+import funkin.play.song.SongData;
+
+/**
+ * This class represents a handler for a type of song event.
+ * It is used by the ScriptedSongEvent class to handle user-defined events.
+ *
+ * Example: Focus on Boyfriend:
+ * ```
+ * {
+ * "e": "FocusCamera",
+ * "v": {
+ * "char": 0,
+ * }
+ * }
+ * ```
+ *
+ * Example: Focus on 10px above Girlfriend:
+ * ```
+ * {
+ * "e": "FocusCamera",
+ * "v": {
+ * "char": 2,
+ * "y": -10,
+ * }
+ * }
+ * ```
+ *
+ * Example: Focus on (100, 100):
+ * ```
+ * {
+ * "e": "FocusCamera",
+ * "v": {
+ * "char": -1,
+ * "x": 100,
+ * "y": 100,
+ * }
+ * }
+ * ```
+ */
+class FocusCameraSongEvent extends SongEvent
+{
+ public function new()
+ {
+ super('FocusCamera');
+ }
+
+ public override function handleEvent(data:SongEventData)
+ {
+ // Does nothing if there is no PlayState camera or stage.
+ if (PlayState.instance == null || PlayState.instance.currentStage == null)
+ return;
+
+ var posX = data.getFloat('x');
+ if (posX == null)
+ posX = 0.0;
+ var posY = data.getFloat('y');
+ if (posY == null)
+ posY = 0.0;
+
+ var char = data.getInt('char');
+
+ if (char == null)
+ char = cast data.value;
+
+ switch (char)
+ {
+ case -1: // Position
+ trace('Focusing camera on static position.');
+ var xTarget = posX;
+ var yTarget = posY;
+
+ PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
+ case 0: // Boyfriend
+ // Focus the camera on the player.
+ trace('Focusing camera on player.');
+ var xTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x + posX;
+ var yTarget = PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y + posY;
+
+ PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
+ case 1: // Dad
+ // Focus the camera on the dad.
+ trace('Focusing camera on dad.');
+ var xTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.x + posX;
+ var yTarget = PlayState.instance.currentStage.getDad().cameraFocusPoint.y + posY;
+
+ PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
+ case 2: // Girlfriend
+ // Focus the camera on the girlfriend.
+ trace('Focusing camera on girlfriend.');
+ var xTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x + posX;
+ var yTarget = PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y + posY;
+
+ PlayState.instance.cameraFollowPoint.setPosition(xTarget, yTarget);
+ default:
+ trace('Unknown camera focus: ' + data);
+ }
+ }
+
+ public override function getTitle():String
+ {
+ return "Focus Camera";
+ }
+
+ /**
+ * ```
+ * {
+ * "char": ENUM, // Which character to point to
+ * "x": FLOAT, // Optional x offset
+ * "y": FLOAT, // Optional y offset
+ * }
+ * @return SongEventSchema
+ */
+ public override function getEventSchema():SongEventSchema
+ {
+ return [
+ {
+ name: "char",
+ title: "Character",
+ defaultValue: 0,
+ type: SongEventFieldType.ENUM,
+ keys: ["Position" => -1, "Boyfriend" => 0, "Dad" => 1, "Girlfriend" => 2]
+ },
+ {
+ name: "x",
+ title: "X Position",
+ defaultValue: 0,
+ step: 10.0,
+ type: SongEventFieldType.FLOAT,
+ },
+ {
+ name: "y",
+ title: "Y Position",
+ defaultValue: 0,
+ step: 10.0,
+ type: SongEventFieldType.FLOAT,
+ }
+ ];
+ }
+}
diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx
new file mode 100644
index 000000000..fcf246d57
--- /dev/null
+++ b/source/funkin/play/event/PlayAnimationSongEvent.hx
@@ -0,0 +1,111 @@
+package funkin.play.event;
+
+import flixel.FlxSprite;
+import funkin.play.character.BaseCharacter;
+import funkin.play.event.SongEvent;
+import funkin.play.song.SongData;
+
+class PlayAnimationSongEvent extends SongEvent
+{
+ public function new()
+ {
+ super('PlayAnimation');
+ }
+
+ public override function handleEvent(data:SongEventData)
+ {
+ // Does nothing if there is no PlayState camera or stage.
+ if (PlayState.instance == null || PlayState.instance.currentStage == null)
+ return;
+
+ var targetName = data.getString('target');
+ var anim = data.getString('anim');
+ var force = data.getBool('force');
+ if (force == null)
+ force = false;
+
+ var target:FlxSprite = null;
+
+ switch (targetName)
+ {
+ case 'boyfriend':
+ trace('Playing animation $anim on boyfriend.');
+ target = PlayState.instance.currentStage.getBoyfriend();
+ case 'bf':
+ trace('Playing animation $anim on boyfriend.');
+ target = PlayState.instance.currentStage.getBoyfriend();
+ case 'player':
+ trace('Playing animation $anim on boyfriend.');
+ target = PlayState.instance.currentStage.getBoyfriend();
+ case 'dad':
+ trace('Playing animation $anim on dad.');
+ target = PlayState.instance.currentStage.getDad();
+ case 'opponent':
+ trace('Playing animation $anim on dad.');
+ target = PlayState.instance.currentStage.getDad();
+ case 'girlfriend':
+ trace('Playing animation $anim on girlfriend.');
+ target = PlayState.instance.currentStage.getGirlfriend();
+ case 'gf':
+ trace('Playing animation $anim on girlfriend.');
+ target = PlayState.instance.currentStage.getGirlfriend();
+ default:
+ target = PlayState.instance.currentStage.getNamedProp(targetName);
+ if (target == null)
+ trace('Unknown animation target: $targetName');
+ else
+ trace('Fetched animation target $targetName from stage.');
+ }
+
+ if (target != null)
+ {
+ if (Std.isOfType(target, BaseCharacter))
+ {
+ var targetChar:BaseCharacter = cast target;
+ targetChar.playAnimation(anim, force, force);
+ }
+ else
+ {
+ target.animation.play(anim, force);
+ }
+ }
+ }
+
+ public override function getTitle():String
+ {
+ return "Play Animation";
+ }
+
+ /**
+ * ```
+ * {
+ * "target": STRING, // Name of character or prop to point to.
+ * "anim": STRING, // Name of animation to play.
+ * "force": BOOL, // Whether to force the animation to play.
+ * }
+ * @return SongEventSchema
+ */
+ public override function getEventSchema():SongEventSchema
+ {
+ return [
+ {
+ name: 'target',
+ title: 'Target',
+ type: SongEventFieldType.STRING,
+ defaultValue: 'boyfriend',
+ },
+ {
+ name: 'anim',
+ title: 'Animation',
+ type: SongEventFieldType.STRING,
+ defaultValue: 'idle',
+ },
+ {
+ name: 'force',
+ title: 'Force',
+ type: SongEventFieldType.BOOL,
+ defaultValue: false
+ }
+ ];
+ }
+}
diff --git a/source/funkin/play/event/ScriptedSongEvent.hx b/source/funkin/play/event/ScriptedSongEvent.hx
new file mode 100644
index 000000000..079e35110
--- /dev/null
+++ b/source/funkin/play/event/ScriptedSongEvent.hx
@@ -0,0 +1,9 @@
+package funkin.play.event;
+
+import funkin.play.song.Song;
+import polymod.hscript.HScriptedClass;
+
+@:hscriptClass
+class ScriptedSongEvent extends SongEvent implements HScriptedClass
+{
+}
diff --git a/source/funkin/play/event/SongEvent.hx b/source/funkin/play/event/SongEvent.hx
index 4c0e29575..a06cfb23a 100644
--- a/source/funkin/play/event/SongEvent.hx
+++ b/source/funkin/play/event/SongEvent.hx
@@ -1,303 +1,270 @@
package funkin.play.event;
-import flixel.FlxSprite;
-import funkin.play.PlayState;
-import funkin.play.character.BaseCharacter;
-import funkin.play.song.SongData.RawSongEventData;
-import haxe.DynamicAccess;
+import funkin.util.macro.ClassMacro;
+import funkin.play.song.SongData.SongEventData;
-typedef RawSongEvent =
+/**
+ * This class represents a handler for a type of song event.
+ * It is used by the ScriptedSongEvent class to handle user-defined events.
+ */
+class SongEvent
{
- > RawSongEventData,
+ public var id:String;
- /**
- * Whether the event has been activated or not.
- */
- var a:Bool;
+ public function new(id:String)
+ {
+ this.id = id;
+ }
+
+ public function handleEvent(data:SongEventData)
+ {
+ throw 'SongEvent.handleEvent() must be overridden!';
+ }
+
+ public function getEventSchema():SongEventSchema
+ {
+ return null;
+ }
+
+ public function getTitle():String
+ {
+ return this.id.toTitleCase();
+ }
+
+ public function toString():String
+ {
+ return 'SongEvent(${this.id})';
+ }
}
-@:forward
-abstract SongEvent(RawSongEvent)
+class SongEventParser
{
- public function new(time:Float, event:String, value:Dynamic = null)
- {
- this = {
- t: time,
- e: event,
- v: value,
- a: false
- };
- }
+ /**
+ * Every built-in event class must be added to this list.
+ * Thankfully, with the power of `SongEventMacro`, this is done automatically.
+ */
+ private static final BUILTIN_EVENTS:List> = ClassMacro.listSubclassesOf(SongEvent);
- public var time(get, set):Float;
+ /**
+ * Map of internal handlers for song events.
+ * These may be either `ScriptedSongEvents` or built-in classes extending `SongEvent`.
+ */
+ static final eventCache:Map = new Map();
- public function get_time():Float
- {
- return this.t;
- }
+ public static function loadEventCache():Void
+ {
+ clearEventCache();
- public function set_time(value:Float):Float
- {
- return this.t = value;
- }
+ //
+ // BASE GAME EVENTS
+ //
+ registerBaseEvents();
+ registerScriptedEvents();
+ }
- public var event(get, set):String;
+ static function registerBaseEvents()
+ {
+ trace('Instantiating ${BUILTIN_EVENTS.length} built-in song events...');
+ for (eventCls in BUILTIN_EVENTS)
+ {
+ var eventClsName:String = Type.getClassName(eventCls);
+ if (eventClsName == 'funkin.play.event.SongEvent' || eventClsName == 'funkin.play.event.ScriptedSongEvent')
+ continue;
- public function get_event():String
- {
- return this.e;
- }
+ var event:SongEvent = Type.createInstance(eventCls, ["UNKNOWN"]);
- public function set_event(value:String):String
- {
- return this.e = value;
- }
+ if (event != null)
+ {
+ trace(' Loaded built-in song event: (${event.id})');
+ eventCache.set(event.id, event);
+ }
+ else
+ {
+ trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
+ }
+ }
+ }
- public var value(get, set):Dynamic;
+ static function registerScriptedEvents()
+ {
+ var scriptedEventClassNames:Array = ScriptedSongEvent.listScriptClasses();
+ if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0)
+ return;
- public function get_value():Dynamic
- {
- return this.v;
- }
+ trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
+ for (eventCls in scriptedEventClassNames)
+ {
+ var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
- public function set_value(value:Dynamic):Dynamic
- {
- return this.v = value;
- }
+ if (event != null)
+ {
+ trace(' Loaded scripted song event: ${event.id}');
+ eventCache.set(event.id, event);
+ }
+ else
+ {
+ trace(' Failed to instantiate scripted song event class: ${eventCls}');
+ }
+ }
+ }
- public inline function getBool():Bool
- {
- return cast this.v;
- }
+ public static function listEventIds():Array
+ {
+ return eventCache.keys().array();
+ }
- public inline function getInt():Int
- {
- return cast this.v;
- }
+ public static function listEvents():Array
+ {
+ return eventCache.values();
+ }
- public inline function getFloat():Float
- {
- return cast this.v;
- }
+ public static function getEvent(id:String):SongEvent
+ {
+ return eventCache.get(id);
+ }
- public inline function getString():String
- {
- return cast this.v;
- }
+ public static function getEventSchema(id:String):SongEventSchema
+ {
+ var event:SongEvent = getEvent(id);
+ if (event == null)
+ return null;
- public inline function getArray():Array
- {
- return cast this.v;
- }
+ return event.getEventSchema();
+ }
- public inline function getMap():DynamicAccess
- {
- return cast this.v;
- }
+ static function clearEventCache()
+ {
+ eventCache.clear();
+ }
- public inline function getBoolArray():Array
- {
- return cast this.v;
- }
+ public static function handleEvent(data:SongEventData):Void
+ {
+ var eventType:String = data.event;
+ var eventHandler:SongEvent = eventCache.get(eventType);
+
+ if (eventHandler != null)
+ {
+ eventHandler.handleEvent(data);
+ }
+ else
+ {
+ trace('WARNING: No event handler for event with id: ${eventType}');
+ }
+
+ data.activated = true;
+ }
+
+ public static inline function handleEvents(events:Array):Void
+ {
+ for (event in events)
+ {
+ handleEvent(event);
+ }
+ }
+
+ /**
+ * Given a list of song events and the current timestamp,
+ * return a list of events that should be handled.
+ */
+ public static function queryEvents(events:Array, currentTime:Float):Array
+ {
+ return events.filter(function(event:SongEventData):Bool
+ {
+ // If the event is already activated, don't activate it again.
+ if (event.activated)
+ return false;
+
+ // If the event is in the future, don't activate it.
+ if (event.time > currentTime)
+ return false;
+
+ return true;
+ });
+ }
+
+ /**
+ * Reset activation of all the provided events.
+ */
+ public static function resetEvents(events:Array):Void
+ {
+ for (event in events)
+ {
+ event.activated = false;
+ // TODO: Add an onReset() method to SongEvent?
+ }
+ }
}
-typedef SongEventCallback = SongEvent->Void;
-
-class SongEventHandler
+enum abstract SongEventFieldType(String) from String to String
{
- private static final eventCallbacks:Map = new Map();
+ /**
+ * The STRING type will display as a text field.
+ */
+ var STRING = "string";
- public static function registerCallback(event:String, callback:SongEventCallback):Void
- {
- eventCallbacks.set(event, callback);
- }
+ /**
+ * The INTEGER type will display as a text field that only accepts numbers.
+ */
+ var INTEGER = "integer";
- public static function unregisterCallback(event:String):Void
- {
- eventCallbacks.remove(event);
- }
+ /**
+ * The FLOAT type will display as a text field that only accepts numbers.
+ */
+ var FLOAT = "float";
- public static function clearCallbacks():Void
- {
- eventCallbacks.clear();
- }
+ /**
+ * The BOOL type will display as a checkbox.
+ */
+ var BOOL = "bool";
- /**
- * Register each of the event callbacks provided by the base game.
- */
- public static function registerBaseEventCallbacks():Void
- {
- // TODO: Add a system for mods to easily add their own event callbacks.
- // Should be easy as creating character or stage scripts.
- registerCallback('FocusCamera', VanillaEventCallbacks.focusCamera);
- registerCallback('PlayAnimation', VanillaEventCallbacks.playAnimation);
- }
-
- /**
- * Given a list of song events and the current timestamp,
- * return a list of events that should be activated.
- */
- public static function queryEvents(events:Array, currentTime:Float):Array
- {
- return events.filter(function(event:SongEvent):Bool
- {
- // If the event is already activated, don't activate it again.
- if (event.a)
- return false;
-
- // If the event is in the future, don't activate it.
- if (event.time > currentTime)
- return false;
-
- return true;
- });
- }
-
- public static function activateEvents(events:Array):Void
- {
- for (event in events)
- {
- activateEvent(event);
- }
- }
-
- public static function activateEvent(event:SongEvent):Void
- {
- if (event.a)
- {
- trace('Event already activated: ' + event);
- return;
- }
-
- // Prevent the event from being activated again.
- event.a = true;
-
- // Perform the action.
- if (eventCallbacks.exists(event.event))
- {
- eventCallbacks.get(event.event)(event);
- }
- }
-
- public static function resetEvents(events:Array):Void
- {
- for (event in events)
- {
- resetEvent(event);
- }
- }
-
- public static function resetEvent(event:SongEvent):Void
- {
- // TODO: Add a system for mods to easily add their reset callbacks.
- event.a = false;
- }
+ /**
+ * The ENUM type will display as a dropdown.
+ * Make sure to specify the `keys` field in the schema.
+ */
+ var ENUM = "enum";
}
-class VanillaEventCallbacks
+typedef SongEventSchemaField =
{
- /**
- * Event Name: "FocusCamera"
- * Event Value: Int
- * 0: Focus on the player.
- * 1: Focus on the opponent.
- * 2: Focus on the girlfriend.
- */
- public static function focusCamera(event:SongEvent):Void
- {
- // Does nothing if there is no PlayState camera or stage.
- if (PlayState.instance == null || PlayState.instance.currentStage == null)
- return;
+ /**
+ * The name of the property as it should be saved in the event data.
+ */
+ name:String,
- switch (event.getInt())
- {
- case 0: // Boyfriend
- // Focus the camera on the player.
- trace('[EVENT] Focusing camera on player.');
- PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.x,
- PlayState.instance.currentStage.getBoyfriend().cameraFocusPoint.y);
- case 1: // Dad
- // Focus the camera on the dad.
- trace('[EVENT] Focusing camera on dad.');
- PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getDad().cameraFocusPoint.x,
- PlayState.instance.currentStage.getDad().cameraFocusPoint.y);
- case 2: // Girlfriend
- // Focus the camera on the girlfriend.
- trace('[EVENT] Focusing camera on girlfriend.');
- PlayState.instance.cameraFollowPoint.setPosition(PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.x,
- PlayState.instance.currentStage.getGirlfriend().cameraFocusPoint.y);
- default:
- trace('[EVENT] Unknown camera focus: ' + event.value);
- }
- }
+ /**
+ * The title of the field to display in the UI.
+ */
+ title:String,
- /**
- * Event Name: "playAnimation"
- * Event Value: Object
- * {
- * target: String, // "player", "dad", "girlfriend", or
- * animation: String,
- * force: Bool // optional
- * }
- */
- public static function playAnimation(event:SongEvent):Void
- {
- // Does nothing if there is no PlayState camera or stage.
- if (PlayState.instance == null || PlayState.instance.currentStage == null)
- return;
+ /**
+ * The type of the field.
+ */
+ type:SongEventFieldType,
- var data:Dynamic = event.value;
-
- var targetName:String = Reflect.field(data, 'target');
- var anim:String = Reflect.field(data, 'anim');
- var force:Null = Reflect.field(data, 'force');
- if (force == null)
- force = false;
-
- var target:FlxSprite = null;
-
- switch (targetName)
- {
- case 'boyfriend':
- trace('[EVENT] Playing animation $anim on boyfriend.');
- target = PlayState.instance.currentStage.getBoyfriend();
- case 'bf':
- trace('[EVENT] Playing animation $anim on boyfriend.');
- target = PlayState.instance.currentStage.getBoyfriend();
- case 'player':
- trace('[EVENT] Playing animation $anim on boyfriend.');
- target = PlayState.instance.currentStage.getBoyfriend();
- case 'dad':
- trace('[EVENT] Playing animation $anim on dad.');
- target = PlayState.instance.currentStage.getDad();
- case 'opponent':
- trace('[EVENT] Playing animation $anim on dad.');
- target = PlayState.instance.currentStage.getDad();
- case 'girlfriend':
- trace('[EVENT] Playing animation $anim on girlfriend.');
- target = PlayState.instance.currentStage.getGirlfriend();
- case 'gf':
- trace('[EVENT] Playing animation $anim on girlfriend.');
- target = PlayState.instance.currentStage.getGirlfriend();
- default:
- target = PlayState.instance.currentStage.getNamedProp(targetName);
- if (target == null)
- trace('[EVENT] Unknown animation target: $targetName');
- else
- trace('[EVENT] Fetched animation target $targetName from stage.');
- }
-
- if (target != null)
- {
- if (Std.isOfType(target, BaseCharacter))
- {
- var targetChar:BaseCharacter = cast target;
- targetChar.playAnimation(anim, force, force);
- }
- else
- {
- target.animation.play(anim, force);
- }
- }
- }
+ /**
+ * Used for ENUM values.
+ * The key is the display name and the value is the actual value.
+ */
+ ?keys:Map,
+ /**
+ * Used for INTEGER and FLOAT values.
+ * The minimum value that can be entered.
+ */
+ ?min:Float,
+ /**
+ * Used for INTEGER and FLOAT values.
+ * The maximum value that can be entered.
+ */
+ ?max:Float,
+ /**
+ * Used for INTEGER and FLOAT values.
+ * The step value that will be used when incrementing/decrementing the value.
+ */
+ ?step:Float,
+ /**
+ * An optional default value for the field.
+ */
+ ?defaultValue:Dynamic,
}
+
+typedef SongEventSchema = Array;
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 08ce6818f..f15f4dafb 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -22,239 +22,239 @@ import funkin.play.song.SongData.SongTimeFormat;
*/
class Song // implements IPlayStateScriptedClass
{
- public final songId:String;
+ public final songId:String;
- final _metadata:Array;
+ final _metadata:Array;
- final variations:Array;
- final difficulties:Map;
+ final variations:Array;
+ final difficulties:Map;
- public function new(id:String)
- {
- this.songId = id;
+ public function new(id:String)
+ {
+ this.songId = id;
- variations = [];
- difficulties = new Map();
+ variations = [];
+ difficulties = new Map();
- _metadata = SongDataParser.parseSongMetadata(songId);
- if (_metadata == null || _metadata.length == 0)
- {
- throw 'Could not find song data for songId: $songId';
- }
+ _metadata = SongDataParser.parseSongMetadata(songId);
+ if (_metadata == null || _metadata.length == 0)
+ {
+ throw 'Could not find song data for songId: $songId';
+ }
- populateFromMetadata();
- }
+ populateFromMetadata();
+ }
- public function getRawMetadata():Array
- {
- return _metadata;
- }
+ public function getRawMetadata():Array
+ {
+ return _metadata;
+ }
- /**
- * Populate the song data from the provided metadata,
- * including data from individual difficulties. Does not load chart data.
- */
- function populateFromMetadata():Void
- {
- // Variations may have different artist, time format, generatedBy, etc.
- for (metadata in _metadata)
- {
- for (diffId in metadata.playData.difficulties)
- {
- var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
+ /**
+ * Populate the song data from the provided metadata,
+ * including data from individual difficulties. Does not load chart data.
+ */
+ function populateFromMetadata():Void
+ {
+ // Variations may have different artist, time format, generatedBy, etc.
+ for (metadata in _metadata)
+ {
+ for (diffId in metadata.playData.difficulties)
+ {
+ var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
- variations.push(metadata.variation);
+ variations.push(metadata.variation);
- difficulty.songName = metadata.songName;
- difficulty.songArtist = metadata.artist;
- difficulty.timeFormat = metadata.timeFormat;
- difficulty.divisions = metadata.divisions;
- difficulty.timeChanges = metadata.timeChanges;
- difficulty.loop = metadata.loop;
- difficulty.generatedBy = metadata.generatedBy;
+ difficulty.songName = metadata.songName;
+ difficulty.songArtist = metadata.artist;
+ difficulty.timeFormat = metadata.timeFormat;
+ difficulty.divisions = metadata.divisions;
+ difficulty.timeChanges = metadata.timeChanges;
+ difficulty.loop = metadata.loop;
+ difficulty.generatedBy = metadata.generatedBy;
- difficulty.stage = metadata.playData.stage;
- // difficulty.noteSkin = metadata.playData.noteSkin;
+ difficulty.stage = metadata.playData.stage;
+ // difficulty.noteSkin = metadata.playData.noteSkin;
- difficulty.chars = new Map();
- for (charId in metadata.playData.playableChars.keys())
- {
- var char = metadata.playData.playableChars.get(charId);
+ difficulty.chars = new Map();
+ for (charId in metadata.playData.playableChars.keys())
+ {
+ var char = metadata.playData.playableChars.get(charId);
- difficulty.chars.set(charId, char);
- }
+ difficulty.chars.set(charId, char);
+ }
- difficulties.set(diffId, difficulty);
- }
- }
- }
+ difficulties.set(diffId, difficulty);
+ }
+ }
+ }
- /**
- * Parse and cache the chart for all difficulties of this song.
- */
- public function cacheCharts(?force:Bool = false):Void
- {
- if (force)
- {
- clearCharts();
- }
+ /**
+ * Parse and cache the chart for all difficulties of this song.
+ */
+ public function cacheCharts(?force:Bool = false):Void
+ {
+ if (force)
+ {
+ clearCharts();
+ }
- trace('Caching ${variations.length} chart files for song $songId');
- for (variation in variations)
- {
- var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation);
- var chartNotes = chartData.notes;
+ trace('Caching ${variations.length} chart files for song $songId');
+ for (variation in variations)
+ {
+ var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation);
+ var chartNotes = chartData.notes;
- for (diffId in chartNotes.keys())
- {
- // Retrieve the cached difficulty data.
- var difficulty:Null = difficulties.get(diffId);
- if (difficulty == null)
- {
- trace('Could not find difficulty $diffId for song $songId');
- continue;
- }
- // Add the chart data to the difficulty.
- difficulty.notes = chartData.notes.get(diffId);
- difficulty.scrollSpeed = chartData.getScrollSpeed(diffId);
+ for (diffId in chartNotes.keys())
+ {
+ // Retrieve the cached difficulty data.
+ var difficulty:Null = difficulties.get(diffId);
+ if (difficulty == null)
+ {
+ trace('Could not find difficulty $diffId for song $songId');
+ continue;
+ }
+ // Add the chart data to the difficulty.
+ difficulty.notes = chartData.notes.get(diffId);
+ difficulty.scrollSpeed = chartData.getScrollSpeed(diffId);
- difficulty.events = chartData.events;
- }
- }
- trace('Done caching charts.');
- }
+ difficulty.events = chartData.events;
+ }
+ }
+ trace('Done caching charts.');
+ }
- /**
- * Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
- */
- public inline function getDifficulty(?diffId:String):SongDifficulty
- {
- if (diffId == null)
- diffId = difficulties.keys().array()[0];
+ /**
+ * Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
+ */
+ public inline function getDifficulty(?diffId:String):SongDifficulty
+ {
+ if (diffId == null)
+ diffId = difficulties.keys().array()[0];
- return difficulties.get(diffId);
- }
+ return difficulties.get(diffId);
+ }
- /**
- * Purge the cached chart data for each difficulty of this song.
- */
- public function clearCharts():Void
- {
- for (diff in difficulties)
- {
- diff.clearChart();
- }
- }
+ /**
+ * Purge the cached chart data for each difficulty of this song.
+ */
+ public function clearCharts():Void
+ {
+ for (diff in difficulties)
+ {
+ diff.clearChart();
+ }
+ }
- public function toString():String
- {
- return 'Song($songId)';
- }
+ public function toString():String
+ {
+ return 'Song($songId)';
+ }
}
class SongDifficulty
{
- /**
- * The parent song for this difficulty.
- */
- public final song:Song;
+ /**
+ * The parent song for this difficulty.
+ */
+ public final song:Song;
- /**
- * The difficulty ID, such as `easy` or `hard`.
- */
- public final difficulty:String;
+ /**
+ * The difficulty ID, such as `easy` or `hard`.
+ */
+ public final difficulty:String;
- /**
- * The metadata file that contains this difficulty.
- */
- public final variation:String;
+ /**
+ * The metadata file that contains this difficulty.
+ */
+ public final variation:String;
- /**
- * The note chart for this difficulty.
- */
- public var notes:Array;
+ /**
+ * The note chart for this difficulty.
+ */
+ public var notes:Array;
- /**
- * The event chart for this difficulty.
- */
- public var events:Array;
+ /**
+ * The event chart for this difficulty.
+ */
+ public var events:Array;
- public var songName:String = SongValidator.DEFAULT_SONGNAME;
- public var songArtist:String = SongValidator.DEFAULT_ARTIST;
- public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
- public var divisions:Int = SongValidator.DEFAULT_DIVISIONS;
- public var loop:Bool = SongValidator.DEFAULT_LOOP;
- public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
+ public var songName:String = SongValidator.DEFAULT_SONGNAME;
+ public var songArtist:String = SongValidator.DEFAULT_ARTIST;
+ public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
+ public var divisions:Int = SongValidator.DEFAULT_DIVISIONS;
+ public var loop:Bool = SongValidator.DEFAULT_LOOP;
+ public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
- public var timeChanges:Array = [];
+ public var timeChanges:Array = [];
- public var stage:String = SongValidator.DEFAULT_STAGE;
- public var chars:Map = null;
+ public var stage:String = SongValidator.DEFAULT_STAGE;
+ public var chars:Map = null;
- public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED;
+ public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED;
- public function new(song:Song, diffId:String, variation:String)
- {
- this.song = song;
- this.difficulty = diffId;
- this.variation = variation;
- }
+ public function new(song:Song, diffId:String, variation:String)
+ {
+ this.song = song;
+ this.difficulty = diffId;
+ this.variation = variation;
+ }
- public function clearChart():Void
- {
- notes = null;
- }
+ public function clearChart():Void
+ {
+ notes = null;
+ }
- public function getStartingBPM():Float
- {
- if (timeChanges.length == 0)
- {
- return 0;
- }
+ public function getStartingBPM():Float
+ {
+ if (timeChanges.length == 0)
+ {
+ return 0;
+ }
- return timeChanges[0].bpm;
- }
+ return timeChanges[0].bpm;
+ }
- public function getPlayableChar(id:String):SongPlayableChar
- {
- return chars.get(id);
- }
+ public function getPlayableChar(id:String):SongPlayableChar
+ {
+ return chars.get(id);
+ }
- public function getPlayableChars():Array
- {
- return chars.keys().array();
- }
+ public function getPlayableChars():Array
+ {
+ return chars.keys().array();
+ }
- public function getEvents():Array
- {
- return cast events;
- }
+ public function getEvents():Array
+ {
+ return cast events;
+ }
- public inline function cacheInst()
- {
- FlxG.sound.cache(Paths.inst(this.song.songId));
- }
+ public inline function cacheInst()
+ {
+ FlxG.sound.cache(Paths.inst(this.song.songId));
+ }
- public inline function playInst(volume:Float = 1.0, looped:Bool = false)
- {
- FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped);
- }
+ public inline function playInst(volume:Float = 1.0, looped:Bool = false)
+ {
+ FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped);
+ }
- public inline function cacheVocals()
- {
- FlxG.sound.cache(Paths.voices(this.song.songId));
- }
+ public inline function cacheVocals()
+ {
+ FlxG.sound.cache(Paths.voices(this.song.songId));
+ }
- public function buildVoiceList():Array
- {
- // TODO: Implement.
+ public function buildVoiceList():Array
+ {
+ // TODO: Implement.
- return [""];
- }
+ return [""];
+ }
- public function buildVocals(charId:String = "bf"):VoicesGroup
- {
- var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList());
- return result;
- }
+ public function buildVocals(charId:String = "bf"):VoicesGroup
+ {
+ var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList());
+ return result;
+ }
}
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index 775e78c11..480c3aab5 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -13,743 +13,771 @@ import thx.semver.Version;
*/
class SongDataParser
{
- /**
- * A list containing all the songs available to the game.
- */
- static final songCache:Map = new Map();
+ /**
+ * A list containing all the songs available to the game.
+ */
+ static final songCache:Map = new Map();
- static final DEFAULT_SONG_ID = 'UNKNOWN';
- static final SONG_DATA_PATH = 'songs/';
- static final SONG_DATA_SUFFIX = '-metadata.json';
+ static final DEFAULT_SONG_ID = 'UNKNOWN';
+ static final SONG_DATA_PATH = 'songs/';
+ static final SONG_DATA_SUFFIX = '-metadata.json';
- /**
- * Parses and preloads the game's song metadata and scripts when the game starts.
- *
- * If you want to force song metadata to be reloaded, you can just call this function again.
- */
- public static function loadSongCache():Void
- {
- clearSongCache();
- trace("[SONGDATA] Loading song cache...");
+ /**
+ * Parses and preloads the game's song metadata and scripts when the game starts.
+ *
+ * If you want to force song metadata to be reloaded, you can just call this function again.
+ */
+ public static function loadSongCache():Void
+ {
+ clearSongCache();
+ trace("Loading song cache...");
- //
- // SCRIPTED SONGS
- //
- var scriptedSongClassNames:Array = ScriptedSong.listScriptClasses();
- trace(' Instantiating ${scriptedSongClassNames.length} scripted songs...');
- for (songCls in scriptedSongClassNames)
- {
- var song:Song = ScriptedSong.init(songCls, DEFAULT_SONG_ID);
- if (song != null)
- {
- trace(' Loaded scripted song: ${song.songId}');
- songCache.set(song.songId, song);
- }
- else
- {
- trace(' Failed to instantiate scripted song class: ${songCls}');
- }
- }
+ //
+ // SCRIPTED SONGS
+ //
+ var scriptedSongClassNames:Array = ScriptedSong.listScriptClasses();
+ trace(' Instantiating ${scriptedSongClassNames.length} scripted songs...');
+ for (songCls in scriptedSongClassNames)
+ {
+ var song:Song = ScriptedSong.init(songCls, DEFAULT_SONG_ID);
+ if (song != null)
+ {
+ trace(' Loaded scripted song: ${song.songId}');
+ songCache.set(song.songId, song);
+ }
+ else
+ {
+ trace(' Failed to instantiate scripted song class: ${songCls}');
+ }
+ }
- //
- // UNSCRIPTED SONGS
- //
- var songIdList:Array = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String
- {
- return songDataPath.split('/')[0];
- });
- var unscriptedSongIds:Array = songIdList.filter(function(songId:String):Bool
- {
- return !songCache.exists(songId);
- });
- trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...');
- for (songId in unscriptedSongIds)
- {
- try
- {
- var song = new Song(songId);
- if (song != null)
- {
- trace(' Loaded song data: ${song.songId}');
- songCache.set(song.songId, song);
- }
- }
- catch (e)
- {
- trace(' An error occurred while loading song data: ${songId}');
- trace(e);
- // Assume error was already logged.
- continue;
- }
- }
+ //
+ // UNSCRIPTED SONGS
+ //
+ var songIdList:Array = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String
+ {
+ return songDataPath.split('/')[0];
+ });
+ var unscriptedSongIds:Array = songIdList.filter(function(songId:String):Bool
+ {
+ return !songCache.exists(songId);
+ });
+ trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...');
+ for (songId in unscriptedSongIds)
+ {
+ try
+ {
+ var song = new Song(songId);
+ if (song != null)
+ {
+ trace(' Loaded song data: ${song.songId}');
+ songCache.set(song.songId, song);
+ }
+ }
+ catch (e)
+ {
+ trace(' An error occurred while loading song data: ${songId}');
+ trace(e);
+ // Assume error was already logged.
+ continue;
+ }
+ }
- trace(' Successfully loaded ${Lambda.count(songCache)} stages.');
- }
+ trace(' Successfully loaded ${Lambda.count(songCache)} stages.');
+ }
- /**
- * Retrieves a particular song from the cache.
- */
- public static function fetchSong(songId:String):Null