From ea491e57a058660edf3b79deb65de14e5bf07bc0 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 15 Mar 2024 17:16:44 -0400 Subject: [PATCH 01/48] Switch "Skip Cutscene" and "Restart Cutscene" (Dave's request) --- source/funkin/play/PauseSubState.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index 03681ce13..10df25e90 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -72,8 +72,8 @@ class PauseSubState extends MusicBeatSubState */ static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array = [ {text: 'Resume', callback: resume}, - {text: 'Restart Cutscene', callback: restartVideoCutscene}, {text: 'Skip Cutscene', callback: skipVideoCutscene}, + {text: 'Restart Cutscene', callback: restartVideoCutscene}, {text: 'Exit to Menu', callback: quitToMenu}, ]; From e4eb543fa7511180319ccb4b90f9d10a20d1dc67 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 15 Mar 2024 21:53:07 -0400 Subject: [PATCH 02/48] Fix a bug where title music starts blaringly loud. --- source/funkin/audio/FunkinSound.hx | 17 ++++++++++++----- source/funkin/ui/title/TitleState.hx | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 9efa6ed50..a0bf8c58c 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -241,10 +241,16 @@ class FunkinSound extends FlxSound implements ICloneable @:allow(flixel.sound.FlxSoundGroup) override function updateTransform():Void { - _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end - (group != null ? group.volume : 1) * _volume * _volumeAdjust; + if (_transform != null) + { + _transform.volume = #if FLX_SOUND_SYSTEM ((FlxG.sound.muted || this.muted) ? 0 : 1) * FlxG.sound.volume * #end + (group != null ? group.volume : 1) * _volume * _volumeAdjust; + } - if (_channel != null) _channel.soundTransform = _transform; + if (_channel != null) + { + _channel.soundTransform = _transform; + } } public function clone():FunkinSound @@ -270,11 +276,12 @@ class FunkinSound extends FlxSound implements ICloneable * Creates a new `FunkinSound` object and loads it as the current music track. * * @param key The key of the music you want to play. Music should be at `music//.ogg`. + * @param startingVolume The volume you want the music to start at. * @param overrideExisting Whether to override music if it is already playing. * @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with. * Data should be at `music//-metadata.json`. */ - public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void + public static function playMusic(key:String, startingVolume:Float = 1.0, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void { if (!overrideExisting && FlxG.sound.music?.playing) return; @@ -292,7 +299,7 @@ class FunkinSound extends FlxSound implements ICloneable } } - FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key')); + FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), startingVolume); // Prevent repeat update() and onFocus() calls. FlxG.sound.list.remove(FlxG.sound.music); diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 1c194d80d..26f6612be 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -222,9 +222,9 @@ class TitleState extends MusicBeatState { var shouldFadeIn = (FlxG.sound.music == null); // Load music. Includes logic to handle BPM changes. - FunkinSound.playMusic('freakyMenu', false, true); + FunkinSound.playMusic('freakyMenu', 0.0, false, true); // Fade from 0.0 to 0.7 over 4 seconds - if (shouldFadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7); + if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); } function getIntroTextShit():Array> From 5733386519a705e3f119e2e48465cbb93db61331 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 16 Mar 2024 00:55:05 -0400 Subject: [PATCH 03/48] Update Polymod to improve error handling in scripts. --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index 42d17743f..6a8575eb0 100644 --- a/hmm.json +++ b/hmm.json @@ -146,7 +146,7 @@ "name": "polymod", "type": "git", "dir": null, - "ref": "be712450e5d3ba446008884921bb56873b299a64", + "ref": "682548319a272f6e4c9efa97aca081866426d5c7", "url": "https://github.com/larsiusprime/polymod" }, { From d56c33cd172eecbed8c7788437580588c7e6526e Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 16 Mar 2024 00:55:57 -0400 Subject: [PATCH 04/48] Fix a dozen tiny issues with 2hot's audio and visuals (and some script crashes!). --- assets | 2 +- source/funkin/Paths.hx | 2 +- source/funkin/audio/FunkinSound.hx | 63 ++++++- source/funkin/audio/SoundGroup.hx | 25 ++- .../modding/events/ScriptEventDispatcher.hx | 7 +- source/funkin/play/GameOverSubState.hx | 165 +++++++++++------- source/funkin/play/PlayState.hx | 14 +- source/funkin/play/PlayStatePlaylist.hx | 8 +- source/funkin/play/stage/Stage.hx | 3 +- .../ui/debug/charting/ChartEditorState.hx | 50 +++--- source/funkin/ui/freeplay/FreeplayState.hx | 20 ++- source/funkin/ui/mainmenu/MainMenuState.hx | 7 +- source/funkin/ui/story/StoryMenuState.hx | 7 +- source/funkin/ui/title/TitleState.hx | 9 +- source/funkin/ui/transition/LoadingState.hx | 94 +++++++--- source/funkin/util/Constants.hx | 6 + 16 files changed, 332 insertions(+), 150 deletions(-) diff --git a/assets b/assets index 0e2c5bf21..82ee26e1f 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0e2c5bf2134c7e517b70cf74afd58abe5c7b5e50 +Subproject commit 82ee26e1f733d1b2f30015ae69925e6a39d6526b diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index 6006939be..b00d13def 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -9,7 +9,7 @@ import openfl.utils.Assets as OpenFlAssets; */ class Paths { - static var currentLevel:String; + static var currentLevel:Null = null; static public function setCurrentLevel(name:String) { diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index a0bf8c58c..3e521ed0d 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -276,16 +276,27 @@ class FunkinSound extends FlxSound implements ICloneable * Creates a new `FunkinSound` object and loads it as the current music track. * * @param key The key of the music you want to play. Music should be at `music//.ogg`. - * @param startingVolume The volume you want the music to start at. - * @param overrideExisting Whether to override music if it is already playing. - * @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with. + * @param params A set of additional optional parameters. * Data should be at `music//-metadata.json`. */ - public static function playMusic(key:String, startingVolume:Float = 1.0, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void + public static function playMusic(key:String, params:FunkinSoundPlayMusicParams):Void { - if (!overrideExisting && FlxG.sound.music?.playing) return; + if (!(params.overrideExisting ?? false) && FlxG.sound.music?.playing) return; - if (mapTimeChanges) + if (!(params.restartTrack ?? false) && FlxG.sound.music?.playing) + { + if (FlxG.sound.music != null && Std.isOfType(FlxG.sound.music, FunkinSound)) + { + var existingSound:FunkinSound = cast FlxG.sound.music; + // Stop here if we would play a matching music track. + if (existingSound._label == Paths.music('$key/$key')) + { + return; + } + } + } + + if (params?.mapTimeChanges ?? true) { var songMusicData:Null = SongRegistry.instance.parseMusicData(key); // Will fall back and return null if the metadata doesn't exist or can't be parsed. @@ -299,7 +310,13 @@ class FunkinSound extends FlxSound implements ICloneable } } - FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), startingVolume); + if (FlxG.sound.music != null) + { + FlxG.sound.music.stop(); + FlxG.sound.music.kill(); + } + + FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, true, false, true); // Prevent repeat update() and onFocus() calls. FlxG.sound.list.remove(FlxG.sound.music); @@ -333,10 +350,10 @@ class FunkinSound extends FlxSound implements ICloneable sound._label = embeddedSound; } + if (autoPlay) sound.play(); sound.volume = volume; sound.group = FlxG.sound.defaultSoundGroup; sound.persist = true; - if (autoPlay) sound.play(); // Call onLoad() because the sound already loaded if (onLoad != null && sound._sound != null) onLoad(); @@ -356,3 +373,33 @@ class FunkinSound extends FlxSound implements ICloneable return sound; } } + +/** + * Additional parameters for `FunkinSound.playMusic()` + */ +typedef FunkinSoundPlayMusicParams = +{ + /** + * The volume you want the music to start at. + * @default `1.0` + */ + var ?startingVolume:Float; + + /** + * Whether to override music if a different track is already playing. + * @default `false` + */ + var ?overrideExisting:Bool; + + /** + * Whether to override music if the same track is already playing. + * @default `false` + */ + var ?restartTrack:Bool; + + /** + * Whether to check for `SongMusicData` to update the Conductor with. + * @default `true` + */ + var ?mapTimeChanges:Bool; +} diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx index a26537c2a..2c14099bd 100644 --- a/source/funkin/audio/SoundGroup.hx +++ b/source/funkin/audio/SoundGroup.hx @@ -151,14 +151,14 @@ class SoundGroup extends FlxTypedGroup /** * Stop all the sounds in the group. */ - public function stop() + public function stop():Void { forEachAlive(function(sound:FunkinSound) { sound.stop(); }); } - public override function destroy() + public override function destroy():Void { stop(); super.destroy(); @@ -176,9 +176,14 @@ class SoundGroup extends FlxTypedGroup function get_time():Float { - if (getFirstAlive() != null) return getFirstAlive().time; + if (getFirstAlive() != null) + { + return getFirstAlive().time; + } else + { return 0; + } } function set_time(time:Float):Float @@ -193,16 +198,26 @@ class SoundGroup extends FlxTypedGroup function get_playing():Bool { - if (getFirstAlive() != null) return getFirstAlive().playing; + if (getFirstAlive() != null) + { + return getFirstAlive().playing; + } else + { return false; + } } function get_volume():Float { - if (getFirstAlive() != null) return getFirstAlive().volume; + if (getFirstAlive() != null) + { + return getFirstAlive().volume; + } else + { return 1; + } } // in PlayState, adjust the code so that it only mutes the player1 vocal tracks? diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx index fd58d0fad..c262c311d 100644 --- a/source/funkin/modding/events/ScriptEventDispatcher.hx +++ b/source/funkin/modding/events/ScriptEventDispatcher.hx @@ -8,7 +8,12 @@ import funkin.modding.IScriptedClass; */ class ScriptEventDispatcher { - public static function callEvent(target:IScriptedClass, event:ScriptEvent):Void + /** + * Invoke the given event hook on the given scripted class. + * @param target The target class to call script hooks on. + * @param event The event, which determines the script hook to call and provides parameters for it. + */ + public static function callEvent(target:Null, event:ScriptEvent):Void { if (target == null || event == null) return; diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 95304d762..f84bc8d7f 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -2,20 +2,18 @@ package funkin.play; import flixel.FlxG; import flixel.FlxObject; -import flixel.FlxSprite; -import flixel.sound.FlxSound; -import funkin.audio.FunkinSound; +import flixel.input.touch.FlxTouch; import flixel.util.FlxColor; import flixel.util.FlxTimer; +import funkin.audio.FunkinSound; import funkin.graphics.FunkinSprite; 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; +import funkin.util.MathUtil; import openfl.utils.Assets; /** @@ -24,13 +22,14 @@ import openfl.utils.Assets; * * The newest implementation uses a substate, which prevents having to reload the song and stage each reset. */ +@:nullSafety class GameOverSubState extends MusicBeatSubState { /** * The currently active GameOverSubState. * There should be only one GameOverSubState in existance at a time, we can use a singleton. */ - public static var instance:GameOverSubState = null; + public static var instance:Null = null; /** * Which alternate animation on the character to use. @@ -38,7 +37,7 @@ class GameOverSubState extends MusicBeatSubState * For example, playing a different animation when BF dies in Week 4 * or Pico dies in Weekend 1. */ - public static var animationSuffix:String = ""; + public static var animationSuffix:String = ''; /** * Which alternate game over music to use. @@ -46,17 +45,19 @@ class GameOverSubState extends MusicBeatSubState * For example, the bf-pixel script sets this to `-pixel` * and the pico-playable script sets this to `Pico`. */ - public static var musicSuffix:String = ""; + public static var musicSuffix:String = ''; /** * Which alternate "blue ball" sound effect to use. */ - public static var blueBallSuffix:String = ""; + public static var blueBallSuffix:String = ''; + + static var blueballed:Bool = false; /** * The boyfriend character. */ - var boyfriend:BaseCharacter; + var boyfriend:Null = null; /** * The invisible object in the scene which the camera focuses on. @@ -83,7 +84,8 @@ class GameOverSubState extends MusicBeatSubState var transparent:Bool; - final CAMERA_ZOOM_DURATION:Float = 0.5; + static final CAMERA_ZOOM_DURATION:Float = 0.5; + var targetCameraZoom:Float = 1.0; public function new(params:GameOverParams) @@ -92,24 +94,27 @@ class GameOverSubState extends MusicBeatSubState this.isChartingMode = params?.isChartingMode ?? false; transparent = params.transparent; + + cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); } /** * Reset the game over configuration to the default. */ - public static function reset() + public static function reset():Void { - animationSuffix = ""; - musicSuffix = ""; - blueBallSuffix = ""; + animationSuffix = ''; + musicSuffix = ''; + blueBallSuffix = ''; + blueballed = false; } - override public function create() + public override function create():Void { if (instance != null) { // TODO: Do something in this case? IDK. - trace('WARNING: GameOverSubState instance already exists. This should not happen.'); + FlxG.log.warn('WARNING: GameOverSubState instance already exists. This should not happen.'); } instance = this; @@ -120,7 +125,7 @@ class GameOverSubState extends MusicBeatSubState // // Add a black background to the screen. - var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); + var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK); // We make this transparent so that we can see the stage underneath during debugging, // but it's normally opaque. bg.alpha = transparent ? 0.25 : 1.0; @@ -135,18 +140,7 @@ class GameOverSubState extends MusicBeatSubState add(boyfriend); boyfriend.resetCharacter(); - // Assign a camera follow point to the boyfriend's position. - cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); - cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; - cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; - var offsets:Array = boyfriend.getDeathCameraOffsets(); - cameraFollowPoint.x += offsets[0]; - cameraFollowPoint.y += offsets[1]; - add(cameraFollowPoint); - - FlxG.camera.target = null; - FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01); - targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom(); + setCameraTarget(); // // Set up the audio @@ -156,6 +150,26 @@ class GameOverSubState extends MusicBeatSubState Conductor.instance.update(0); } + @:nullSafety(Off) + function setCameraTarget():Void + { + // Assign a camera follow point to the boyfriend's position. + cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; + cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; + var offsets:Array = boyfriend.getDeathCameraOffsets(); + cameraFollowPoint.x += offsets[0]; + cameraFollowPoint.y += offsets[1]; + add(cameraFollowPoint); + + FlxG.camera.target = null; + FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2); + targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom(); + } + + /** + * Forcibly reset the camera zoom level to that of the current stage. + * This prevents camera zoom events from adversely affecting the game over state. + */ public function resetCameraZoom():Void { // Apply camera zoom level from stage data. @@ -164,21 +178,24 @@ class GameOverSubState extends MusicBeatSubState var hasStartedAnimation:Bool = false; - override function update(elapsed:Float) + override function update(elapsed:Float):Void { if (!hasStartedAnimation) { hasStartedAnimation = true; - if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100)) + if (boyfriend != null) { - boyfriend.playAnimation('fakeoutDeath', true, false); - } - else - { - boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over! - // Play the "blue balled" sound. May play a variant if one has been assigned. - playBlueBalledSFX(); + if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100)) + { + boyfriend.playAnimation('fakeoutDeath', true, false); + } + else + { + boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over! + // Play the "blue balled" sound. May play a variant if one has been assigned. + playBlueBalledSFX(); + } } } @@ -192,10 +209,10 @@ class GameOverSubState extends MusicBeatSubState // MOBILE ONLY: Restart the level when tapping Boyfriend. if (FlxG.onMobile) { - var touch = FlxG.touches.getFirst(); + var touch:FlxTouch = FlxG.touches.getFirst(); if (touch != null) { - if (touch.overlaps(boyfriend)) + if (boyfriend == null || touch.overlaps(boyfriend)) { confirmDeath(); } @@ -215,7 +232,7 @@ class GameOverSubState extends MusicBeatSubState blueballed = false; PlayState.instance.deathCounter = 0; // PlayState.seenCutscene = false; // old thing... - gameOverMusic.stop(); + if (gameOverMusic != null) gameOverMusic.stop(); if (isChartingMode) { @@ -239,14 +256,14 @@ class GameOverSubState extends MusicBeatSubState // This enables the stepHit and beatHit events. Conductor.instance.update(gameOverMusic.time); } - else + else if (boyfriend != null) { // Music hasn't started yet. switch (PlayStatePlaylist.campaignId) { // TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded. - // This will simplify the class and make it easier for mods to add death quotes. - case "week7": + // This will simplify the class and make it easier for mods or future weeks to add death quotes. + case 'week7': if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote) { playingJeffQuote = true; @@ -279,7 +296,7 @@ class GameOverSubState extends MusicBeatSubState isEnding = true; startDeathMusic(1.0, true); // isEnding changes this function's behavior. - boyfriend.playAnimation('deathConfirm' + animationSuffix, true); + if (boyfriend != null) boyfriend.playAnimation('deathConfirm' + animationSuffix, true); // After the animation finishes... new FlxTimer().start(0.7, function(tmr:FlxTimer) { @@ -290,9 +307,12 @@ class GameOverSubState extends MusicBeatSubState PlayState.instance.needsReset = true; // Readd Boyfriend to the stage. - boyfriend.isDead = false; - remove(boyfriend); - PlayState.instance.currentStage.addCharacter(boyfriend, BF); + if (boyfriend != null) + { + boyfriend.isDead = false; + remove(boyfriend); + PlayState.instance.currentStage.addCharacter(boyfriend, BF); + } // Snap reset the camera which may have changed because of the player character data. resetCameraZoom(); @@ -304,7 +324,7 @@ class GameOverSubState extends MusicBeatSubState } } - public override function dispatchEvent(event:ScriptEvent) + public override function dispatchEvent(event:ScriptEvent):Void { super.dispatchEvent(event); @@ -317,11 +337,11 @@ class GameOverSubState extends MusicBeatSubState */ function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null { - var basePath = 'gameplay/gameover/gameOver'; - if (starting) basePath += 'Start'; - else if (ending) basePath += 'End'; + var basePath:String = 'gameplay/gameover/gameOver'; + if (ending) basePath += 'End'; + else if (starting) basePath += 'Start'; - var musicPath = Paths.music(basePath + suffix); + var musicPath:String = Paths.music(basePath + suffix); while (!Assets.exists(musicPath) && suffix.length > 0) { suffix = suffix.split('-').slice(0, -1).join('-'); @@ -334,23 +354,26 @@ class GameOverSubState extends MusicBeatSubState /** * Starts the death music at the appropriate volume. - * @param startingVolume + * @param startingVolume The initial volume for the music. + * @param force Whether or not to force the music to restart. */ public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void { - var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); - var onComplete = null; + var musicPath:Null = resolveMusicPath(musicSuffix, isStarting, isEnding); + var onComplete:() -> Void = () -> {}; + if (isStarting) { if (musicPath == null) { + // Looked for starting music and didn't find it. Use middle music instead. isStarting = false; musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding); } else { onComplete = function() { - isStarting = false; + isStarting = true; // We need to force to ensure that the non-starting music plays. startDeathMusic(1.0, true); }; @@ -359,13 +382,16 @@ class GameOverSubState extends MusicBeatSubState if (musicPath == null) { - trace('Could not find game over music!'); + FlxG.log.warn('[GAMEOVER] Could not find game over music at path ($musicPath)!'); return; } else if (gameOverMusic == null || !gameOverMusic.playing || force) { if (gameOverMusic != null) gameOverMusic.stop(); + gameOverMusic = FunkinSound.load(musicPath); + if (gameOverMusic == null) return; + gameOverMusic.volume = startingVolume; gameOverMusic.looped = !(isEnding || isStarting); gameOverMusic.onComplete = onComplete; @@ -378,13 +404,11 @@ class GameOverSubState extends MusicBeatSubState } } - static var blueballed:Bool = false; - /** * Play the sound effect that occurs when * boyfriend's testicles get utterly annihilated. */ - public static function playBlueBalledSFX() + public static function playBlueBalledSFX():Void { blueballed = true; if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix))) @@ -403,7 +427,7 @@ class GameOverSubState extends MusicBeatSubState * Week 7-specific hardcoded behavior, to play a custom death quote. * TODO: Make this a module somehow. */ - function playJeffQuote() + function playJeffQuote():Void { var randomCensor:Array = []; @@ -418,20 +442,27 @@ class GameOverSubState extends MusicBeatSubState }); } - public override function destroy() + public override function destroy():Void { super.destroy(); - if (gameOverMusic != null) gameOverMusic.stop(); - gameOverMusic = null; + if (gameOverMusic != null) + { + gameOverMusic.stop(); + gameOverMusic = null; + } + blueballed = false; instance = null; } public override function toString():String { - return "GameOverSubState"; + return 'GameOverSubState'; } } +/** + * Parameters used to instantiate a GameOverSubState. + */ typedef GameOverParams = { var isChartingMode:Bool; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 984f27c26..43a7d1615 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1499,7 +1499,7 @@ class PlayState extends MusicBeatSubState public function resetCameraZoom():Void { // Apply camera zoom level from stage data. - defaultCameraZoom = currentStage.camZoom; + defaultCameraZoom = currentStage?.camZoom ?? 1.0; } /** @@ -2712,7 +2712,12 @@ class PlayState extends MusicBeatSubState if (targetSongId == null) { - FunkinSound.playMusic('freakyMenu'); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); // transIn = FlxTransitionableState.defaultTransIn; // transOut = FlxTransitionableState.defaultTransOut; @@ -2993,7 +2998,10 @@ class PlayState extends MusicBeatSubState */ public function resetCamera():Void { - FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04); + // Apply camera zoom level from stage data. + defaultCameraZoom = currentStage?.camZoom ?? 1.0; + + FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE); FlxG.camera.targetOffset.set(); FlxG.camera.zoom = defaultCameraZoom; // Snap the camera to the follow point immediately. diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx index 3b0fb01f6..e47a6288a 100644 --- a/source/funkin/play/PlayStatePlaylist.hx +++ b/source/funkin/play/PlayStatePlaylist.hx @@ -5,12 +5,13 @@ package funkin.play; * * TODO: Add getters/setters for all these properties to validate them. */ +@:nullSafety class PlayStatePlaylist { /** * Whether the game is currently in Story Mode. If false, we are in Free Play Mode. */ - public static var isStoryMode(default, default):Bool = false; + public static var isStoryMode:Bool = false; /** * The loist of upcoming songs to be played. @@ -31,8 +32,9 @@ class PlayStatePlaylist /** * The internal ID of the current playlist, for example `week4` or `weekend-1`. + * @default `null`, used when no playlist is loaded */ - public static var campaignId:String = 'unknown'; + public static var campaignId:Null = null; public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY; @@ -45,7 +47,7 @@ class PlayStatePlaylist playlistSongIds = []; campaignScore = 0; campaignTitle = 'UNKNOWN'; - campaignId = 'unknown'; + campaignId = null; campaignDifficulty = Constants.DEFAULT_DIFFICULTY; } } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 56026469a..e80cbe0ae 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -220,7 +220,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements if (propSprite.frames == null || propSprite.frames.numFrames == 0) { - trace(' ERROR: Could not build texture for prop.'); + @:privateAccess + trace(' ERROR: Could not build texture for prop. Check the asset path (${Paths.currentLevel ?? 'default'}, ${dataProp.assetPath}).'); continue; } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c59a5abdb..64ce14d9d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -6,18 +6,15 @@ import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; -import flixel.graphics.FlxGraphic; import flixel.group.FlxGroup.FlxTypedGroup; -import funkin.graphics.FunkinCamera; import flixel.group.FlxSpriteGroup; import flixel.input.keyboard.FlxKey; +import funkin.play.PlayStatePlaylist; import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.sound.FlxSound; -import flixel.system.debug.log.LogStyle; -import flixel.system.FlxAssets.FlxSoundAsset; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; @@ -27,26 +24,19 @@ import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.FunkinSound; import funkin.audio.visualize.PolygonSpectogram; -import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; import funkin.audio.waveform.WaveformSprite; import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.song.SongData.SongCharacterData; -import funkin.data.song.SongData.SongCharacterData; -import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongDataUtils; -import funkin.data.song.SongDataUtils; -import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry; import funkin.data.stage.StageData; +import funkin.graphics.FunkinCamera; import funkin.graphics.FunkinSprite; import funkin.input.Cursor; import funkin.input.TurboKeyHandler; @@ -62,8 +52,6 @@ import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.ui.debug.charting.commands.ChartEditorCommand; -import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.CopyItemsCommand; import funkin.ui.debug.charting.commands.CutItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; @@ -96,6 +84,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.ui.mainmenu.MainMenuState; +import funkin.ui.transition.LoadingState; import funkin.util.Constants; import funkin.util.FileUtil; import funkin.util.logging.CrashHandler; @@ -120,7 +109,6 @@ import haxe.ui.containers.Grid; import haxe.ui.containers.HBox; import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuBar; -import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.ScrollView; @@ -131,7 +119,6 @@ import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; import haxe.ui.events.MouseEvent; import haxe.ui.events.UIEvent; -import haxe.ui.events.UIEvent; import haxe.ui.focus.FocusManager; import haxe.ui.Toolkit; import openfl.display.BitmapData; @@ -5330,30 +5317,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } catch (e) { - this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); + this.error('Could Not Playtest', 'Got an error trying to playtest the song.\n${e}'); return; } - // TODO: Rework asset system so we can remove this. + // TODO: Rework asset system so we can remove this jank. switch (currentSongStage) { case 'mainStage': - Paths.setCurrentLevel('week1'); + PlayStatePlaylist.campaignId = 'week1'; case 'spookyMansion': - Paths.setCurrentLevel('week2'); + PlayStatePlaylist.campaignId = 'week2'; case 'phillyTrain': - Paths.setCurrentLevel('week3'); + PlayStatePlaylist.campaignId = 'week3'; case 'limoRide': - Paths.setCurrentLevel('week4'); + PlayStatePlaylist.campaignId = 'week4'; case 'mallXmas' | 'mallEvil': - Paths.setCurrentLevel('week5'); + PlayStatePlaylist.campaignId = 'week5'; case 'school' | 'schoolEvil': - Paths.setCurrentLevel('week6'); + PlayStatePlaylist.campaignId = 'week6'; case 'tankmanBattlefield': - Paths.setCurrentLevel('week7'); + PlayStatePlaylist.campaignId = 'week7'; case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2': - Paths.setCurrentLevel('weekend1'); + PlayStatePlaylist.campaignId = 'weekend1'; } + Paths.setCurrentLevel(PlayStatePlaylist.campaignId); subStateClosed.add(reviveUICamera); subStateClosed.add(resetConductorAfterTest); @@ -5361,7 +5349,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransOut = false; - var targetState = new PlayState( + var targetStateParams = { targetSong: targetSong, targetDifficulty: selectedDifficulty, @@ -5372,14 +5360,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState startTimestamp: startTimestamp, playbackRate: playbackRate, overrideMusic: true, - }); + }; // Override music. if (audioInstTrack != null) { FlxG.sound.music = audioInstTrack; } - targetState.vocals = audioVocalTrackGroup; // Kill and replace the UI camera so it doesn't get destroyed during the state transition. uiCamera.kill(); @@ -5389,7 +5376,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.persistentUpdate = false; this.persistentDraw = false; stopWelcomeMusic(); - openSubState(targetState); + + LoadingState.loadPlayState(targetStateParams, false, true, function(targetState) { + targetState.vocals = audioVocalTrackGroup; + }); } /** diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 7ade5a2a6..068b57a9c 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -179,7 +179,7 @@ class FreeplayState extends MusicBeatSubState #if discord_rpc // Updating Discord Rich Presence - DiscordClient.changePresence("In the Menus", null); + DiscordClient.changePresence('In the Menus', null); #end var isDebug:Bool = false; @@ -188,14 +188,19 @@ class FreeplayState extends MusicBeatSubState isDebug = true; #end - FunkinSound.playMusic('freakyMenu'); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); // Add a null entry that represents the RANDOM option songs.push(null); // TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later. // Default character (BF) shows default and Erect variations. Pico shows only Pico variations. - displayedVariations = (currentCharacter == "bf") ? [Constants.DEFAULT_VARIATION, "erect"] : [currentCharacter]; + displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter]; // programmatically adds the songs via LevelRegistry and SongRegistry for (levelId in LevelRegistry.instance.listBaseGameLevelIds()) @@ -205,7 +210,7 @@ class FreeplayState extends MusicBeatSubState var song:Song = SongRegistry.instance.fetchEntry(songId); // Only display songs which actually have available charts for the current character. - var availableDifficultiesForSong = song.listDifficulties(displayedVariations); + var availableDifficultiesForSong:Array = song.listDifficulties(displayedVariations); if (availableDifficultiesForSong.length == 0) continue; songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations)); @@ -1226,7 +1231,12 @@ class FreeplayState extends MusicBeatSubState // TODO: Stream the instrumental of the selected song? if (prevSelected == 0) { - FunkinSound.playMusic('freakyMenu'); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); FlxG.sound.music.fadeIn(2, 0, 0.8); } } diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 1892bdec1..38654dcb8 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -155,7 +155,12 @@ class MainMenuState extends MusicBeatState function playMenuMusic():Void { - FunkinSound.playMusic('freakyMenu'); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); } function resetCamStuff() diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 1f78eb375..82b419373 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -235,7 +235,12 @@ class StoryMenuState extends MusicBeatState function playMenuMusic():Void { - FunkinSound.playMusic('freakyMenu'); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: false + }); } function updateData():Void diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 26f6612be..eb4404c78 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -220,9 +220,14 @@ class TitleState extends MusicBeatState function playMenuMusic():Void { - var shouldFadeIn = (FlxG.sound.music == null); + var shouldFadeIn:Bool = (FlxG.sound.music == null); // Load music. Includes logic to handle BPM changes. - FunkinSound.playMusic('freakyMenu', 0.0, false, true); + FunkinSound.playMusic('freakyMenu', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: true + }); // Fade from 0.0 to 0.7 over 4 seconds if (shouldFadeIn) FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); } diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 23b3db6a9..304922988 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -22,10 +22,12 @@ import openfl.filters.ShaderFilter; import openfl.utils.Assets; import flixel.util.typeLimit.NextState; -class LoadingState extends MusicBeatState +class LoadingState extends MusicBeatSubState { inline static var MIN_TIME = 1.0; + var asSubState:Bool = false; + var target:NextState; var playParams:Null; var stopMusic:Bool = false; @@ -173,7 +175,16 @@ class LoadingState extends MusicBeatState { if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop(); - FlxG.switchState(target); + if (asSubState) + { + this.close(); + // We will assume the target is a valid substate. + FlxG.state.openSubState(cast target); + } + else + { + FlxG.switchState(target); + } } static function getSongPath():String @@ -185,17 +196,41 @@ class LoadingState extends MusicBeatState * Starts the transition to a new `PlayState` to start a new song. * First switches to the `LoadingState` if assets need to be loaded. * @param params The parameters for the next `PlayState`. + * @param asSubState Whether to open as a substate rather than switching to the `PlayState`. * @param shouldStopMusic Whether to stop the current music while loading. */ - public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false):Void + public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void { Paths.setCurrentLevel(PlayStatePlaylist.campaignId); - var playStateCtor:NextState = () -> new PlayState(params); + var playStateCtor:() -> PlayState = function() { + return new PlayState(params); + }; + + if (onConstruct != null) + { + playStateCtor = function() { + var result = new PlayState(params); + onConstruct(result); + return result; + }; + } #if NO_PRELOAD_ALL // Switch to loading state while we load assets (default on HTML5 target). - var loadStateCtor:NextState = () -> new LoadingState(playStateCtor, shouldStopMusic, params); - FlxG.switchState(loadStateCtor); + var loadStateCtor:NextState = function() { + var result = new LoadingState(playStateCtor, shouldStopMusic, params); + @:privateAccess + result.asSubState = asSubState; + return result; + } + if (asSubState) + { + FlxG.state.openSubState(loadStateCtor); + } + else + { + FlxG.switchState(loadStateCtor); + } #else // All assets preloaded, switch directly to play state (defualt on other targets). if (shouldStopMusic && FlxG.sound.music != null) @@ -209,6 +244,34 @@ class LoadingState extends MusicBeatState params.targetSong.cacheCharts(true); } + var shouldPreloadLevelAssets:Bool = !(params?.minimalMode ?? false); + + if (shouldPreloadLevelAssets) preloadLevelAssets(); + + if (asSubState) + { + FlxG.state.openSubState(cast playStateCtor()); + } + else + { + FlxG.switchState(playStateCtor); + } + #end + } + + #if NO_PRELOAD_ALL + static function isSoundLoaded(path:String):Bool + { + return Assets.cache.hasSound(path); + } + + static function isLibraryLoaded(library:String):Bool + { + return Assets.getLibrary(library) != null; + } + #else + static function preloadLevelAssets():Void + { // TODO: This section is a hack! Redo this later when we have a proper asset caching system. FunkinSprite.preparePurgeCache(); FunkinSprite.cacheTexture(Paths.image('combo')); @@ -241,7 +304,10 @@ class LoadingState extends MusicBeatState // List all image assets in the level's library. // This is crude and I want to remove it when we have a proper asset caching system. // TODO: Get rid of this junk! - var library = openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId); + var library = PlayStatePlaylist.campaignId != null ? openfl.utils.Assets.getLibrary(PlayStatePlaylist.campaignId) : null; + + if (library == null) return; // We don't need to do anymore precaching. + var assets = library.list(lime.utils.AssetType.IMAGE); trace('Got ${assets.length} assets: ${assets}'); @@ -272,20 +338,6 @@ class LoadingState extends MusicBeatState // FunkinSprite.cacheAllSongTextures(stage) FunkinSprite.purgeCache(); - - FlxG.switchState(playStateCtor); - #end - } - - #if NO_PRELOAD_ALL - static function isSoundLoaded(path:String):Bool - { - return Assets.cache.hasSound(path); - } - - static function isLibraryLoaded(library:String):Bool - { - return Assets.getLibrary(library) != null; } #end diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 1005b312e..8d7800c00 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -442,4 +442,10 @@ class Constants * The vertical offset of the strumline from the top edge of the screen. */ public static final STRUMLINE_Y_OFFSET:Float = 24; + + /** + * The rate at which the camera lerps to its target. + * 0.04 = 4% of distance per frame. + */ + public static final DEFAULT_CAMERA_FOLLOW_RATE:Float = 0.04; } From efd0bef89f25467d2d762afada9071435f971617 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 16 Mar 2024 01:08:20 -0400 Subject: [PATCH 05/48] Remove checkbox that should have been removed earlier. --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 82ee26e1f..fbfaad0c4 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 82ee26e1f733d1b2f30015ae69925e6a39d6526b +Subproject commit fbfaad0c4e35fbf48937fb7e28e3888587cf16b1 From 1e888658f75eb859f51b698ddde8a5d3af188e8b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 26 Mar 2024 12:33:54 -0400 Subject: [PATCH 06/48] First implementation of credits state. --- Project.xml | 1 + assets | 2 +- source/funkin/data/BaseRegistry.hx | 9 - source/funkin/data/JsonFile.hx | 10 + source/funkin/data/song/SongRegistry.hx | 6 +- source/funkin/ui/credits/CreditsData.hx | 26 +++ .../funkin/ui/credits/CreditsDataHandler.hx | 130 ++++++++++++ source/funkin/ui/credits/CreditsDataMacro.hx | 67 ++++++ source/funkin/ui/credits/CreditsState.hx | 200 ++++++++++++++++++ source/funkin/ui/mainmenu/MainMenuState.hx | 7 + 10 files changed, 445 insertions(+), 13 deletions(-) create mode 100644 source/funkin/data/JsonFile.hx create mode 100644 source/funkin/ui/credits/CreditsData.hx create mode 100644 source/funkin/ui/credits/CreditsDataHandler.hx create mode 100644 source/funkin/ui/credits/CreditsDataMacro.hx create mode 100644 source/funkin/ui/credits/CreditsState.hx diff --git a/Project.xml b/Project.xml index ffc8382a4..5962b9dd8 100644 --- a/Project.xml +++ b/Project.xml @@ -175,6 +175,7 @@ +
diff --git a/assets b/assets index 5f1726f1b..edccf0421 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5f1726f1b0c11fc747b7473708cf4e5f28be05f1 +Subproject commit edccf04217c49c730b11c80736e2b2d98a25ee95 diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index 7419d9425..118516bec 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -325,12 +325,3 @@ abstract class BaseRegistry & Constructible return ScriptedSong.listScriptClasses(); } - function loadEntryMetadataFile(id:String, ?variation:String):Null + function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); @@ -438,7 +438,7 @@ class SongRegistry extends BaseRegistry return {fileName: entryFilePath, contents: rawJson}; } - function loadMusicDataFile(id:String, ?variation:String):Null + function loadMusicDataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); @@ -456,7 +456,7 @@ class SongRegistry extends BaseRegistry return openfl.Assets.exists(entryFilePath); } - function loadEntryChartFile(id:String, ?variation:String):Null + function loadEntryChartFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); diff --git a/source/funkin/ui/credits/CreditsData.hx b/source/funkin/ui/credits/CreditsData.hx new file mode 100644 index 000000000..0f6ea6bcd --- /dev/null +++ b/source/funkin/ui/credits/CreditsData.hx @@ -0,0 +1,26 @@ +package funkin.ui.credits; + +/** + * The members of the Funkin' Crew, organized by their roles. + */ +typedef CreditsData = +{ + var roles:Array; +} + +/** + * The members of a specific role on the Funkin' Crew. + */ +typedef CreditsDataRole = +{ + var roleName:String; + var members:Array; +} + +/** + * A member of a specific person on the Funkin' Crew. + */ +typedef CreditsDataMember = +{ + var fullName:String; +} diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx new file mode 100644 index 000000000..6317dd55d --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -0,0 +1,130 @@ +package funkin.ui.credits; + +import funkin.data.JsonFile; + +using StringTools; + +@:nullSafety +class CreditsDataHandler +{ + public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers'; + + #if HARDCODED_CREDITS + static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json"; + #else + static final CREDITS_DATA_PATH:String = "assets/data/credits.json"; + #end + + public static function debugPrint(data:Null):Void + { + if (data == null) + { + trace('CreditsData(NULL)'); + + return; + } + + var roleCount = data.roles.length; + var memberCount = 0; + for (role in data.roles) + { + memberCount += role.members.length; + } + + trace('CreditsData($roleCount roles with $memberCount members)'); + } + + /** + * If for some reason the full credits won't load, + * use this hardcoded data for the original Funkin' Crew. + * + * @return `CreditsData` + */ + public static inline function getFallback():CreditsData + { + return { + roles: [ + { + roleName: 'Founders', + members: [ + {fullName: 'ninjamuffin99'}, + {fullName: 'PhantomArcade'}, + {fullName: 'KawaiSprite'}, + {fullName: 'evilsk8r'}, + ] + } + ] + }; + } + + public static function fetchBackerEntries():Array + { + // TODO: Replace this with a web request. + // We can't just grab the current Kickstarter data and include it in builds, + // because we don't want to deadname people who haven't logged into the portal yet. + // It can be async and paginated for performance! + return ['See the list of backers at $BACKER_PUBLIC_URL.']; + } + + #if HARDCODED_CREDITS + /** + * The data for the credits. + * Hardcoded into game via a macro at compile time. + */ + public static final CREDITS_DATA:Null = #if macro null #else CreditsDataMacro.loadCreditsData() #end; + #else + + /** + * The data for the credits. + * Loaded dynamically from the game folder when needed. + * Nullable because data may fail to parse. + */ + public static var CREDITS_DATA(get, default):Null = null; + + static function get_CREDITS_DATA():Null + { + if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData()); + + return CREDITS_DATA; + } + + static function fetchCreditsData():funkin.data.JsonFile + { + var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim(); + + return { + fileName: CREDITS_DATA_PATH, + contents: rawJson + }; + } + + static function parseCreditsData(file:JsonFile):Null + { + #if !macro + if (file.contents == null) return null; + + var parser = new json2object.JsonParser(); + parser.ignoreUnknownVariables = false; + trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}'); + parser.fromJson(file.contents, file.fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, file.fileName); + return null; + } + return parser.value; + #else + return null; + #end + } + + static function printErrors(errors:Array, id:String = ''):Void + { + trace('[CREDITS] Failed to parse credits data: ${id}'); + + for (error in errors) + funkin.data.DataError.printError(error); + } + #end +} diff --git a/source/funkin/ui/credits/CreditsDataMacro.hx b/source/funkin/ui/credits/CreditsDataMacro.hx new file mode 100644 index 000000000..c97770eef --- /dev/null +++ b/source/funkin/ui/credits/CreditsDataMacro.hx @@ -0,0 +1,67 @@ +package funkin.ui.credits; + +#if macro +import haxe.macro.Context; +#end + +@:access(funkin.ui.credits.CreditsDataHandler) +class CreditsDataMacro +{ + public static macro function loadCreditsData():haxe.macro.Expr.ExprOf + { + #if !display + trace('Hardcoding credits data...'); + var json = CreditsDataMacro.fetchJSON(); + + if (json == null) + { + Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + var creditsData = CreditsDataMacro.parseJSON(json); + + if (creditsData == null) + { + Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos()); + return macro $v{CreditsDataHandler.getFallback()}; + } + + CreditsDataHandler.debugPrint(creditsData); + return macro $v{creditsData}; + // return macro $v{null}; + #else + // `#if display` is used for code completion. In this case we return + // a minimal value to keep code completion fast. + return macro $v{CreditsDataHandler.getFallback()}; + #end + } + + #if macro + static function fetchJSON():Null + { + return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH); + } + + /** + * Parse the JSON data for the credits. + * + * @param json The string data to parse. + * @return The parsed data. + */ + static function parseJSON(json:String):Null + { + try + { + // TODO: Use something with better validation but that still works at macro time. + return haxe.Json.parse(json); + } + catch (e) + { + trace('[ERROR] Failed to parse JSON data for credits.'); + trace(e); + return null; + } + } + #end +} diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx new file mode 100644 index 000000000..1e5965695 --- /dev/null +++ b/source/funkin/ui/credits/CreditsState.hx @@ -0,0 +1,200 @@ +package funkin.ui.credits; + +import flixel.text.FlxText; +import flixel.util.FlxColor; +import funkin.audio.FunkinSound; +import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup; + +/** + * The state used to display the credits scroll. + * AAA studios often fail to credit properly, and we're better than them! + */ +class CreditsState extends MusicBeatState +{ + /** + * The height the credits should start at. + * Make this an instanced variable so it gets set by the constructor. + */ + final STARTING_HEIGHT = FlxG.height; + + /** + * The padding on each side of the screen. + */ + static final SCREEN_PAD = 24; + + /** + * The width of the screen the credits should maximally fill up. + * Make this an instanced variable so it gets set by the constructor. + */ + final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2); + + /** + * The font to use to display the text. + * To use a font from the `assets` folder, use `Paths.font(...)`. + * Choose something that will render Unicode properly. + */ + static final CREDITS_FONT = 'Arial'; + + /** + * The size of the font. + */ + static final CREDITS_FONT_SIZE = 48; + + static final CREDITS_HEADER_FONT_SIZE = 72; + + /** + * The color of the text itself. + */ + static final CREDITS_FONT_COLOR = FlxColor.WHITE; + + /** + * The color of the text's outline. + */ + static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK; + + /** + * The speed the credits scroll at, in pixels per second. + */ + static final CREDITS_SCROLL_BASE_SPEED = 25.0; + + /** + * The speed the credits scroll at while the button is held, in pixels per second. + */ + static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0; + + /** + * The actual sprites and text used to display the credits. + */ + var creditsGroup:FlxSpriteGroup; + + var scrollPaused:Bool = false; + + public function new() + { + super(); + } + + public override function create():Void + { + super.create(); + + // Background + var bg = new FlxSprite(Paths.image('menuDesat')); + bg.scrollFactor.x = 0; + bg.scrollFactor.y = 0; + bg.setGraphicSize(Std.int(FlxG.width)); + bg.updateHitbox(); + bg.x = 0; + bg.y = 0; + bg.visible = true; + bg.color = 0xFFB57EDC; // Lavender + add(bg); + + // TODO: Once we need to display Kickstarter backers, + // make this use a recycled pool so we don't kill peformance. + creditsGroup = new FlxSpriteGroup(); + creditsGroup.x = SCREEN_PAD; + creditsGroup.y = STARTING_HEIGHT; + + buildCreditsGroup(); + + add(creditsGroup); + + // Music + FunkinSound.playMusic('freeplayRandom', + { + startingVolume: 0.0, + overrideExisting: true, + restartTrack: true, + loop: true + }); + FlxG.sound.music.fadeIn(2, 0, 0.8); + } + + function buildCreditsGroup():Void + { + var y = 0; + + for (role in CreditsDataHandler.CREDITS_DATA.roles) + { + creditsGroup.add(buildCreditsLine(role.roleName, y, true, CreditsSide.Center)); + y += CREDITS_HEADER_FONT_SIZE; + + for (member in role.members) + { + creditsGroup.add(buildCreditsLine(member.fullName, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + + // Padding between each role. + y += CREDITS_FONT_SIZE * 2; + } + } + + function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText + { + // CreditsSide.Center: Full screen width + // CreditsSide.Left: Left half of screen + // CreditsSide.Right: Right half of screen + var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0; + var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2); + var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE; + + var creditsLine:FlxText = new FlxText(xPos, yPos, width, text); + creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true); + + return creditsLine; + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (!scrollPaused) + { + // TODO: Replace with whatever the special note button is. + if (controls.ACCEPT || FlxG.keys.pressed.SPACE) + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed; + } + else + { + // Move the whole group. + creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed; + } + } + + if (controls.BACK || hasEnded()) + { + exit(); + } + else if (controls.PAUSE) + { + scrollPaused = !scrollPaused; + } + } + + function hasEnded():Bool + { + return creditsGroup.y < -creditsGroup.height; + } + + function exit():Void + { + FlxG.switchState(new funkin.ui.mainmenu.MainMenuState()); + } + + public override function destroy():Void + { + super.destroy(); + } +} + +enum CreditsSide +{ + Left; + Center; + Right; +} diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index a8c2039ab..e536554d0 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -117,6 +117,10 @@ class MainMenuState extends MusicBeatState startExitState(() -> new funkin.ui.options.OptionsState()); }); + createMenuItem('options', 'mainmenu/options', function() { + startExitState(() -> new funkin.ui.credits.CreditsState()); + }); + // Reset position of menu items. var spacing = 160; var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2; @@ -125,6 +129,9 @@ class MainMenuState extends MusicBeatState var menuItem = menuItems.members[i]; menuItem.x = FlxG.width / 2; menuItem.y = top + spacing * i; + menuItem.scrollFactor.x = 0.0; + // This one affects how much the menu items move when you scroll between them. + menuItem.scrollFactor.y = 0.4; } resetCamStuff(); From f1811a8594c79f22447af3a62a81fdfb755a9217 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 27 Mar 2024 17:42:29 -0400 Subject: [PATCH 07/48] Implement Pico pause music --- assets | 2 +- source/funkin/play/PauseSubState.hx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 8013845e3..8ee8c3bd9 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8013845e331015b40c4cc35230f6d02bd2148d52 +Subproject commit 8ee8c3bd9a828ced8e89c7c924842e859fea9a8c diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index f16aa00d8..ed847402a 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -230,7 +230,7 @@ class PauseSubState extends MusicBeatSubState */ function startPauseMusic():Void { - var pauseMusicPath:String = Paths.music('breakfast$musicSuffix'); + var pauseMusicPath:String = Paths.music('breakfast$musicSuffix/breakfast$musicSuffix'); pauseMusic = FunkinSound.load(pauseMusicPath, true, true); if (pauseMusic == null) From 195f366b6571b5a2dc99d6b2058a6b87b9c65a64 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 27 Mar 2024 17:43:15 -0400 Subject: [PATCH 08/48] Fix an issue where the Random button would crash Freeplay. --- source/funkin/audio/FunkinSound.hx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 6520ff27f..56b36d5df 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -399,10 +399,16 @@ class FunkinSound extends FlxSound implements ICloneable return sound; } + @:nullSafety(Off) public override function destroy():Void { // trace('[FunkinSound] Destroying sound "${this._label}"'); super.destroy(); + if (fadeTween != null) + { + fadeTween.cancel(); + fadeTween = null; + } FlxTween.cancelTweensOf(this); this._label = 'unknown'; } From f4dd11e2a976951355a047d97695d00be9f42b29 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 27 Mar 2024 18:44:59 -0400 Subject: [PATCH 09/48] Update more game audio. --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 8ee8c3bd9..46cbc7524 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8ee8c3bd9a828ced8e89c7c924842e859fea9a8c +Subproject commit 46cbc752477fd973960efc658a3c62fd57007afe From 8fc7e82c61cbbcc3aa0b3957b53e449cf7e49e4d Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 27 Mar 2024 22:50:53 -0400 Subject: [PATCH 10/48] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 46cbc7524..be892870e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 46cbc752477fd973960efc658a3c62fd57007afe +Subproject commit be892870e4d4397240d0b47275c4a8259583a942 From b771b46f1cb0fca132355daa09a685022028ac76 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 00:01:18 -0400 Subject: [PATCH 11/48] Fix a build issue. --- source/funkin/audio/FunkinSound.hx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 92f910335..51533f371 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -337,7 +337,11 @@ class FunkinSound extends FlxSound implements ICloneable FlxG.sound.music.kill(); } - FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, true, false, true); + // Apparently HaxeFlixel isn't null safe. + @:nullSafety(Off) + { + FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, true, false, true); + } var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true); if (music != null) From 893822532e8897926cf7283043e4131a0066995f Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 00:01:42 -0400 Subject: [PATCH 12/48] Fix an issue causing the Kickstarter video to not play --- source/funkin/ui/title/AttractState.hx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/funkin/ui/title/AttractState.hx b/source/funkin/ui/title/AttractState.hx index 0af97afd9..a42a6c3d9 100644 --- a/source/funkin/ui/title/AttractState.hx +++ b/source/funkin/ui/title/AttractState.hx @@ -17,7 +17,7 @@ import funkin.ui.MusicBeatState; */ class AttractState extends MusicBeatState { - static final ATTRACT_VIDEO_PATH:String = Paths.videos('kickstarterTrailer'); + static final ATTRACT_VIDEO_PATH:String = Paths.stripLibrary(Paths.videos('kickstarterTrailer', 'shared')); public override function create():Void { @@ -29,10 +29,12 @@ class AttractState extends MusicBeatState } #if html5 + trace('Playing web video ${ATTRACT_VIDEO_PATH}'); playVideoHTML5(ATTRACT_VIDEO_PATH); #end #if hxCodec + trace('Playing native video ${ATTRACT_VIDEO_PATH}'); playVideoNative(ATTRACT_VIDEO_PATH); #end } From c1c2621e1d2412994863360e21701e1faf9823b0 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 00:18:39 -0400 Subject: [PATCH 13/48] Fix bug where Bad and Shit didn't count at Notes Hit in the results screen. --- source/funkin/play/PlayState.hx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 6dc41e3f9..1cf69489d 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2568,32 +2568,38 @@ class PlayState extends MusicBeatSubState */ function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void { - vocals.playerVolume = 1; - if (daRating == 'miss') { // If daRating is 'miss', that means we made a mistake and should not continue. - trace('[WARNING] popUpScore judged a note as a miss!'); + FlxG.log.warn('popUpScore judged a note as a miss!'); // TODO: Remove this. comboPopUps.displayRating('miss'); return; } + vocals.playerVolume = 1; + var isComboBreak = false; switch (daRating) { case 'sick': Highscore.tallies.sick += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK; case 'good': Highscore.tallies.good += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK; case 'bad': Highscore.tallies.bad += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': Highscore.tallies.shit += 1; + Highscore.tallies.totalNotesHit++; isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; + default: + FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!'); } health += healthChange; From 837fcee98386cb150cb6e633054b4bb3b0900519 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 00:19:57 -0400 Subject: [PATCH 14/48] Fix a bug where 000 would display any time a Bad or Shit is hit (instead of just when a combo break is hit) --- source/funkin/play/PlayState.hx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 1cf69489d..a161c04c5 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2441,7 +2441,8 @@ class PlayState extends MusicBeatSubState if (Highscore.tallies.combo != 0) { // Break the combo. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } if (playSound) @@ -2607,18 +2608,18 @@ class PlayState extends MusicBeatSubState if (isComboBreak) { // Break the combo, but don't increment tallies.misses. - Highscore.tallies.combo = comboPopUps.displayCombo(0); + if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); + Highscore.tallies.combo = 0; } else { Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; } playerStrumline.hitNote(daNote, !isComboBreak); - if (daRating == "sick") + if (daRating == 'sick') { playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } From b13c6563dff894a969a3129faaae8212e5a29f19 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 01:46:50 -0400 Subject: [PATCH 15/48] Fix issues with Freeplay OST text and add animations. --- assets | 2 +- source/funkin/data/freeplay/AlbumData.hx | 9 +++++ source/funkin/ui/freeplay/Album.hx | 11 ++++++ source/funkin/ui/freeplay/AlbumRoll.hx | 39 +++++++++++++++++++--- source/funkin/ui/freeplay/FreeplayState.hx | 19 ++++++++--- 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/assets b/assets index 8013845e3..92bd680af 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 8013845e331015b40c4cc35230f6d02bd2148d52 +Subproject commit 92bd680af3c627211656205dac85f9de5ae5abee diff --git a/source/funkin/data/freeplay/AlbumData.hx b/source/funkin/data/freeplay/AlbumData.hx index 265a01fce..ca851376d 100644 --- a/source/funkin/data/freeplay/AlbumData.hx +++ b/source/funkin/data/freeplay/AlbumData.hx @@ -1,5 +1,7 @@ package funkin.data.freeplay; +import funkin.data.animation.AnimationData; + /** * A type definition for the data for an album of songs. * It includes things like what graphics to display in Freeplay. @@ -33,4 +35,11 @@ typedef AlbumData = * The album title will be displayed below the album art in Freeplay. */ public var albumTitleAsset:String; + + /** + * An optional array of animations for the album title. + */ + @:optional + @:default([]) + public var albumTitleAnimations:Array; } diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx index 7291c7357..3060d3eb8 100644 --- a/source/funkin/ui/freeplay/Album.hx +++ b/source/funkin/ui/freeplay/Album.hx @@ -1,6 +1,7 @@ package funkin.ui.freeplay; import funkin.data.freeplay.AlbumData; +import funkin.data.animation.AnimationData; import funkin.data.freeplay.AlbumRegistry; import funkin.data.IRegistryEntry; import flixel.graphics.FlxGraphic; @@ -75,6 +76,16 @@ class Album implements IRegistryEntry return _data.albumTitleAsset; } + public function hasAlbumTitleAnimations() + { + return _data.albumTitleAnimations.length > 0; + } + + public function getAlbumTitleAnimations():Array + { + return _data.albumTitleAnimations; + } + public function toString():String { return 'Album($id)'; diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx index a1e63c9a1..bde946e79 100644 --- a/source/funkin/ui/freeplay/AlbumRoll.hx +++ b/source/funkin/ui/freeplay/AlbumRoll.hx @@ -7,6 +7,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxTimer; import flixel.tweens.FlxEase; import funkin.data.freeplay.AlbumRegistry; +import funkin.util.assets.FlxAnimationUtil; import funkin.graphics.FunkinSprite; import funkin.util.SortUtil; import openfl.utils.Assets; @@ -21,9 +22,9 @@ class AlbumRoll extends FlxSpriteGroup * The ID of the album to display. * Modify this value to automatically update the album art and title. */ - public var albumId(default, set):String; + public var albumId(default, set):Null; - function set_albumId(value:String):String + function set_albumId(value:Null):Null { if (this.albumId != value) { @@ -65,6 +66,17 @@ class AlbumRoll extends FlxSpriteGroup */ function updateAlbum():Void { + if (albumId == null) + { + albumArt.visible = false; + albumTitle.visible = false; + if (titleTimer != null) + { + titleTimer.cancel(); + titleTimer = null; + } + } + albumData = AlbumRegistry.instance.fetchEntry(albumId); if (albumData == null) @@ -94,7 +106,15 @@ class AlbumRoll extends FlxSpriteGroup if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey()))) { - albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); + if (albumData.hasAlbumTitleAnimations()) + { + albumTitle.loadSparrow(albumData.getAlbumTitleAssetKey()); + FlxAnimationUtil.addAtlasAnimations(albumTitle, albumData.getAlbumTitleAnimations()); + } + else + { + albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey())); + } } else { @@ -155,6 +175,8 @@ class AlbumRoll extends FlxSpriteGroup }); } + var titleTimer:Null = null; + /** * Play the intro animation on the album art. */ @@ -164,7 +186,14 @@ class AlbumRoll extends FlxSpriteGroup FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut}); albumTitle.visible = false; - new FlxTimer().start(0.75, function(_) { + + if (titleTimer != null) + { + titleTimer.cancel(); + titleTimer = null; + } + + titleTimer = new FlxTimer().start(0.75, function(_) { showTitle(); }); } @@ -179,6 +208,8 @@ class AlbumRoll extends FlxSpriteGroup public function showTitle():Void { albumTitle.visible = true; + albumTitle.animation.play('active'); + albumTitle.animation.finishCallback = (_) -> albumTitle.animation.play('idle'); } /** diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index f7554197f..6dd96b36d 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState } albumRoll = new AlbumRoll(); - albumRoll.albumId = 'volume1'; + albumRoll.albumId = null; add(albumRoll); albumRoll.applyExitMovers(exitMovers); @@ -881,6 +881,8 @@ class FreeplayState extends MusicBeatSubState for (spr in grpSpr) { + if (spr == null) continue; + var funnyMoveShit:MoveData = moveData; if (moveData.x == null) funnyMoveShit.x = spr.x; @@ -1019,7 +1021,7 @@ class FreeplayState extends MusicBeatSubState albumRoll.setDifficultyStars(daSong?.songRating); // Set the album graphic and play the animation if relevant. - var newAlbumId:String = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID; + var newAlbumId:String = daSong?.albumId; if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1162,6 +1164,7 @@ class FreeplayState extends MusicBeatSubState intendedCompletion = 0.0; rememberedSongId = null; rememberedDifficulty = null; + albumRoll.albumId = null; } for (index => capsule in grpCapsules.members) @@ -1311,7 +1314,7 @@ class FreeplaySongData public var songName(default, null):String = ''; public var songCharacter(default, null):String = ''; public var songRating(default, null):Int = 0; - public var albumId(default, null):String = ''; + public var albumId(default, null):Null = null; public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION]; @@ -1345,7 +1348,15 @@ class FreeplaySongData this.songName = songDifficulty.songName; this.songCharacter = songDifficulty.characters.opponent; this.songRating = songDifficulty.difficultyRating; - this.albumId = songDifficulty.album; + if (songDifficulty.album == null) + { + FlxG.log.warn('No album for: ${songDifficulty.songName}'); + this.albumId = Constants.DEFAULT_ALBUM_ID; + } + else + { + this.albumId = songDifficulty.album; + } } } From 5311b043ac81b2b38acbc5e57a24590f537b0a4a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 02:57:22 -0400 Subject: [PATCH 16/48] Rework credits data structure. --- source/funkin/ui/credits/CreditsData.hx | 16 ++++++++--- .../funkin/ui/credits/CreditsDataHandler.hx | 28 +++++++++++-------- source/funkin/ui/credits/CreditsState.hx | 25 +++++++++++++---- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/source/funkin/ui/credits/CreditsData.hx b/source/funkin/ui/credits/CreditsData.hx index 0f6ea6bcd..bf7f13ad5 100644 --- a/source/funkin/ui/credits/CreditsData.hx +++ b/source/funkin/ui/credits/CreditsData.hx @@ -5,7 +5,7 @@ package funkin.ui.credits; */ typedef CreditsData = { - var roles:Array; + var entries:Array; } /** @@ -13,8 +13,16 @@ typedef CreditsData = */ typedef CreditsDataRole = { - var roleName:String; - var members:Array; + @:optional + var header:String; + + @:optional + @:default([]) + var body:Array; + + @:optional + @:default(false) + var appendBackers:Bool; } /** @@ -22,5 +30,5 @@ typedef CreditsDataRole = */ typedef CreditsDataMember = { - var fullName:String; + var line:String; } diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx index 6317dd55d..f2722ffbf 100644 --- a/source/funkin/ui/credits/CreditsDataHandler.hx +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -24,14 +24,14 @@ class CreditsDataHandler return; } - var roleCount = data.roles.length; - var memberCount = 0; - for (role in data.roles) + var entryCount = data.entries.length; + var lineCount = 0; + for (entry in data.entries) { - memberCount += role.members.length; + lineCount += entry?.body?.length ?? 0; } - trace('CreditsData($roleCount roles with $memberCount members)'); + trace('CreditsData($entryCount entries containing $lineCount lines)'); } /** @@ -43,15 +43,19 @@ class CreditsDataHandler public static inline function getFallback():CreditsData { return { - roles: [ + entries: [ { - roleName: 'Founders', - members: [ - {fullName: 'ninjamuffin99'}, - {fullName: 'PhantomArcade'}, - {fullName: 'KawaiSprite'}, - {fullName: 'evilsk8r'}, + header: 'Founders', + body: [ + {line: 'ninjamuffin99'}, + {line: 'PhantomArcade'}, + {line: 'KawaiSprite'}, + {line: 'evilsk8r'}, ] + }, + { + header: 'Kickstarter Backers', + appendBackers: true } ] }; diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx index 1e5965695..d43e25114 100644 --- a/source/funkin/ui/credits/CreditsState.hx +++ b/source/funkin/ui/credits/CreditsState.hx @@ -116,17 +116,30 @@ class CreditsState extends MusicBeatState { var y = 0; - for (role in CreditsDataHandler.CREDITS_DATA.roles) + for (entry in CreditsDataHandler.CREDITS_DATA.entries) { - creditsGroup.add(buildCreditsLine(role.roleName, y, true, CreditsSide.Center)); - y += CREDITS_HEADER_FONT_SIZE; - - for (member in role.members) + if (entry.header != null) { - creditsGroup.add(buildCreditsLine(member.fullName, y, false, CreditsSide.Center)); + creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center)); + y += CREDITS_HEADER_FONT_SIZE; + } + + for (line in entry?.body ?? []) + { + creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center)); y += CREDITS_FONT_SIZE; } + if (entry.appendBackers) + { + var backers = CreditsDataHandler.fetchBackerEntries(); + for (backer in backers) + { + creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center)); + y += CREDITS_FONT_SIZE; + } + } + // Padding between each role. y += CREDITS_FONT_SIZE * 2; } From 3a86b47292b88674dae074f22d8b5d37b9671348 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 02:57:51 -0400 Subject: [PATCH 17/48] Fix issue where main menu music wouldn't play after credits. --- source/funkin/ui/mainmenu/MainMenuState.hx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index e536554d0..02632628f 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -51,10 +51,7 @@ class MainMenuState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - if (!(FlxG?.sound?.music?.playing ?? false)) - { - playMenuMusic(); - } + playMenuMusic(); persistentUpdate = persistentDraw = true; @@ -109,15 +106,18 @@ class MainMenuState extends MusicBeatState }); #if CAN_OPEN_LINKS + // In order to prevent popup blockers from triggering, + // we need to open the link as an immediate result of a keypress event, + // so we can't wait for the flicker animation to complete. var hasPopupBlocker = #if web true #else false #end; - createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker); + createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker); #end createMenuItem('options', 'mainmenu/options', function() { startExitState(() -> new funkin.ui.options.OptionsState()); }); - createMenuItem('options', 'mainmenu/options', function() { + createMenuItem('credits', 'mainmenu/credits', function() { startExitState(() -> new funkin.ui.credits.CreditsState()); }); @@ -219,6 +219,11 @@ class MainMenuState extends MusicBeatState { WindowUtil.openURL(Constants.URL_ITCH); } + + function selectMerch() + { + WindowUtil.openURL(Constants.URL_MERCH); + } #end #if newgrounds From 3d14024fd8747d704910570f66a3592a8042e520 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 02:58:08 -0400 Subject: [PATCH 18/48] Implement merch link. --- source/funkin/util/Constants.hx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c7bc03139..7ea537935 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -59,6 +59,11 @@ class Constants */ // ============================== + /** + * Link to buy merch for the game. + */ + public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin'; + /** * Link to download the game on Itch.io. */ From e5eca37dc067e82d3a75f849fd62a8c24fe67dca Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 02:58:46 -0400 Subject: [PATCH 19/48] Update assets submodule. --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index edccf0421..289810289 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit edccf04217c49c730b11c80736e2b2d98a25ee95 +Subproject commit 289810289d66cbaf5d55494e396e71bdf9085b1e From c7d67b46e0089a1a1ddfb81bbb470f3100a1b88a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 20:29:32 -0400 Subject: [PATCH 20/48] Update art submodule --- art | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/art b/art index 00463685f..03e7c2a23 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48 +Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 From 1c9b087f2ff6a597255311064f0e576565491d18 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 21:20:08 -0400 Subject: [PATCH 21/48] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 485243fdd..65e6ff18c 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 485243fdd44acbc4db6a97ec7bf10a8b18350be9 +Subproject commit 65e6ff18c7fcbd646ac7a3676ca5c2baa95b5fea From 8020624366a48b1563945722744cca20702e078b Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 21:40:16 -0400 Subject: [PATCH 22/48] Fix a crash I found. --- source/funkin/audio/VoicesGroup.hx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 91054cfb0..5037ee1d0 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -159,10 +159,18 @@ class VoicesGroup extends SoundGroup public override function destroy():Void { - playerVoices.destroy(); - playerVoices = null; - opponentVoices.destroy(); - opponentVoices = null; + if (playerVoices != null) + { + playerVoices.destroy(); + playerVoices = null; + } + + if (opponentVoices != null) + { + opponentVoices.destroy(); + opponentVoices = null; + } + super.destroy(); } } From aa7ff6fbc45a0820dc2da581ea5b84e44d5df7c7 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 21:40:47 -0400 Subject: [PATCH 23/48] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 485243fdd..15c3f16c7 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 485243fdd44acbc4db6a97ec7bf10a8b18350be9 +Subproject commit 15c3f16c7ec162b7c8d86421b624d74501b9616f From 342782c3d379d67127d1ef8acc5662fdfc370ae6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 22:33:50 -0400 Subject: [PATCH 24/48] Fix issue with pink screen when moving from PlayState->Freeplay->Main Menu --- source/funkin/play/GameOverSubState.hx | 4 ++-- source/funkin/play/PauseSubState.hx | 3 ++- source/funkin/play/PlayState.hx | 17 ++++------------- source/funkin/play/ResultState.hx | 2 +- source/funkin/ui/freeplay/FreeplayState.hx | 19 +++++++++++++++++-- source/funkin/ui/mainmenu/MainMenuState.hx | 5 ++--- source/funkin/ui/title/TitleState.hx | 12 ------------ 7 files changed, 28 insertions(+), 34 deletions(-) diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index a1796e912..652ba1484 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -238,11 +238,11 @@ class GameOverSubState extends MusicBeatSubState } else if (PlayStatePlaylist.isStoryMode) { - FlxG.switchState(() -> new StoryMenuState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker))); } else { - FlxG.switchState(() -> new FreeplayState()); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(sticker))); } } diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index f16aa00d8..471f8cf02 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -12,6 +12,7 @@ import flixel.tweens.FlxTween; import flixel.util.FlxColor; import funkin.audio.FunkinSound; import funkin.data.song.SongRegistry; +import funkin.ui.freeplay.FreeplayState; import funkin.graphics.FunkinSprite; import funkin.play.cutscene.VideoCutscene; import funkin.play.PlayState; @@ -658,7 +659,7 @@ class PauseSubState extends MusicBeatSubState } else { - state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker))); + state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index fd6463bb1..678f2430e 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2785,18 +2785,6 @@ class PlayState extends MusicBeatSubState if (targetSongId == null) { - FunkinSound.playMusic('freakyMenu', - { - overrideExisting: true, - restartTrack: false - }); - - // transIn = FlxTransitionableState.defaultTransIn; - // transOut = FlxTransitionableState.defaultTransOut; - - // TODO: Rework week unlock logic. - // StoryMenuState.weekUnlocked[Std.int(Math.min(storyWeek + 1, StoryMenuState.weekUnlocked.length - 1))] = true; - if (currentSong.validScore) { NGio.unlockMedal(60961); @@ -3205,7 +3193,10 @@ class PlayState extends MusicBeatSubState // Don't go back in time to before the song started. targetTimeMs = Math.max(0, targetTimeMs); - FlxG.sound.music.time = targetTimeMs; + if (FlxG.sound.music != null) + { + FlxG.sound.music.time = targetTimeMs; + } handleSkippedNotes(); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 821f4ba3c..12f395d0f 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -365,7 +365,7 @@ class ResultState extends MusicBeatSubState } else { - openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new FreeplayState(null, sticker))); + openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker))); } } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6cb0d1d9a..40081b2ec 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -145,7 +145,7 @@ class FreeplayState extends MusicBeatSubState stickerSubState = stickers; } - super(); + super(FlxColor.TRANSPARENT); } override function create():Void @@ -899,7 +899,7 @@ class FreeplayState extends MusicBeatSubState if (Type.getClass(FlxG.state) == MainMenuState) { - FlxG.state.persistentUpdate = true; + FlxG.state.persistentUpdate = false; FlxG.state.persistentDraw = true; } @@ -1201,6 +1201,21 @@ class FreeplayState extends MusicBeatSubState grpCapsules.members[curSelected].selected = true; } } + + /** + * Build an instance of `FreeplayState` that is above the `MainMenuState`. + * @return The MainMenuState with the FreeplayState as a substate. + */ + public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState + { + var result = new MainMenuState(); + result.persistentUpdate = false; + result.persistentDraw = true; + + result.openSubState(new FreeplayState(params, stickers)); + + return result; + } } /** diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index a8c2039ab..df81cf6f2 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -56,7 +56,8 @@ class MainMenuState extends MusicBeatState playMenuMusic(); } - persistentUpdate = persistentDraw = true; + persistentUpdate = false; + persistentDraw = true; var bg:FlxSprite = new FlxSprite(Paths.image('menuBG')); bg.scrollFactor.x = 0; @@ -311,8 +312,6 @@ class MainMenuState extends MusicBeatState // Open the debug menu, defaults to ` / ~ if (controls.DEBUG_MENU) { - this.persistentUpdate = false; - this.persistentDraw = false; FlxG.state.openSubState(new DebugMenuSubState()); } diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 1a4e13ab1..7bd4a84af 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -290,18 +290,6 @@ class TitleState extends MusicBeatState // do controls.PAUSE | controls.ACCEPT instead? var pressedEnter:Bool = FlxG.keys.justPressed.ENTER; - if (FlxG.onMobile) - { - for (touch in FlxG.touches.list) - { - if (touch.justPressed) - { - FlxG.switchState(() -> new FreeplayState()); - pressedEnter = true; - } - } - } - var gamepad:FlxGamepad = FlxG.gamepads.lastActive; if (gamepad != null) From ddb41c9ef7d8c08e5100253216979dfdc6c14ef7 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 28 Mar 2024 23:13:47 -0400 Subject: [PATCH 25/48] Increase AFK timer, and add SFX over it --- assets | 2 +- source/funkin/ui/freeplay/DJBoyfriend.hx | 29 ++++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/assets b/assets index 15c3f16c7..04605b7e3 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 15c3f16c7ec162b7c8d86421b624d74501b9616f +Subproject commit 04605b7e3ab7556b395476aa31b8853ff5243c6f diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx index 33f264301..5f1144fab 100644 --- a/source/funkin/ui/freeplay/DJBoyfriend.hx +++ b/source/funkin/ui/freeplay/DJBoyfriend.hx @@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite var gotSpooked:Bool = false; - static final SPOOK_PERIOD:Float = 10.0; - static final TV_PERIOD:Float = 10.0; + static final SPOOK_PERIOD:Float = 120.0; + static final TV_PERIOD:Float = 180.0; // Time since dad last SPOOKED you. var timeSinceSpook:Float = 0; @@ -43,7 +43,14 @@ class DJBoyfriend extends FlxAtlasSprite switch (name) { case "Boyfriend DJ watchin tv OG": - if (number == 85) runTvLogic(); + if (number == 80) + { + FunkinSound.playOnce(Paths.sound('remote_click')); + } + if (number == 85) + { + runTvLogic(); + } default: } }; @@ -219,19 +226,17 @@ class DJBoyfriend extends FlxAtlasSprite if (cartoonSnd == null) { // tv is OFF, but getting turned on - // Eric got FUCKING TROLLED there is no `tv_on` or `channel_switch` sound! - // FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { - // }); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() { + loadCartoon(); + }); } else { // plays it smidge after the click - // new FlxTimer().start(0.1, function(_) { - // // FunkinSound.playOnce(Paths.sound('channel_switch')); - // }); - cartoonSnd.destroy(); - loadCartoon(); + FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() { + cartoonSnd.destroy(); + loadCartoon(); + }); } // loadCartoon(); From 7c04630bb775161e9878e6554146a313aaab5c2a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 00:49:02 -0400 Subject: [PATCH 26/48] Fix a bug where duplicate notes would get placed at the beginning --- source/funkin/play/notes/Strumline.hx | 5 +++++ source/funkin/ui/transition/LoadingState.hx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 9a6699c43..a3b5dafc5 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -295,6 +295,11 @@ class Strumline extends FlxSpriteGroup { if (noteData.length == 0) return; + // Ensure note data gets reset if the song happens to loop. + // NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times. + // I don't remember what bug I was trying to fix by adding this. + // if (conductorInUse.currentStep == 0) nextNoteIndex = 0; + var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS; diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index 980c264e3..d913b8099 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -210,7 +210,8 @@ class LoadingState extends MusicBeatState } // Load and cache the song's charts. - if (params?.targetSong != null) + // Don't do this if we already provided the music and charts. + if (params?.targetSong != null && !params.overrideMusic) { params.targetSong.cacheCharts(true); } From 28462681b2af4a55c82fb60aa55b51ac4ab29966 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 00:52:20 -0400 Subject: [PATCH 27/48] Fix bug where 100ms sustains wouldn't update their rendering. --- source/funkin/play/notes/Strumline.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index a3b5dafc5..2b10c05ee 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -827,7 +827,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new SustainTrail(0, 100, noteStyle); + result = new SustainTrail(0, 0, noteStyle); this.holdNotes.add(result); } From 9b7bfff817cdd9d50468a263053d9837fa9c3f6d Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 01:23:36 -0400 Subject: [PATCH 28/48] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 485243fdd..44d74c089 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 485243fdd44acbc4db6a97ec7bf10a8b18350be9 +Subproject commit 44d74c0898b630a98344ef22987be73140e932c3 From f50c1ce86fb38d5f5b95a864ed7e0de684414b70 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 10:17:16 -0400 Subject: [PATCH 29/48] Update assets submodule --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 65e6ff18c..0965a86e2 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 65e6ff18c7fcbd646ac7a3676ca5c2baa95b5fea +Subproject commit 0965a86e2248bd9b8b2387f8b4f6b16385499db0 From 5be47e6619aa13ca6251c1c580d9dbc4ef330818 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 10:34:59 -0400 Subject: [PATCH 30/48] Fix limo ride too --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 0965a86e2..5d78b0705 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 0965a86e2248bd9b8b2387f8b4f6b16385499db0 +Subproject commit 5d78b070535d6a1d88c5b450fc092eb6cd331f78 From f19507bd957a45c784883f4beef37017aa57e1ca Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 29 Mar 2024 23:00:42 -0400 Subject: [PATCH 31/48] Add a bunch of erect songs --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 65e6ff18c..208d1ab80 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 65e6ff18c7fcbd646ac7a3676ca5c2baa95b5fea +Subproject commit 208d1ab80739cb728de3a64535ef3e3bee0843b0 From 846df8de6fa3f207cc4a12cfa175215679c833ea Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 30 Mar 2024 03:06:39 -0400 Subject: [PATCH 32/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 5d78b0705..3b168f7ca 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5d78b070535d6a1d88c5b450fc092eb6cd331f78 +Subproject commit 3b168f7cac41e1843de9a223453d0ff4c04b0283 From 3637e3594ba85e1e0344ab33053b8960edd86b3b Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 30 Mar 2024 03:24:30 -0400 Subject: [PATCH 33/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 15c3f16c7..200658724 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 15c3f16c7ec162b7c8d86421b624d74501b9616f +Subproject commit 200658724592b298f49b13016f2c706c54ad538f From fe498b38e51babb076a1a94003be002dc17ac2fe Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Sat, 30 Mar 2024 03:32:17 -0400 Subject: [PATCH 34/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 2a0afcd76..763c833cb 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 2a0afcd76a26251dbbebb6901df4651f25a92c23 +Subproject commit 763c833cbcde724d50ff31f5bac9f2ac3d5e61a7 From 1df4a354cb3ac7663f2c43da96e555edec7c6724 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 1 Apr 2024 18:34:26 -0400 Subject: [PATCH 35/48] Freeplay menu now filters to supported songs when you change difficulty --- source/funkin/ui/freeplay/FreeplayState.hx | 70 +++++++++++++++++----- source/funkin/util/Constants.hx | 6 ++ 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6cb0d1d9a..471fe7e28 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -536,21 +536,18 @@ class FreeplayState extends MusicBeatSubState }); } + var currentFilter:SongFilter = null; + var currentFilteredSongs:Array = []; + /** * Given the current filter, rebuild the current song list. * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param force + * @param onlyIfChanged Only apply the filter if the song list has changed */ - public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void + public function generateSongList(filterStuff:Null, force:Bool = false, onlyIfChanged:Bool = true):Void { - curSelected = 1; - - for (cap in grpCapsules.members) - { - cap.kill(); - } - var tempSongs:Array = songs; if (filterStuff != null) @@ -582,6 +579,35 @@ class FreeplayState extends MusicBeatSubState } } + // Filter further by current selected difficulty. + if (currentDifficulty != null) + { + tempSongs = tempSongs.filter(song -> { + if (song == null) return true; // Random + return song.songDifficulties.contains(currentDifficulty); + }); + } + + if (onlyIfChanged) + { + // == performs equality by reference + if (tempSongs.isEqualUnordered(currentFilteredSongs)) return; + } + + // Only now do we know that the filter is actually changing. + + rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId; + + for (cap in grpCapsules.members) + { + cap.kill(); + } + + currentFilter = filterStuff; + + currentFilteredSongs = tempSongs; + curSelected = 0; + var hsvShader:HSVShader = new HSVShader(); var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); @@ -658,11 +684,12 @@ class FreeplayState extends MusicBeatSubState if (FlxG.keys.justPressed.F) { - if (songs[curSelected] != null) + var targetSong = grpCapsules.members[curSelected]?.songData; + if (targetSong != null) { var realShit:Int = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + targetSong.isFav = !targetSong.isFav; + if (targetSong.isFav) { FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, { @@ -854,11 +881,13 @@ class FreeplayState extends MusicBeatSubState { dj.resetAFKTimer(); changeDiff(-1); + generateSongList(currentFilter, true); } if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) { dj.resetAFKTimer(); changeDiff(1); + generateSongList(currentFilter, true); } if (controls.BACK && !typing.hasFocus) @@ -926,7 +955,7 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected]?.songData; if (daSong != null) { clearDaCache(daSong.songName); @@ -948,10 +977,10 @@ class FreeplayState extends MusicBeatSubState currentDifficulty = diffIdsCurrent[currentDifficultyIndex]; - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected].songData; if (daSong != null) { - var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty); + var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore?.accuracy ?? 0.0; rememberedDifficulty = currentDifficulty; @@ -1103,6 +1132,12 @@ class FreeplayState extends MusicBeatSubState targetVariation: targetVariation, practiceMode: false, minimalMode: false, + + #if (debug || FORCE_DEBUG_VERSION) + botPlayMode: FlxG.keys.pressed.SHIFT, + #else + botPlayMode: false, + #end // TODO: Make these an option! It's currently only accessible via chart editor. // startTimestamp: 0.0, // playbackRate: 0.5, @@ -1115,10 +1150,12 @@ class FreeplayState extends MusicBeatSubState { if (rememberedSongId != null) { - curSelected = songs.findIndex(function(song) { + curSelected = currentFilteredSongs.findIndex(function(song) { if (song == null) return false; return song.songId == rememberedSongId; }); + + if (curSelected == -1) curSelected = 0; } if (rememberedDifficulty != null) @@ -1127,7 +1164,7 @@ class FreeplayState extends MusicBeatSubState } // Set the difficulty star count on the right. - var daSong:Null = songs[curSelected]; + var daSong:Null = grpCapsules.members[curSelected]?.songData; albumRoll.setDifficultyStars(daSong?.songRating ?? 0); } @@ -1156,6 +1193,7 @@ class FreeplayState extends MusicBeatSubState { intendedScore = 0; intendedCompletion = 0.0; + diffIdsCurrent = diffIdsTotal; rememberedSongId = null; rememberedDifficulty = null; } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 5d355f2da..13a6f65b5 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -181,6 +181,12 @@ class Constants */ public static final DEFAULT_DIFFICULTY_LIST:Array = ['easy', 'normal', 'hard']; + /** + * List of all difficulties used by the base game. + * Includes Erect and Nightmare. + */ + public static final DEFAULT_DIFFICULTY_LIST_FULL:Array = ['easy', 'normal', 'hard', 'erect', 'nightmare']; + /** * Default player character for charts. */ From 927b2a7cfc5ef956e77aae1a7dd7660f8d9a738a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 1 Apr 2024 13:05:16 -0400 Subject: [PATCH 36/48] Put judgements below the notes, and remove COMBO word. --- source/funkin/play/PlayState.hx | 6 ++++-- source/funkin/play/components/PopUpStuff.hx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 274ee4fe8..97dd96016 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -728,6 +728,10 @@ class PlayState extends MusicBeatSubState #end initialized = true; + + // This step ensures z-indexes are applied properly, + // and it's important to call it last so all elements get affected. + refresh(); } public override function draw():Void @@ -1720,8 +1724,6 @@ class PlayState extends MusicBeatSubState playerStrumline.fadeInArrows(); opponentStrumline.fadeInArrows(); } - - this.refresh(); } /** diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 39fc192a0..724bf0cb9 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -85,7 +85,7 @@ class PopUpStuff extends FlxTypedGroup comboSpr.velocity.y -= 150; comboSpr.velocity.x += FlxG.random.int(1, 10); - add(comboSpr); + // add(comboSpr); if (PlayState.instance.currentStageId.startsWith('school')) { From 67e096e4430a84bac3e04da02fe249240866c7e0 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 1 Apr 2024 21:59:53 -0400 Subject: [PATCH 37/48] Bunch of results screen fixes --- source/funkin/Highscore.hx | 4 ++ source/funkin/play/PauseSubState.hx | 2 + source/funkin/play/PlayState.hx | 43 +++++++++--- source/funkin/play/ResultState.hx | 68 +++++++++++++------ source/funkin/play/components/TallyCounter.hx | 23 +++++-- source/funkin/ui/freeplay/FreeplayState.hx | 4 +- 6 files changed, 106 insertions(+), 38 deletions(-) diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 996e2367e..94f41cea4 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -59,6 +59,7 @@ abstract Tallies(RawTallies) totalNotes: 0, totalNotesHit: 0, maxCombo: 0, + score: 0, isNewHighscore: false } } @@ -81,6 +82,9 @@ typedef RawTallies = var good:Int; var sick:Int; var maxCombo:Int; + + var score:Int; + var isNewHighscore:Bool; /** diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx index ed847402a..2af04749f 100644 --- a/source/funkin/play/PauseSubState.hx +++ b/source/funkin/play/PauseSubState.hx @@ -567,6 +567,8 @@ class PauseSubState extends MusicBeatSubState PlayStatePlaylist.campaignDifficulty = difficulty; PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty; + FreeplayState.rememberedDifficulty = difficulty; + PlayState.instance.needsReset = true; state.close(); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 274ee4fe8..438941f90 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2731,7 +2731,7 @@ class PlayState extends MusicBeatSubState */ public function endSong(rightGoddamnNow:Bool = false):Void { - FlxG.sound.music.volume = 0; + if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; vocals.volume = 0; mayPauseGame = false; @@ -2749,6 +2749,8 @@ class PlayState extends MusicBeatSubState deathCounter = 0; + var isNewHighscore = false; + if (currentSong != null && currentSong.validScore) { // crackhead double thingie, sets whether was new highscore, AND saves the song! @@ -2779,11 +2781,14 @@ class PlayState extends MusicBeatSubState #if newgrounds NGio.postScore(score, currentSong.id); #end + isNewHighscore = true; } } if (PlayStatePlaylist.isStoryMode) { + isNewHighscore = false; + PlayStatePlaylist.campaignScore += songScore; // Pop the next song ID from the list. @@ -2833,6 +2838,7 @@ class PlayState extends MusicBeatSubState #if newgrounds NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); #end + isNewHighscore = true; } } @@ -2844,11 +2850,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2909,11 +2915,11 @@ class PlayState extends MusicBeatSubState { if (rightGoddamnNow) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } else { - zoomIntoResultsScreen(); + zoomIntoResultsScreen(isNewHighscore); } } } @@ -2987,7 +2993,7 @@ class PlayState extends MusicBeatSubState /** * Play the camera zoom animation and then move to the results screen once it's done. */ - function zoomIntoResultsScreen():Void + function zoomIntoResultsScreen(isNewHighscore:Bool):Void { trace('WENT TO RESULTS SCREEN!'); @@ -3044,7 +3050,7 @@ class PlayState extends MusicBeatSubState { ease: FlxEase.expoIn, onComplete: function(_) { - moveToResultsScreen(); + moveToResultsScreen(isNewHighscore); } }); }); @@ -3053,7 +3059,7 @@ class PlayState extends MusicBeatSubState /** * Move to the results screen right goddamn now. */ - function moveToResultsScreen():Void + function moveToResultsScreen(isNewHighscore:Bool):Void { persistentUpdate = false; vocals.stop(); @@ -3065,7 +3071,24 @@ class PlayState extends MusicBeatSubState { storyMode: PlayStatePlaylist.isStoryMode, title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'), - tallies: talliesToUse, + scoreData: + { + score: songScore, + tallies: + { + sick: Highscore.tallies.sick, + good: Highscore.tallies.good, + bad: Highscore.tallies.bad, + shit: Highscore.tallies.shit, + missed: Highscore.tallies.missed, + combo: Highscore.tallies.combo, + maxCombo: Highscore.tallies.maxCombo, + totalNotesHit: Highscore.tallies.totalNotesHit, + totalNotes: Highscore.tallies.totalNotes, + }, + accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, + }, + isNewHighscore: isNewHighscore }); res.camera = camHUD; openSubState(res); @@ -3212,7 +3235,7 @@ class PlayState extends MusicBeatSubState // Don't go back in time to before the song started. targetTimeMs = Math.max(0, targetTimeMs); - FlxG.sound.music.time = targetTimeMs; + if (FlxG.sound.music != null) FlxG.sound.music.time = targetTimeMs; handleSkippedNotes(); SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 821f4ba3c..7dbaf087f 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -1,5 +1,6 @@ package funkin.play; +import funkin.util.MathUtil; import funkin.ui.story.StoryMenuState; import funkin.graphics.adobeanimate.FlxAtlasSprite; import flixel.FlxSprite; @@ -16,6 +17,8 @@ import flixel.tweens.FlxTween; import funkin.audio.FunkinSound; import flixel.util.FlxGradient; import flixel.util.FlxTimer; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import funkin.graphics.shaders.LeftMaskShader; import funkin.play.components.TallyCounter; @@ -42,12 +45,15 @@ class ResultState extends MusicBeatSubState override function create():Void { - if (params.tallies.sick == params.tallies.totalNotesHit - && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT; - else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50) - resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! - else - resultsVariation = NORMAL; + /* + if (params.scoreData.sick == params.scoreData.totalNotesHit + && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT; + else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50) + resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! + else + resultsVariation = NORMAL; + */ + resultsVariation = NORMAL; FunkinSound.playMusic('results$resultsVariation', { @@ -130,12 +136,16 @@ class ResultState extends MusicBeatSubState var diffSpr:String = switch (PlayState.instance.currentDifficulty) { - case 'EASY': + case 'easy': 'difEasy'; - case 'NORMAL': + case 'normal': 'difNormal'; - case 'HARD': + case 'hard': 'difHard'; + case 'erect': + 'difErect'; + case 'nightmare': + 'difNightmare'; case _: 'difNormal'; } @@ -195,29 +205,33 @@ class ResultState extends MusicBeatSubState * NOTE: We display how many notes were HIT, not how many notes there were in total. * */ - var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit); + var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.scoreData.tallies.totalNotesHit); ratingGrp.add(totalHit); - var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo); + var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.scoreData.tallies.maxCombo); ratingGrp.add(maxCombo); hStuf += 2; var extraYOffset:Float = 5; - var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E); + var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E); ratingGrp.add(tallySick); - var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5); + var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.scoreData.tallies.good, 0xFF89C9E5); ratingGrp.add(tallyGood); - var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A); + var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.scoreData.tallies.bad, 0xFFE6CF8A); ratingGrp.add(tallyBad); - var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A); + var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.scoreData.tallies.shit, 0xFFE68C8A); ratingGrp.add(tallyShit); - var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6); + var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6); ratingGrp.add(tallyMissed); + var score:TallyCounter = new TallyCounter(825, 630, params.scoreData.score, RIGHT); + score.scale.set(2, 2); + ratingGrp.add(score); + for (ind => rating in ratingGrp.members) { rating.visible = false; @@ -235,9 +249,16 @@ class ResultState extends MusicBeatSubState scorePopin.animation.play("score"); scorePopin.visible = true; - highscoreNew.visible = true; - highscoreNew.animation.play("new"); - FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + if (params.isNewHighscore) + { + highscoreNew.visible = true; + highscoreNew.animation.play("new"); + FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut}); + } + else + { + highscoreNew.visible = false; + } }; switch (resultsVariation) @@ -276,8 +297,6 @@ class ResultState extends MusicBeatSubState } }); - if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!"); - super.create(); } @@ -393,8 +412,13 @@ typedef ResultsStateParams = */ var title:String; + /** + * Whether the displayed score is a new highscore + */ + var isNewHighscore:Bool; + /** * The score, accuracy, and judgements. */ - var tallies:Highscore.Tallies; + var scoreData:SaveScoreData; }; diff --git a/source/funkin/play/components/TallyCounter.hx b/source/funkin/play/components/TallyCounter.hx index 77e6ef4ec..35a8f3f51 100644 --- a/source/funkin/play/components/TallyCounter.hx +++ b/source/funkin/play/components/TallyCounter.hx @@ -6,6 +6,8 @@ import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxMath; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; +import flixel.text.FlxText.FlxTextAlign; +import funkin.util.MathUtil; /** * Numerical counters used next to each judgement in the Results screen. @@ -13,18 +15,23 @@ import flixel.tweens.FlxTween; class TallyCounter extends FlxTypedSpriteGroup { public var curNumber:Float = 0; - public var neededNumber:Int = 0; + public var flavour:Int = 0xFFFFFFFF; - public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF) + public var align:FlxTextAlign = FlxTextAlign.LEFT; + + public function new(x:Float, y:Float, neededNumber:Int = 0, ?flavour:Int = 0xFFFFFFFF, align:FlxTextAlign = FlxTextAlign.LEFT) { super(x, y); + this.align = align; + this.flavour = flavour; this.neededNumber = neededNumber; - drawNumbers(); + + if (curNumber == neededNumber) drawNumbers(); } var tmr:Float = 0; @@ -41,6 +48,8 @@ class TallyCounter extends FlxTypedSpriteGroup var seperatedScore:Array = []; var tempCombo:Int = Math.round(curNumber); + var fullNumberDigits:Int = Std.int(Math.max(1, Math.ceil(MathUtil.logBase(10, neededNumber)))); + while (tempCombo != 0) { seperatedScore.push(tempCombo % 10); @@ -55,7 +64,13 @@ class TallyCounter extends FlxTypedSpriteGroup { if (ind >= members.length) { - var numb:TallyNumber = new TallyNumber(ind * 43, 0, num); + var xPos = ind * (43 * this.scale.x); + if (this.align == FlxTextAlign.RIGHT) + { + xPos -= (fullNumberDigits * (43 * this.scale.x)); + } + var numb:TallyNumber = new TallyNumber(xPos, 0, num); + numb.scale.set(this.scale.x, this.scale.y); add(numb); numb.color = flavour; } diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 6cb0d1d9a..058f61a5b 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -133,8 +133,8 @@ class FreeplayState extends MusicBeatSubState var stickerSubState:StickerSubState; - static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; - static var rememberedSongId:Null = null; + public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; + public static var rememberedSongId:Null = null; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { From 626cc5cc78d6543fae7b1095049e97609c2e9f59 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:06:44 -0400 Subject: [PATCH 38/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index b2144938c..d7e85ef60 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b2144938c899e4a5d2d05466f710aa75ff4e1d1c +Subproject commit d7e85ef60933ca93d47e1db6295aba8aa64fcbdf From 02b8aa0f1246f4e5c710759668e5f8b9a7fa9d19 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:13:38 -0400 Subject: [PATCH 39/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 04605b7e3..d7e85ef60 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 04605b7e3ab7556b395476aa31b8853ff5243c6f +Subproject commit d7e85ef60933ca93d47e1db6295aba8aa64fcbdf From d4117c2e6a263152a0a8bc278b3fb5318497d3d1 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:21:10 -0400 Subject: [PATCH 40/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 349b3e018..346da48a8 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 349b3e01813ce7174cd4406be3fbefe94c61946e +Subproject commit 346da48a86f07a9c6372bf92c64d68802ae75078 From 5ecedef88aac329c8e1262fc0702f26f275fee3d Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:30:54 -0400 Subject: [PATCH 41/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 208d1ab80..dfa5ca25a 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 208d1ab80739cb728de3a64535ef3e3bee0843b0 +Subproject commit dfa5ca25a32c834c1f21c39b0c4d6e4830d6f799 From 8cb11d081d2f6ab40175a4ca0901d59b297dadc9 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:31:19 -0400 Subject: [PATCH 42/48] art submod --- art | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/art b/art index 03e7c2a23..00463685f 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 +Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48 From ddda7f6a01f83f52bbfe6122161cda906aea32cf Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:35:56 -0400 Subject: [PATCH 43/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 44d74c089..cb862903e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 44d74c0898b630a98344ef22987be73140e932c3 +Subproject commit cb862903ec0975364c13080297ccbfb13f26f5cb From 7ae44c56201ed1c5e9bd58c61cd13eb7d76b8a9c Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 1 Apr 2024 22:39:48 -0400 Subject: [PATCH 44/48] assets submod --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 77e64da9e..a54eb8517 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 77e64da9e9836c0272ff7351444881aa90f60eb6 +Subproject commit a54eb8517914e2a90b77e122ecd81cf73d60adaa From 90cf53b959ed014946a7d0d87841f5924d5b74ad Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 2 Apr 2024 17:27:29 -0400 Subject: [PATCH 45/48] Someone forgot to merge assets from the credits branch. --- assets | 2 +- source/funkin/ui/credits/CreditsDataHandler.hx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/assets b/assets index a54eb8517..3ccfe33ac 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit a54eb8517914e2a90b77e122ecd81cf73d60adaa +Subproject commit 3ccfe33acef6e62c40317af583af764838544a24 diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx index f2722ffbf..86afdafd1 100644 --- a/source/funkin/ui/credits/CreditsDataHandler.hx +++ b/source/funkin/ui/credits/CreditsDataHandler.hx @@ -20,7 +20,12 @@ class CreditsDataHandler if (data == null) { trace('CreditsData(NULL)'); + return; + } + if (data.entries == null || data.entries.length == 0) + { + trace('CreditsData(EMPTY)'); return; } From f7ff381bc7d526977d55a61f059fc60de747ef4a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 2 Apr 2024 23:33:10 -0400 Subject: [PATCH 46/48] Fix some build issues with HTML5. --- source/funkin/ui/debug/latency/LatencyState.hx | 5 +---- source/funkin/ui/transition/LoadingState.hx | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/source/funkin/ui/debug/latency/LatencyState.hx b/source/funkin/ui/debug/latency/LatencyState.hx index 7b2eabb1c..875a956e0 100644 --- a/source/funkin/ui/debug/latency/LatencyState.hx +++ b/source/funkin/ui/debug/latency/LatencyState.hx @@ -171,10 +171,7 @@ class LatencyState extends MusicBeatSubState trace(FlxG.sound.music._channel.position); */ - #if FLX_DEBUG - funnyStatsGraph.update(FlxG.sound.music.time % 500); - realStats.update(swagSong.getTimeWithDiff() % 500); - #end + localConductor.update(swagSong.time, false); if (FlxG.keys.justPressed.S) { diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index e4f4bf004..af8798ae2 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -222,7 +222,7 @@ class LoadingState extends MusicBeatSubState #if NO_PRELOAD_ALL // Switch to loading state while we load assets (default on HTML5 target). - var loadStateCtor:NextState = function() { + var loadStateCtor = function() { var result = new LoadingState(playStateCtor, shouldStopMusic, params); @:privateAccess result.asSubState = asSubState; @@ -230,7 +230,7 @@ class LoadingState extends MusicBeatSubState } if (asSubState) { - FlxG.state.openSubState(loadStateCtor); + FlxG.state.openSubState(cast loadStateCtor()); } else { From 2b4bf42ac4c55ec3524be7d0109e6814e241bf6a Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 3 Apr 2024 01:40:08 -0400 Subject: [PATCH 47/48] Fix multiple music, and crashes in freeplay --- source/funkin/audio/FunkinSound.hx | 20 +++++++------------- source/funkin/ui/freeplay/FreeplayState.hx | 9 +++++++-- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx index 100cee262..8c1bf3b41 100644 --- a/source/funkin/audio/FunkinSound.hx +++ b/source/funkin/audio/FunkinSound.hx @@ -321,6 +321,13 @@ class FunkinSound extends FlxSound implements ICloneable } } + if (FlxG.sound.music != null) + { + FlxG.sound.music.fadeTween?.cancel(); + FlxG.sound.music.stop(); + FlxG.sound.music.kill(); + } + if (params?.mapTimeChanges ?? true) { var songMusicData:Null = SongRegistry.instance.parseMusicData(key); @@ -335,19 +342,6 @@ class FunkinSound extends FlxSound implements ICloneable } } - if (FlxG.sound.music != null) - { - FlxG.sound.music.fadeTween?.cancel(); - FlxG.sound.music.stop(); - FlxG.sound.music.kill(); - } - - // Apparently HaxeFlixel isn't null safe. - @:nullSafety(Off) - { - FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, true, false, true); - } - var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true); if (music != null) { diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 531167a95..455805479 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -134,7 +134,7 @@ class FreeplayState extends MusicBeatSubState var stickerSubState:StickerSubState; public static var rememberedDifficulty:Null = Constants.DEFAULT_DIFFICULTY; - public static var rememberedSongId:Null = null; + public static var rememberedSongId:Null = 'tutorial'; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { @@ -596,7 +596,7 @@ class FreeplayState extends MusicBeatSubState // Only now do we know that the filter is actually changing. - rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId; + rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId; for (cap in grpCapsules.members) { @@ -939,6 +939,11 @@ class FreeplayState extends MusicBeatSubState FlxTransitionableState.skipNextTransOut = true; if (Type.getClass(FlxG.state) == MainMenuState) { + FunkinSound.playMusic('freakyMenu', + { + overrideExisting: true, + restartTrack: false + }); close(); } else From f7141e7096f25a4a3a9986afe6c26357376ae49e Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 3 Apr 2024 01:01:58 -0400 Subject: [PATCH 48/48] Fixed an issue with save data not loading defaults properly. --- source/funkin/save/Save.hx | 8 +-- .../funkin/save/migrator/SaveDataMigrator.hx | 3 +- source/funkin/ui/freeplay/FreeplayState.hx | 2 +- source/funkin/util/StructureUtil.hx | 61 +++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 source/funkin/util/StructureUtil.hx diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 73ba8efa0..dc7c5f989 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -14,7 +14,7 @@ import thx.semver.Version; 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"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -650,9 +650,9 @@ class Save if (legacySaveData != null) { trace('[SAVE] Found legacy save data, converting...'); - var gameSave = SaveDataMigrator.migrate(legacySaveData); + var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); @:privateAccess - FlxG.save.mergeData(gameSave.data); + FlxG.save.mergeData(gameSave.data, true); } else { @@ -664,7 +664,7 @@ class Save trace('[SAVE] Loaded save data.'); @:privateAccess var gameSave = SaveDataMigrator.migrate(FlxG.save.data); - FlxG.save.mergeData(gameSave.data); + FlxG.save.mergeData(gameSave.data, true); } } diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 00637d52a..3ed59e726 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -3,6 +3,7 @@ package funkin.save.migrator; import funkin.save.Save; import funkin.save.migrator.RawSaveData_v1_0_0; import thx.semver.Version; +import funkin.util.StructureUtil; import funkin.util.VersionUtil; @:nullSafety @@ -26,7 +27,7 @@ class SaveDataMigrator if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) { // Simply import the structured data. - var save:Save = new Save(inputData); + var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData)); return save; } else diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 455805479..66c829e11 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -962,7 +962,7 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null = grpCapsules.members[curSelected]?.songData; + var daSong:Null = currentFilteredSongs[curSelected]; if (daSong != null) { clearDaCache(daSong.songName); diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx new file mode 100644 index 000000000..351d0e0a8 --- /dev/null +++ b/source/funkin/util/StructureUtil.hx @@ -0,0 +1,61 @@ +package funkin.util; + +import haxe.DynamicAccess; + +/** + * Utilities for working with anonymous structures. + */ +class StructureUtil +{ + /** + * Merge two structures, with the second overwriting the first. + * Performs a SHALLOW clone, where child structures are not merged. + * @param a The base structure. + * @param b The new structure. + * @return The merged structure. + */ + public static function merge(a:Dynamic, b:Dynamic):Dynamic + { + var result:DynamicAccess = Reflect.copy(a); + + for (field in Reflect.fields(b)) + { + result.set(field, Reflect.field(b, field)); + } + + return result; + } + + /** + * Merge two structures, with the second overwriting the first. + * Performs a DEEP clone, where child structures are also merged recursively. + * @param a The base structure. + * @param b The new structure. + * @return The merged structure. + */ + public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic + { + if (a == null) return b; + if (b == null) return null; + if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b; + + var result:DynamicAccess = Reflect.copy(a); + + for (field in Reflect.fields(b)) + { + if (Reflect.isObject(b)) + { + // Note that isObject also returns true for class instances, + // but we just assume that's not a problem here. + result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field))); + } + else + { + // If we're here, b[field] is a primitive. + result.set(field, Reflect.field(b, field)); + } + } + + return result; + } +}