diff --git a/source/funkin/Character.hx b/source/funkin/Character.hx index 5c266078a..a81d599a6 100644 --- a/source/funkin/Character.hx +++ b/source/funkin/Character.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.util.Constants; import funkin.Note.NoteData; import funkin.SongLoad.SwagSong; import funkin.Section.SwagSection; @@ -128,7 +129,7 @@ class Character extends FlxSprite playAnim('danceRight'); - setGraphicSize(Std.int(width * PlayState.daPixelZoom)); + setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE)); updateHitbox(); antialiasing = false; diff --git a/source/funkin/DialogueBox.hx b/source/funkin/DialogueBox.hx index cc582cc84..599077563 100644 --- a/source/funkin/DialogueBox.hx +++ b/source/funkin/DialogueBox.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.util.Constants; import flixel.FlxSprite; import flixel.addons.text.FlxTypeText; import flixel.graphics.frames.FlxAtlasFrames; @@ -38,7 +39,7 @@ class DialogueBox extends FlxSpriteGroup { super(); - switch (PlayState.SONG.song.toLowerCase()) + switch (PlayState.currentSong.song.toLowerCase()) { case 'senpai': FlxG.sound.playMusic(Paths.music('Lunchbox'), 0); @@ -63,7 +64,7 @@ class DialogueBox extends FlxSpriteGroup portraitLeft = new FlxSprite(-20, 40); portraitLeft.frames = Paths.getSparrowAtlas('weeb/senpaiPortrait'); portraitLeft.animation.addByPrefix('enter', 'Senpai Portrait Enter', 24, false); - portraitLeft.setGraphicSize(Std.int(portraitLeft.width * PlayState.daPixelZoom * 0.9)); + portraitLeft.setGraphicSize(Std.int(portraitLeft.width * Constants.PIXEL_ART_SCALE * 0.9)); portraitLeft.updateHitbox(); portraitLeft.scrollFactor.set(); add(portraitLeft); @@ -72,7 +73,7 @@ class DialogueBox extends FlxSpriteGroup portraitRight = new FlxSprite(0, 40); portraitRight.frames = Paths.getSparrowAtlas('weeb/bfPortrait'); portraitRight.animation.addByPrefix('enter', 'Boyfriend portrait enter', 24, false); - portraitRight.setGraphicSize(Std.int(portraitRight.width * PlayState.daPixelZoom * 0.9)); + portraitRight.setGraphicSize(Std.int(portraitRight.width * Constants.PIXEL_ART_SCALE * 0.9)); portraitRight.updateHitbox(); portraitRight.scrollFactor.set(); add(portraitRight); @@ -81,7 +82,7 @@ class DialogueBox extends FlxSpriteGroup box = new FlxSprite(-20, 45); var hasDialog = false; - switch (PlayState.SONG.song.toLowerCase()) + switch (PlayState.currentSong.song.toLowerCase()) { case 'senpai': hasDialog = true; @@ -113,7 +114,7 @@ class DialogueBox extends FlxSpriteGroup return; box.animation.play('normalOpen'); - box.setGraphicSize(Std.int(box.width * PlayState.daPixelZoom * 0.9)); + box.setGraphicSize(Std.int(box.width * Constants.PIXEL_ART_SCALE * 0.9)); box.updateHitbox(); add(box); @@ -121,7 +122,7 @@ class DialogueBox extends FlxSpriteGroup portraitLeft.screenCenter(X); handSelect = new FlxSprite(1042, 590).loadGraphic(Paths.image('weeb/pixelUI/hand_textbox')); - handSelect.setGraphicSize(Std.int(handSelect.width * PlayState.daPixelZoom * 0.9)); + handSelect.setGraphicSize(Std.int(handSelect.width * Constants.PIXEL_ART_SCALE * 0.9)); handSelect.updateHitbox(); handSelect.visible = false; add(handSelect); @@ -154,9 +155,9 @@ class DialogueBox extends FlxSpriteGroup override function update(elapsed:Float) { // HARD CODING CUZ IM STUPDI - if (PlayState.SONG.song.toLowerCase() == 'roses') + if (PlayState.currentSong.song.toLowerCase() == 'roses') portraitLeft.visible = false; - if (PlayState.SONG.song.toLowerCase() == 'thorns') + if (PlayState.currentSong.song.toLowerCase() == 'thorns') { portraitLeft.color = FlxColor.BLACK; swagDialogue.color = FlxColor.WHITE; @@ -192,7 +193,7 @@ class DialogueBox extends FlxSpriteGroup { isEnding = true; - if (PlayState.SONG.song.toLowerCase() == 'senpai' || PlayState.SONG.song.toLowerCase() == 'thorns') + if (PlayState.currentSong.song.toLowerCase() == 'senpai' || PlayState.currentSong.song.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0); new FlxTimer().start(0.2, function(tmr:FlxTimer) diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index c309ec5e0..882c7795f 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -538,7 +538,7 @@ class FreeplayState extends MusicBeatSubstate curDifficulty = 1; }*/ - PlayState.SONG = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase()); + PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase()); PlayState.isStoryMode = false; PlayState.storyDifficulty = curDifficulty; // SongLoad.curDiff = Highscore.formatSong() diff --git a/source/funkin/GameOverSubstate.hx b/source/funkin/GameOverSubstate.hx index f35d5f094..3c397ee78 100644 --- a/source/funkin/GameOverSubstate.hx +++ b/source/funkin/GameOverSubstate.hx @@ -20,12 +20,12 @@ class GameOverSubstate extends MusicBeatSubstate var gameOverMusic:FlxSound; - public function new(x:Float, y:Float) + public function new() { gameOverMusic = new FlxSound(); FlxG.sound.list.add(gameOverMusic); - var daStage = PlayState.curStageId; + var daStage = PlayState.instance.currentStageId; var daBf:String = ''; switch (daStage) { @@ -36,7 +36,7 @@ class GameOverSubstate extends MusicBeatSubstate daBf = 'bf'; } - var daSong = PlayState.SONG.song.toLowerCase(); + var daSong = PlayState.currentSong.song.toLowerCase(); switch (daSong) { @@ -48,7 +48,9 @@ class GameOverSubstate extends MusicBeatSubstate Conductor.songPosition = 0; - bf = new Boyfriend(x, y, daBf); + var bfXPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().x; + var bfYPos = PlayState.instance.currentStage.getBoyfriend().getScreenPosition().y; + bf = new Boyfriend(bfXPos, bfYPos, daBf); add(bf); camFollow = new FlxObject(bf.getGraphicMidpoint().x, bf.getGraphicMidpoint().y, 1, 1); @@ -57,7 +59,7 @@ class GameOverSubstate extends MusicBeatSubstate FlxG.sound.play(Paths.sound('fnf_loss_sfx' + stageSuffix)); // Conductor.changeBPM(100); - switch (PlayState.SONG.player1) + switch (PlayState.currentSong.player1) { case 'pico': stageSuffix = 'Pico'; diff --git a/source/funkin/HealthIcon.hx b/source/funkin/HealthIcon.hx index e3b48a153..617af2b74 100644 --- a/source/funkin/HealthIcon.hx +++ b/source/funkin/HealthIcon.hx @@ -37,7 +37,7 @@ class HealthIcon extends FlxSprite if (isOldIcon) changeIcon('bf-old'); else - changeIcon(PlayState.SONG.player1); + changeIcon(PlayState.currentSong.player1); } var pixelArrayFunny:Array = CoolUtil.coolTextFile(Paths.file('images/icons/pixelIcons.txt')); diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 812df1b51..78729e22a 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.modding.module.ModuleHandler; import funkin.play.stage.StageData; import funkin.charting.ChartingState; import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond; @@ -122,6 +123,8 @@ class InitState extends FlxTransitionableState StageDataParser.loadStageCache(); + ModuleHandler.loadModuleCache(); + #if song var song = getSong(); @@ -186,7 +189,7 @@ class InitState extends FlxTransitionableState { var dif = getDif(); - PlayState.SONG = SongLoad.loadFromJson(song, song); + PlayState.currentSong = SongLoad.loadFromJson(song, song); PlayState.isStoryMode = isStoryMode; PlayState.storyDifficulty = dif; SongLoad.curDiff = switch (dif) diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index 3b9d88b4f..b163b49d0 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -57,9 +57,9 @@ class LoadingState extends MusicBeatState callbacks = new MultiCallback(onLoad); var introComplete = callbacks.add("introComplete"); checkLoadSong(getSongPath()); - if (PlayState.SONG.needsVoices) + if (PlayState.currentSong.needsVoices) { - var files = PlayState.SONG.voiceList; + var files = PlayState.currentSong.voiceList; if (files == null) files = [""]; // loads with no file name assumption, to load "Voices.ogg" or whatev normally @@ -173,12 +173,12 @@ class LoadingState extends MusicBeatState static function getSongPath() { - return Paths.inst(PlayState.SONG.song); + return Paths.inst(PlayState.currentSong.song); } static function getVocalPath(?suffix:String) { - return Paths.voices(PlayState.SONG.song, suffix); + return Paths.voices(PlayState.currentSong.song, suffix); } inline static public function loadAndSwitchState(target:FlxState, stopMusic = false) @@ -198,7 +198,7 @@ class LoadingState extends MusicBeatState } #if NO_PRELOAD_ALL var loaded = isSoundLoaded(getSongPath()) - && (!PlayState.SONG.needsVoices || isSoundLoaded(getVocalPath())) + && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath())) && isLibraryLoaded("shared"); if (!loaded) diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index f85c2a3c3..1f6bbf9da 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -1,5 +1,7 @@ package funkin; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import funkin.modding.module.ModuleHandler; import funkin.NGio; import flixel.FlxObject; import flixel.FlxSprite; @@ -272,6 +274,8 @@ class MainMenuState extends MusicBeatState override function update(elapsed:Float) { + super.update(elapsed); + if (FlxG.onMobile) { var touch:FlxTouch = FlxG.touches.getFirst(); @@ -307,7 +311,8 @@ class MainMenuState extends MusicBeatState FlxG.switchState(new TitleState()); } - super.update(elapsed); + var event:UpdateScriptEvent = new UpdateScriptEvent(elapsed); + ModuleHandler.callEvent(event); } } diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx index 290fea0b6..d523d59a5 100644 --- a/source/funkin/Note.hx +++ b/source/funkin/Note.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.util.Constants; import flixel.FlxSprite; import flixel.math.FlxMath; import funkin.shaderslmfao.ColorSwap; @@ -109,10 +110,8 @@ class Note extends FlxSprite data.noteData = noteData; - var daStage:String = PlayState.curStageId; - // TODO: Make this logic more generic - switch (daStage) + switch (PlayState.instance.currentStageId) { case 'school' | 'schoolEvil': loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); @@ -137,7 +136,7 @@ class Note extends FlxSprite animation.add('bluehold', [1]); } - setGraphicSize(Std.int(width * PlayState.daPixelZoom)); + setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE)); updateHitbox(); default: @@ -194,7 +193,7 @@ class Note extends FlxSprite x -= width / 2; - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) x += 30; if (prevNote.isSustainNote) diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 14cbb63a8..81e6fb9f5 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -51,7 +51,7 @@ class PauseSubState extends MusicBeatSubstate add(bg); var levelInfo:FlxText = new FlxText(20, 15, 0, "", 32); - levelInfo.text += PlayState.SONG.song; + levelInfo.text += PlayState.currentSong.song; levelInfo.scrollFactor.set(); levelInfo.setFormat(Paths.font("vcr.ttf"), 32); levelInfo.updateHitbox(); @@ -76,7 +76,7 @@ class PauseSubState extends MusicBeatSubstate practiceText.setFormat(Paths.font('vcr.ttf'), 32); practiceText.updateHitbox(); practiceText.x = FlxG.width - (practiceText.width + 20); - practiceText.visible = PlayState.practiceMode; + practiceText.visible = PlayState.isPracticeMode; add(practiceText); levelDifficulty.alpha = 0; @@ -157,7 +157,7 @@ class PauseSubState extends MusicBeatSubstate case "Resume": close(); case "EASY" | 'NORMAL' | "HARD": - PlayState.SONG = SongLoad.loadFromJson(PlayState.SONG.song.toLowerCase(), PlayState.SONG.song.toLowerCase()); + PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase()); SongLoad.curDiff = daSelected.toLowerCase(); PlayState.storyDifficulty = curSelected; @@ -167,8 +167,8 @@ class PauseSubState extends MusicBeatSubstate close(); case 'Toggle Practice Mode': - PlayState.practiceMode = !PlayState.practiceMode; - practiceText.visible = PlayState.practiceMode; + PlayState.isPracticeMode = !PlayState.isPracticeMode; + practiceText.visible = PlayState.isPracticeMode; case 'Change Difficulty': menuItems = difficultyChoices; diff --git a/source/funkin/StoryMenuState.hx b/source/funkin/StoryMenuState.hx index 00e482a8a..dff3fce14 100644 --- a/source/funkin/StoryMenuState.hx +++ b/source/funkin/StoryMenuState.hx @@ -311,7 +311,7 @@ class StoryMenuState extends MusicBeatState PlayState.isStoryMode = true; selectedWeek = true; - PlayState.SONG = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase()); + PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase()); PlayState.storyWeek = curWeek; PlayState.campaignScore = 0; diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx index fd7b739de..e079cb3f3 100644 --- a/source/funkin/charting/ChartingState.hx +++ b/source/funkin/charting/ChartingState.hx @@ -122,9 +122,9 @@ class ChartingState extends MusicBeatState curRenderedNotes = new FlxTypedGroup(); curRenderedSustains = new FlxTypedGroup(); - if (PlayState.SONG != null) + if (PlayState.currentSong != null) { - _song = SongLoad.songData = PlayState.SONG; + _song = SongLoad.songData = PlayState.currentSong; trace("LOADED A PLAYSTATE SONGFILE"); } else @@ -814,7 +814,7 @@ class ChartingState extends MusicBeatState lastSection = curSection; - PlayState.SONG = _song; + PlayState.currentSong = _song; // JUST FOR DEBUG DARNELL STUFF, GENERALIZE THIS FOR BETTER LOADING ELSEWHERE TOO! PlayState.storyWeek = 8; @@ -1462,13 +1462,13 @@ class ChartingState extends MusicBeatState function loadJson(song:String):Void { - PlayState.SONG = SongLoad.loadFromJson(song.toLowerCase(), song.toLowerCase()); + PlayState.currentSong = SongLoad.loadFromJson(song.toLowerCase(), song.toLowerCase()); LoadingState.loadAndSwitchState(new ChartingState()); } function loadAutosave():Void { - PlayState.SONG = FlxG.save.data.autosave; + PlayState.currentSong = FlxG.save.data.autosave; FlxG.resetState(); } diff --git a/source/funkin/modding/IHook.hx b/source/funkin/modding/IHook.hx index fdcf9da27..51a4b1088 100644 --- a/source/funkin/modding/IHook.hx +++ b/source/funkin/modding/IHook.hx @@ -3,7 +3,6 @@ package funkin.modding; import polymod.hscript.HScriptable; /** - * Add this interface to a class to make it a scriptable object. * Functions annotated with @:hscript will call the relevant script. */ @:hscript({ diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx new file mode 100644 index 000000000..0445d2c32 --- /dev/null +++ b/source/funkin/modding/IScriptedClass.hx @@ -0,0 +1,50 @@ +package funkin.modding; + +import funkin.modding.events.ScriptEvent; + +/** + * Defines a set of callbacks available to all scripted classes. + * + * Includes events handling basic life cycle relevant to all scripted classes. + */ +interface IScriptedClass +{ + public function onScriptEvent(event:ScriptEvent):Void; + + public function onCreate(event:ScriptEvent):Void; + public function onDestroy(event:ScriptEvent):Void; + public function onUpdate(event:UpdateScriptEvent):Void; +} + +/** + * Defines a set of callbacks available to scripted classes that involve player input. + */ +interface IInputScriptedClass extends IScriptedClass +{ + public function onKeyDown(event:KeyboardInputScriptEvent):Void; + public function onKeyUp(event:KeyboardInputScriptEvent):Void; +} + +/** + * Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State. + */ +interface IPlayStateScriptedClass extends IScriptedClass +{ + public function onPause(event:ScriptEvent):Void; + public function onResume(event:ScriptEvent):Void; + + public function onSongStart(event:ScriptEvent):Void; + public function onSongEnd(event:ScriptEvent):Void; + public function onSongReset(event:ScriptEvent):Void; + public function onGameOver(event:ScriptEvent):Void; + public function onGameRetry(event:ScriptEvent):Void; + + public function onNoteHit(event:NoteScriptEvent):Void; + public function onNoteMiss(event:NoteScriptEvent):Void; + + public function onStepHit(event:SongTimeScriptEvent):Void; + public function onBeatHit(event:SongTimeScriptEvent):Void; + + public function onCountdownStart(event:CountdownScriptEvent):Void; + public function onCountdownStep(event:CountdownScriptEvent):Void; +} diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx new file mode 100644 index 000000000..c10a3c3b5 --- /dev/null +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -0,0 +1,327 @@ +package funkin.modding.events; + +import funkin.play.Countdown.CountdownStep; +import openfl.events.EventType; +import openfl.events.KeyboardEvent; + +typedef ScriptEventType = EventType; + +/** + * This is a base class for all events that are issued to scripted classes. + * It can be used to identify the type of event called, store data, and cancel event propagation. + */ +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 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 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 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 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 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 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 is reset. This can happen from the pause menu or the game over screen. + * + * This event is not cancelable. + */ + public static inline final SONG_RESET:ScriptEventType = "SONG_RESET"; + + /** + * 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 PlayState.beginCountdown(). + * - Note that calling startCountdown() 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 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 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 presses a key to restart the game after the death animation. + * + * This event IS cancelable! Canceling this event will prevent the game from restarting. + */ + public static inline final GAME_RETRY:ScriptEventType = "GAME_RETRY"; + + /** + * 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 player releases a key on the keyboard. + * + * This event is not cancelable. + */ + public static inline final KEY_UP:ScriptEventType = "KEY_UP"; + + /** + * If true, the behavior associated with this event can be prevented. + * For example, cancelling COUNTDOWN_BEGIN 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; + + /** + * The type associated with the event. + */ + public var type(default, null):ScriptEventType; + + /** + * Whether the event should continue to be triggered on additional targets. + */ + public var shouldPropagate(default, null):Bool; + + @:noCompletion private var __eventCanceled:Bool; + + public function new(type:ScriptEventType, cancelable:Bool = false):Void + { + this.type = type; + this.cancelable = cancelable; + this.__eventCanceled = false; + this.shouldPropagate = true; + } + + /** + * Call this function on a cancelable event to cancel the associated behavior. + * For example, cancelling COUNTDOWN_BEGIN 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 toString():String + { + return 'ScriptEvent(type=$type, cancelable=$cancelable)'; + } +} + +/** + * SPECIFIC EVENTS + */ +/** + * An event that is fired associated with a specific note. + */ +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; + + public function new(type:ScriptEventType, note:Note, cancelable:Bool = false):Void + { + super(type, cancelable); + this.note = note; + } + + public override function toString():String + { + return 'NoteScriptEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ')'; + } +} + +/** + * An event that is fired during the update loop. + */ +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; + + public function new(elapsed:Float):Void + { + super(ScriptEvent.UPDATE, false); + this.elapsed = elapsed; + } + + public override function toString():String + { + return 'UpdateScriptEvent(elapsed=$elapsed)'; + } +} + +/** + * An event that is fired regularly during the song. + * May be on beat or on step. + */ +class SongTimeScriptEvent extends ScriptEvent +{ + /** + * The current beat of the song. + */ + public var beat(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, false); + this.beat = beat; + this.step = step; + } + + public override function toString():String + { + return 'SongTimeScriptEvent(type=' + type + ', beat=' + beat + ', step=' + step + ')'; + } +} + +/** + * An event that is fired regularly during the song. + * May be on beat or on step. + */ +class CountdownScriptEvent extends ScriptEvent +{ + /** + * The current step of the countdown. + */ + public var step(default, null):CountdownStep; + + public function new(type:ScriptEventType, step:CountdownStep):Void + { + super(type, false); + this.step = step; + } + + public override function toString():String + { + return 'CountdownScriptEvent(type=' + type + ', step=' + step + ')'; + } +} + +/** + * An event that is fired when the player presses a key. + */ +class KeyboardInputScriptEvent extends ScriptEvent +{ + /** + * 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 override function toString():String + { + return 'KeyboardInputScriptEvent(type=' + type + ', event=' + event + ')'; + } +} diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx new file mode 100644 index 000000000..29ad736ab --- /dev/null +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -0,0 +1,127 @@ +package funkin.modding.events; + +import funkin.modding.IScriptedClass; +import funkin.modding.IScriptedClass.IInputScriptedClass; +import funkin.modding.IScriptedClass.IPlayStateScriptedClass; + +/** + * Utility functions to assist with handling scripted classes. + */ +class ScriptEventDispatcher +{ + public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void + { + target.onScriptEvent(event); + + // If one target says to stop propagation, stop. + if (!event.shouldPropagate) + { + return; + } + + // IScriptedClass + switch (event.type) + { + case ScriptEvent.CREATE: + target.onCreate(event); + return; + case ScriptEvent.DESTROY: + target.onDestroy(event); + return; + case ScriptEvent.UPDATE: + target.onUpdate(cast event); + return; + } + + if (Std.isOfType(target, IInputScriptedClass)) + { + var t = cast(target, IInputScriptedClass); + switch (event.type) + { + case ScriptEvent.KEY_DOWN: + t.onKeyDown(cast event); + return; + case ScriptEvent.KEY_UP: + t.onKeyUp(cast event); + return; + } + } + + if (Std.isOfType(target, IPlayStateScriptedClass)) + { + var t = cast(target, IPlayStateScriptedClass); + switch (event.type) + { + case ScriptEvent.NOTE_HIT: + t.onNoteHit(cast event); + return; + case ScriptEvent.NOTE_MISS: + t.onNoteMiss(cast event); + return; + case ScriptEvent.SONG_BEAT_HIT: + t.onBeatHit(cast event); + return; + case ScriptEvent.SONG_STEP_HIT: + t.onStepHit(cast event); + return; + case ScriptEvent.SONG_START: + t.onSongStart(event); + return; + case ScriptEvent.SONG_END: + t.onSongEnd(event); + return; + case ScriptEvent.SONG_RESET: + t.onSongReset(event); + return; + case ScriptEvent.PAUSE: + t.onPause(event); + return; + case ScriptEvent.RESUME: + t.onResume(event); + return; + case ScriptEvent.COUNTDOWN_START: + t.onCountdownStart(cast event); + return; + case ScriptEvent.COUNTDOWN_STEP: + t.onCountdownStep(cast event); + return; + } + } + + throw "No helper for event type: " + event.type; + } + + public static function callEventOnAllTargets(targets:Iterator, event:ScriptEvent):Void + { + if (targets == null || event == null) + { + return; + } + + if (Std.isOfType(targets, Array)) + { + var t = cast(targets, Array); + if (t.length == 0) + { + return; + } + } + + for (target in targets) + { + var t:IScriptedClass = cast target; + if (t == null) + { + continue; + } + + callEvent(t, event); + + // If one target says to stop propagation, stop. + if (!event.shouldPropagate) + { + return; + } + } + } +} diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx new file mode 100644 index 000000000..fda644890 --- /dev/null +++ b/source/funkin/modding/module/Module.hx @@ -0,0 +1,87 @@ +package funkin.modding.module; + +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent; +import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; +import funkin.modding.events.ScriptEvent.CountdownScriptEvent; +import funkin.modding.IScriptedClass.IPlayStateScriptedClass; +import funkin.modding.IScriptedClass.IInputScriptedClass; + +/** + * A module is a scripted class which receives all events without requiring a specific context. + * You may have the module active at all times, or only when another script enables it. + */ +class Module implements IInputScriptedClass implements IPlayStateScriptedClass +{ + /** + * Whether the module is currently active. + */ + public var active(default, set):Bool = false; + + function set_active(value:Bool):Bool + { + this.active = value; + return value; + } + + public var moduleId(default, null):String = 'UNKNOWN'; + + /** + * Called when the module is initialized. + * It may not be safe to reference other modules here since they may not be loaded yet. + * + * @param startActive Whether to start with the module active. + * If false, the module will be inactive and must be enabled by another script, + * such as a stage or another module. + */ + public function new(moduleId:String, startActive:Bool) + { + this.moduleId = moduleId; + this.active = startActive; + } + + public function toString() + { + return 'Module(' + this.moduleId + ')'; + } + + public function onScriptEvent(event:ScriptEvent) {} + + public function onCreate(event:ScriptEvent) {} + + public function onDestroy(event:ScriptEvent) {} + + public function onUpdate(event:UpdateScriptEvent) {} + + public function onKeyDown(event:KeyboardInputScriptEvent) {} + + public function onKeyUp(event:KeyboardInputScriptEvent) {} + + public function onPause(event:ScriptEvent) {} + + public function onResume(event:ScriptEvent) {} + + public function onSongStart(event:ScriptEvent) {} + + public function onSongEnd(event:ScriptEvent) {} + + public function onSongReset(event:ScriptEvent) {} + + public function onGameOver(event:ScriptEvent) {} + + public function onGameRetry(event:ScriptEvent) {} + + public function onNoteHit(event:NoteScriptEvent) {} + + public function onNoteMiss(event:NoteScriptEvent) {} + + public function onStepHit(event:SongTimeScriptEvent) {} + + public function onBeatHit(event:SongTimeScriptEvent) {} + + public function onCountdownStart(event:CountdownScriptEvent) {} + + public function onCountdownStep(event:CountdownScriptEvent) {} +} diff --git a/source/funkin/modding/module/ModuleHandler.hx b/source/funkin/modding/module/ModuleHandler.hx new file mode 100644 index 000000000..88fdfeb44 --- /dev/null +++ b/source/funkin/modding/module/ModuleHandler.hx @@ -0,0 +1,67 @@ +package funkin.modding.module; + +import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; + +/** + * Utility functions for loading and manipulating active modules. + */ +class ModuleHandler +{ + static final moduleCache:Map = new Map(); + + /** + * Whether modules start active by default. + */ + static final DEFAULT_STARTACTIVE:Bool = true; + + /** + * 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, DEFAULT_STARTACTIVE); + if (module != null) + { + trace(' Loaded module: ${moduleCls}'); + + // Then store it. + moduleCache.set(module.moduleId, module); + } + else + { + trace(' Failed to instantiate module: ${moduleCls}'); + } + } + + trace("[MODULEHANDLER] Module cache loaded."); + } + + public static function clearModuleCache():Void + { + if (moduleCache != null) + { + // for (module in moduleCache) + // { + // module.destroy(); + // } + moduleCache.clear(); + } + } + + public static function callEvent(event:ScriptEvent):Void + { + ScriptEventDispatcher.callEventOnAllTargets(moduleCache.iterator(), event); + } +} diff --git a/source/funkin/modding/module/ScriptedModule.hx b/source/funkin/modding/module/ScriptedModule.hx new file mode 100644 index 000000000..31c79addb --- /dev/null +++ b/source/funkin/modding/module/ScriptedModule.hx @@ -0,0 +1,9 @@ +package funkin.modding.module; + +import funkin.modding.IHook; + +@:hscriptClass +class ScriptedModule extends Module implements IHook +{ + // No body needed for this class, it's magic ;) +} diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx new file mode 100644 index 000000000..cdd5b01da --- /dev/null +++ b/source/funkin/play/Countdown.hx @@ -0,0 +1,11 @@ +package funkin.play; + +enum abstract CountdownStep(String) from String to String +{ + var BEFORE = "BEFORE"; + var THREE = "THREE"; + var TWO = "TWO"; + var ONE = "ONE"; + var GO = "GO"; + var AFTER = "AFTER"; +} diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 811b86d3e..4391fe930 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,12 +1,13 @@ package funkin.play; +import funkin.play.Strumline.StrumlineStyle; +import flixel.addons.effects.FlxTrail; +import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; -import flixel.addons.effects.FlxTrail; -import flixel.addons.transition.FlxTransitionableState; import flixel.group.FlxGroup; import flixel.math.FlxMath; import flixel.math.FlxPoint; @@ -18,18 +19,23 @@ import flixel.ui.FlxBar; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; -import funkin.Note; -import funkin.Section.SwagSection; -import funkin.SongLoad.SwagSong; import funkin.charting.ChartingState; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.module.ModuleHandler; +import funkin.Note; import funkin.play.stage.Stage; import funkin.play.stage.StageData; +import funkin.Section.SwagSection; import funkin.shaderslmfao.ColorSwap; +import funkin.SongLoad.SwagSong; import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; -import haxe.Json; +import funkin.util.Constants; +import funkin.util.SortUtil; import lime.ui.Haptic; -import lime.utils.Assets; using StringTools; @@ -39,91 +45,191 @@ import Discord.DiscordClient; class PlayState extends MusicBeatState { - // TODO: Reorganize these variables (maybe there should be a separate class like Conductor just to hold them?) - public static var curStageId:String = ''; - public static var SONG:SwagSong; - public static var isStoryMode:Bool = false; - public static var storyWeek:Int = 0; - public static var storyPlaylist:Array = []; - public static var storyDifficulty:Int = 1; - public static var deathCounter:Int = 0; - public static var practiceMode:Bool = false; - public static var needsReset:Bool = false; - - private var vocals:VoicesGroup; - private var vocalsFinished:Bool = false; - - private var dad:Character; - private var gf:Character; - private var boyfriend:Boyfriend; - /** - * Notes that should be ON SCREEN and have UPDATES running on them! + * 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. */ - private var notes:FlxTypedGroup; - - private var unspawnNotes:Array = []; - - private var strumLine:FlxSprite; - - private var camFollow:FlxObject; - - private static var prevCamFollow:FlxObject; - - private var strumLineNotes:FlxTypedGroup; - /** * 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; /** - * Strumline for player + * The currently active song. Includes data about what stage should be used, what characters, + * and the notes to be played. */ - private var playerStrums:FlxTypedGroup; + public static var currentSong:SwagSong = null; - private var camZooming:Bool = false; - private var curSong:String = ""; + /** + * Whether the game is currently in Story Mode. If false, we are in Free Play Mode. + */ + public static var isStoryMode:Bool = false; - private var gfSpeed:Int = 1; + /** + * 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; - public static var health:Float = 1; + /** + * 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; - private var healthDisplay:Float = 1; - private var combo:Int = 0; + /** + * Used to persist the position of the `cameraFollowPosition` between resets. + */ + private static var previousCameraFollowPoint:FlxObject = null; - private var healthBarBG:FlxSprite; + /** + * 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; + + /** + * 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; + + /** + * 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; + + /** + * 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. + */ + private var cameraFollowPoint:FlxObject; + + /** + * An object which the strumline (and its notes) are positioned relative to. + */ + private var strumlineAnchor:FlxObject; + + /** + * 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; + + /** + * 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`). + */ private 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 sprite group containing active player's strumline notes. + */ + public var playerStrumline:Strumline; + + /** + * The sprite group containing opponent's strumline notes. + */ + public var enemyStrumline:Strumline; + + /** + * 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 needsReset:Bool = false; + public static var seenCutscene:Bool = false; + public static var campaignScore:Int = 0; + + private var vocals:VoicesGroup; + private var vocalsFinished:Bool = false; + + private var playerStrums:FlxTypedGroup; + 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; - private var iconP1:HealthIcon; private var iconP2:HealthIcon; private var camHUD:FlxCamera; private var camGame:FlxCamera; - var dialogue:Array; - - public static var seenCutscene:Bool = false; - + var startedCountdown:Bool = false; var talking:Bool = true; var songScore:Int = 0; - var scoreTxt:FlxText; - - // Dunno why its called doof lol, it's just the dialogue box var doof:DialogueBox; - var grpNoteSplashes:FlxTypedGroup; - - public static var campaignScore:Int = 0; - var defaultCamZoom:Float = 1.05; - - // how big to stretch the pixel art assets - public static var daPixelZoom:Float = 6; - var inCutscene:Bool = false; + var camPos:FlxPoint; + var comboPopUps:PopUpStuff; + var startTimer:FlxTimer = new FlxTimer(); + var perfectMode:Bool = false; + var previousFrameTime:Int = 0; + var songTime:Float = 0; + var cameraRightSide:Bool = false; #if discord_rpc // Discord RPC variables @@ -134,30 +240,30 @@ class PlayState extends MusicBeatState var detailsPausedText:String = ""; #end - var camPos:FlxPoint; - - var comboPopUps:PopUpStuff; - override public function create() { + super.create(); + instance = this; - initCameras(); + // Reduce physics accuracy (who cares!!!) to improve animation quality. + FlxG.fixedTimestep = false; - // Starting health. - health = 1; + // 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; - persistentUpdate = true; - persistentDraw = true; + Conductor.songPosition = -5000; - if (SONG == null) - SONG = SongLoad.loadFromJson('tutorial'); + if (currentSong == null) + currentSong = SongLoad.loadFromJson('tutorial'); - Conductor.mapBPMChanges(SONG); - Conductor.changeBPM(SONG.bpm); + Conductor.mapBPMChanges(currentSong); + Conductor.changeBPM(currentSong.bpm); // dialogue init shit, just for week 5 really (for now...?) - switch (SONG.song.toLowerCase()) + switch (currentSong.song.toLowerCase()) { case 'senpai': dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue')); @@ -167,6 +273,9 @@ class PlayState extends MusicBeatState dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue')); } + // Initialize stage stuff. + initCameras(); + #if discord_rpc initDiscord(); #end @@ -182,17 +291,6 @@ class PlayState extends MusicBeatState doof.cameras = [camHUD]; } - Conductor.songPosition = -5000; - - strumLine = new FlxSprite(0, 50).makeGraphic(FlxG.width, 10); - - if (PreferencesMenu.getPref('downscroll')) - strumLine.y = FlxG.height - 150; // 150 just random ass number lol - - strumLine.scrollFactor.set(); - strumLineNotes = new FlxTypedGroup(); - add(strumLineNotes); - // fake notesplash cache type deal so that it loads in the graphic? comboPopUps = new PopUpStuff(); @@ -210,62 +308,53 @@ class PlayState extends MusicBeatState generateSong(); - // add(strumLine); + cameraFollowPoint = new FlxObject(0, 0, 1, 1); + cameraFollowPoint.setPosition(camPos.x, camPos.y); - camFollow = new FlxObject(0, 0, 1, 1); - - camFollow.setPosition(camPos.x, camPos.y); - - if (prevCamFollow != null) + if (previousCameraFollowPoint != null) { - camFollow = prevCamFollow; - prevCamFollow = null; + cameraFollowPoint = previousCameraFollowPoint; + previousCameraFollowPoint = null; } - add(camFollow); - - resetCamFollow(); + add(cameraFollowPoint); + resetCamera(); FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); - FlxG.fixedTimestep = false; - - healthBarBG = new FlxSprite(0, FlxG.height * 0.9).loadGraphic(Paths.image('healthBar')); + 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(); + healthBarBG.scrollFactor.set(0, 0); add(healthBarBG); - if (PreferencesMenu.getPref('downscroll')) - healthBarBG.y = FlxG.height * 0.1; - healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, - 'healthDisplay', 0, 2); + 'healthLerp', 0, 2); healthBar.scrollFactor.set(); - healthBar.createFilledBar(0xFFFF0000, 0xFF66FF33); - // healthBar + healthBar.createFilledBar(Constants.HEALTH_BAR_RED, Constants.HEALTH_BAR_GREEN); add(healthBar); - scoreTxt = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, "", 20); - scoreTxt.setFormat(Paths.font("vcr.ttf"), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); - scoreTxt.scrollFactor.set(); - add(scoreTxt); + 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); - iconP1 = new HealthIcon(SONG.player1, true); + iconP1 = new HealthIcon(currentSong.player1, true); iconP1.y = healthBar.y - (iconP1.height / 2); add(iconP1); - iconP2 = new HealthIcon(SONG.player2, false); + iconP2 = new HealthIcon(currentSong.player2, false); iconP2.y = healthBar.y - (iconP2.height / 2); add(iconP2); + // Attach the groups to the HUD camera so they are rendered independent of the stage. grpNoteSplashes.cameras = [camHUD]; - strumLineNotes.cameras = [camHUD]; - notes.cameras = [camHUD]; + activeNotes.cameras = [camHUD]; healthBar.cameras = [camHUD]; healthBarBG.cameras = [camHUD]; iconP1.cameras = [camHUD]; iconP2.cameras = [camHUD]; - scoreTxt.cameras = [camHUD]; + scoreText.cameras = [camHUD]; // if (SONG.song == 'South') // FlxG.camera.alpha = 0.7; @@ -278,7 +367,7 @@ class PlayState extends MusicBeatState { seenCutscene = true; - switch (curSong.toLowerCase()) + switch (currentSong.song.toLowerCase()) { case "winter-horrorland": var blackScreen:FlxSprite = new FlxSprite(0, 0).makeGraphic(Std.int(FlxG.width * 2), Std.int(FlxG.height * 2), FlxColor.BLACK); @@ -290,9 +379,9 @@ class PlayState extends MusicBeatState { remove(blackScreen); FlxG.sound.play(Paths.sound('Lights_Turn_On')); - camFollow.y = -2050; - camFollow.x += 200; - FlxG.camera.focusOn(camFollow.getPosition()); + cameraFollowPoint.y = -2050; + cameraFollowPoint.x += 200; + FlxG.camera.focusOn(cameraFollowPoint.getPosition()); FlxG.camera.zoom = 1.5; new FlxTimer().start(0.8, function(tmr:FlxTimer) @@ -323,24 +412,13 @@ class PlayState extends MusicBeatState } else { - switch (curSong.toLowerCase()) - { - // REMOVE THIS LATER - // case 'ugh': - // ughIntro(); - // case 'stress': - // stressIntro(); - // case 'guns': - // gunsIntro(); - - default: - startCountdown(); - } + startCountdown(); } - - super.create(); } + /** + * Initializes the position of the camera. + */ function initCameras() { defaultCamZoom = FlxCamera.defaultZoom; @@ -350,8 +428,8 @@ class PlayState extends MusicBeatState if (FlxG.sound.music != null) FlxG.sound.music.stop(); - FlxG.sound.cache(Paths.inst(PlayState.SONG.song)); - FlxG.sound.cache(Paths.voices(PlayState.SONG.song)); + FlxG.sound.cache(Paths.inst(currentSong.song)); + FlxG.sound.cache(Paths.voices(currentSong.song)); // var gameCam:FlxCamera = FlxG.camera; camGame = new SwagCamera(); @@ -365,32 +443,33 @@ class PlayState extends MusicBeatState function initStage() { // TODO: Move stageId to the song file. - switch (SONG.song.toLowerCase()) + switch (currentSong.song.toLowerCase()) { case 'spookeez' | 'monster' | 'south': - curStageId = "spookyMansion"; + currentStageId = "spookyMansion"; case 'pico' | 'blammed' | 'philly': - curStageId = 'phillyTrain'; + currentStageId = 'phillyTrain'; case "milf" | 'satin-panties' | 'high': - curStageId = 'limoRide'; + currentStageId = 'limoRide'; case "cocoa" | 'eggnog': - curStageId = 'mallXmas'; + currentStageId = 'mallXmas'; case 'winter-horrorland': - curStageId = 'mallEvil'; + currentStageId = 'mallEvil'; case 'pyro': - curStageId = 'pyro'; + currentStageId = 'pyro'; case 'senpai' | 'roses': - curStageId = 'school'; + currentStageId = 'school'; case "darnell": - curStageId = 'phillyStreets'; + currentStageId = 'phillyStreets'; case 'thorns': - curStageId = 'schoolEvil'; + currentStageId = 'schoolEvil'; case 'guns' | 'stress' | 'ugh': - curStageId = 'tankmanBattlefield'; + currentStageId = 'tankmanBattlefield'; default: - curStageId = "mainStage"; + currentStageId = "mainStage"; } - loadStage(curStageId); + // Loads the relevant stage based on its ID. + loadStage(currentStageId); } function initCharacters() @@ -398,7 +477,7 @@ class PlayState extends MusicBeatState // all dis is shitty, redo later for stage shit var gfVersion:String = 'gf'; - switch (curStageId) + switch (currentStageId) { case 'limoRide': gfVersion = 'gf-car'; @@ -410,15 +489,15 @@ class PlayState extends MusicBeatState gfVersion = 'gf-tankmen'; } - if (SONG.player1 == "pico") + if (currentSong.player1 == "pico") { gfVersion = "nene"; } - if (SONG.song.toLowerCase() == 'stress') + if (currentSong.song.toLowerCase() == 'stress') gfVersion = 'pico-speaker'; - gf = new Character(400, 130, gfVersion); + var gf = new Character(400, 130, gfVersion); gf.scrollFactor.set(0.95, 0.95); switch (gfVersion) @@ -428,11 +507,11 @@ class PlayState extends MusicBeatState gf.y -= 200; } - dad = new Character(100, 100, SONG.player2); + var dad = new Character(100, 100, currentSong.player2); camPos = new FlxPoint(dad.getGraphicMidpoint().x, dad.getGraphicMidpoint().y); - switch (SONG.player2) + switch (currentSong.player2) { case 'gf': dad.setPosition(gf.x, gf.y); @@ -467,10 +546,10 @@ class PlayState extends MusicBeatState dad.y += 180; } - boyfriend = new Boyfriend(770, 450, SONG.player1); + var boyfriend = new Boyfriend(770, 450, currentSong.player1); // REPOSITIONING PER STAGE - switch (curStageId) + switch (currentStageId) { case 'schoolEvil': var evilTrail = new FlxTrail(dad, null, 4, 24, 0.3, 0.069); @@ -492,16 +571,16 @@ class PlayState extends MusicBeatState } } - if (curStage != null) + if (currentStage != null) { // We're using Eric's stage handler. // Characters get added to the stage, not the main scene. - curStage.addCharacter(gf, GF); - curStage.addCharacter(boyfriend, BF); - curStage.addCharacter(dad, DAD); + currentStage.addCharacter(gf, GF); + currentStage.addCharacter(boyfriend, BF); + currentStage.addCharacter(dad, DAD); // Redo z-indexes. - curStage.refresh(); + currentStage.refresh(); } else { @@ -537,8 +616,8 @@ class PlayState extends MusicBeatState FlxG.camera.zoom = defaultCamZoom * 1.2; - camFollow.x += 100; - camFollow.y += 100; + cameraFollowPoint.x += 100; + cameraFollowPoint.y += 100; /* FlxG.sound.playMusic(Paths.music('DISTORTO'), 0); @@ -617,26 +696,28 @@ class PlayState extends MusicBeatState { // 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.curStage != null) + if (this.currentStage != null) { - remove(curStage); - curStage.kill(); - curStage = null; + remove(currentStage); + var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false); + ScriptEventDispatcher.callEvent(currentStage, event); + currentStage = null; } + ModuleHandler.clearModuleCache(); + // Forcibly reload scripts so that scripted stages can be edited. polymod.hscript.PolymodScriptClass.clearScriptClasses(); polymod.hscript.PolymodScriptClass.registerAllScriptClasses(); // Reload the stages in cache. This might cause a lag spike but who cares this is a debug utility. StageDataParser.loadStageCache(); + ModuleHandler.loadModuleCache(); // Reload the level. This should use new data from the assets folder. LoadingState.loadAndSwitchState(new PlayState()); } - public var curStage:Stage; - /** * Loads stage data from cache, assembles the props, * and adds it to the state. @@ -644,18 +725,19 @@ class PlayState extends MusicBeatState */ function loadStage(id:String) { - curStage = StageDataParser.fetchStage(id); + currentStage = StageDataParser.fetchStage(id); - if (curStage != null) + if (currentStage != null) { // Actually create and position the sprites. - curStage.buildStage(); + var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + ScriptEventDispatcher.callEvent(currentStage, event); // Apply camera zoom. - defaultCamZoom *= curStage.camZoom; + defaultCamZoom *= currentStage.camZoom; // Add the stage to the scene. - this.add(curStage); + this.add(currentStage); } } @@ -766,248 +848,13 @@ class PlayState extends MusicBeatState startCountdown(); cameraMovement(); #end - - /* camHUD.visible = false; - - // for story mode shit - camFollow.setPosition(camPos.x, camPos.y); - - var dummyLoaderShit:FlxGroup = new FlxGroup(); - - add(dummyLoaderShit); - - for (i in 0...7) - { - var dummyLoader:FlxSprite = new FlxSprite(); - dummyLoader.loadGraphic(Paths.image('cutsceneStuff/gfHoldup-' + i)); - dummyLoaderShit.add(dummyLoader); - dummyLoader.alpha = 0.01; - dummyLoader.y = FlxG.height - 20; - // dummyLoader.drawFrame(true); - } - - dad.visible = false; - - // gf.y += 300; - gf.alpha = 0.01; - - var gfTankmen:FlxSprite = new FlxSprite(210, 70); - gfTankmen.frames = Paths.getSparrowAtlas('characters/gfTankmen'); - gfTankmen.animation.addByPrefix('loop', 'GF Dancing at Gunpoint', 24, true); - gfTankmen.animation.play('loop'); - gfTankmen.antialiasing = true; - gfCutsceneLayer.add(gfTankmen); - - var tankCutscene:TankCutscene = new TankCutscene(-70, 320); - tankCutscene.frames = Paths.getSparrowAtlas('cutsceneStuff/tankTalkSong3-pt1'); - tankCutscene.animation.addByPrefix('tankyguy', 'TANK TALK 3 P1 UNCUT', 24, false); - // tankCutscene.animation.addByPrefix('weed', 'sexAmbig', 24, false); - tankCutscene.animation.play('tankyguy'); - - tankCutscene.antialiasing = true; - bfTankCutsceneLayer.add(tankCutscene); // add(); - - var alsoTankCutscene:FlxSprite = new FlxSprite(20, 320); - alsoTankCutscene.frames = Paths.getSparrowAtlas('cutsceneStuff/tankTalkSong3-pt2'); - alsoTankCutscene.animation.addByPrefix('swagTank', 'TANK TALK 3 P2 UNCUT', 24, false); - alsoTankCutscene.antialiasing = true; - - bfTankCutsceneLayer.add(alsoTankCutscene); - - alsoTankCutscene.y = FlxG.height + 100; - - camFollow.setPosition(gf.x + 350, gf.y + 560); - FlxG.camera.focusOn(camFollow.getPosition()); - - boyfriend.visible = false; - - var fakeBF:Character = new Character(boyfriend.x, boyfriend.y, 'bf', true); - bfTankCutsceneLayer.add(fakeBF); - - // var atlasCutscene:Animation - // var animAssets:AssetManager = new AssetManager(); - - // var url = 'images/gfDemon'; - - // // animAssets.enqueueSingle(Paths.file(url + "/spritemap1.png")); - // // animAssets.enqueueSingle(Paths.file(url + "/spritemap1.json")); - // // animAssets.enqueueSingle(Paths.file(url + "/Animation.json")); - - // animAssets.loadQueue(function(asssss:AssetManager) - // { - // var daAnim:Animation = asssss.createAnimation('GF Turnin Demon W Effect'); - // FlxG.addChildBelowMouse(daAnim); - // }); - - var bfCatchGf:FlxSprite = new FlxSprite(boyfriend.x - 10, boyfriend.y - 90); - bfCatchGf.frames = Paths.getSparrowAtlas('cutsceneStuff/bfCatchesGF'); - bfCatchGf.animation.addByPrefix('catch', 'BF catches GF', 24, false); - bfCatchGf.antialiasing = true; - add(bfCatchGf); - bfCatchGf.visible = false; - - if (PreferencesMenu.getPref('censor-naughty')) - tankCutscene.startSyncAudio = FlxG.sound.play(Paths.sound('stressCutscene')); - else - { - tankCutscene.startSyncAudio = FlxG.sound.play(Paths.sound('song3censor')); - // cutsceneSound.loadEmbedded(Paths.sound('song3censor')); - - var censor:FlxSprite = new FlxSprite(); - censor.frames = Paths.getSparrowAtlas('cutsceneStuff/censor'); - censor.animation.addByPrefix('censor', 'mouth censor', 24); - censor.animation.play('censor'); - add(censor); - censor.visible = false; - // - - new FlxTimer().start(4.6, function(censorTimer:FlxTimer) - { - censor.visible = true; - censor.setPosition(dad.x + 160, dad.y + 180); - - new FlxTimer().start(0.2, function(endThing:FlxTimer) - { - censor.visible = false; - }); - }); - - new FlxTimer().start(25.1, function(censorTimer:FlxTimer) - { - censor.visible = true; - censor.setPosition(dad.x + 120, dad.y + 170); - - new FlxTimer().start(0.9, function(endThing:FlxTimer) - { - censor.visible = false; - }); - }); - - new FlxTimer().start(30.7, function(censorTimer:FlxTimer) - { - censor.visible = true; - censor.setPosition(dad.x + 210, dad.y + 190); - - new FlxTimer().start(0.4, function(endThing:FlxTimer) - { - censor.visible = false; - }); - }); - - new FlxTimer().start(33.8, function(censorTimer:FlxTimer) - { - censor.visible = true; - censor.setPosition(dad.x + 180, dad.y + 170); - - new FlxTimer().start(0.6, function(endThing:FlxTimer) - { - censor.visible = false; - }); - }); - } - - // new FlxTimer().start(0.01, function(tmr) cutsceneSound.play()); // cutsceneSound.play(); - // cutsceneSound.play(); - // tankCutscene.startSyncAudio = cutsceneSound; - // tankCutscene.animation.curAnim.curFrame - - FlxG.camera.zoom = defaultCamZoom * 1.15; - - camFollow.x -= 200; - - // cutsceneSound.onComplete = startCountdown; - - // Cunt 1 - new FlxTimer().start(31.5, function(cunt:FlxTimer) - { - camFollow.x += 400; - camFollow.y += 150; - FlxG.camera.zoom = defaultCamZoom * 1.4; - FlxTween.tween(FlxG.camera, {zoom: FlxG.camera.zoom + 0.1}, 0.5, {ease: FlxEase.elasticOut}); - FlxG.camera.focusOn(camFollow.getPosition()); - boyfriend.playAnim('singUPmiss'); - boyfriend.animation.finishCallback = function(animFinish:String) - { - camFollow.x -= 400; - camFollow.y -= 150; - FlxG.camera.zoom /= 1.4; - FlxG.camera.focusOn(camFollow.getPosition()); - - boyfriend.animation.finishCallback = null; - }; - }); - - new FlxTimer().start(15.1, function(tmr:FlxTimer) - { - camFollow.y -= 170; - camFollow.x += 200; - FlxTween.tween(FlxG.camera, {zoom: FlxG.camera.zoom * 1.3}, 2.1, { - ease: FlxEase.quadInOut - }); - - new FlxTimer().start(2.2, function(swagTimer:FlxTimer) - { - // FlxTween.tween(FlxG.camera, {zoom: defaultCamZoom}, 0.7, {ease: FlxEase.elasticOut}); - FlxG.camera.zoom = 0.8; - // camFollow.y -= 100; - boyfriend.visible = false; - bfCatchGf.visible = true; - bfCatchGf.animation.play('catch'); - - bfTankCutsceneLayer.remove(fakeBF); - - bfCatchGf.animation.finishCallback = function(anim:String) - { - bfCatchGf.visible = false; - boyfriend.visible = true; - }; - - new FlxTimer().start(3, function(weedShitBaby:FlxTimer) - { - camFollow.y += 180; - camFollow.x -= 80; - }); - - new FlxTimer().start(2.3, function(gayLol:FlxTimer) - { - bfTankCutsceneLayer.remove(tankCutscene); - alsoTankCutscene.y = 320; - alsoTankCutscene.animation.play('swagTank'); - // tankCutscene.animation.play('weed'); - }); - }); - - gf.visible = false; - var cutsceneShit:CutsceneCharacter = new CutsceneCharacter(210, 70, 'gfHoldup'); - gfCutsceneLayer.add(cutsceneShit); - gfCutsceneLayer.remove(gfTankmen); - - cutsceneShit.onFinish = function() - { - gf.alpha = 1; - gf.visible = true; - }; - - // add(cutsceneShit); - new FlxTimer().start(20, function(alsoTmr:FlxTimer) - { - dad.visible = true; - bfTankCutsceneLayer.remove(alsoTankCutscene); - startCountdown(); - remove(dummyLoaderShit); - dummyLoaderShit.destroy(); - dummyLoaderShit = null; - - gfCutsceneLayer.remove(cutsceneShit); - }); - });*/ } function initDiscord():Void { #if discord_rpc storyDifficultyText = difficultyString(); - iconRPC = SONG.player2; + iconRPC = currentSong.player2; // To avoid having duplicate images in Discord assets switch (iconRPC) @@ -1025,7 +872,7 @@ class PlayState extends MusicBeatState detailsPausedText = "Paused - " + detailsText; // Updating Discord Rich Presence. - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); + DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); #end } @@ -1041,19 +888,19 @@ class PlayState extends MusicBeatState 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 * daPixelZoom)); + senpaiEvil.setGraphicSize(Std.int(senpaiEvil.width * Constants.PIXEL_ART_SCALE)); senpaiEvil.scrollFactor.set(); senpaiEvil.updateHitbox(); senpaiEvil.screenCenter(); senpaiEvil.x += senpaiEvil.width / 5; - camFollow.setPosition(camPos.x, camPos.y); + cameraFollowPoint.setPosition(camPos.x, camPos.y); - if (SONG.song.toLowerCase() == 'roses' || SONG.song.toLowerCase() == 'thorns') + if (currentSong.song.toLowerCase() == 'roses' || currentSong.song.toLowerCase() == 'thorns') { remove(black); - if (SONG.song.toLowerCase() == 'thorns') + if (currentSong.song.toLowerCase() == 'thorns') { add(red); camHUD.visible = false; @@ -1075,7 +922,7 @@ class PlayState extends MusicBeatState { inCutscene = true; - if (SONG.song.toLowerCase() == 'thorns') + if (currentSong.song.toLowerCase() == 'thorns') { add(senpaiEvil); senpaiEvil.alpha = 0; @@ -1115,16 +962,12 @@ class PlayState extends MusicBeatState }); } - var startTimer:FlxTimer = new FlxTimer(); - var perfectMode:Bool = false; - function startCountdown():Void { inCutscene = false; camHUD.visible = true; - generateStaticArrows(0); - generateStaticArrows(1); + buildStrumlines(); talking = false; @@ -1144,30 +987,30 @@ class PlayState extends MusicBeatState { // this just based on beatHit stuff but compact if (swagCounter % gfSpeed == 0) - gf.dance(); + currentStage.getGirlfriend().dance(); if (swagCounter % 2 == 0) { - if (boyfriend.animation != null) + if (currentStage.getBoyfriend().animation != null) { - if (!boyfriend.animation.curAnim.name.startsWith("sing")) - boyfriend.playAnim('idle'); + if (!currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing")) + currentStage.getBoyfriend().playAnim('idle'); } - if (dad.animation != null) + if (currentStage.getDad().animation != null) { - if (!dad.animation.curAnim.name.startsWith("sing")) - dad.dance(); + if (!currentStage.getDad().animation.curAnim.name.startsWith("sing")) + currentStage.getDad().dance(); } } - else if (dad.curCharacter == 'spooky' && !dad.animation.curAnim.name.startsWith("sing")) - dad.dance(); + else if (currentStage.getDad().curCharacter == 'spooky' && !currentStage.getDad().animation.curAnim.name.startsWith("sing")) + currentStage.getDad().dance(); if (generatedMusic) - notes.sort(sortNotes, FlxSort.DESCENDING); + activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); var introSprPaths:Array = ["ready", "set", "go"]; var altSuffix:String = ""; - if (curStageId.startsWith("school")) + if (currentStageId.startsWith("school")) { altSuffix = '-pixel'; introSprPaths = ['weeb/pixelUI/ready-pixel', 'weeb/pixelUI/set-pixel', 'weeb/pixelUI/date-pixel']; @@ -1182,18 +1025,6 @@ class PlayState extends MusicBeatState readySetGo(introSprPaths[swagCounter - 1]); FlxG.sound.play(Paths.sound(introSndPaths[swagCounter]), 0.6); - /* switch (swagCounter) - { - case 0: - - case 1: - - case 2: - - case 3: - - }*/ - swagCounter += 1; }, 4); } @@ -1203,8 +1034,8 @@ class PlayState extends MusicBeatState var spr:FlxSprite = new FlxSprite().loadGraphic(Paths.image(path)); spr.scrollFactor.set(); - if (curStageId.startsWith('school')) - spr.setGraphicSize(Std.int(spr.width * daPixelZoom)); + if (currentStageId.startsWith('school')) + spr.setGraphicSize(Std.int(spr.width * Constants.PIXEL_ART_SCALE)); spr.updateHitbox(); spr.screenCenter(); @@ -1218,21 +1049,18 @@ class PlayState extends MusicBeatState }); } - var previousFrameTime:Int = 0; - var songTime:Float = 0; - function startSong():Void { startingSong = false; previousFrameTime = FlxG.game.ticks; - if (!paused) + if (!isGamePaused) { // if (FlxG.sound.music != null) // FlxG.sound.music.play(true); // else - FlxG.sound.playMusic(Paths.inst(SONG.song), 1, false); + FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false); } FlxG.sound.music.onComplete = endSong; @@ -1243,7 +1071,7 @@ class PlayState extends MusicBeatState songLength = FlxG.sound.music.length; // Updating Discord Rich Presence (with Time Left) - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength); + DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength); #end } @@ -1251,22 +1079,23 @@ class PlayState extends MusicBeatState { // FlxG.log.add(ChartParser.parse()); - Conductor.changeBPM(SONG.bpm); + Conductor.changeBPM(currentSong.bpm); - curSong = SONG.song; + currentSong.song = currentSong.song; - if (SONG.needsVoices) - vocals = new VoicesGroup(SONG.song, SONG.voiceList); + if (currentSong.needsVoices) + vocals = new VoicesGroup(currentSong.song, currentSong.voiceList); else - vocals = new VoicesGroup(SONG.song, null, false); + vocals = new VoicesGroup(currentSong.song, null, false); vocals.members[0].onComplete = function() { vocalsFinished = true; }; - notes = new FlxTypedGroup(); - add(notes); + activeNotes = new FlxTypedGroup(); + activeNotes.zIndex = 1000; + add(activeNotes); regenNoteData(); @@ -1276,9 +1105,9 @@ class PlayState extends MusicBeatState function regenNoteData():Void { // make unspawn notes shit def empty - unspawnNotes = []; + inactiveNotes = []; - notes.forEach(function(nt) + activeNotes.forEach(function(nt) { nt.followsTime = false; FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, { @@ -1286,7 +1115,7 @@ class PlayState extends MusicBeatState onComplete: function(twn) { nt.kill(); - notes.remove(nt, true); + activeNotes.remove(nt, true); nt.destroy(); } }); @@ -1310,8 +1139,8 @@ class PlayState extends MusicBeatState gottaHitNote = !section.mustHitSection; var oldNote:Note; - if (unspawnNotes.length > 0) - oldNote = unspawnNotes[Std.int(unspawnNotes.length - 1)]; + if (inactiveNotes.length > 0) + oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)]; else oldNote = null; @@ -1322,15 +1151,15 @@ class PlayState extends MusicBeatState var susLength:Float = swagNote.data.sustainLength; susLength = susLength / Conductor.stepCrochet; - unspawnNotes.push(swagNote); + inactiveNotes.push(swagNote); for (susNote in 0...Math.round(susLength)) { - oldNote = unspawnNotes[Std.int(unspawnNotes.length - 1)]; + oldNote = inactiveNotes[Std.int(inactiveNotes.length - 1)]; var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true); sustainNote.scrollFactor.set(); - unspawnNotes.push(sustainNote); + inactiveNotes.push(sustainNote); sustainNote.mustPress = gottaHitNote; @@ -1345,129 +1174,10 @@ class PlayState extends MusicBeatState } } - unspawnNotes.sort(sortByShit); - } - - // Now you are probably wondering why I made 2 of these very similar functions - // sortByShit(), and sortNotes(). sortNotes is meant to be used by both sortByShit(), and the notes FlxGroup - // sortByShit() is meant to be used only by the unspawnNotes array. - // and the array sorting function doesnt need that order variable thingie - // this is good enough for now lololol HERE IS COMMENT FOR THIS SORTA DUMB DECISION LOL - function sortByShit(Obj1:Note, Obj2:Note):Int - { - return sortNotes(FlxSort.ASCENDING, Obj1, Obj2); - } - - function sortNotes(order:Int = FlxSort.ASCENDING, Obj1:Note, Obj2:Note) - { - return FlxSort.byValues(order, Obj1.data.strumTime, Obj2.data.strumTime); - } - - // ^ These two sorts also look cute together ^ - - private function generateStaticArrows(player:Int):Void - { - for (i in 0...4) + inactiveNotes.sort(function(a:Note, b:Note):Int { - // FlxG.log.add(i); - var babyArrow:FlxSprite = new FlxSprite(0, strumLine.y); - var colorswap:ColorSwap = new ColorSwap(); - babyArrow.shader = colorswap.shader; - colorswap.update(Note.arrowColors[i]); - - switch (curStageId) - { - case 'school' | 'schoolEvil': - babyArrow.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); - babyArrow.animation.add('green', [6]); - babyArrow.animation.add('red', [7]); - babyArrow.animation.add('blue', [5]); - babyArrow.animation.add('purplel', [4]); - - babyArrow.setGraphicSize(Std.int(babyArrow.width * daPixelZoom)); - babyArrow.updateHitbox(); - babyArrow.antialiasing = false; - - switch (Math.abs(i)) - { - case 0: - babyArrow.x += Note.swagWidth * 0; - babyArrow.animation.add('static', [0]); - babyArrow.animation.add('pressed', [4, 8], 12, false); - babyArrow.animation.add('confirm', [12, 16], 24, false); - case 1: - babyArrow.x += Note.swagWidth * 1; - babyArrow.animation.add('static', [1]); - babyArrow.animation.add('pressed', [5, 9], 12, false); - babyArrow.animation.add('confirm', [13, 17], 24, false); - case 2: - babyArrow.x += Note.swagWidth * 2; - babyArrow.animation.add('static', [2]); - babyArrow.animation.add('pressed', [6, 10], 12, false); - babyArrow.animation.add('confirm', [14, 18], 12, false); - case 3: - babyArrow.x += Note.swagWidth * 3; - babyArrow.animation.add('static', [3]); - babyArrow.animation.add('pressed', [7, 11], 12, false); - babyArrow.animation.add('confirm', [15, 19], 24, false); - } - - default: - babyArrow.frames = Paths.getSparrowAtlas('NOTE_assets'); - babyArrow.animation.addByPrefix('green', 'arrowUP'); - babyArrow.animation.addByPrefix('blue', 'arrowDOWN'); - babyArrow.animation.addByPrefix('purple', 'arrowLEFT'); - babyArrow.animation.addByPrefix('red', 'arrowRIGHT'); - - babyArrow.antialiasing = true; - babyArrow.setGraphicSize(Std.int(babyArrow.width * 0.7)); - - switch (Math.abs(i)) - { - case 0: - babyArrow.x += Note.swagWidth * 0; - babyArrow.animation.addByPrefix('static', 'arrow static instance 1'); - babyArrow.animation.addByPrefix('pressed', 'left press', 24, false); - babyArrow.animation.addByPrefix('confirm', 'left confirm', 24, false); - case 1: - babyArrow.x += Note.swagWidth * 1; - babyArrow.animation.addByPrefix('static', 'arrow static instance 2'); - babyArrow.animation.addByPrefix('pressed', 'down press', 24, false); - babyArrow.animation.addByPrefix('confirm', 'down confirm', 24, false); - case 2: - babyArrow.x += Note.swagWidth * 2; - babyArrow.animation.addByPrefix('static', 'arrow static instance 4'); - babyArrow.animation.addByPrefix('pressed', 'up press', 24, false); - babyArrow.animation.addByPrefix('confirm', 'up confirm', 24, false); - case 3: - babyArrow.x += Note.swagWidth * 3; - babyArrow.animation.addByPrefix('static', 'arrow static instance 3'); - babyArrow.animation.addByPrefix('pressed', 'right press', 24, false); - babyArrow.animation.addByPrefix('confirm', 'right confirm', 24, false); - } - } - - babyArrow.updateHitbox(); - babyArrow.scrollFactor.set(); - - if (!isStoryMode) - { - babyArrow.y -= 10; - babyArrow.alpha = 0; - FlxTween.tween(babyArrow, {y: babyArrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * i)}); - } - - babyArrow.ID = i; - - if (player == 1) - playerStrums.add(babyArrow); - - babyArrow.animation.play('static'); - babyArrow.x += 50; - babyArrow.x += ((FlxG.width / 2) * player); - - strumLineNotes.add(babyArrow); - } + return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b); + }); } function tweenCamIn():Void @@ -1475,54 +1185,16 @@ class PlayState extends MusicBeatState FlxTween.tween(FlxG.camera, {zoom: 1.3 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut}); } - override function openSubState(SubState:FlxSubState) - { - if (paused) - { - if (FlxG.sound.music != null) - { - FlxG.sound.music.pause(); - vocals.pause(); - } - - if (!startTimer.finished) - startTimer.active = false; - } - - super.openSubState(SubState); - } - - override function closeSubState() - { - if (paused) - { - if (FlxG.sound.music != null && !startingSong) - resyncVocals(); - - if (!startTimer.finished) - startTimer.active = true; - paused = false; - - #if discord_rpc - if (startTimer.finished) - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength - Conductor.songPosition); - else - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); - #end - } - - super.closeSubState(); - } - #if discord_rpc override public function onFocus():Void { if (health > 0 && !paused && FlxG.autoPause) { if (Conductor.songPosition > 0.0) - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC, true, songLength - Conductor.songPosition); + DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC, true, + songLength - Conductor.songPosition); else - DiscordClient.changePresence(detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); + DiscordClient.changePresence(detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); } super.onFocus(); @@ -1531,7 +1203,7 @@ class PlayState extends MusicBeatState override public function onFocusLost():Void { if (health > 0 && !paused && FlxG.autoPause) - DiscordClient.changePresence(detailsPausedText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); + DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); super.onFocusLost(); } @@ -1553,19 +1225,16 @@ class PlayState extends MusicBeatState vocals.play(); } - private var paused:Bool = false; - var startedCountdown:Bool = false; - var canPause:Bool = true; - override public function update(elapsed:Float) { - healthDisplay = FlxMath.lerp(healthDisplay, health, 0.15); + super.update(elapsed); + + updateHealthBar(); if (needsReset) { - resetCamFollow(); + resetCamera(); - paused = false; persistentUpdate = true; persistentDraw = true; @@ -1574,7 +1243,9 @@ class PlayState extends MusicBeatState FlxG.sound.music.pause(); vocals.pause(); - curStage.resetStage(); + var event:ScriptEvent = new ScriptEvent(ScriptEvent.SONG_RESET, false); + ScriptEventDispatcher.callEvent(currentStage, event); + ModuleHandler.callEvent(event); FlxG.sound.music.time = 0; regenNoteData(); // loads the note data from start @@ -1582,17 +1253,6 @@ class PlayState extends MusicBeatState restartCountdownTimer(); needsReset = false; - - // FlxScreenGrab.grab(null, true, true); - - /* - var png:ByteArray = new ByteArray(); - png = FlxG.camera.screen.pixels.encode(FlxG.camera.screen.pixels.rect, new PNGEncoderOptions()); - var f = sys.io.File.write('./swag.png', true); - f.writeString(png.readUTFBytes(png.length)); - f.close(); - */ - // sys.io.File.saveContent('./swag.png', png.readUTFBytes(png.length)); } #if !debug @@ -1600,16 +1260,6 @@ class PlayState extends MusicBeatState #else if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; - if (FlxG.keys.justPressed.K) - { - // @:privateAccess - // var funnyData:Array = cast FlxG.sound.music._channel.__source.buffer.data; - - // funnyData.reverse(); - - // @:privateAccess - // FlxG.sound.music._channel.__source.buffer.data = cast funnyData; - } #end // do this BEFORE super.update() so songPosition is accurate @@ -1630,7 +1280,7 @@ class PlayState extends MusicBeatState Conductor.songPosition = FlxG.sound.music.time + Conductor.offset; // 20 is THE MILLISECONDS?? // Conductor.songPosition += FlxG.elapsed * 1000; - if (!paused) + if (!isGamePaused) { songTime += FlxG.game.ticks - previousFrameTime; previousFrameTime = FlxG.game.ticks; @@ -1647,19 +1297,16 @@ class PlayState extends MusicBeatState // Conductor.lastSongPos = FlxG.sound.music.time; } - super.update(elapsed); // idk if there's a particular reason why some code is before super.update(), and some is after. Prob nothing too much to worry about. - var androidPause:Bool = false; #if android androidPause = FlxG.android.justPressed.BACK; #end - if ((controls.PAUSE || androidPause) && startedCountdown && canPause) + if ((controls.PAUSE || androidPause) && startedCountdown && mayPauseGame) { persistentUpdate = false; persistentDraw = true; - paused = true; // There is a 1/1000 change to use a special pause menu. // This prevents the player from resuming, but that's the point. @@ -1670,7 +1317,7 @@ class PlayState extends MusicBeatState } else { - var boyfriendPos = boyfriend.getScreenPosition(); + var boyfriendPos = currentStage.getBoyfriend().getScreenPosition(); var pauseSubState = new PauseSubState(boyfriendPos.x, boyfriendPos.y); openSubState(pauseSubState); pauseSubState.camera = camHUD; @@ -1678,7 +1325,7 @@ class PlayState extends MusicBeatState } #if discord_rpc - DiscordClient.changePresence(detailsPausedText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); + DiscordClient.changePresence(detailsPausedText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); #end } @@ -1691,9 +1338,6 @@ class PlayState extends MusicBeatState #end } - // UI UPDATES - scoreTxt.text = "Score:" + songScore; - if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); @@ -1727,9 +1371,6 @@ class PlayState extends MusicBeatState else iconP2.animation.curAnim.curFrame = 0; - /* if (FlxG.keys.justPressed.NINE) - FlxG.switchState(new Charting()); */ - #if debug if (FlxG.keys.justPressed.ONE) endSong(); @@ -1756,7 +1397,7 @@ class PlayState extends MusicBeatState FlxG.watch.addQuick("beatShit", curBeat); FlxG.watch.addQuick("stepShit", curStep); - if (curSong == 'Fresh') + if (currentSong.song == 'Fresh') { switch (curBeat) { @@ -1789,41 +1430,36 @@ class PlayState extends MusicBeatState } #end - if (health <= 0 && !practiceMode) + if (health <= 0 && !isPracticeMode) { - // boyfriend.stunned = true; - persistentUpdate = false; persistentDraw = false; - paused = true; vocals.pause(); FlxG.sound.music.pause(); - // unloadAssets(); - deathCounter += 1; - openSubState(new GameOverSubstate(boyfriend.getScreenPosition().x, boyfriend.getScreenPosition().y)); + openSubState(new GameOverSubstate()); #if discord_rpc // Game Over doesn't get his own variable because it's only used here - DiscordClient.changePresence("Game Over - " + detailsText, SONG.song + " (" + storyDifficultyText + ")", iconRPC); + DiscordClient.changePresence("Game Over - " + detailsText, currentSong.song + " (" + storyDifficultyText + ")", iconRPC); #end } } - while (unspawnNotes[0] != null && unspawnNotes[0].data.strumTime - Conductor.songPosition < 1800 / SongLoad.getSpeed()) + while (inactiveNotes[0] != null && inactiveNotes[0].data.strumTime - Conductor.songPosition < 1800 / SongLoad.getSpeed()) { - var dunceNote:Note = unspawnNotes[0]; - notes.add(dunceNote); + var dunceNote:Note = inactiveNotes[0]; + activeNotes.add(dunceNote); - unspawnNotes.shift(); + inactiveNotes.shift(); } if (generatedMusic) { - notes.forEachAlive(function(daNote:Note) + activeNotes.forEachAlive(function(daNote:Note) { if ((PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height) || (!PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height)) @@ -1837,7 +1473,7 @@ class PlayState extends MusicBeatState daNote.active = true; } - var strumLineMid = strumLine.y + Note.swagWidth / 2; + var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2; if (daNote.followsTime) daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(SongLoad.getSpeed(), @@ -1845,7 +1481,7 @@ class PlayState extends MusicBeatState if (PreferencesMenu.getPref('downscroll')) { - daNote.y += strumLine.y; + daNote.y += playerStrumline.offset.y; if (daNote.isSustainNote) { if (daNote.animation.curAnim.name.endsWith("end") && daNote.prevNote != null) @@ -1863,7 +1499,7 @@ class PlayState extends MusicBeatState else { if (daNote.followsTime) - daNote.y = strumLine.y - daNote.y; + daNote.y = playerStrumline.offset.y - daNote.y; if (daNote.isSustainNote && (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit))) && daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid) @@ -1874,7 +1510,7 @@ class PlayState extends MusicBeatState if (!daNote.mustPress && daNote.wasGoodHit) { - if (SONG.song != 'Tutorial') + if (currentSong.song != 'Tutorial') camZooming = true; var altAnim:String = ""; @@ -1890,16 +1526,16 @@ class PlayState extends MusicBeatState if (!daNote.isSustainNote) { - dad.playAnim('sing' + daNote.dirNameUpper + altAnim, true); + currentStage.getDad().playAnim('sing' + daNote.dirNameUpper + altAnim, true); } - dad.holdTimer = 0; + currentStage.getDad().holdTimer = 0; - if (SONG.needsVoices) + if (currentSong.needsVoices) vocals.volume = 1; daNote.kill(); - notes.remove(daNote, true); + activeNotes.remove(daNote, true); daNote.destroy(); } @@ -1921,7 +1557,7 @@ class PlayState extends MusicBeatState daNote.visible = false; daNote.kill(); - notes.remove(daNote, true); + activeNotes.remove(daNote, true); daNote.destroy(); } } @@ -1930,9 +1566,11 @@ class PlayState extends MusicBeatState // TODO: Why the hell is the noteMiss logic in two different places? if (daNote.tooLate) { - if (curStage != null) + if (currentStage != null) { - curStage.onNoteMiss(daNote); + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, daNote, true); + ScriptEventDispatcher.callEvent(currentStage, event); + ModuleHandler.callEvent(event); } health -= 0.0775; vocals.volume = 0; @@ -1943,7 +1581,7 @@ class PlayState extends MusicBeatState daNote.visible = false; daNote.kill(); - notes.remove(daNote, true); + activeNotes.remove(daNote, true); daNote.destroy(); } }); @@ -1952,18 +1590,20 @@ class PlayState extends MusicBeatState if (!inCutscene) keyShit(); - if (curStage != null) + var event:UpdateScriptEvent = new UpdateScriptEvent(elapsed); + if (currentStage != null) { // We're using Eric's stage handler. - curStage.onUpdate(elapsed); + ScriptEventDispatcher.callEvent(currentStage, event); } + ModuleHandler.callEvent(event); } 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 = strumLine.y + Note.swagWidth / 2; + var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2; if (PreferencesMenu.getPref('downscroll')) { @@ -1981,8 +1621,8 @@ class PlayState extends MusicBeatState function killCombo():Void { - if (combo > 5 && gf.animOffsets.exists('sad')) - gf.playAnim('sad'); + if (combo > 5 && currentStage.getGirlfriend().animOffsets.exists('sad')) + currentStage.getGirlfriend().playAnim('sad'); if (combo != 0) { @@ -1995,7 +1635,7 @@ class PlayState extends MusicBeatState { FlxG.sound.music.pause(); - var daBPM:Float = SONG.bpm; + var daBPM:Float = currentSong.bpm; var daPos:Float = 0; for (i in 0...(Std.int(curStep / 16 + sec))) { @@ -2016,12 +1656,12 @@ class PlayState extends MusicBeatState { seenCutscene = false; deathCounter = 0; - canPause = false; + mayPauseGame = false; FlxG.sound.music.volume = 0; vocals.volume = 0; - if (SONG.validScore) + if (currentSong.validScore) { - Highscore.saveScore(SONG.song, songScore, storyDifficulty); + Highscore.saveScore(currentSong.song, songScore, storyDifficulty); } if (isStoryMode) @@ -2037,7 +1677,7 @@ class PlayState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - switch (PlayState.storyWeek) + switch (storyWeek) { case 7: FlxG.switchState(new VideoState()); @@ -2048,7 +1688,7 @@ class PlayState extends MusicBeatState // if () StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true; - if (SONG.validScore) + if (currentSong.validScore) { NGio.unlockMedal(60961); Highscore.saveWeekScore(storyWeek, campaignScore, storyDifficulty); @@ -2076,7 +1716,7 @@ class PlayState extends MusicBeatState FlxG.sound.music.stop(); vocals.stop(); - if (SONG.song.toLowerCase() == 'eggnog') + 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); @@ -2088,15 +1728,15 @@ class PlayState extends MusicBeatState FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() { // no camFollow so it centers on horror tree - SONG = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]); + currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]); LoadingState.loadAndSwitchState(new PlayState()); }); } else { - prevCamFollow = camFollow; + previousCameraFollowPoint = cameraFollowPoint; - SONG = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]); + currentSong = SongLoad.loadFromJson(storyPlaylist[0].toLowerCase() + difficulty, storyPlaylist[0]); LoadingState.loadAndSwitchState(new PlayState()); } } @@ -2147,9 +1787,11 @@ class PlayState extends MusicBeatState health += healthMulti; // TODO: Redo note hit logic to make sure this always gets called - if (curStage != null) + if (currentStage != null) { - curStage.onNoteHit(daNote); + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, true); + ScriptEventDispatcher.callEvent(currentStage, event); + ModuleHandler.callEvent(event); } if (isSick) @@ -2161,7 +1803,7 @@ class PlayState extends MusicBeatState } // Only add the score if you're not on practice mode - if (!practiceMode) + if (!isPracticeMode) songScore += score; comboPopUps.displayRating(daRating); @@ -2170,47 +1812,48 @@ class PlayState extends MusicBeatState comboPopUps.displayCombo(combo); } - var cameraRightSide:Bool = false; - function cameraMovement() { - if (camFollow.x != dad.getMidpoint().x + 150 && !cameraRightSide) + if (currentStage == null) + return; + + if (cameraFollowPoint.x != currentStage.getDad().getMidpoint().x + 150 && !cameraRightSide) { - camFollow.setPosition(dad.getMidpoint().x + 150, dad.getMidpoint().y - 100); + cameraFollowPoint.setPosition(currentStage.getDad().getMidpoint().x + 150, currentStage.getDad().getMidpoint().y - 100); // camFollow.setPosition(lucky.getMidpoint().x - 120, lucky.getMidpoint().y + 210); - switch (dad.curCharacter) + switch (currentStage.getDad().curCharacter) { case 'mom': - camFollow.y = dad.getMidpoint().y; + cameraFollowPoint.y = currentStage.getDad().getMidpoint().y; case 'senpai' | 'senpai-angry': - camFollow.y = dad.getMidpoint().y - 430; - camFollow.x = dad.getMidpoint().x - 100; + cameraFollowPoint.y = currentStage.getDad().getMidpoint().y - 430; + cameraFollowPoint.x = currentStage.getDad().getMidpoint().x - 100; } - if (dad.curCharacter == 'mom') + if (currentStage.getDad().curCharacter == 'mom') vocals.volume = 1; - if (SONG.song.toLowerCase() == 'tutorial') + if (currentSong.song.toLowerCase() == 'tutorial') tweenCamIn(); } - if (cameraRightSide && camFollow.x != boyfriend.getMidpoint().x - 100) + if (cameraRightSide && cameraFollowPoint.x != currentStage.getBoyfriend().getMidpoint().x - 100) { - camFollow.setPosition(boyfriend.getMidpoint().x - 100, boyfriend.getMidpoint().y - 100); + cameraFollowPoint.setPosition(currentStage.getBoyfriend().getMidpoint().x - 100, currentStage.getBoyfriend().getMidpoint().y - 100); - switch (curStageId) + switch (currentStageId) { case 'limo': - camFollow.x = boyfriend.getMidpoint().x - 300; + cameraFollowPoint.x = currentStage.getBoyfriend().getMidpoint().x - 300; case 'mall': - camFollow.y = boyfriend.getMidpoint().y - 200; + cameraFollowPoint.y = currentStage.getBoyfriend().getMidpoint().y - 200; case 'school' | 'schoolEvil': - camFollow.x = boyfriend.getMidpoint().x - 200; - camFollow.y = boyfriend.getMidpoint().y - 200; + cameraFollowPoint.x = currentStage.getBoyfriend().getMidpoint().x - 200; + cameraFollowPoint.y = currentStage.getBoyfriend().getMidpoint().y - 200; } - if (SONG.song.toLowerCase() == 'tutorial') + if (currentSong.song.toLowerCase() == 'tutorial') FlxTween.tween(FlxG.camera, {zoom: 1 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut}); } } @@ -2231,64 +1874,10 @@ class PlayState extends MusicBeatState controls.NOTE_UP_R, controls.NOTE_RIGHT_R ]; - /* - var widHalf = FlxG.width / 2; - var heightHalf = FlxG.height / 2; - - if (FlxG.onMobile) - { - for (touch in FlxG.touches.list) - { - var getHeight:Int = Math.floor(touch.justPressedPosition.y / (FlxG.height / 3)); - var getWid:Int = Math.floor(touch.justPressedPosition.x / (FlxG.width / 4)); - if (touch.justPressed) - { - switch (getWid) - { - case 1: - pressArray[3] = true; - case 2: - pressArray[0] = true; - default: - switch (getHeight) - { - case 0: - pressArray[2] = true; - case 1: - touch.justPressedPosition.x < widHalf ? pressArray[0] = true : pressArray[3] = true; - case 2: - pressArray[1] = true; - } - } - } - - switch (getWid) - { - case 1: - holdArray[3] = true; - - case 2: - holdArray[0] = true; - - default: - switch (getHeight) - { - case 0: - holdArray[2] = true; - case 1: - touch.justPressedPosition.x < widHalf ? holdArray[0] = true : holdArray[3] = true; - case 2: - holdArray[1] = true; - } - } - } - } - */ - // HOLDS, check for sustain notes - if (holdArray.contains(true) && /*!boyfriend.stunned && */ generatedMusic) + if (holdArray.contains(true) && generatedMusic) { - notes.forEachAlive(function(daNote:Note) + activeNotes.forEachAlive(function(daNote:Note) { if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) goodNoteHit(daNote); @@ -2296,17 +1885,17 @@ class PlayState extends MusicBeatState } // PRESSES, check for note hits - if (pressArray.contains(true) && /*!boyfriend.stunned && */ generatedMusic) + if (pressArray.contains(true) && generatedMusic) { Haptic.vibrate(100, 100); - boyfriend.holdTimer = 0; + 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 - notes.forEachAlive(function(daNote:Note) + activeNotes.forEachAlive(function(daNote:Note) { if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit) { @@ -2341,7 +1930,7 @@ class PlayState extends MusicBeatState { FlxG.log.add("killing dumb ass note at " + note.data.strumTime); note.kill(); - notes.remove(note, true); + activeNotes.remove(note, true); note.destroy(); } @@ -2370,11 +1959,13 @@ class PlayState extends MusicBeatState } } - if (boyfriend.holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) + if (currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) { - if (boyfriend.animation.curAnim.name.startsWith('sing') && !boyfriend.animation.curAnim.name.endsWith('miss')) + if (currentStage.getBoyfriend().animation != null + && currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing') + && !currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss')) { - boyfriend.playAnim('idle'); + currentStage.getBoyfriend().playAnim('idle'); } } @@ -2385,7 +1976,7 @@ class PlayState extends MusicBeatState if (!holdArray[spr.ID]) spr.animation.play('static'); - if (spr.animation.curAnim.name == 'confirm' && !curStageId.startsWith('school')) + if (spr.animation.curAnim.name == 'confirm' && !currentStageId.startsWith('school')) { spr.centerOffsets(); spr.offset.x -= 13; @@ -2396,77 +1987,21 @@ class PlayState extends MusicBeatState }); } - function performCleanup() - { - // Uncache the song. - openfl.utils.Assets.cache.clear(Paths.inst(SONG.song)); - openfl.utils.Assets.cache.clear(Paths.voices(SONG.song)); - - // Remove reference to stage and remove sprites from it to save memory. - if (curStage != null) - { - remove(curStage); - curStage.kill(); - curStage = null; - } - - // Clear the static reference to this state. - instance = null; - } - - /** - * This function is called before switching to a new FlxState. - */ - override function switchTo(nextState:FlxState):Bool - { - performCleanup(); - - return super.switchTo(nextState); - } - function noteMiss(direction:NoteDir = 1):Void { // whole function used to be encased in if (!boyfriend.stunned) health -= 0.07; killCombo(); - if (!practiceMode) + if (!isPracticeMode) songScore -= 10; vocals.volume = 0; FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); - /* boyfriend.stunned = true; - - // get stunned for 5 seconds - new FlxTimer().start(5 / 60, function(tmr:FlxTimer) - { - boyfriend.stunned = false; - });*/ - - boyfriend.playAnim('sing' + direction.nameUpper + 'miss', true); + currentStage.getBoyfriend().playAnim('sing' + direction.nameUpper + 'miss', true); } - /* not used anymore lol - - function badNoteHit() - { - // just double pasting this shit cuz fuk u - // REDO THIS SYSTEM! - var leftP = controls.NOTE_LEFT_P; - var downP = controls.NOTE_DOWN_P; - var upP = controls.NOTE_UP_P; - var rightP = controls.NOTE_RIGHT_P; - - if (leftP) - noteMiss(0); - if (downP) - noteMiss(1); - if (upP) - noteMiss(2); - if (rightP) - noteMiss(3); - }*/ function goodNoteHit(note:Note):Void { if (!note.wasGoodHit) @@ -2477,7 +2012,7 @@ class PlayState extends MusicBeatState popUpScore(note.data.strumTime, note); } - boyfriend.playAnim('sing' + note.dirNameUpper, true); + currentStage.getBoyfriend().playAnim('sing' + note.dirNameUpper, true); playerStrums.forEach(function(spr:FlxSprite) { @@ -2493,33 +2028,27 @@ class PlayState extends MusicBeatState if (!note.isSustainNote) { note.kill(); - notes.remove(note, true); + activeNotes.remove(note, true); note.destroy(); } } } - function resetCamFollow():Void - { - FlxG.camera.follow(camFollow, LOCKON, 0.04); - // FlxG.camera.setScrollBounds(0, FlxG.width, 0, FlxG.height); - FlxG.camera.zoom = defaultCamZoom; - FlxG.camera.focusOn(camFollow.getPosition()); - } - override function stepHit() { super.stepHit(); if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 20 - || (SONG.needsVoices && Math.abs(vocals.time - (Conductor.songPosition - Conductor.offset)) > 20)) + || (currentSong.needsVoices && Math.abs(vocals.time - (Conductor.songPosition - Conductor.offset)) > 20)) { resyncVocals(); } - if (curStage != null) + if (currentStage != null) { - // We're using Eric's stage handler. The stage should know that a beat has been hit. - curStage.onStepHit(curBeat); + // We're using Eric's stage handler. The stage should know that a step has been hit. + var event:SongTimeScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, curBeat, curStep); + ScriptEventDispatcher.callEvent(currentStage, event); + ModuleHandler.callEvent(event); } } @@ -2529,7 +2058,7 @@ class PlayState extends MusicBeatState if (generatedMusic) { - notes.sort(sortNotes, FlxSort.DESCENDING); + activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); } if (SongLoad.getSong()[Math.floor(curStep / 16)] != null) @@ -2539,16 +2068,13 @@ class PlayState extends MusicBeatState Conductor.changeBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm); FlxG.log.add('CHANGED BPM!'); } - // else - // Conductor.changeBPM(SONG.bpm); } - // FlxG.log.add('change bpm' + SONG.notes[SongLoad.curDiff][Std.int(curStep / 16)].changeBPM); // HARDCODING FOR MILF ZOOMS! if (PreferencesMenu.getPref('camera-zoom')) { - if (curSong.toLowerCase() == 'milf' && curBeat >= 168 && curBeat < 200 && camZooming && FlxG.camera.zoom < 1.35) + if (currentSong.song.toLowerCase() == 'milf' && curBeat >= 168 && curBeat < 200 && camZooming && FlxG.camera.zoom < 1.35) { FlxG.camera.zoom += 0.015 * FlxCamera.defaultZoom; camHUD.zoom += 0.03; @@ -2577,7 +2103,6 @@ class PlayState extends MusicBeatState { var animShit:ComboCounter = new ComboCounter(-100, 300, combo); animShit.scrollFactor.set(0.6, 0.6); - // add(animShit); var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation @@ -2588,65 +2113,194 @@ class PlayState extends MusicBeatState } if (curBeat % gfSpeed == 0) - gf.dance(); + currentStage.getGirlfriend().dance(); if (curBeat % 2 == 0) { - if (boyfriend.animation != null && !boyfriend.animation.curAnim.name.startsWith("sing")) - boyfriend.playAnim('idle'); - if (dad.animation != null && !dad.animation.curAnim.name.startsWith("sing")) - dad.dance(); + if (currentStage.getBoyfriend().animation != null && !currentStage.getBoyfriend().animation.curAnim.name.startsWith("sing")) + currentStage.getBoyfriend().playAnim('idle'); + if (currentStage.getDad().animation != null && !currentStage.getDad().animation.curAnim.name.startsWith("sing")) + currentStage.getDad().dance(); } - else if (dad.curCharacter == 'spooky') + else if (currentStage.getDad().curCharacter == 'spooky') { - if (!dad.animation.curAnim.name.startsWith("sing")) - dad.dance(); + if (!currentStage.getDad().animation.curAnim.name.startsWith("sing")) + currentStage.getDad().dance(); } - if (curBeat % 8 == 7 && curSong == 'Bopeebo') + if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo') { - boyfriend.playAnim('hey', true); + currentStage.getBoyfriend().playAnim('hey', true); } - if (curBeat % 16 == 15 && SONG.song == 'Tutorial' && dad.curCharacter == 'gf' && curBeat > 16 && curBeat < 48) + if (curBeat % 16 == 15 + && currentSong.song == 'Tutorial' + && currentStage.getDad().curCharacter == 'gf' + && curBeat > 16 + && curBeat < 48) { - boyfriend.playAnim('hey', true); - dad.playAnim('cheer', true); + currentStage.getBoyfriend().playAnim('hey', true); + currentStage.getDad().playAnim('cheer', true); } - if (curStage != null) + if (currentStage != null) { // We're using Eric's stage handler. The stage should know that a beat has been hit. - curStage.onBeatHit(curBeat); + var event:SongTimeScriptEvent = new SongTimeScriptEvent(ScriptEvent.SONG_STEP_HIT, curBeat, curStep); + ScriptEventDispatcher.callEvent(currentStage, event); + ModuleHandler.callEvent(event); } } - var curLight:Int = 0; -} + function buildStrumlines():Void + { + var strumlineStyle:StrumlineStyle = NORMAL; -typedef StageData = -{ - var camZoom:Float; - var propsBackground:Array; -} + // TODO: Put this in the chart or something? + switch (currentStageId) + { + case 'school': + strumlineStyle = PIXEL; + case 'schoolEvil': + strumlineStyle = PIXEL; + } -typedef Props = -{ - var x:Float; - var y:Float; - var scrollX:Float; - var scrollY:Float; - var propname:String; - var path:String; - var scaleX:Float; - var scaleY:Float; - var ?animBullshit:PropAnimData; - var ?updateHitbox:Bool; - var ?antialiasing:Bool; -} + var strumlineYPos = Strumline.getYPos(); -typedef PropAnimData = -{ - var isLooping:Bool; - var anims:Array; + playerStrumline = new Strumline(0, strumlineStyle, 4); + playerStrumline.offset = new FlxPoint(50 + FlxG.width / 2, strumlineYPos); + playerStrumline.zIndex = 100; + add(playerStrumline); + playerStrumline.cameras = [camHUD]; + + enemyStrumline = new Strumline(1, strumlineStyle, 4); + enemyStrumline.offset = new FlxPoint(50, strumlineYPos); + enemyStrumline.zIndex = 100; + add(enemyStrumline); + enemyStrumline.cameras = [camHUD]; + + 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 restart timer. + if (!startTimer.finished) + startTimer.active = false; + } + + super.openSubState(subState); + } + + /** + * Function called before closing the current substate. + * @param subState + */ + override function closeSubState() + { + if (isGamePaused) + { + if (FlxG.sound.music != null && !startingSong) + resyncVocals(); + + if (!startTimer.finished) + startTimer.active = true; + + #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(); + } + + /** + * 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. + */ + function resetCamera():Void + { + FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04); + FlxG.camera.zoom = defaultCamZoom; + FlxG.camera.focusOn(cameraFollowPoint.getPosition()); + } + + /** + * Perform necessary cleanup before leaving the PlayState. + */ + function performCleanup() + { + // Uncache the song. + 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(); + currentStage = null; + } + + // Clear the static reference to this state. + instance = null; + } + + /** + * Refreshes the state, by redoing the render order of all elements. + * It does this based on the `zIndex` of each element. + */ + public function refresh() + { + sort(SortUtil.byZIndex, FlxSort.ASCENDING); + trace('Stage sorted by z-index'); + } + + /** + * This function is called whenever Flixel switches switching to a new FlxState. + */ + override function switchTo(nextState:FlxState):Bool + { + performCleanup(); + + return super.switchTo(nextState); + } } diff --git a/source/funkin/play/Strumline.hx b/source/funkin/play/Strumline.hx new file mode 100644 index 000000000..b583b9aab --- /dev/null +++ b/source/funkin/play/Strumline.hx @@ -0,0 +1,245 @@ +package funkin.play; + +import funkin.ui.PreferencesMenu; +import funkin.Note.NoteColor; +import funkin.Note.NoteDir; +import funkin.Note.NoteType; +import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; +import funkin.util.Constants; +import flixel.FlxSprite; +import flixel.math.FlxPoint; +import flixel.group.FlxGroup.FlxTypedGroup; + +/** + * A group controlling the individual notes of the strumline for a given player. + */ +class Strumline extends FlxTypedGroup +{ + public var offset(default, set):FlxPoint = new FlxPoint(0, 0); + + function set_offset(value:FlxPoint):FlxPoint + { + this.offset = value; + updatePositions(); + return value; + } + + /** + * The style of the strumline. + * Options are normal and pixel. + */ + var style:StrumlineStyle; + + /** + * The player this strumline belongs to. + * 0 is Player 1, etc. + */ + var playerId:Int; + + /** + * The number of notes in the strumline. + */ + var size:Int; + + public function new(playerId:Int = 0, style:StrumlineStyle = NORMAL, size:Int = 4) + { + super(0); + this.playerId = playerId; + this.style = style; + this.size = size; + + generateStrumline(); + } + + function generateStrumline():Void + { + for (index in 0...size) + { + createStrumlineArrow(index); + } + } + + function createStrumlineArrow(index:Int):Void + { + var arrow:FlxSprite = new FlxSprite(0, 0); + + arrow.ID = index; + + // Color changing for arrows is a WIP. + /* + var colorSwapShader:ColorSwap = new ColorSwap(); + colorSwapShader.update(Note.arrowColors[i]); + arrow.shader = colorSwapShader; + */ + + switch (style) + { + case NORMAL: + createNormalNote(arrow); + case PIXEL: + createPixelNote(arrow); + } + + arrow.updateHitbox(); + arrow.scrollFactor.set(); + + arrow.animation.play('static'); + + applyFadeIn(arrow); + + add(arrow); + } + + /** + * Apply a small animation which moves the arrow down and fades it in. + * Only plays at the start of Free Play songs I guess? + * @param arrow The arrow to animate. + * @param index The index of the arrow in the strumline. + */ + function applyFadeIn(arrow:FlxSprite):Void + { + if (!PlayState.isStoryMode) + { + arrow.y -= 10; + arrow.alpha = 0; + FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)}); + } + } + + /** + * Applies the default note style to an arrow. + * @param arrow The arrow to apply the style to. + * @param index The index of the arrow in the strumline. + */ + function createNormalNote(arrow:FlxSprite):Void + { + arrow.frames = Paths.getSparrowAtlas('NOTE_assets'); + + arrow.animation.addByPrefix('green', 'arrowUP'); + arrow.animation.addByPrefix('blue', 'arrowDOWN'); + arrow.animation.addByPrefix('purple', 'arrowLEFT'); + arrow.animation.addByPrefix('red', 'arrowRIGHT'); + + arrow.setGraphicSize(Std.int(arrow.width * 0.7)); + arrow.antialiasing = true; + + arrow.x += Note.swagWidth * arrow.ID; + + switch (Math.abs(arrow.ID)) + { + case 0: + arrow.animation.addByPrefix('static', 'arrow static instance 1'); + arrow.animation.addByPrefix('pressed', 'left press', 24, false); + arrow.animation.addByPrefix('confirm', 'left confirm', 24, false); + case 1: + arrow.animation.addByPrefix('static', 'arrow static instance 2'); + arrow.animation.addByPrefix('pressed', 'down press', 24, false); + arrow.animation.addByPrefix('confirm', 'down confirm', 24, false); + case 2: + arrow.animation.addByPrefix('static', 'arrow static instance 4'); + arrow.animation.addByPrefix('pressed', 'up press', 24, false); + arrow.animation.addByPrefix('confirm', 'up confirm', 24, false); + case 3: + arrow.animation.addByPrefix('static', 'arrow static instance 3'); + arrow.animation.addByPrefix('pressed', 'right press', 24, false); + arrow.animation.addByPrefix('confirm', 'right confirm', 24, false); + } + } + + /** + * Applies the pixel note style to an arrow. + * @param arrow The arrow to apply the style to. + * @param index The index of the arrow in the strumline. + */ + function createPixelNote(arrow:FlxSprite):Void + { + arrow.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); + + arrow.animation.add('purplel', [4]); + arrow.animation.add('blue', [5]); + arrow.animation.add('green', [6]); + arrow.animation.add('red', [7]); + + arrow.setGraphicSize(Std.int(arrow.width * Constants.PIXEL_ART_SCALE)); + arrow.updateHitbox(); + + // Forcibly disable anti-aliasing on pixel graphics to stop blur. + arrow.antialiasing = false; + + arrow.x += Note.swagWidth * arrow.ID; + + // TODO: Seems weird that these are hardcoded... + switch (Math.abs(arrow.ID)) + { + case 0: + arrow.animation.add('static', [0]); + arrow.animation.add('pressed', [4, 8], 12, false); + arrow.animation.add('confirm', [12, 16], 24, false); + case 1: + arrow.animation.add('static', [1]); + arrow.animation.add('pressed', [5, 9], 12, false); + arrow.animation.add('confirm', [13, 17], 24, false); + case 2: + arrow.animation.add('static', [2]); + arrow.animation.add('pressed', [6, 10], 12, false); + arrow.animation.add('confirm', [14, 18], 12, false); + case 3: + arrow.animation.add('static', [3]); + arrow.animation.add('pressed', [7, 11], 12, false); + arrow.animation.add('confirm', [15, 19], 24, false); + } + } + + function updatePositions() + { + for (arrow in members) + { + arrow.x = Note.swagWidth * arrow.ID; + arrow.x += offset.x; + + arrow.y = 0; + arrow.y += offset.y; + } + } + + /** + * Retrieves the arrow at the given position in the strumline. + * @param index The index to retrieve. + * @return The corresponding FlxSprite. + */ + public inline function getArrow(value:Int):FlxSprite + { + // members maintains the order that the arrows were added. + return this.members[value]; + } + + public inline function getArrowByNoteType(value:NoteType):FlxSprite + { + return getArrow(value.int); + } + + public inline function getArrowByNoteDir(value:NoteDir):FlxSprite + { + return getArrow(value.int); + } + + public inline function getArrowByNoteColor(value:NoteColor):FlxSprite + { + return getArrow(value.int); + } + + public static inline function getYPos():Int + { + return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50; + } +} + +/** + * TODO: Unhardcode this and make it part of the note style system. + */ +enum StrumlineStyle +{ + NORMAL; + PIXEL; +} diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 3105847b2..d73b38c56 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -1,12 +1,18 @@ package funkin.play.stage; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; +import funkin.modding.events.ScriptEvent.CountdownScriptEvent; +import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import flixel.FlxSprite; /** * A Bopper is a stage prop which plays a dance animation. * Y'know, a thingie that bops. A bopper. */ -class Bopper extends FlxSprite +class Bopper extends FlxSprite implements IPlayStateScriptedClass { /** * The bopper plays the dance animation once every `danceEvery` beats. @@ -76,9 +82,9 @@ class Bopper extends FlxSprite /** * Called once every beat of the song. */ - public function onBeatHit(curBeat:Int):Void + public function onBeatHit(event:SongTimeScriptEvent):Void { - if (curBeat % danceEvery == 0) + if (event.beat % danceEvery == 0) { dance(); } @@ -87,7 +93,7 @@ class Bopper extends FlxSprite /** * Called every `danceEvery` beats of the song. */ - public function dance():Void + function dance():Void { if (this.animation == null) { @@ -116,4 +122,36 @@ class Bopper extends FlxSprite this.animation.play('idle$idleSuffix'); } } + + public function onScriptEvent(event:ScriptEvent) {} + + public function onCreate(event:ScriptEvent) {} + + public function onDestroy(event:ScriptEvent) {} + + public function onUpdate(event:UpdateScriptEvent) {} + + public function onPause(event:ScriptEvent) {} + + public function onResume(event:ScriptEvent) {} + + public function onSongStart(event:ScriptEvent) {} + + public function onSongEnd(event:ScriptEvent) {} + + public function onSongReset(event:ScriptEvent) {} + + public function onGameOver(event:ScriptEvent) {} + + public function onGameRetry(event:ScriptEvent) {} + + public function onNoteHit(event:NoteScriptEvent) {} + + public function onNoteMiss(event:NoteScriptEvent) {} + + public function onStepHit(event:SongTimeScriptEvent) {} + + public function onCountdownStart(event:CountdownScriptEvent) {} + + public function onCountdownStep(event:CountdownScriptEvent) {} } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 502e8ed76..b5d77b8f0 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -1,5 +1,10 @@ package funkin.play.stage; +import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.events.ScriptEvent; +import funkin.modding.events.ScriptEvent.CountdownScriptEvent; +import funkin.modding.events.ScriptEvent.KeyboardInputScriptEvent; +import funkin.modding.IScriptedClass; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; @@ -14,7 +19,7 @@ import funkin.util.SortUtil; * * A Stage is comprised of one or more props, each of which is a FlxSprite. */ -class Stage extends FlxSpriteGroup implements IHook +class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScriptedClass implements IInputScriptedClass { public final stageId:String; public final stageName:String; @@ -50,16 +55,24 @@ class Stage extends FlxSpriteGroup implements IHook } } + /** + * Called when the player is moving into the PlayState where the song will be played. + */ + public function onCreate(event:ScriptEvent):Void + { + buildStage(); + this.refresh(); + } + /** * The default stage construction routine. Called when the stage is going to be played in. * Instantiates each prop and adds it to the stage, while setting its parameters. */ - public function buildStage() + function buildStage() { trace('Building stage for display: ${this.stageId}'); this.camZoom = _data.cameraZoom; - // this.scrollFactor = new FlxPoint(1, 1); for (dataProp in _data.props) { @@ -162,8 +175,6 @@ class Stage extends FlxSpriteGroup implements IHook } trace(' Prop placed.'); } - - this.refresh(); } /** @@ -200,39 +211,6 @@ class Stage extends FlxSpriteGroup implements IHook trace('Stage sorted by z-index'); } - /** - * Resets the stage and it's props (needs to be overridden with your own logic!) - */ - public function resetStage() - { - // Override me in your script to reset stage shit however you please! - // also note: maybe add some default behaviour to reset stage stuff? - } - - /** - * A function that should get called every frame. - */ - public function onUpdate(elapsed:Float):Void - { - // Override me in your scripted stage to perform custom behavior! - } - - /** - * A function that gets called when the player hits a note. - */ - public function onNoteHit(note:Note):Void - { - // Override me in your scripted stage to perform custom behavior! - } - - /** - * A function that gets called when the player hits a note. - */ - public function onNoteMiss(note:Note):Void - { - // Override me in your scripted stage to perform custom behavior! - } - /** * Adjusts the position and other properties of the soon-to-be child of this sprite group. * Private helper to avoid duplicate code in `add()` and `insert()`. @@ -253,30 +231,6 @@ class Stage extends FlxSpriteGroup implements IHook clipRectTransform(sprite, clipRect); } - /** - * A function that gets called once per step in the song. - * @param curStep The current step number. - */ - public function onStepHit(curStep:Int):Void - { - // Override me in your scripted stage to perform custom behavior! - } - - /** - * A function that gets called once per beat in the song (once every four steps). - * @param curStep The current beat number. - */ - public function onBeatHit(curBeat:Int):Void - { - // Override me in your scripted stage to perform custom behavior! - // Make sure to call super.onBeatHit(curBeat) if you want to keep the boppers dancing. - - for (bopper in boppers) - { - bopper.onBeatHit(curBeat); - } - } - /** * Used by the PlayState to add a character to the stage. */ @@ -360,9 +314,11 @@ class Stage extends FlxSpriteGroup implements IHook /** * Perform cleanup for when you are leaving the level. */ - public override function kill() + public function onDestroy(event:ScriptEvent):Void { - super.kill(); + // Make sure to call kill() when returning a stage to cache, + // and destroy() only when performing a hard cache refresh. + kill(); for (prop in this.namedProps) { @@ -390,13 +346,56 @@ class Stage extends FlxSpriteGroup implements IHook } /** - * Perform cleanup for when you are destroying the stage - * and removing all its data from cache. - * - * Call this ONLY when you are performing a hard cache clear. + * A function that gets called once per step in the song. + * @param curStep The current step number. */ - public override function destroy() + public function onStepHit(event:SongTimeScriptEvent):Void {} + + /** + * A function that gets called once per beat in the song (once every four steps). + * @param curStep The current beat number. + */ + public function onBeatHit(event:SongTimeScriptEvent):Void { - super.destroy(); + // Override me in your scripted stage to perform custom behavior! + // Make sure to call super.onBeatHit(curBeat) if you want to keep the boppers dancing. + + ScriptEventDispatcher.callEventOnAllTargets(cast boppers, event); } + + public function onScriptEvent(event:ScriptEvent) {} + + public function onPause(event:ScriptEvent) {} + + public function onResume(event:ScriptEvent) {} + + public function onSongStart(event:ScriptEvent) {} + + public function onSongEnd(event:ScriptEvent) {} + + /** + * Resets the stage and its props. + */ + public function onSongReset(event:ScriptEvent) {} + + public function onGameOver(event:ScriptEvent) {} + + public function onGameRetry(event:ScriptEvent) {} + + public function onCountdownStart(event:CountdownScriptEvent) {} + + public function onCountdownStep(event:CountdownScriptEvent) {} + + public function onKeyDown(event:KeyboardInputScriptEvent) {} + + public function onKeyUp(event:KeyboardInputScriptEvent) {} + + /** + * A function that should get called every frame. + */ + public function onUpdate(event:UpdateScriptEvent) {} + + public function onNoteHit(event:NoteScriptEvent) {} + + public function onNoteMiss(event:NoteScriptEvent) {} } diff --git a/source/funkin/ui/PopUpStuff.hx b/source/funkin/ui/PopUpStuff.hx index 02f08687d..0542ff14c 100644 --- a/source/funkin/ui/PopUpStuff.hx +++ b/source/funkin/ui/PopUpStuff.hx @@ -1,5 +1,6 @@ package funkin.ui; +import funkin.util.Constants; import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.tweens.FlxTween; @@ -22,7 +23,7 @@ class PopUpStuff extends FlxTypedGroup var rating:FlxSprite = new FlxSprite(); var ratingPath:String = daRating; - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel"; rating.loadGraphic(Paths.image(ratingPath)); @@ -40,9 +41,9 @@ class PopUpStuff extends FlxTypedGroup add(rating); - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) { - rating.setGraphicSize(Std.int(rating.width * PlayState.daPixelZoom * 0.7)); + rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); } else { @@ -69,7 +70,7 @@ class PopUpStuff extends FlxTypedGroup var pixelShitPart1:String = ""; var pixelShitPart2:String = ''; - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) { pixelShitPart1 = 'weeb/pixelUI/'; pixelShitPart2 = '-pixel'; @@ -90,9 +91,9 @@ class PopUpStuff extends FlxTypedGroup add(comboSpr); - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) { - comboSpr.setGraphicSize(Std.int(comboSpr.width * PlayState.daPixelZoom * 0.7)); + comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7)); } else { @@ -129,9 +130,9 @@ class PopUpStuff extends FlxTypedGroup var numScore:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2)); numScore.y = comboSpr.y; - if (PlayState.curStageId.startsWith('school')) + if (PlayState.instance.currentStageId.startsWith('school')) { - numScore.setGraphicSize(Std.int(numScore.width * PlayState.daPixelZoom)); + numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE)); } else { diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx new file mode 100644 index 000000000..d3e307d1d --- /dev/null +++ b/source/funkin/util/Constants.hx @@ -0,0 +1,14 @@ +package funkin.util; + +import flixel.util.FlxColor; + +class Constants +{ + /** + * The scale factor to use when increasing the size of pixel art graphics. + */ + public static final PIXEL_ART_SCALE = 6; + + public static final HEALTH_BAR_RED:FlxColor = 0xFFFF0000; + public static final HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33; +} diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx index 29f64fb0d..b0bd798dc 100644 --- a/source/funkin/util/SortUtil.hx +++ b/source/funkin/util/SortUtil.hx @@ -1,9 +1,8 @@ package funkin.util; #if !macro -import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.FlxBasic; import flixel.util.FlxSort; -import flixel.FlxObject; #end class SortUtil @@ -12,8 +11,18 @@ class SortUtil * You can use this function in FlxTypedGroup.sort() to sort FlxObjects by their z-index values. * The value defaults to 0, but by assigning it you can easily rearrange objects as desired. */ - public static inline function byZIndex(Order:Int, Obj1:FlxObject, Obj2:FlxObject):Int + public static inline function byZIndex(Order:Int, Obj1:FlxBasic, Obj2:FlxBasic):Int { return FlxSort.byValues(Order, Obj1.zIndex, Obj2.zIndex); } + + /** + * Given two Notes, returns 1 or -1 based on whether `a` or `b` has an earlier strumtime. + * + * @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING` + */ + public static inline function byStrumtime(order:Int, a:Note, b:Note) + { + return FlxSort.byValues(order, a.data.strumTime, b.data.strumTime); + } } diff --git a/source/modding/module/Module.hx b/source/modding/module/Module.hx new file mode 100644 index 000000000..51964a82b --- /dev/null +++ b/source/modding/module/Module.hx @@ -0,0 +1,80 @@ +package modding.module; + +import modding.module.events.ModuleEvent; + +/** + * A module is an interface which provides for scripts to perform custom behavior + * without requiring a specific context. + * + * You may have the module active at all times, or only when another script enables it. + */ +class Module +{ + /** + * Whether the module is currently active. + */ + public var active(default, set):Bool = false; + + function set_active(value:Bool):Bool + { + this.active = value; + return value; + } + + public var moduleId(default, null):String = 'UNKNOWN'; + + /** + * Called when the module is initialized. + * It may not be safe to reference other modules here since they may not be loaded yet. + * + * @param startActive Whether to start with the module active. + * If false, the module will be inactive and must be enabled by another script, + * such as a stage or another module. + */ + public function new(moduleId:String, startActive:Bool) + { + this.moduleId = moduleId; + this.active = startActive; + } + + /** + * Called after the module was initialized, but before anything else. + * Other modules may still be uninitialized at this stage. + */ + public function onPostCreate() {} + + /** + * Called at the beginning of a song, before the countdown begins. + */ + public function onSongStart() {} + + /** + * Called at the end of a song, after the song fades out. + */ + public function onSongEnd() {} + + /** + * Called at the beginning of the countdown. + */ + public function onBeginCountdown(event:ModuleEvent) {} + + /** + * Called four times per section of a song. + */ + public function onSongBeat() {} + + /** + * Called sixteen times per section of a song. + */ + public function onSongStep() {} + + /** + * Called at the end of the `update()` loop. + * Be careful! Using this can have a significant impact on performance. + */ + public function onUpdate(event:UpdateModuleEvent) {} + + public function onNoteHit() {} + + public function onNoteMiss() {} +} diff --git a/source/modding/module/ModuleHandler.hx b/source/modding/module/ModuleHandler.hx new file mode 100644 index 000000000..1d6179951 --- /dev/null +++ b/source/modding/module/ModuleHandler.hx @@ -0,0 +1,79 @@ +package modding.module; + +import modding.module.ModuleEvent; +import modding.module.ModuleEvent.UpdateModuleEvent; + +class ModuleHandler +{ + static final moduleCache:Map = new Map(); + + /** + * Whether modules start active by default. + */ + static final DEFAULT_STARTACTIVE:Bool = true; + + /** + * 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, DEFAULT_STARTACTIVE); + if (module != null) + { + trace(' Loaded module: ${moduleCls}'); + + // Then store it. + moduleCache.set(module.moduleId, module); + } + else + { + trace(' Failed to instantiate module: ${moduleCls}'); + } + } + + trace("[MODULEHANDLER] Module cache loaded."); + + call_onPostCreate(); + } + + static function clearModuleCache():Void + { + if (moduleCache != null) + { + moduleCache.clear(); + } + } + + /** + * Calls onPostCreate on all modules. + */ + public static function call_onPostCreate():Void + { + for (module in moduleCache) + { + module.onPostCreate(); + } + } + + /** + * Calls onUpdate on all modules. + */ + public static function call_onUpdate(elapsed:Float):Void + { + var event = new UpdateModuleEvent(elapsed); + for (module in moduleCache) + { + module.onUpdate(event); + } + } +} diff --git a/source/modding/module/ScriptedModule.hx b/source/modding/module/ScriptedModule.hx new file mode 100644 index 000000000..f311e08ca --- /dev/null +++ b/source/modding/module/ScriptedModule.hx @@ -0,0 +1,9 @@ +package modding.module; + +import modding.IHook; + +@:hscriptClass +class ScriptedModule extends Module implements IHook +{ + // No body needed for this class, it's magic ;) +} diff --git a/source/modding/module/events/ModuleEvent.hx b/source/modding/module/events/ModuleEvent.hx new file mode 100644 index 000000000..892b28ab4 --- /dev/null +++ b/source/modding/module/events/ModuleEvent.hx @@ -0,0 +1,128 @@ +package modding.module; + +import openfl.events.EventType; + +typedef ModuleEventType = EventType; + +class ModuleEvent +{ + public static inline var SONG_START:ModuleEventType = "SONG_START"; + public static inline var SONG_END:ModuleEventType = "SONG_END"; + public static inline var COUNTDOWN_BEGIN:ModuleEventType = "COUNTDOWN_BEGIN"; + public static inline var COUNTDOWN_STEP:ModuleEventType = "COUNTDOWN_STEP"; + public static inline var SONG_BEAT_HIT:ModuleEventType = "SONG_BEAT_HIT"; + public static inline var SONG_STEP_HIT:ModuleEventType = "SONG_STEP_HIT"; + + public static inline var PAUSE:ModuleEventType = "PAUSE"; + public static inline var RESUME:ModuleEventType = "RESUME"; + public static inline var UPDATE:ModuleEventType = "UPDATE"; + + /** + * Note hit success, health gained, note data, player vs opponent, etc + * are all provided as event parameters. + * + * Event is cancelable, which will cause the press to be ignored and the note to be missed. + */ + public static inline var NOTE_HIT:ModuleEventType = "NOTE_HIT"; + + public static inline var NOTE_MISS:ModuleEventType = "NOTE_MISS"; + public static inline var GAME_OVER:ModuleEventType = "GAME_OVER"; + public static inline var RETRY:ModuleEventType = "RETRY"; + + /** + * If true, the behavior associated with this event can be prevented. + * For example, cancelling COUNTDOWN_BEGIN 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; + + /** + * The type associated with the event. + */ + public var type(default, null):ModuleEventType; + + @:noCompletion private var __eventCanceled:Bool; + @:noCompletion private var __shouldPropagate:Bool; + + public function new(type:ModuleEventType, cancelable:Bool = false):Void + { + this.type = type; + this.cancelable = cancelable; + this.__eventCanceled = false; + this.__shouldPropagate = true; + } + + /** + * Call this function on a cancelable event to cancel the associated behavior. + * For example, cancelling COUNTDOWN_BEGIN will prevent the countdown from starting. + */ + public function cancelEvent():Void + { + if (cancelable) + { + __eventCanceled = true; + } + } + + /** + * Call this function to stop any other modules from receiving the event. + */ + public function stopPropagation():Void + { + __shouldPropagate = false; + } + + public function toString():String + { + return 'ModuleEvent(type=$type, cancelable=$cancelable)'; + } +} + +/** + * SPECIFIC EVENTS + */ +/** + * An event that is fired associated with a specific note. + */ +class NoteModuleEvent extends ModuleEvent +{ + /** + * The note associated with this event. + * You cannot replace it, but you can edit it. + */ + public var note(default, null):Note; + + public function new(type:ModuleEventType, note:Note, cancelable:Bool = false):Void + { + super(type, cancelable); + this.note = note; + } + + public override function toString():String + { + return 'NoteModuleEvent(type=' + type + ', cancelable=' + cancelable + ', note=' + note + ')'; + } +} + +/** + * An event that is fired during the update loop. + */ +class UpdateModuleEvent extends ModuleEvent +{ + /** + * 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(ModuleEvent.UPDATE, false); + this.elapsed = elapsed; + } + + public override function toString():String + { + return 'UpdateModuleEvent(elapsed=$elapsed)'; + } +} diff --git a/source/modding/module/events/NoteModuleEvent.hx b/source/modding/module/events/NoteModuleEvent.hx new file mode 100644 index 000000000..3e235064f --- /dev/null +++ b/source/modding/module/events/NoteModuleEvent.hx @@ -0,0 +1 @@ +package modding.module.events;