From 60c6e5ee29dae494b53ebcf358dc725548578671 Mon Sep 17 00:00:00 2001 From: Eric Myllyoja <ericmyllyoja@gmail.com> Date: Mon, 14 Mar 2022 20:48:45 -0400 Subject: [PATCH] Various bug fixes for strumlines --- source/funkin/MusicBeatState.hx | 5 - source/funkin/modding/IHook.hx | 5 +- source/funkin/modding/IScriptedClass.hx | 19 +- source/funkin/modding/events/ScriptEvent.hx | 28 ++ .../modding/events/ScriptEventDispatcher.hx | 13 +- source/funkin/modding/module/Module.hx | 20 +- source/funkin/modding/module/ModuleHandler.hx | 12 + source/funkin/play/Countdown.hx | 2 - source/funkin/play/PlayState.hx | 124 ++++---- source/funkin/play/Strumline.hx | 274 +++++++++--------- source/funkin/play/VanillaCutscenes.hx | 3 +- source/funkin/play/stage/Stage.hx | 17 +- source/funkin/util/Constants.hx | 9 + source/funkin/util/macro/GitCommit.hx | 35 +++ source/funkin/util/macro/HookableMacro.hx | 69 ++++- 15 files changed, 410 insertions(+), 225 deletions(-) create mode 100644 source/funkin/util/macro/GitCommit.hx diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 9b961f176..2dce0272b 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -2,16 +2,11 @@ package funkin; import flixel.util.FlxColor; import flixel.text.FlxText; -import cpp.abi.Abi; import funkin.modding.events.ScriptEvent; import funkin.modding.module.ModuleHandler; import funkin.modding.events.ScriptEvent.UpdateScriptEvent; import funkin.Conductor.BPMChangeEvent; -import flixel.FlxGame; -import flixel.addons.transition.FlxTransitionableState; import flixel.addons.ui.FlxUIState; -import flixel.math.FlxRect; -import flixel.util.FlxTimer; class MusicBeatState extends FlxUIState { diff --git a/source/funkin/modding/IHook.hx b/source/funkin/modding/IHook.hx index d9bc298b7..66a07ec52 100644 --- a/source/funkin/modding/IHook.hx +++ b/source/funkin/modding/IHook.hx @@ -5,10 +5,13 @@ import polymod.hscript.HScriptable; /** * Functions annotated with @:hscript will call the relevant script. * Functions annotated with @:hookable can be reassigned. + * NOTE: If you receive the following error when making a function use @:hookable: + * `Cannot access this or other member field in variable initialization` + * This is because you need to perform calls and assignments using a static variable referencing the target object. */ @:hscript({ // ALL of these values are added to ALL scripts in the child classes. context: [FlxG, FlxSprite, Math, Paths, Std] }) -// @:autoBuild(funkin.util.macro.HookableMacro.build()) +@:autoBuild(funkin.util.macro.HookableMacro.build()) interface IHook extends HScriptable {} diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 9452d04db..9691c1417 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -17,15 +17,24 @@ interface IScriptedClass } /** - * Defines a set of callbacks available to scripted classes that involve player input. + * Defines a set of callbacks available to scripted classes which can follow the game between states. */ -interface IInputScriptedClass extends IScriptedClass +interface IStateChangingScriptedClass extends IScriptedClass { - public function onKeyDown(event:KeyboardInputScriptEvent):Void; - public function onKeyUp(event:KeyboardInputScriptEvent):Void; - // TODO: OnMouseDown, OnMouseUp, OnMouseMove + public function onStateChangeBegin(event:StateChangeScriptEvent):Void; + public function onStateChangeEnd(event:StateChangeScriptEvent):Void; } +/** + * Developer note: + * + * I previously considered adding events for onKeyDown, onKeyUp, mouse events, etc. + * However, I realized that you can simply call something like the following within a module: + * `FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);` + * This is more efficient than adding an entire event handler for every key press. + * + * -Eric + */ /** * Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State. */ diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index efc922ed5..ce4c08743 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -165,6 +165,18 @@ class ScriptEvent */ public static inline final SONG_LOADED:ScriptEventType = "SONG_LOADED"; + /** + * Called when the game is entering the current FlxState. + * + * This event is not cancelable. + */ + public static inline final STATE_ENTER:ScriptEventType = "STATE_ENTER"; + + /** + * Called when the game is exiting the current FlxState. + * + * This event is not cancelable. + */ /** * If true, the behavior associated with this event can be prevented. * For example, cancelling COUNTDOWN_BEGIN should prevent the countdown from starting, @@ -385,3 +397,19 @@ class SongLoadScriptEvent extends ScriptEvent return 'SongLoadScriptEvent(notes=$noteStr, id=$id, difficulty=$difficulty)'; } } + +/** + * An event that is fired when moving out of or into an FlxState. + */ +class StateChangeScriptEvent extends ScriptEvent +{ + public function new(type:ScriptEventType):Void + { + super(type, false); + } + + public override function toString():String + { + return 'StateChangeScriptEvent(type=' + type + ')'; + } +} diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index c3df66170..ecb97a846 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -1,7 +1,6 @@ package funkin.modding.events; import funkin.modding.IScriptedClass; -import funkin.modding.IScriptedClass.IInputScriptedClass; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; /** @@ -36,16 +35,14 @@ class ScriptEventDispatcher return; } - if (Std.isOfType(target, IInputScriptedClass)) + if (Std.isOfType(target, IStateChangingScriptedClass)) { - var t = cast(target, IInputScriptedClass); + var t = cast(target, IStateChangingScriptedClass); + var t = cast(target, IPlayStateScriptedClass); switch (event.type) { - case ScriptEvent.KEY_DOWN: - t.onKeyDown(cast event); - return; - case ScriptEvent.KEY_UP: - t.onKeyUp(cast event); + case ScriptEvent.NOTE_HIT: + t.onNoteHit(cast event); return; } } diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 87b933ccc..3f499c5e1 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -7,13 +7,13 @@ 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; +import funkin.modding.IScriptedClass.IStateChangingScriptedClass; /** * 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 +class Module implements IPlayStateScriptedClass implements IStateChangingScriptedClass { /** * Whether the module is currently active. @@ -68,16 +68,20 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass public function onScriptEvent(event:ScriptEvent) {} + /** + * Called when the module is first created. + * This happens before the title screen appears! + */ public function onCreate(event:ScriptEvent) {} + /** + * Called when a module is destroyed. + * This currently only happens when reloading modules with F5. + */ public function onDestroy(event:ScriptEvent) {} public function onUpdate(event:UpdateScriptEvent) {} - public function onKeyDown(event:KeyboardInputScriptEvent) {} - - public function onKeyUp(event:KeyboardInputScriptEvent) {} - public function onPause(event:ScriptEvent) {} public function onResume(event:ScriptEvent) {} @@ -107,4 +111,8 @@ class Module implements IInputScriptedClass implements IPlayStateScriptedClass public function onCountdownEnd(event:CountdownScriptEvent) {} public function onSongLoaded(eent:SongLoadScriptEvent) {} + + public function onStateChangeBegin(event:StateChangeScriptEvent) {} + + public function onStateChangeEnd(event:StateChangeScriptEvent) {} } diff --git a/source/funkin/modding/module/ModuleHandler.hx b/source/funkin/modding/module/ModuleHandler.hx index d5a296638..908b5c428 100644 --- a/source/funkin/modding/module/ModuleHandler.hx +++ b/source/funkin/modding/module/ModuleHandler.hx @@ -96,10 +96,22 @@ class ModuleHandler } } + /** + * Clear the module cache, forcing all modules to call shutdown events. + */ public static function clearModuleCache():Void { if (moduleCache != null) { + var event = new ScriptEvent(ScriptEvent.DESTROY, false); + + // Note: Ignore stopPropagation() + for (key => value in moduleCache) + { + ScriptEventDispatcher.callEvent(value, event); + moduleCache.remove(key); + } + moduleCache.clear(); modulePriorityOrder = []; } diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index d6770b11a..8413110ca 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -4,8 +4,6 @@ import funkin.util.Constants; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.FlxSprite; -import flixel.input.actions.FlxAction.FlxActionAnalog; -import cpp.abi.Abi; import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.module.ModuleHandler; import funkin.modding.events.ScriptEvent; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 3925288fc..61ede4246 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,6 +1,6 @@ package funkin.play; -import funkin.play.Strumline.StrumlineStyle; +import funkin.play.Strumline.StrumlineArrow; import flixel.addons.effects.FlxTrail; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; @@ -24,12 +24,13 @@ 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.IHook; import funkin.modding.module.ModuleHandler; import funkin.Note; import funkin.play.stage.Stage; import funkin.play.stage.StageData; +import funkin.play.Strumline.StrumlineStyle; import funkin.Section.SwagSection; -import funkin.shaderslmfao.ColorSwap; import funkin.SongLoad.SwagSong; import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; @@ -43,7 +44,7 @@ using StringTools; import Discord.DiscordClient; #end -class PlayState extends MusicBeatState +class PlayState extends MusicBeatState implements IHook { /** * STATIC VARIABLES @@ -139,11 +140,6 @@ class PlayState extends MusicBeatState */ private var inactiveNotes:Array<Note>; - /** - * 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. @@ -231,7 +227,6 @@ class PlayState extends MusicBeatState private var vocals:VoicesGroup; private var vocalsFinished:Bool = false; - private var playerStrums:FlxTypedGroup<FlxSprite>; private var camZooming:Bool = false; private var gfSpeed:Int = 1; private var combo:Int = 0; @@ -331,8 +326,6 @@ class PlayState extends MusicBeatState add(grpNoteSplashes); - playerStrums = new FlxTypedGroup<FlxSprite>(); - generateSong(); cameraFollowPoint = new FlxObject(0, 0, 1, 1); @@ -407,6 +400,10 @@ class PlayState extends MusicBeatState case 'guns': VanillaCutscenes.playGunsCutscene(); default: + // VanillaCutscenes will call startCountdown later. + // TODO: Alternatively: make a song script that allows startCountdown to be called, + // then cancels the countdown, hides the strumline, plays the cutscene, + // then calls Countdown.performCountdown() startCountdown(); } } @@ -415,8 +412,9 @@ class PlayState extends MusicBeatState startCountdown(); } - // this.leftWatermarkText.text = '${currentSong.song.toUpperCase()} - ${SongLoad.curDiff.toUpperCase()}'; + #if debug this.rightWatermarkText.text = Constants.VERSION; + #end } /** @@ -936,6 +934,7 @@ class PlayState extends MusicBeatState super.update(elapsed); updateHealthBar(); + updateScoreText(); if (needsReset) { @@ -1173,7 +1172,7 @@ class PlayState extends MusicBeatState daNote.active = true; } - var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2; + var strumLineMid = playerStrumline.y + Note.swagWidth / 2; if (daNote.followsTime) daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(SongLoad.getSpeed(), @@ -1181,7 +1180,7 @@ class PlayState extends MusicBeatState if (PreferencesMenu.getPref('downscroll')) { - daNote.y += playerStrumline.offset.y; + daNote.y += playerStrumline.y; if (daNote.isSustainNote) { if (daNote.animation.curAnim.name.endsWith("end") && daNote.prevNote != null) @@ -1199,7 +1198,7 @@ class PlayState extends MusicBeatState else { if (daNote.followsTime) - daNote.y = playerStrumline.offset.y - daNote.y; + daNote.y = playerStrumline.y - daNote.y; if (daNote.isSustainNote && (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit))) && daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid) @@ -1284,7 +1283,7 @@ class PlayState extends MusicBeatState } if (!isInCutscene) - keyShit(); + keyShit(true); dispatchEvent(new UpdateScriptEvent(elapsed)); } @@ -1293,7 +1292,7 @@ class PlayState extends MusicBeatState { // clipRect is applied to graphic itself so use frame Heights var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight); - var strumLineMid = playerStrumline.offset.y + Note.swagWidth / 2; + var strumLineMid = playerStrumline.y + Note.swagWidth / 2; if (PreferencesMenu.getPref('downscroll')) { @@ -1549,7 +1548,14 @@ class PlayState extends MusicBeatState } } - private function keyShit():Void + public var test:(PlayState) -> Void = function(instance:PlayState) + { + trace('test'); + trace(instance.currentStageId); + }; + + @:hookable + public function keyShit(test:Bool):Void { // control arrays, order L D R U var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT]; @@ -1566,27 +1572,27 @@ class PlayState extends MusicBeatState controls.NOTE_RIGHT_R ]; // HOLDS, check for sustain notes - if (holdArray.contains(true) && generatedMusic) + if (holdArray.contains(true) && PlayState.instance.generatedMusic) { - activeNotes.forEachAlive(function(daNote:Note) + PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) { if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) - goodNoteHit(daNote); + PlayState.instance.goodNoteHit(daNote); }); } // PRESSES, check for note hits - if (pressArray.contains(true) && generatedMusic) + if (pressArray.contains(true) && PlayState.instance.generatedMusic) { Haptic.vibrate(100, 100); - currentStage.getBoyfriend().holdTimer = 0; + PlayState.instance.currentStage.getBoyfriend().holdTimer = 0; var possibleNotes:Array<Note> = []; // notes that can be hit var directionList:Array<Int> = []; // directions that can be hit var dumbNotes:Array<Note> = []; // notes to kill later - activeNotes.forEachAlive(function(daNote:Note) + PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) { if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit) { @@ -1621,63 +1627,60 @@ class PlayState extends MusicBeatState { FlxG.log.add("killing dumb ass note at " + note.data.strumTime); note.kill(); - activeNotes.remove(note, true); + PlayState.instance.activeNotes.remove(note, true); note.destroy(); } possibleNotes.sort((a, b) -> Std.int(a.data.strumTime - b.data.strumTime)); - if (perfectMode) - goodNoteHit(possibleNotes[0]); + if (PlayState.instance.perfectMode) + PlayState.instance.goodNoteHit(possibleNotes[0]); else if (possibleNotes.length > 0) { for (shit in 0...pressArray.length) { // if a direction is hit that shouldn't be if (pressArray[shit] && !directionList.contains(shit)) - noteMiss(shit); + PlayState.instance.noteMiss(shit); } for (coolNote in possibleNotes) { if (pressArray[coolNote.data.noteData]) - goodNoteHit(coolNote); + PlayState.instance.goodNoteHit(coolNote); } } else { for (shit in 0...pressArray.length) if (pressArray[shit]) - noteMiss(shit); + PlayState.instance.noteMiss(shit); } } - if (currentStage == null) + if (PlayState.instance.currentStage == null) return; - if (currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) + if (PlayState.instance.currentStage.getBoyfriend().holdTimer > Conductor.stepCrochet * 4 * 0.001 && !holdArray.contains(true)) { - if (currentStage.getBoyfriend().animation != null - && currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing') - && !currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss')) + if (PlayState.instance.currentStage.getBoyfriend().animation != null + && PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.startsWith('sing') + && !PlayState.instance.currentStage.getBoyfriend().animation.curAnim.name.endsWith('miss')) { - currentStage.getBoyfriend().playAnim('idle'); + PlayState.instance.currentStage.getBoyfriend().playAnim('idle'); } } - playerStrums.forEach(function(spr:FlxSprite) + for (keyId => isPressed in pressArray) { - if (pressArray[spr.ID] && spr.animation.curAnim.name != 'confirm') - spr.animation.play('pressed'); - if (!holdArray[spr.ID]) - spr.animation.play('static'); + var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId); - if (spr.animation.curAnim.name == 'confirm' && !currentStageId.startsWith('school')) + if (isPressed && arrow.animation.curAnim.name != 'confirm') { - spr.centerOffsets(); - spr.offset.x -= 13; - spr.offset.y -= 13; + arrow.playAnimation('pressed'); } - else - spr.centerOffsets(); - }); + if (!holdArray[keyId]) + { + arrow.playAnimation('static'); + } + } } function noteMiss(direction:NoteDir = 1):Void @@ -1707,13 +1710,7 @@ class PlayState extends MusicBeatState currentStage.getBoyfriend().playAnim('sing' + note.dirNameUpper, true); - playerStrums.forEach(function(spr:FlxSprite) - { - if (Math.abs(note.data.noteData) == spr.ID) - { - spr.animation.play('confirm', true); - } - }); + playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true); note.wasGoodHit = true; vocals.volume = 1; @@ -1848,19 +1845,31 @@ class PlayState extends MusicBeatState var strumlineYPos = Strumline.getYPos(); playerStrumline = new Strumline(0, strumlineStyle, 4); - playerStrumline.offset = new FlxPoint(50 + FlxG.width / 2, strumlineYPos); + playerStrumline.x = 50 + FlxG.width / 2; + playerStrumline.y = strumlineYPos; // Set the z-index so they don't appear in front of notes. playerStrumline.zIndex = 100; add(playerStrumline); playerStrumline.cameras = [camHUD]; + if (!isStoryMode) + { + playerStrumline.fadeInArrows(); + } + enemyStrumline = new Strumline(1, strumlineStyle, 4); - enemyStrumline.offset = new FlxPoint(50, strumlineYPos); + enemyStrumline.x = 50; + enemyStrumline.y = strumlineYPos; // Set the z-index so they don't appear in front of notes. enemyStrumline.zIndex = 100; add(enemyStrumline); enemyStrumline.cameras = [camHUD]; + if (!isStoryMode) + { + enemyStrumline.fadeInArrows(); + } + this.refresh(); } @@ -1997,6 +2006,7 @@ class PlayState extends MusicBeatState { remove(currentStage); currentStage.kill(); + dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false)); currentStage = null; } diff --git a/source/funkin/play/Strumline.hx b/source/funkin/play/Strumline.hx index 4254acb28..1bdbde374 100644 --- a/source/funkin/play/Strumline.hx +++ b/source/funkin/play/Strumline.hx @@ -1,30 +1,23 @@ package funkin.play; -import funkin.ui.PreferencesMenu; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.FlxSprite; +import flixel.math.FlxPoint; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; import funkin.Note.NoteColor; import funkin.Note.NoteDir; import funkin.Note.NoteType; -import flixel.tweens.FlxTween; -import flixel.tweens.FlxEase; +import funkin.ui.PreferencesMenu; 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. + * + * FUN FACT: Setting the X and Y of a FlxSpriteGroup will move all the sprites in the group. */ -class Strumline extends FlxTypedGroup<FlxSprite> +class Strumline extends FlxTypedSpriteGroup<StrumlineArrow> { - 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. @@ -62,132 +55,30 @@ class Strumline extends FlxTypedGroup<FlxSprite> 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); - + var arrow:StrumlineArrow = new StrumlineArrow(index, style); 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? + * Only plays at the start of Free Play songs. + * + * Note that modifying the offset of the whole strumline won't have the * @param arrow The arrow to animate. * @param index The index of the arrow in the strumline. */ - function applyFadeIn(arrow:FlxSprite):Void + function fadeInArrow(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)}); - } + 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 + public function fadeInArrows():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)) + for (arrow in this.members) { - 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 like this... no XML? - 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); + fadeInArrow(arrow); } } @@ -208,33 +99,150 @@ class Strumline extends FlxTypedGroup<FlxSprite> * @param index The index to retrieve. * @return The corresponding FlxSprite. */ - public inline function getArrow(value:Int):FlxSprite + public inline function getArrow(value:Int):StrumlineArrow { // members maintains the order that the arrows were added. return this.members[value]; } - public inline function getArrowByNoteType(value:NoteType):FlxSprite + public inline function getArrowByNoteType(value:NoteType):StrumlineArrow { return getArrow(value.int); } - public inline function getArrowByNoteDir(value:NoteDir):FlxSprite + public inline function getArrowByNoteDir(value:NoteDir):StrumlineArrow { return getArrow(value.int); } - public inline function getArrowByNoteColor(value:NoteColor):FlxSprite + public inline function getArrowByNoteColor(value:NoteColor):StrumlineArrow { return getArrow(value.int); } + /** + * Get the default Y offset of the strumline. + * @return Int + */ public static inline function getYPos():Int { return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50; } } +class StrumlineArrow extends FlxSprite +{ + var style:StrumlineStyle; + + public function new(id:Int, style:StrumlineStyle) + { + super(0, 0); + + this.ID = id; + this.style = style; + + // TODO: Unhardcode this. Maybe use a note style system> + switch (style) + { + case PIXEL: + buildPixelGraphic(); + case NORMAL: + buildNormalGraphic(); + } + + this.updateHitbox(); + scrollFactor.set(0, 0); + animation.play('static'); + } + + public function playAnimation(anim:String, force:Bool = false) + { + animation.play(anim, force); + centerOffsets(); + centerOrigin(); + } + + /** + * Applies the default note style to an arrow. + */ + function buildNormalGraphic():Void + { + this.frames = Paths.getSparrowAtlas('NOTE_assets'); + + this.animation.addByPrefix('green', 'arrowUP'); + this.animation.addByPrefix('blue', 'arrowDOWN'); + this.animation.addByPrefix('purple', 'arrowLEFT'); + this.animation.addByPrefix('red', 'arrowRIGHT'); + + this.setGraphicSize(Std.int(this.width * 0.7)); + this.antialiasing = true; + + this.x += Note.swagWidth * this.ID; + + switch (Math.abs(this.ID)) + { + case 0: + this.animation.addByPrefix('static', 'arrow static instance 1'); + this.animation.addByPrefix('pressed', 'left press', 24, false); + this.animation.addByPrefix('confirm', 'left confirm', 24, false); + case 1: + this.animation.addByPrefix('static', 'arrow static instance 2'); + this.animation.addByPrefix('pressed', 'down press', 24, false); + this.animation.addByPrefix('confirm', 'down confirm', 24, false); + case 2: + this.animation.addByPrefix('static', 'arrow static instance 4'); + this.animation.addByPrefix('pressed', 'up press', 24, false); + this.animation.addByPrefix('confirm', 'up confirm', 24, false); + case 3: + this.animation.addByPrefix('static', 'arrow static instance 3'); + this.animation.addByPrefix('pressed', 'right press', 24, false); + this.animation.addByPrefix('confirm', 'right confirm', 24, false); + } + } + + /** + * Applies the pixel note style to an arrow. + */ + function buildPixelGraphic():Void + { + this.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); + + this.animation.add('purplel', [4]); + this.animation.add('blue', [5]); + this.animation.add('green', [6]); + this.animation.add('red', [7]); + + this.setGraphicSize(Std.int(this.width * Constants.PIXEL_ART_SCALE)); + this.updateHitbox(); + + // Forcibly disable anti-aliasing on pixel graphics to stop blur. + this.antialiasing = false; + + this.x += Note.swagWidth * this.ID; + + // TODO: Seems weird that these are hardcoded like this... no XML? + switch (Math.abs(this.ID)) + { + case 0: + this.animation.add('static', [0]); + this.animation.add('pressed', [4, 8], 12, false); + this.animation.add('confirm', [12, 16], 24, false); + case 1: + this.animation.add('static', [1]); + this.animation.add('pressed', [5, 9], 12, false); + this.animation.add('confirm', [13, 17], 24, false); + case 2: + this.animation.add('static', [2]); + this.animation.add('pressed', [6, 10], 12, false); + this.animation.add('confirm', [14, 18], 12, false); + case 3: + this.animation.add('static', [3]); + this.animation.add('pressed', [7, 11], 12, false); + this.animation.add('confirm', [15, 19], 24, false); + } + } +} + /** * TODO: Unhardcode this and make it part of the note style system. */ diff --git a/source/funkin/play/VanillaCutscenes.hx b/source/funkin/play/VanillaCutscenes.hx index 2d3d059a6..a67ff8773 100644 --- a/source/funkin/play/VanillaCutscenes.hx +++ b/source/funkin/play/VanillaCutscenes.hx @@ -61,7 +61,8 @@ class VanillaCutscenes blackScreen = null; FlxTween.tween(FlxG.camera, {zoom: PlayState.defaultCameraZoom}, (Conductor.crochet / 1000) * 5, {ease: FlxEase.quadInOut}); - Countdown.performCountdown(false); + @:privateAccess + PlayState.instance.startCountdown(); @:privateAccess PlayState.instance.cameraMovement(); } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 6bbc48282..a17f340d4 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -19,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 implements IPlayStateScriptedClass implements IInputScriptedClass +class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScriptedClass { public final stageId:String; public final stageName:String; @@ -312,7 +312,8 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte } /** - * Perform cleanup for when you are leaving the level. + * onDestroy gets called when the player is leaving the PlayState, + * and is used to clean up any objects that need to be destroyed. */ public function onDestroy(event:ScriptEvent):Void { @@ -322,24 +323,32 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte for (prop in this.namedProps) { + remove(prop); + prop.kill(); prop.destroy(); } namedProps.clear(); for (char in this.characters) { + remove(char); + char.kill(); char.destroy(); } characters.clear(); for (bopper in boppers) { + remove(bopper); + bopper.kill(); bopper.destroy(); } boppers = []; for (sprite in this.group) { + remove(sprite); + sprite.kill(); sprite.destroy(); } group.clear(); @@ -391,10 +400,6 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte public function onCountdownEnd(event:CountdownScriptEvent) {} - public function onKeyDown(event:KeyboardInputScriptEvent) {} - - public function onKeyUp(event:KeyboardInputScriptEvent) {} - /** * A function that should get called every frame. */ diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index f6c3b204f..fb121e144 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -18,8 +18,17 @@ class Constants public static final VERSION_SUFFIX = ' PROTOTYPE'; public static var VERSION(get, null):String; + #if debug + public static final GIT_HASH = funkin.util.macro.GitCommit.getGitCommitHash(); + + static function get_VERSION():String + { + return 'v${Application.current.meta.get('version')} (${GIT_HASH})' + VERSION_SUFFIX; + } + #else static function get_VERSION():String { return 'v${Application.current.meta.get('version')}' + VERSION_SUFFIX; } + #end } diff --git a/source/funkin/util/macro/GitCommit.hx b/source/funkin/util/macro/GitCommit.hx new file mode 100644 index 000000000..14c68639a --- /dev/null +++ b/source/funkin/util/macro/GitCommit.hx @@ -0,0 +1,35 @@ +package funkin.util.macro; + +#if debug +class GitCommit +{ + public static macro function getGitCommitHash():haxe.macro.Expr.ExprOf<String> + { + #if !display + // Get the current line number. + var pos = haxe.macro.Context.currentPos(); + + var process = new sys.io.Process('git', ['rev-parse', 'HEAD']); + if (process.exitCode() != 0) + { + var message = process.stderr.readAll().toString(); + haxe.macro.Context.info('[WARN] Could not determine current git commit; is this a proper Git repository?', pos); + } + + // read the output of the process + var commitHash:String = process.stdout.readLine(); + var commitHashSplice:String = commitHash.substr(0, 7); + + trace('Git Commit ID ${commitHashSplice}'); + + // Generates a string expression + return macro $v{commitHashSplice}; + #else + // `#if display` is used for code completion. In this case returning an + // empty string is good enough; We don't want to call git on every hint. + var commitHash:String = ""; + return macro $v{commitHashSplice}; + #end + } +} +#end diff --git a/source/funkin/util/macro/HookableMacro.hx b/source/funkin/util/macro/HookableMacro.hx index 93d9545af..967a92cb6 100644 --- a/source/funkin/util/macro/HookableMacro.hx +++ b/source/funkin/util/macro/HookableMacro.hx @@ -1,3 +1,70 @@ package funkin.util.macro; -class HookableMacro {} +import haxe.macro.Context; +import haxe.macro.Expr; + +using Lambda; + +class HookableMacro +{ + /** + * The @:hookable annotation replaces a given function with a variable that contains a function. + * It's still callable, like normal, but now you can also replace the value! Neat! + * + * NOTE: If you receive the following error when making a function use @:hookable: + * `Cannot access this or other member field in variable initialization` + * This is because you need to perform calls and assignments using a static variable referencing the target object. + */ + public static macro function build():Array<Field> + { + Context.info('Running HookableMacro...', Context.currentPos()); + + var cls:haxe.macro.Type.ClassType = Context.getLocalClass().get(); + var fields:Array<Field> = Context.getBuildFields(); + // Find all fields with @:hookable metadata + for (field in fields) + { + if (field.meta == null) + continue; + var scriptable_meta = field.meta.find(function(m) return m.name == ':hookable'); + if (scriptable_meta != null) + { + Context.info(' @:hookable annotation found on field ${field.name}', Context.currentPos()); + switch (field.kind) + { + case FFun(originalFunc): + // This is the type of the function, like (Int, Int) -> Int + var replFieldTypeRet:ComplexType = originalFunc.ret == null ? Context.toComplexType(Context.getType('Void')) : originalFunc.ret; + var replFieldType:ComplexType = TFunction([for (arg in originalFunc.args) arg.type], replFieldTypeRet); + // This is the expression of the function, i.e. the function body. + + var replFieldExpr:ExprDef = EFunction(FAnonymous, { + ret: originalFunc.ret, + params: originalFunc.params, + args: originalFunc.args, + expr: originalFunc.expr + }); + + var replField:Field = { + name: field.name, + doc: field.doc, + access: field.access, + pos: field.pos, + meta: field.meta, + kind: FVar(replFieldType, { + expr: replFieldExpr, + pos: field.pos + }), + }; + + // Replace the original field with the new field + fields[fields.indexOf(field)] = replField; + default: + Context.error('@:hookable can only be used on functions', field.pos); + } + } + } + + return fields; + } +}