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; }