diff --git a/.prettierignore b/.prettierignore index c92ea0bae..657ff3812 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,8 +1,10 @@ # Ignore artifacts export -# Ignore all asset files (including FlxAnimate JSONs) -assets +# Ignore all JSONS in the images folder (including FlxAnimate JSONs) +assets/preload/images +assets/shared/images # Don't ignore data files -!assets/preload/data +# TODO: These don't work. +!assets/preload/data/ diff --git a/.vscode/settings.json b/.vscode/settings.json index ec86904ea..87ed06aed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,6 +71,7 @@ "haxe.displayPort": "auto", "haxe.enableCompilationServer": false, + "haxe.enableServerView": true, "haxe.displayServer": { "arguments": ["-v"] }, diff --git a/assets b/assets index 9a55f8b24..d0fe6c223 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 9a55f8b24bd41a8b8f03302b4f155e18b97774ff +Subproject commit d0fe6c22328b2bd82bd3f18fe32f09bc4b0ce57a diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx index 2e3e0e425..5a9351bba 100644 --- a/source/funkin/PlayerSettings.hx +++ b/source/funkin/PlayerSettings.hx @@ -84,9 +84,9 @@ class PlayerSettings function addKeyboard():Void { var useDefault = true; - if (Save.get().hasControls(id, Keys)) + if (Save.instance.hasControls(id, Keys)) { - var keyControlData = Save.get().getControls(id, Keys); + var keyControlData = Save.instance.getControls(id, Keys); trace("keyControlData: " + haxe.Json.stringify(keyControlData)); useDefault = false; controls.fromSaveData(keyControlData, Keys); @@ -112,9 +112,9 @@ class PlayerSettings function addGamepad(gamepad:FlxGamepad) { var useDefault = true; - if (Save.get().hasControls(id, Gamepad(gamepad.id))) + if (Save.instance.hasControls(id, Gamepad(gamepad.id))) { - var padControlData = Save.get().getControls(id, Gamepad(gamepad.id)); + var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id)); trace("padControlData: " + haxe.Json.stringify(padControlData)); useDefault = false; controls.addGamepadWithSaveData(gamepad.id, padControlData); @@ -141,7 +141,7 @@ class PlayerSettings if (keyData != null) { trace("saving key data: " + haxe.Json.stringify(keyData)); - Save.get().setControls(id, Keys, keyData); + Save.instance.setControls(id, Keys, keyData); } if (controls.gamepadsAdded.length > 0) @@ -150,7 +150,7 @@ class PlayerSettings if (padData != null) { trace("saving pad data: " + haxe.Json.stringify(padData)); - Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData); + Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData); } } } diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx index 039a4c285..60c7a996a 100644 --- a/source/funkin/Preferences.hx +++ b/source/funkin/Preferences.hx @@ -15,12 +15,12 @@ class Preferences static function get_naughtyness():Bool { - return Save.get().options.naughtyness; + return Save.instance.options.naughtyness; } static function set_naughtyness(value:Bool):Bool { - var save = Save.get(); + var save = Save.instance; save.options.naughtyness = value; save.flush(); return value; @@ -34,12 +34,12 @@ class Preferences static function get_downscroll():Bool { - return Save.get().options.downscroll; + return Save.instance.options.downscroll; } static function set_downscroll(value:Bool):Bool { - var save = Save.get(); + var save = Save.instance; save.options.downscroll = value; save.flush(); return value; @@ -53,12 +53,12 @@ class Preferences static function get_flashingLights():Bool { - return Save.get().options.flashingLights; + return Save.instance.options.flashingLights; } static function set_flashingLights(value:Bool):Bool { - var save = Save.get(); + var save = Save.instance; save.options.flashingLights = value; save.flush(); return value; @@ -72,12 +72,12 @@ class Preferences static function get_zoomCamera():Bool { - return Save.get().options.zoomCamera; + return Save.instance.options.zoomCamera; } static function set_zoomCamera(value:Bool):Bool { - var save = Save.get(); + var save = Save.instance; save.options.zoomCamera = value; save.flush(); return value; @@ -91,17 +91,17 @@ class Preferences static function get_debugDisplay():Bool { - return Save.get().options.debugDisplay; + return Save.instance.options.debugDisplay; } static function set_debugDisplay(value:Bool):Bool { - if (value != Save.get().options.debugDisplay) + if (value != Save.instance.options.debugDisplay) { toggleDebugDisplay(value); } - var save = Save.get(); + var save = Save.instance; save.options.debugDisplay = value; save.flush(); return value; @@ -115,14 +115,14 @@ class Preferences static function get_autoPause():Bool { - return Save.get().options.autoPause; + return Save.instance.options.autoPause; } static function set_autoPause(value:Bool):Bool { - if (value != Save.get().options.autoPause) FlxG.autoPause = value; + if (value != Save.instance.options.autoPause) FlxG.autoPause = value; - var save = Save.get(); + var save = Save.instance; save.options.autoPause = value; save.flush(); return value; diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx index c8289fc46..e9902a798 100644 --- a/source/funkin/api/newgrounds/NGUtil.hx +++ b/source/funkin/api/newgrounds/NGUtil.hx @@ -86,10 +86,10 @@ class NGUtil #end var onSessionFail:Error->Void = null; - if (sessionId == null && Save.get().ngSessionId != null) + if (sessionId == null && Save.instance.ngSessionId != null) { trace("using stored session id"); - sessionId = Save.get().ngSessionId; + sessionId = Save.instance.ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGUtil static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - Save.get().ngSessionId = NG.core.sessionId; - Save.get().flush(); + Save.instance.ngSessionId = NG.core.sessionId; + Save.instance.flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGUtil { NG.core.logOut(); - Save.get().ngSessionId = null; - Save.get().flush(); + Save.instance.ngSessionId = null; + Save.instance.flush(); } // --- MEDALS diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx index e505bdedf..c1f8ad3ba 100644 --- a/source/funkin/api/newgrounds/NGio.hx +++ b/source/funkin/api/newgrounds/NGio.hx @@ -86,10 +86,10 @@ class NGio #end var onSessionFail:Error->Void = null; - if (sessionId == null && Save.get().ngSessionId != null) + if (sessionId == null && Save.instance.ngSessionId != null) { trace("using stored session id"); - sessionId = Save.get().ngSessionId; + sessionId = Save.instance.ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGio static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - Save.get().ngSessionId = NG.core.sessionId; - Save.get().flush(); + Save.instance.ngSessionId = NG.core.sessionId; + Save.instance.flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGio { NG.core.logOut(); - Save.get().ngSessionId = null; - Save.get().flush(); + Save.instance.ngSessionId = null; + Save.instance.flush(); } // --- MEDALS diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 62a7eb0f7..2df0c18f0 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -240,6 +240,7 @@ abstract class BaseRegistry & Constructible { + // We enforce that T is Constructible to ensure this is valid. return new T(id); } diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx index cb914007f..22b883c75 100644 --- a/source/funkin/data/stage/StageData.hx +++ b/source/funkin/data/stage/StageData.hx @@ -32,18 +32,21 @@ class StageData bf: { zIndex: 0, + scale: 1, position: [0, 0], cameraOffsets: [-100, -100] }, dad: { zIndex: 0, + scale: 1, position: [0, 0], cameraOffsets: [100, -100] }, gf: { zIndex: 0, + scale: 1, position: [0, 0], cameraOffsets: [0, 0] } @@ -114,6 +117,7 @@ typedef StageDataProp = @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) @:optional + @:default(Left(1.0)) var scale:haxe.ds.Either>; /** @@ -190,6 +194,13 @@ typedef StageDataCharacter = @:default([0, 0]) var position:Array; + /** + * The scale to render the character at. + */ + @:optional + @:default(1) + var scale:Float; + /** * The camera offsets to apply when focusing on the character on this stage. * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 2329a2791..9a2af8913 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -82,6 +82,8 @@ class FlxAtlasSprite extends FlxAnimate * @param id A string ID of the animation to play. * @param restart Whether to restart the animation if it is already playing. * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing + * @param loop Whether to loop the animation + * NOTE: `loop` and `ignoreOther` are not compatible with each other! */ public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void { @@ -114,11 +116,18 @@ class FlxAtlasSprite extends FlxAnimate } anim.callback = function(_, frame:Int) { - if (frame == (anim.getFrameLabel(id).duration - 1) + anim.getFrameLabel(id).index) + var offset = loop ? 0 : -1; + + if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index) { - if (loop) playAnimation(id, true, false, true); + if (loop) + { + playAnimation(id, true, false, true); + } else + { onAnimationFinish.dispatch(id); + } } }; @@ -176,7 +185,7 @@ class FlxAtlasSprite extends FlxAnimate public function cleanupAnimation(_:String):Void { canPlayOtherAnims = true; - this.currentAnimation = null; - this.anim.stop(); + // this.currentAnimation = null; + this.anim.pause(); } } diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index f1e82aee9..889f63073 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -61,7 +61,7 @@ class PolymodHandler createModRoot(); trace("Initializing Polymod (using configured mods)..."); - loadModsById(Save.get().enabledModIds); + loadModsById(Save.instance.enabledModIds); } /** @@ -236,7 +236,7 @@ class PolymodHandler public static function getEnabledMods():Array { - var modIds = Save.get().enabledModIds; + var modIds = Save.instance.enabledModIds; var modMetadata = getAllMods(); var enabledMods = []; for (item in modMetadata) diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 5d522e3ae..0d424a281 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -107,18 +107,18 @@ class NoteScriptEvent extends ScriptEvent public var playSound(default, default):Bool; /** - * A multiplier to the health gained or lost from this note. + * The health gained or lost from this note. * This affects both hits and misses. Remember that max health is 2.00. */ - public var healthMulti:Float; + public var healthChange:Float; - public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void + public function new(type:ScriptEventType, note:NoteSprite, healthChange:Float, comboCount:Int = 0, cancelable:Bool = false):Void { super(type, cancelable); this.note = note; this.comboCount = comboCount; this.playSound = true; - this.healthMulti = 1.0; + this.healthChange = healthChange; } public override function toString():String @@ -127,6 +127,31 @@ class NoteScriptEvent extends ScriptEvent } } +class HitNoteScriptEvent extends NoteScriptEvent +{ + /** + * The judgement the player received for hitting the note. + */ + public var judgement:String; + + /** + * The score the player received for hitting the note. + */ + public var score:Int; + + public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void + { + super(NOTE_HIT, note, healthChange, comboCount, true); + this.score = score; + this.judgement = judgement; + } + + public override function toString():String + { + return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')'; + } +} + /** * An event that is fired when you press a key with no note present. */ diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 857cda524..95304d762 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -12,6 +12,7 @@ import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; import funkin.play.PlayState; +import funkin.util.MathUtil; import funkin.ui.freeplay.FreeplayState; import funkin.ui.MusicBeatSubState; import funkin.ui.story.StoryMenuState; @@ -82,6 +83,9 @@ class GameOverSubState extends MusicBeatSubState var transparent:Bool; + final CAMERA_ZOOM_DURATION:Float = 0.5; + var targetCameraZoom:Float = 1.0; + public function new(params:GameOverParams) { super(); @@ -142,6 +146,7 @@ class GameOverSubState extends MusicBeatSubState FlxG.camera.target = null; FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01); + targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom(); // // Set up the audio @@ -177,6 +182,9 @@ class GameOverSubState extends MusicBeatSubState } } + // Smoothly lerp the camera + FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION); + // // Handle user inputs. // @@ -286,6 +294,9 @@ class GameOverSubState extends MusicBeatSubState remove(boyfriend); PlayState.instance.currentStage.addCharacter(boyfriend, BF); + // Snap reset the camera which may have changed because of the player character data. + resetCameraZoom(); + // Close the substate. close(); }); diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 2b705ea9e..03681ce13 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -386,7 +386,6 @@ class PauseSubState extends MusicBeatSubState // Set the position. var targetX = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 20 + 90; var targetY = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 120 + (FlxG.height * 0.48); - trace(targetY); FlxTween.globalManager.cancelTweensOf(text); FlxTween.tween(text, {x: targetX, y: targetY}, 0.33, {ease: FlxEase.quartOut}); } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index dd507963c..d090b4f8a 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -97,7 +97,7 @@ typedef PlayStateParams = ?targetDifficulty:String, /** * The variation to play on. - * @default `Constants.DEFAULT_VARIATION` . + * @default `Constants.DEFAULT_VARIATION` */ ?targetVariation:String, /** @@ -118,8 +118,14 @@ typedef PlayStateParams = ?minimalMode:Bool, /** * If specified, the game will jump to the specified timestamp after the countdown ends. + * @default `0.0` */ ?startTimestamp:Float, + /** + * If specified, the game will play the song with the given speed. + * @default `1.0` for 100% speed. + */ + ?playbackRate:Float, /** * If specified, the game will not load the instrumental or vocal tracks, * and must be loaded externally. @@ -210,6 +216,12 @@ class PlayState extends MusicBeatSubState */ public var startTimestamp:Float = 0.0; + /** + * Play back the song at this speed. + * @default `1.0` for normal speed. + */ + public var playbackRate:Float = 1.0; + /** * An empty FlxObject contained in the scene. * The current gameplay camera will always follow this object. Tween its position to move the camera smoothly. @@ -270,6 +282,12 @@ class PlayState extends MusicBeatSubState */ public var isPracticeMode:Bool = false; + /** + * Whether the player has dropped below zero health, + * and we are just waiting for an animation to play out before transitioning. + */ + public var isPlayerDying:Bool = false; + /** * In Minimal Mode, the stage and characters are not loaded and a standard background is used. */ @@ -550,6 +568,7 @@ class PlayState extends MusicBeatSubState isPracticeMode = params.practiceMode ?? false; isMinimalMode = params.minimalMode ?? false; startTimestamp = params.startTimestamp ?? 0.0; + playbackRate = params.playbackRate ?? 1.0; overrideMusic = params.overrideMusic ?? false; previousCameraFollowPoint = params.cameraFollowPoint; @@ -772,11 +791,13 @@ class PlayState extends MusicBeatSubState persistentDraw = true; startingSong = true; + isPlayerDying = false; inputSpitter = []; // Reset music properly. FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; + FlxG.sound.music.pitch = playbackRate; FlxG.sound.music.pause(); if (!overrideMusic) @@ -913,7 +934,7 @@ class PlayState extends MusicBeatSubState camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95); } - if (currentStage != null) + if (currentStage != null && currentStage.getBoyfriend() != null) { FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation()); } @@ -939,7 +960,7 @@ class PlayState extends MusicBeatSubState } #end - if (health <= Constants.HEALTH_MIN && !isPracticeMode) + if (health <= Constants.HEALTH_MIN && !isPracticeMode && !isPlayerDying) { vocals.pause(); FlxG.sound.music.pause(); @@ -965,20 +986,30 @@ class PlayState extends MusicBeatSubState } #end - var gameOverSubState = new GameOverSubState( - { - isChartingMode: isChartingMode, - transparent: persistentDraw + isPlayerDying = true; + + var deathPreTransitionDelay = currentStage?.getBoyfriend()?.getDeathPreTransitionDelay() ?? 0.0; + if (deathPreTransitionDelay > 0) + { + new FlxTimer().start(deathPreTransitionDelay, function(_) { + moveToGameOver(); }); - FlxTransitionableSubState.skipNextTransIn = true; - FlxTransitionableSubState.skipNextTransOut = true; - openSubState(gameOverSubState); + } + else + { + // Transition immediately. + moveToGameOver(); + } #if discord_rpc // Game Over doesn't get his own variable because it's only used here DiscordClient.changePresence('Game Over - ' + detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); #end } + else if (isPlayerDying) + { + // Wait up. + } } processSongEvents(); @@ -994,6 +1025,18 @@ class PlayState extends MusicBeatSubState justUnpaused = false; } + function moveToGameOver():Void + { + var gameOverSubState = new GameOverSubState( + { + isChartingMode: isChartingMode, + transparent: persistentDraw + }); + FlxTransitionableSubState.skipNextTransIn = true; + FlxTransitionableSubState.skipNextTransOut = true; + openSubState(gameOverSubState); + } + function processSongEvents():Void { // Query and activate song events. @@ -1484,17 +1527,17 @@ class PlayState extends MusicBeatSubState if (dad != null) { dad.characterType = CharacterType.DAD; - } - // - // OPPONENT HEALTH ICON - // - iconP2 = new HealthIcon('dad', 1); - iconP2.y = healthBar.y - (iconP2.height / 2); - dad.initHealthIcon(true); // Apply the character ID here - iconP2.zIndex = 850; - add(iconP2); - iconP2.cameras = [camHUD]; + // + // OPPONENT HEALTH ICON + // + iconP2 = new HealthIcon('dad', 1); + iconP2.y = healthBar.y - (iconP2.height / 2); + dad.initHealthIcon(true); // Apply the character ID here + iconP2.zIndex = 850; + add(iconP2); + iconP2.cameras = [camHUD]; + } // // BOYFRIEND @@ -1504,17 +1547,17 @@ class PlayState extends MusicBeatSubState if (boyfriend != null) { boyfriend.characterType = CharacterType.BF; - } - // - // PLAYER HEALTH ICON - // - iconP1 = new HealthIcon('bf', 0); - iconP1.y = healthBar.y - (iconP1.height / 2); - boyfriend.initHealthIcon(false); // Apply the character ID here - iconP1.zIndex = 850; - add(iconP1); - iconP1.cameras = [camHUD]; + // + // PLAYER HEALTH ICON + // + iconP1 = new HealthIcon('bf', 0); + iconP1.y = healthBar.y - (iconP1.height / 2); + boyfriend.initHealthIcon(false); // Apply the character ID here + iconP1.zIndex = 850; + add(iconP1); + iconP1.cameras = [camHUD]; + } // // ADD CHARACTERS TO SCENE @@ -1783,14 +1826,17 @@ class PlayState extends MusicBeatSubState // A negative instrumental offset means the song skips the first few milliseconds of the track. // This just gets added into the startTimestamp behavior so we don't need to do anything extra. FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset); + FlxG.sound.music.pitch = playbackRate; // I am going insane. FlxG.sound.music.volume = 1.0; + FlxG.sound.music.fadeTween?.cancel(); trace('Playing vocals...'); add(vocals); vocals.play(); + vocals.pitch = playbackRate; resyncVocals(); #if discord_rpc @@ -2000,7 +2046,7 @@ class PlayState extends MusicBeatSubState { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2009,7 +2055,7 @@ class PlayState extends MusicBeatSubState // Judge the miss. // NOTE: This is what handles the scoring. trace('Missed note! ${note.noteData}'); - onNoteMiss(note, event.playSound, event.healthMulti); + onNoteMiss(note, event.playSound, event.healthChange); note.handledMiss = true; } @@ -2155,13 +2201,41 @@ class PlayState extends MusicBeatSubState function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void { - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true); + // Calculate the input latency (do this as late as possible). + // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}'); + var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp; + var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS; + // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!'); + + // Get the offset and compensate for input latency. + // Round inward (trim remainder) for consistency. + var noteDiff:Int = Std.int(Conductor.instance.songPosition - note.noteData.time - inputLatencyMs); + + var score = Scoring.scoreNote(noteDiff, PBOT1); + var daRating = Scoring.judgeNote(noteDiff, PBOT1); + + var healthChange = 0.0; + switch (daRating) + { + case 'sick': + healthChange = Constants.HEALTH_SICK_BONUS; + case 'good': + healthChange = Constants.HEALTH_GOOD_BONUS; + case 'bad': + healthChange = Constants.HEALTH_BAD_BONUS; + case 'shit': + healthChange = Constants.HEALTH_SHIT_BONUS; + } + + // Send the note hit event. + var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; - popUpScore(note, input, event.healthMulti); + // Display the combo meter and add the calculation to the score. + popUpScore(note, event.score, event.judgement, event.healthChange); if (note.isHoldNote && note.holdNoteSprite != null) { @@ -2175,11 +2249,11 @@ class PlayState extends MusicBeatSubState * Called when a note leaves the screen and is considered missed by the player. * @param note */ - function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthLossMulti:Float = 1.0):Void + function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void { // If we are here, we already CALLED the onNoteMiss script hook! - health -= Constants.HEALTH_MISS_PENALTY * healthLossMulti; + health += healthChange; songScore -= 10; if (!isPracticeMode) @@ -2351,23 +2425,10 @@ class PlayState extends MusicBeatSubState /** * Handles health, score, and rating popups when a note is hit. */ - function popUpScore(daNote:NoteSprite, input:PreciseInputEvent, healthGainMulti:Float = 1.0):Void + function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void { vocals.playerVolume = 1; - // Calculate the input latency (do this as late as possible). - // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}'); - var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp; - var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS; - // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!'); - - // Get the offset and compensate for input latency. - // Round inward (trim remainder) for consistency. - var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs); - - var score = Scoring.scoreNote(noteDiff, PBOT1); - var daRating = Scoring.judgeNote(noteDiff, PBOT1); - if (daRating == 'miss') { // If daRating is 'miss', that means we made a mistake and should not continue. @@ -2382,22 +2443,20 @@ class PlayState extends MusicBeatSubState { case 'sick': Highscore.tallies.sick += 1; - health += Constants.HEALTH_SICK_BONUS * healthGainMulti; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; - health += Constants.HEALTH_GOOD_BONUS * healthGainMulti; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; - health += Constants.HEALTH_BAD_BONUS * healthGainMulti; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; - health += Constants.HEALTH_SHIT_BONUS * healthGainMulti; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; } + health += healthChange; + if (isComboBreak) { // Break the combo, but don't increment tallies.misses. @@ -2563,9 +2622,9 @@ class PlayState extends MusicBeatSubState accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }; - if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data)) + if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data)) { - Save.get().setSongScore(currentSong.id, currentDifficulty, data); + Save.instance.setSongScore(currentSong.id, currentDifficulty, data); #if newgrounds NGio.postScore(score, currentSong.id); #end @@ -2613,9 +2672,9 @@ class PlayState extends MusicBeatSubState accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, }; - if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) + if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) { - Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data); + Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data); #if newgrounds NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); #end diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 418982bef..9e7aa98bf 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -46,7 +46,8 @@ class AnimateAtlasCharacter extends BaseCharacter var _skipTransformChildren:Bool = false; var animations:Map = new Map(); - var currentAnimation:String; + var currentAnimName:Null = null; + var animFinished:Bool = false; public function new(id:String) { @@ -77,6 +78,7 @@ class AnimateAtlasCharacter extends BaseCharacter var atlasSprite:FlxAtlasSprite = loadAtlasSprite(); setSprite(atlasSprite); + loadAnimations(); super.onCreate(event); @@ -86,10 +88,36 @@ class AnimateAtlasCharacter extends BaseCharacter { if ((!canPlayOtherAnims && !ignoreOther)) return; - currentAnimation = name; - var prefix:String = getAnimationData(name).prefix; - if (prefix == null) prefix = name; - this.mainSprite.playAnimation(prefix, restart, ignoreOther); + var correctName = correctAnimationName(name); + if (correctName == null) + { + trace('Could not find Atlas animation: ' + name); + return; + } + + var animData = getAnimationData(correctName); + currentAnimName = correctName; + var prefix:String = animData.prefix; + if (prefix == null) prefix = correctName; + var loop:Bool = animData.looped; + + this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop); + + animFinished = false; + } + + public override function hasAnimation(name:String):Bool + { + return getAnimationData(name) != null; + } + + /** + * Returns true if the animation has finished playing. + * Never true if animation is configured to loop. + */ + public override function isAnimationFinished():Bool + { + return animFinished; } function loadAtlasSprite():FlxAtlasSprite @@ -114,7 +142,13 @@ class AnimateAtlasCharacter extends BaseCharacter } else { + // Make the game hold on the last frame. this.mainSprite.cleanupAnimation(prefix); + // currentAnimName = null; + animFinished = true; + + // Fallback to idle! + // playAnimation('idle', true, false); } } @@ -140,19 +174,30 @@ class AnimateAtlasCharacter extends BaseCharacter function loadAnimations():Void { - trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}'); + trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}'); var animData:Array = cast _data.animations; for (anim in animData) { + // Validate the animation before adding. + var prefix = anim.prefix; + if (!this.mainSprite.hasAnimation(prefix)) + { + FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}'); + continue; + } animations.set(anim.name, anim); + trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}'); } + + trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}'); } public override function getCurrentAnimation():String { - return this.mainSprite.getCurrentAnimation(); + // return this.mainSprite.getCurrentAnimation(); + return currentAnimName; } function getAnimationData(name:String = null):AnimateAtlasAnimation diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 390864148..cf5311bdc 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -60,7 +60,7 @@ class BaseCharacter extends Bopper @:allow(funkin.ui.debug.anim.DebugBoundingState) final _data:CharacterData; - final singTimeSec:Float; + final singTimeSteps:Float; /** * The offset between the corner of the sprite and the origin of the sprite (at the character's feet). @@ -180,7 +180,7 @@ class BaseCharacter extends Bopper { this.characterName = _data.name; this.name = _data.name; - this.singTimeSec = _data.singTime; + this.singTimeSteps = _data.singTime; this.globalOffsets = _data.offsets; this.flipX = _data.flipX; } @@ -193,6 +193,16 @@ class BaseCharacter extends Bopper return _data.death?.cameraOffsets ?? [0.0, 0.0]; } + public function getDeathCameraZoom():Float + { + return _data.death?.cameraZoom ?? 1.0; + } + + public function getDeathPreTransitionDelay():Float + { + return _data.death?.preTransitionDelay ?? 0.0; + } + /** * Gets the value of flipX from the character data. * `!getFlipX()` is the direction Boyfriend should face. @@ -367,9 +377,9 @@ class BaseCharacter extends Bopper // This lets you add frames to the end of the sing animation to ease back into the idle! holdTimer += event.elapsed; - var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms. + var singTimeSec:Float = singTimeSteps * (Conductor.instance.stepLengthMs / Constants.MS_PER_SEC); // x beats, to ms. - if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss + if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss??? // Without this check here, the player character would only play the `sing` animation // for one beat, as opposed to holding it as long as the player is holding the button. @@ -378,7 +388,7 @@ class BaseCharacter extends Bopper FlxG.watch.addQuick('singTimeSec-${characterId}', singTimeSec); if (holdTimer > singTimeSec && shouldStopSinging) { - // trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation'); + trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation'); holdTimer = 0; dance(true); } diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index f3c7d7613..56d7b7793 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -744,4 +744,17 @@ typedef DeathData = * @default [0, 0] */ var ?cameraOffsets:Array; + + /** + * The amount to zoom the camera by while focusing on this character as they die. + * Value is a multiplier of the default camera zoom for the stage. + * @default 1.0 + */ + var ?cameraZoom:Float; + + /** + * Impose a delay between when the character reaches `0` health and when the death animation plays. + * @default 0.0 + */ + var ?preTransitionDelay:Float; } diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index f848d79c8..0d29481c4 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -218,25 +218,25 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe // If the animation exists, we're good. if (hasAnimation(name)) return name; - trace('[BOPPER] Animation "$name" does not exist!'); + FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...'); // Attempt to strip a `-alt` suffix, if it exists. if (name.lastIndexOf('-') != -1) { var correctName = name.substring(0, name.lastIndexOf('-')); - trace('[BOPPER] Attempting to fallback to "$correctName"'); + FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...'); return correctAnimationName(correctName); } else { if (name != 'idle') { - trace('[BOPPER] Attempting to fallback to "idle"'); + FlxG.log.warn('Speaker tried to play animation "$name" that does not exist, fallback to idle...'); return correctAnimationName('idle'); } else { - trace('[BOPPER] Failing animation playback.'); + FlxG.log.error('Speaker tried to play animation "idle" that does not exist! This is bad!'); return null; } } diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index 45862b26d..b16b88466 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -16,26 +16,53 @@ class NoteSprite extends FunkinSprite var hsvShader:HSVShader; /** - * The time at which the note should be hit, in milliseconds. + * The strum time at which the note should be hit, in milliseconds. */ - public var strumTime(default, set):Float; + public var strumTime(get, set):Float; + + function get_strumTime():Float + { + return this.noteData?.time ?? 0.0; + } function set_strumTime(value:Float):Float { - this.strumTime = value; - return this.strumTime; + if (this.noteData == null) return value; + return this.noteData.time = value; + } + + /** + * The length for which the note should be held, in milliseconds. + * Defaults to 0 for single notes. + */ + public var length(get, set):Float; + + function get_length():Float + { + return this.noteData?.length ?? 0.0; + } + + function set_length(value:Float):Float + { + if (this.noteData == null) return value; + return this.noteData.length = value; } /** * An extra attribute for the note. * For example, whether the note is an "alt" note, or whether it has custom behavior on hit. */ - public var kind(default, set):String; + public var kind(get, set):Null; + + function get_kind():Null + { + return this.noteData?.kind; + } function set_kind(value:String):String { - this.kind = value; - return this.kind; + if (this.noteData == null) return value; + return this.noteData.kind = value; } /** @@ -100,16 +127,13 @@ class NoteSprite extends FunkinSprite */ public var handledMiss:Bool; - public function new(noteStyle:NoteStyle, strumTime:Float = 0, direction:Int = 0) + public function new(noteStyle:NoteStyle, direction:Int = 0) { super(0, -9999); - this.strumTime = strumTime; this.direction = direction; this.hsvShader = new HSVShader(); - if (this.strumTime < 0) this.strumTime = 0; - setupNoteGraphic(noteStyle); // Disables the update() function for performance. diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 5fdd3945f..190aa3ee0 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -659,7 +659,6 @@ class Strumline extends FlxSpriteGroup if (noteSprite != null) { - noteSprite.strumTime = note.time; noteSprite.direction = note.getDirection(); noteSprite.noteData = note; diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 1bc0632f9..7974900d8 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -236,25 +236,25 @@ class Bopper extends StageProp implements IPlayStateScriptedClass // If the animation exists, we're good. if (hasAnimation(name)) return name; - trace('[BOPPER] Animation "$name" does not exist!'); + FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...'); // Attempt to strip a `-alt` suffix, if it exists. if (name.lastIndexOf('-') != -1) { var correctName = name.substring(0, name.lastIndexOf('-')); - trace('[BOPPER] Attempting to fallback to "$correctName"'); + FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...'); return correctAnimationName(correctName); } else { if (name != 'idle') { - trace('[BOPPER] Attempting to fallback to "idle"'); + FlxG.log.warn('Bopper tried to play animation "$name" that does not exist, fallback to idle...'); return correctAnimationName('idle'); } else { - trace('[BOPPER] Failing animation playback.'); + FlxG.log.error('Bopper tried to play animation "idle" that does not exist! This is bad!'); return null; } } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index c20202245..32c0509a5 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -110,6 +110,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements getBoyfriend().resetCharacter(true); // Reapply the camera offsets. var charData = _data.characters.bf; + getBoyfriend().scale.set(charData.scale, charData.scale); getBoyfriend().cameraFocusPoint.x += charData.cameraOffsets[0]; getBoyfriend().cameraFocusPoint.y += charData.cameraOffsets[1]; } @@ -122,6 +123,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements getGirlfriend().resetCharacter(true); // Reapply the camera offsets. var charData = _data.characters.gf; + getGirlfriend().scale.set(charData.scale, charData.scale); getGirlfriend().cameraFocusPoint.x += charData.cameraOffsets[0]; getGirlfriend().cameraFocusPoint.y += charData.cameraOffsets[1]; } @@ -130,6 +132,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements getDad().resetCharacter(true); // Reapply the camera offsets. var charData = _data.characters.dad; + getDad().scale.set(charData.scale, charData.scale); getDad().cameraFocusPoint.x += charData.cameraOffsets[0]; getDad().cameraFocusPoint.y += charData.cameraOffsets[1]; } @@ -226,7 +229,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements switch (dataProp.scale) { case Left(value): - propSprite.scale.set(value); + propSprite.scale.set(value, value); case Right(values): propSprite.scale.set(values[0], values[1]); @@ -435,6 +438,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements character.originalPosition.y = character.y + character.animOffsets[1]; } + character.scale.set(charData.scale, charData.scale); character.cameraFocusPoint.x += charData.cameraOffsets[0]; character.cameraFocusPoint.y += charData.cameraOffsets[1]; @@ -637,7 +641,30 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements */ public function dispatchToCharacters(event:ScriptEvent):Void { - for (characterId in characters.keys()) + var charList = this.characters.keys().array(); + + // Dad, then BF, then GF, in that order. + + if (charList.contains('dad')) + { + dispatchToCharacter('dad', event); + charList.remove('dad'); + } + + if (charList.contains('bf')) + { + dispatchToCharacter('bf', event); + charList.remove('bf'); + } + + if (charList.contains('gf')) + { + dispatchToCharacter('gf', event); + charList.remove('gf'); + } + + // Then the rest of the characters, if any. + for (characterId in charList) { dispatchToCharacter(characterId, event); } diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 6246dcb58..73ba8efa0 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -11,8 +11,7 @@ import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import thx.semver.Version; @:nullSafety -@:forward(volume, mute) -abstract Save(RawSaveData) +class Save { // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null. public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2"; @@ -25,6 +24,20 @@ abstract Save(RawSaveData) static final SAVE_PATH_LEGACY:String = 'ninjamuffin99'; static final SAVE_NAME_LEGACY:String = 'funkin'; + public static var instance(get, never):Save; + static var _instance:Null = null; + + static function get_instance():Save + { + if (_instance == null) + { + _instance = new Save(FlxG.save.data); + } + return _instance; + } + + var data:RawSaveData; + public static function load():Void { trace("[SAVE] Loading save..."); @@ -33,84 +46,85 @@ abstract Save(RawSaveData) loadFromSlot(1); } - public static function get():Save - { - return FlxG.save.data; - } - /** * Constructing a new Save will load the default values. */ - public function new() + public function new(data:RawSaveData) { - this = - { - version: Save.SAVE_DATA_VERSION, + this.data = data; - volume: 1.0, - mute: false, + if (this.data == null) data = Save.getDefault(); + } - api: - { - newgrounds: - { - sessionId: null, - } - }, - scores: - { - // No saved scores. - levels: [], - songs: [], - }, - options: - { - // Reasonable defaults. - naughtyness: true, - downscroll: false, - flashingLights: true, - zoomCamera: true, - debugDisplay: false, - autoPause: true, + public static function getDefault():RawSaveData + { + return { + version: Save.SAVE_DATA_VERSION, - controls: - { - // Leave controls blank so defaults are loaded. - p1: - { - keyboard: {}, - gamepad: {}, - }, - p2: - { - keyboard: {}, - gamepad: {}, - }, - }, - }, + volume: 1.0, + mute: false, - mods: - { - // No mods enabled. - enabledMods: [], - modOptions: [], - }, + api: + { + newgrounds: + { + sessionId: null, + } + }, + scores: + { + // No saved scores. + levels: [], + songs: [], + }, + options: + { + // Reasonable defaults. + naughtyness: true, + downscroll: false, + flashingLights: true, + zoomCamera: true, + debugDisplay: false, + autoPause: true, - optionsChartEditor: - { - // Reasonable defaults. - previousFiles: [], - noteQuant: 3, - chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None, - theme: ChartEditorTheme.Light, - playtestStartTime: false, - downscroll: false, - metronomeVolume: 1.0, - hitsoundVolumePlayer: 1.0, - hitsoundVolumeOpponent: 1.0, - themeMusic: true - }, - }; + controls: + { + // Leave controls blank so defaults are loaded. + p1: + { + keyboard: {}, + gamepad: {}, + }, + p2: + { + keyboard: {}, + gamepad: {}, + }, + }, + }, + + mods: + { + // No mods enabled. + enabledMods: [], + modOptions: [], + }, + + optionsChartEditor: + { + // Reasonable defaults. + previousFiles: [], + noteQuant: 3, + chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None, + theme: ChartEditorTheme.Light, + playtestStartTime: false, + downscroll: false, + metronomeVolume: 1.0, + hitsoundVolumePlayer: 1.0, + hitsoundVolumeOpponent: 1.0, + themeMusic: true + }, + }; } /** @@ -120,7 +134,7 @@ abstract Save(RawSaveData) function get_options():SaveDataOptions { - return this.options; + return data.options; } /** @@ -130,7 +144,7 @@ abstract Save(RawSaveData) function get_modOptions():Map { - return this.mods.modOptions; + return data.mods.modOptions; } /** @@ -140,232 +154,232 @@ abstract Save(RawSaveData) function get_ngSessionId():Null { - return this.api.newgrounds.sessionId; + return data.api.newgrounds.sessionId; } function set_ngSessionId(value:Null):Null { - this.api.newgrounds.sessionId = value; + data.api.newgrounds.sessionId = value; flush(); - return this.api.newgrounds.sessionId; + return data.api.newgrounds.sessionId; } public var enabledModIds(get, set):Array; function get_enabledModIds():Array { - return this.mods.enabledMods; + return data.mods.enabledMods; } function set_enabledModIds(value:Array):Array { - this.mods.enabledMods = value; + data.mods.enabledMods = value; flush(); - return this.mods.enabledMods; + return data.mods.enabledMods; } public var chartEditorPreviousFiles(get, set):Array; function get_chartEditorPreviousFiles():Array { - if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = []; + if (data.optionsChartEditor.previousFiles == null) data.optionsChartEditor.previousFiles = []; - return this.optionsChartEditor.previousFiles; + return data.optionsChartEditor.previousFiles; } function set_chartEditorPreviousFiles(value:Array):Array { // Set and apply. - this.optionsChartEditor.previousFiles = value; + data.optionsChartEditor.previousFiles = value; flush(); - return this.optionsChartEditor.previousFiles; + return data.optionsChartEditor.previousFiles; } public var chartEditorHasBackup(get, set):Bool; function get_chartEditorHasBackup():Bool { - if (this.optionsChartEditor.hasBackup == null) this.optionsChartEditor.hasBackup = false; + if (data.optionsChartEditor.hasBackup == null) data.optionsChartEditor.hasBackup = false; - return this.optionsChartEditor.hasBackup; + return data.optionsChartEditor.hasBackup; } function set_chartEditorHasBackup(value:Bool):Bool { // Set and apply. - this.optionsChartEditor.hasBackup = value; + data.optionsChartEditor.hasBackup = value; flush(); - return this.optionsChartEditor.hasBackup; + return data.optionsChartEditor.hasBackup; } public var chartEditorNoteQuant(get, set):Int; function get_chartEditorNoteQuant():Int { - if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3; + if (data.optionsChartEditor.noteQuant == null) data.optionsChartEditor.noteQuant = 3; - return this.optionsChartEditor.noteQuant; + return data.optionsChartEditor.noteQuant; } function set_chartEditorNoteQuant(value:Int):Int { // Set and apply. - this.optionsChartEditor.noteQuant = value; + data.optionsChartEditor.noteQuant = value; flush(); - return this.optionsChartEditor.noteQuant; + return data.optionsChartEditor.noteQuant; } public var chartEditorLiveInputStyle(get, set):ChartEditorLiveInputStyle; function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle { - if (this.optionsChartEditor.chartEditorLiveInputStyle == null) this.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None; + if (data.optionsChartEditor.chartEditorLiveInputStyle == null) data.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None; - return this.optionsChartEditor.chartEditorLiveInputStyle; + return data.optionsChartEditor.chartEditorLiveInputStyle; } function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle { // Set and apply. - this.optionsChartEditor.chartEditorLiveInputStyle = value; + data.optionsChartEditor.chartEditorLiveInputStyle = value; flush(); - return this.optionsChartEditor.chartEditorLiveInputStyle; + return data.optionsChartEditor.chartEditorLiveInputStyle; } public var chartEditorDownscroll(get, set):Bool; function get_chartEditorDownscroll():Bool { - if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false; + if (data.optionsChartEditor.downscroll == null) data.optionsChartEditor.downscroll = false; - return this.optionsChartEditor.downscroll; + return data.optionsChartEditor.downscroll; } function set_chartEditorDownscroll(value:Bool):Bool { // Set and apply. - this.optionsChartEditor.downscroll = value; + data.optionsChartEditor.downscroll = value; flush(); - return this.optionsChartEditor.downscroll; + return data.optionsChartEditor.downscroll; } public var chartEditorPlaytestStartTime(get, set):Bool; function get_chartEditorPlaytestStartTime():Bool { - if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false; + if (data.optionsChartEditor.playtestStartTime == null) data.optionsChartEditor.playtestStartTime = false; - return this.optionsChartEditor.playtestStartTime; + return data.optionsChartEditor.playtestStartTime; } function set_chartEditorPlaytestStartTime(value:Bool):Bool { // Set and apply. - this.optionsChartEditor.playtestStartTime = value; + data.optionsChartEditor.playtestStartTime = value; flush(); - return this.optionsChartEditor.playtestStartTime; + return data.optionsChartEditor.playtestStartTime; } public var chartEditorTheme(get, set):ChartEditorTheme; function get_chartEditorTheme():ChartEditorTheme { - if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light; + if (data.optionsChartEditor.theme == null) data.optionsChartEditor.theme = ChartEditorTheme.Light; - return this.optionsChartEditor.theme; + return data.optionsChartEditor.theme; } function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme { // Set and apply. - this.optionsChartEditor.theme = value; + data.optionsChartEditor.theme = value; flush(); - return this.optionsChartEditor.theme; + return data.optionsChartEditor.theme; } public var chartEditorMetronomeVolume(get, set):Float; function get_chartEditorMetronomeVolume():Float { - if (this.optionsChartEditor.metronomeVolume == null) this.optionsChartEditor.metronomeVolume = 1.0; + if (data.optionsChartEditor.metronomeVolume == null) data.optionsChartEditor.metronomeVolume = 1.0; - return this.optionsChartEditor.metronomeVolume; + return data.optionsChartEditor.metronomeVolume; } function set_chartEditorMetronomeVolume(value:Float):Float { // Set and apply. - this.optionsChartEditor.metronomeVolume = value; + data.optionsChartEditor.metronomeVolume = value; flush(); - return this.optionsChartEditor.metronomeVolume; + return data.optionsChartEditor.metronomeVolume; } public var chartEditorHitsoundVolumePlayer(get, set):Float; function get_chartEditorHitsoundVolumePlayer():Float { - if (this.optionsChartEditor.hitsoundVolumePlayer == null) this.optionsChartEditor.hitsoundVolumePlayer = 1.0; + if (data.optionsChartEditor.hitsoundVolumePlayer == null) data.optionsChartEditor.hitsoundVolumePlayer = 1.0; - return this.optionsChartEditor.hitsoundVolumePlayer; + return data.optionsChartEditor.hitsoundVolumePlayer; } function set_chartEditorHitsoundVolumePlayer(value:Float):Float { // Set and apply. - this.optionsChartEditor.hitsoundVolumePlayer = value; + data.optionsChartEditor.hitsoundVolumePlayer = value; flush(); - return this.optionsChartEditor.hitsoundVolumePlayer; + return data.optionsChartEditor.hitsoundVolumePlayer; } public var chartEditorHitsoundVolumeOpponent(get, set):Float; function get_chartEditorHitsoundVolumeOpponent():Float { - if (this.optionsChartEditor.hitsoundVolumeOpponent == null) this.optionsChartEditor.hitsoundVolumeOpponent = 1.0; + if (data.optionsChartEditor.hitsoundVolumeOpponent == null) data.optionsChartEditor.hitsoundVolumeOpponent = 1.0; - return this.optionsChartEditor.hitsoundVolumeOpponent; + return data.optionsChartEditor.hitsoundVolumeOpponent; } function set_chartEditorHitsoundVolumeOpponent(value:Float):Float { // Set and apply. - this.optionsChartEditor.hitsoundVolumeOpponent = value; + data.optionsChartEditor.hitsoundVolumeOpponent = value; flush(); - return this.optionsChartEditor.hitsoundVolumeOpponent; + return data.optionsChartEditor.hitsoundVolumeOpponent; } public var chartEditorThemeMusic(get, set):Bool; function get_chartEditorThemeMusic():Bool { - if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true; + if (data.optionsChartEditor.themeMusic == null) data.optionsChartEditor.themeMusic = true; - return this.optionsChartEditor.themeMusic; + return data.optionsChartEditor.themeMusic; } function set_chartEditorThemeMusic(value:Bool):Bool { // Set and apply. - this.optionsChartEditor.themeMusic = value; + data.optionsChartEditor.themeMusic = value; flush(); - return this.optionsChartEditor.themeMusic; + return data.optionsChartEditor.themeMusic; } public var chartEditorPlaybackSpeed(get, set):Float; function get_chartEditorPlaybackSpeed():Float { - if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0; + if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0; - return this.optionsChartEditor.playbackSpeed; + return data.optionsChartEditor.playbackSpeed; } function set_chartEditorPlaybackSpeed(value:Float):Float { // Set and apply. - this.optionsChartEditor.playbackSpeed = value; + data.optionsChartEditor.playbackSpeed = value; flush(); - return this.optionsChartEditor.playbackSpeed; + return data.optionsChartEditor.playbackSpeed; } /** @@ -377,11 +391,11 @@ abstract Save(RawSaveData) */ public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null { - var level = this.scores.levels.get(levelId); + var level = data.scores.levels.get(levelId); if (level == null) { level = []; - this.scores.levels.set(levelId, level); + data.scores.levels.set(levelId, level); } return level.get(difficultyId); @@ -392,11 +406,11 @@ abstract Save(RawSaveData) */ public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void { - var level = this.scores.levels.get(levelId); + var level = data.scores.levels.get(levelId); if (level == null) { level = []; - this.scores.levels.set(levelId, level); + data.scores.levels.set(levelId, level); } level.set(difficultyId, score); @@ -405,11 +419,11 @@ abstract Save(RawSaveData) public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool { - var level = this.scores.levels.get(levelId); + var level = data.scores.levels.get(levelId); if (level == null) { level = []; - this.scores.levels.set(levelId, level); + data.scores.levels.set(levelId, level); } var currentScore = level.get(difficultyId); @@ -448,11 +462,11 @@ abstract Save(RawSaveData) */ public function getSongScore(songId:String, difficultyId:String = 'normal'):Null { - var song = this.scores.songs.get(songId); + var song = data.scores.songs.get(songId); if (song == null) { song = []; - this.scores.songs.set(songId, song); + data.scores.songs.set(songId, song); } return song.get(difficultyId); } @@ -462,11 +476,11 @@ abstract Save(RawSaveData) */ public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void { - var song = this.scores.songs.get(songId); + var song = data.scores.songs.get(songId); if (song == null) { song = []; - this.scores.songs.set(songId, song); + data.scores.songs.set(songId, song); } song.set(difficultyId, score); @@ -482,11 +496,11 @@ abstract Save(RawSaveData) */ public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool { - var song = this.scores.songs.get(songId); + var song = data.scores.songs.get(songId); if (song == null) { song = []; - this.scores.songs.set(songId, song); + data.scores.songs.set(songId, song); } var currentScore = song.get(difficultyId); @@ -527,9 +541,9 @@ abstract Save(RawSaveData) switch (inputType) { case Keys: - return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard; + return (playerId == 0) ? data.options.controls.p1.keyboard : data.options.controls.p2.keyboard; case Gamepad(_): - return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad; + return (playerId == 0) ? data.options.controls.p1.gamepad : data.options.controls.p2.gamepad; } } @@ -547,20 +561,20 @@ abstract Save(RawSaveData) case Keys: if (playerId == 0) { - this.options.controls.p1.keyboard = controls; + data.options.controls.p1.keyboard = controls; } else { - this.options.controls.p2.keyboard = controls; + data.options.controls.p2.keyboard = controls; } case Gamepad(_): if (playerId == 0) { - this.options.controls.p1.gamepad = controls; + data.options.controls.p1.gamepad = controls; } else { - this.options.controls.p2.gamepad = controls; + data.options.controls.p2.gamepad = controls; } } @@ -581,6 +595,36 @@ abstract Save(RawSaveData) } } + /** + * The user's current volume setting. + */ + public var volume(get, set):Float; + + function get_volume():Float + { + return data.volume; + } + + function set_volume(value:Float):Float + { + return data.volume = value; + } + + /** + * Whether the user's volume is currently muted. + */ + public var mute(get, set):Bool; + + function get_mute():Bool + { + return data.mute; + } + + function set_mute(value:Bool):Bool + { + return data.mute = value; + } + /** * Call this to make sure the save data is written to disk. */ @@ -606,17 +650,22 @@ abstract Save(RawSaveData) if (legacySaveData != null) { trace('[SAVE] Found legacy save data, converting...'); - FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData)); + var gameSave = SaveDataMigrator.migrate(legacySaveData); + @:privateAccess + FlxG.save.mergeData(gameSave.data); + } + else + { + trace('[SAVE] No legacy save data found.'); } } else { trace('[SAVE] Loaded save data.'); - FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data)); + @:privateAccess + var gameSave = SaveDataMigrator.migrate(FlxG.save.data); + FlxG.save.mergeData(gameSave.data); } - - trace('[SAVE] Done loading save data.'); - trace(FlxG.save.data); } static function fetchLegacySaveData():Null diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index f995660f7..00637d52a 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -19,21 +19,21 @@ class SaveDataMigrator { trace('[SAVE] No version found in save data! Returning blank data.'); trace(inputData); - return new Save(); + return new Save(Save.getDefault()); } else { if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) { - // Simply cast the structured data. - var save:Save = inputData; + // Simply import the structured data. + var save:Save = new Save(inputData); return save; } else { trace('[SAVE] Invalid save data version! Returning blank data.'); trace(inputData); - return new Save(); + return new Save(Save.getDefault()); } } } @@ -45,7 +45,7 @@ class SaveDataMigrator { var inputSaveData:RawSaveData_v1_0_0 = cast inputData; - var result:Save = new Save(); + var result:Save = new Save(Save.getDefault()); result.volume = inputSaveData.volume; result.mute = inputSaveData.mute; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e46779483..78e73bf27 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -920,12 +920,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_shouldShowBackupAvailableDialog():Bool { - return Save.get().chartEditorHasBackup; + return Save.instance.chartEditorHasBackup; } function set_shouldShowBackupAvailableDialog(value:Bool):Bool { - return Save.get().chartEditorHasBackup = value; + return Save.instance.chartEditorHasBackup = value; } /** @@ -2163,7 +2163,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState public function loadPreferences():Void { - var save:Save = Save.get(); + var save:Save = Save.instance; if (previousWorkingFilePaths[0] == null) { @@ -2191,7 +2191,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState public function writePreferences(hasBackup:Bool):Void { - var save:Save = Save.get(); + var save:Save = Save.instance; // Can't use filter() because of null safety checking! var filteredWorkingFilePaths:Array = []; @@ -5308,6 +5308,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var startTimestamp:Float = 0; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; + var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0; + playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5% + playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200% + var targetSong:Song; try { @@ -5357,6 +5361,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState practiceMode: playtestPracticeMode, minimalMode: minimal, startTimestamp: startTimestamp, + playbackRate: playbackRate, overrideMusic: true, }); diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx index 26015161b..b53de174e 100644 --- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx +++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx @@ -117,18 +117,32 @@ class ChartEditorDropdowns "ugh" => "Ugh (Week 7)", "hehPrettyGood" => "Heh, Pretty Good (Week 7)", // Weekend 1 - "weekend-1-lightcan" => "Light Can (2hot)", - "weekend-1-kickcan" => "Kick Can (2hot)", - "weekend-1-kneecan" => "Knee Can (2hot)", - "weekend-1-cockgun" => "Cock Gun (2hot)", - "weekend-1-firegun" => "Fire Gun (2hot)", - "weekend-1-punchlow" => "Punch Low (Blazin)", - "weekend-1-punchhigh" => "Punch High (Blazin)", - "weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)", - "weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)", - "weekend-1-dodgelow" => "Dodge Low (Blazin)", - "weekend-1-blockhigh" => "Block High (Blazin)", - "weekend-1-fakeout" => "Fakeout (Blazin)", + "weekend-1-punchhigh" => "Punch High (Blazin')", + "weekend-1-punchhighdodged" => "Punch High (Dodge) (Blazin')", + "weekend-1-punchhighblocked" => "Punch High (Block) (Blazin')", + "weekend-1-punchhighspin" => "Punch High (Spin) (Blazin')", + "weekend-1-punchlow" => "Punch Low (Blazin')", + "weekend-1-punchlowdodged" => "Punch Low (Dodge) (Blazin')", + "weekend-1-punchlowblocked" => "Punch Low (Block) (Blazin')", + "weekend-1-punchlowspin" => "Punch High (Spin) (Blazin')", + "weekend-1-picouppercutprep" => "Pico Uppercut (Prep) (Blazin')", + "weekend-1-picouppercut" => "Pico Uppercut (Blazin')", + "weekend-1-blockhigh" => "Block High (Blazin')", + "weekend-1-blocklow" => "Dodge High (Blazin')", + "weekend-1-blockspin" => "Block High (Spin) (Blazin')", + "weekend-1-dodgehigh" => "Block Low (Blazin')", + "weekend-1-dodgelow" => "Dodge Low (Blazin')", + "weekend-1-dodgespin" => "Dodge High (Spin) (Blazin')", + "weekend-1-hithigh" => "Hit High (Blazin')", + "weekend-1-hitlow" => "Hit Low (Blazin')", + "weekend-1-hitspin" => "Hit High (Spin) (Blazin')", + "weekend-1-darnelluppercutprep" => "Darnell Uppercut (Prep) (Blazin')", + "weekend-1-darnelluppercut" => "Darnell Uppercut (Blazin')", + "weekend-1-idle" => "Idle (Blazin')", + "weekend-1-fakeout" => "Fakeout (Blazin')", + "weekend-1-taunt" => "Taunt (If Fakeout) (Blazin')", + "weekend-1-tauntforce" => "Taunt (Forced) (Blazin')", + "weekend-1-reversefakeout" => "Fakeout (Reverse) (Blazin')", ]; public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry diff --git a/source/funkin/ui/debug/latency/LatencyState.hx b/source/funkin/ui/debug/latency/LatencyState.hx index 70ef97fd0..9ebd29537 100644 --- a/source/funkin/ui/debug/latency/LatencyState.hx +++ b/source/funkin/ui/debug/latency/LatencyState.hx @@ -130,7 +130,7 @@ class LatencyState extends MusicBeatSubState for (i in 0...32) { - var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.instance.beatLengthMs * i); + var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); noteGrp.add(note); } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index e4a6b96d8..50f85571b 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -1001,7 +1001,7 @@ class FreeplayState extends MusicBeatSubState var daSong = songs[curSelected]; if (daSong != null) { - var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, currentDifficulty); + var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore?.accuracy ?? 0.0; rememberedDifficulty = currentDifficulty; @@ -1143,6 +1143,12 @@ class FreeplayState extends MusicBeatSubState targetSong: targetSong, targetDifficulty: targetDifficulty, targetVariation: targetVariation, + // TODO: Make this an option! + // startTimestamp: 0.0, + // TODO: Make this an option! + // playbackRate: 0.5, + practiceMode: false, + minimalMode: false, }, true); }); } @@ -1183,7 +1189,7 @@ class FreeplayState extends MusicBeatSubState var daSongCapsule = grpCapsules.members[curSelected]; if (daSongCapsule.songData != null) { - var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, currentDifficulty); + var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore?.accuracy ?? 0.0; diffIdsCurrent = daSongCapsule.songData.songDifficulties; diff --git a/source/funkin/ui/options/ColorsMenu.hx b/source/funkin/ui/options/ColorsMenu.hx index 928f74ba8..146384564 100644 --- a/source/funkin/ui/options/ColorsMenu.hx +++ b/source/funkin/ui/options/ColorsMenu.hx @@ -23,7 +23,7 @@ class ColorsMenu extends Page for (i in 0...4) { - var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), 0, i); + var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), i); note.x = (100 * i) + i; note.screenCenter(Y); diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 9012f3672..ba1d2ed21 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -649,7 +649,7 @@ class StoryMenuState extends MusicBeatState tracklistText.screenCenter(X); tracklistText.x -= FlxG.width * 0.35; - var levelScore:Null = Save.get().getLevelScore(currentLevelId, currentDifficultyId); + var levelScore:Null = Save.instance.getLevelScore(currentLevelId, currentDifficultyId); highScore = levelScore?.score ?? 0; // levelScore.accuracy }