diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml index 6f890f53426..1de00e62f8f 100644 --- a/.github/changed-lines-count-labeler.yml +++ b/.github/changed-lines-count-labeler.yml @@ -7,6 +7,6 @@ medium: min: 10 max: 99 -# Add 'large' to any changes for more than 100 lines +# Add 'large' to any changes of at least 100 lines large: min: 100 diff --git a/.github/labeler.yml b/.github/labeler.yml index e8250b4e775..9cf784f4ca6 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,11 +1,11 @@ -# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder +# Add Documentation tag to PR's changing markdown files, or anything in the docs folder Documentation: - changed-files: - any-glob-to-any-file: - docs/* - '**/*.md' -# Adds Haxe tag to PR's changing haxe code files +# Add Haxe tag to PR's changing haxe code files Haxe: - changed-files: - any-glob-to-any-file: '**/*.hx' diff --git a/.gitmodules b/.gitmodules index ad8099e6021..f9d5fd49724 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "assets"] path = assets - url = https://github.com/FunkinCrew/Funkin-assets-secret + url = https://github.com/FunkinCrew/Funkin.assets [submodule "art"] path = art - url = https://github.com/FunkinCrew/Funkin-art-secret + url = https://github.com/FunkinCrew/Funkin.art diff --git a/CHANGELOG.md b/CHANGELOG.md index a21455ef508..0d727b223e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,92 @@ All notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.2] - 2024-10-?? +### Fixed +- Fixed an issue where exiting the Chart Editor would sometimes crash the game. +- Fixed an issue where holding down a direction key then selecting a character would select the locked character and crash the game (community fix by ACrazyTown) +- Fixed an issue where the player and girlfriend would disappear or overlap themselves in Character Select (community fix by gamerbross) +- Fixed an issue where the game would show the wrong girlfriend in Character Select (community fix by gamerbross) +- Fixed an issue where the cursor wouldn't update properly in Character Select (community fix by gamerbross) +- Fixed an issue where the player would display double after entering character select or when spamming buttons (community fix by gamerbross) + +## [0.5.1] - 2024-09-30 +### Added +- Readded the Merch button to the main menu. + - Click it to check out our Makeship campaign! +- Added Discord Rich Presence support. People can now see what song you are playing from Discord! + - We'll get mod support working for this eventually. +- Added an FPS limit option to the Preferences menu. + - You can now change how high the game tries to push your frame rate, from as little as 30 to as high as 300. +- Added support for the Tracy instrumenation-based profiling tool in development environments. Enable it with the `-DFEATURE_DEBUG_TRACY` compilation flag. + - For the people who aren't nerds, this is a tool for tracking down performance issues! +- Playable Character data now defines an asset location for an Animate Atlas to display Girlfriend. + - This includes the option to display a visualizer, if configured correctly. +- Separated the Perfect and Perfect (Gold) animations in the Playable Character data. + - Base game just uses the same animation for both, but modders can split the animations up on their custom characters now. +- Added a bunch of Flash project files from the Weekend 1 and Playable Pico updates to the `Funkin.art` repository. +- Added the `flipX` and `flipY` parameters to props in the Stage data. (community feature by abnormalpoof) +### Changed +- The game's mod API version check is now more dynamic. + - The update accepts mods with API version `0.5.0` as well as `0.5.1`. +- Pico is no longer unlocked for all players automatically. + - You need to beat Weekend 1 in Story Mode in order to unlock him in Character Select. +- Removed some of the more spammy `trace()` calls to improve debugging a bit. +- Improved some of the compilation and modding documentation. +- The game now complains if you create a song variation with symbols in the name. +- Switched the force crash keybind from Ctrl-Shift-L to Ctrl-Alt-Shift-L. +- Added some additional functions to `funkin.Assets` after `openfl.utils.Assets` had to get blacklisted from scripts. +### Fixed +- Fixed an issue where DadBattle (Pico Mix) was not properly credited to `TeraVex (ft. Saruky)`. +- Fixed an issue where DadBattle (Pico Mix) was missing charts on the Normal and Easy difficulty. +- Fixed an issue where Spookeez (Pico Mix) was not properly credited to `Six Impala (ft. Saster)`. +- Fixed an issue where the "Shit!" judgement would display with anti-aliasing in Week 6. +- Fixed an issue where Pico Erect could be played with different instrumentals. +- Fixed an issue where Pico would not play his shooting animations in Stress. +- Fixed an issue where Freeplay would display no custom songs when switching characters. +- Fixed an issue where Freeplay would sometimes display the wrong text on the capsules. +- Fixed an issue where duplicate difficulties from custom variations wouldn't display properly in Freeplay. +- Fixed an issue where custom note styles would sometimes just use default values rather than using the fallback note style. +- Fixed an issue where custom note styles would randomly fail to fetch information about their fallback note style. +- Fixed an issue where the Screenshots and Chart Editor binds displayed in the controls menu on Web builds (where they are disabled). +- Fixed an issue where the Stage Editor bind displayed in the controls menu even when the feature was disabled. +- Fixed an issue where the Freeplay Character Select keybind displayed weird in the keybinds menu. +- Fixed an issue where setting the input offset or visual offset too high would cause the song to skip. +- Fixed an issue where video cutscenes would not scale their volume properly. +- Fixed an issue where Cocoa Erect (Erect difficulty) had a tap note stacked on top of a hold note, +- Fixed an issue where the game would stutter when playing on the Week 5 Remix stage. +- Fixed an issue where the save data version number wouldn't get written to save data properly. +- Fixed an issue where the example mod could not be loaded. +- Fixed an issue where a script error would display when entering Blazin'. +- Fixed an issue where pressing F5 to force reload a song would sometimes cause the game to crash. +- Fixed an issue where animations on Animate Atlas characters would throw a bunch of warnings in the console. +- Fixed an issue where characters with high offsets would shift over after the player dies or restarts. +- Fixed an issue where Pico wouldn't play out his full burp animation in South (Pico Mix). +- Fixed an issue where Results screen audio could continue into Freeplay or even gameplay. +- Fixed an issue where some audio tracks would get destroyed even if they were flagged as persistent. +- Fixed an issue where the audio track would stay muted if you missed a note just before Pico burps. +- Fixed an issue where the curtains in Week 1 would display in front of larger characters. +- Fixed an issue where Boyfriend wouldn't play his death animation properly on Week 2's Remix stage. +- Fixed an issue in Freeplay where the clear % would not display after switching characters. +- Fixed an issue in Freeplay where character remixes would display the base song's highscore. +- Fixed an issue where Pico would become locked every time the game starts, and you would have to watch the unlock animation each game boot. + - The animation should now play only once per save file. +- Fixed an issue where Spirit's trail in Week 6 would not display correctly. +- Fixed an issue where the Input Offsets menu would crash when entering it before playing a song on web builds. +- Fixed an issue where the Results screen would spam the percentage tick noise instead of playing when the value changes. +- Fixed an issue where parts of the Chart Editor could not be interacted with. (community fix by KadeDeveloper) +- Fixed an issue where classic FocusCamera song events could cause the camera to snap in place. (community fix by NebulaZorua) +- Fixed an issue where achieving the same rank on a song (but a different clear %) would override your clear %, even if it was lower. (community fix by lemz1) +- Fixed an issue where the FPS counter would display even if Debug Display was turned off. (community fix by Lethrial) +- Fixed an issue where selecting the area to the left of the Chart Editor would select some of the player's notes (community fix by NotHyper474) +- Fixed an issue where pixel icons in the Chart Editor would not display correctly. (community fix by Techniktil) +- Fixed an issue where `Stage.addCharacter` would not properly assign the `characterType`. (community fix by KadeDeveloper) +- Fixed an issue where players should interact with Character Select during the unlock sequence, causing a crash. (community fix by actualmandm) +- Fixed an issue where hold notes in Week 6 were not scaled/positioned correctly. (community fix by dombomb64) +- Fixed an issue where audio offets would not interact with the Chart Editor properly. (community fix by KadeDev) +- Fixed an issue where fetching Modules during the `onDestroy` event would fail at random. (community fix by cyn0x8) +- Fixed an issue where `onSubStateOpenEnd` and `onSubStateCloseEnd` script events would not always get called. (community fix by lemz1) + ## [0.5.0] - 2024-09-12 ### Added - Added a new Character Select screen to switch between playable characters in Freeplay diff --git a/CODESTYLE.md b/CODESTYLE.md index 2641febfa80..2b6333258ad 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -4,14 +4,14 @@ Code style is enforced using Visual Studio Code extensions. ## .hx Formatting is handled by the `nadako.vshaxe` extension, which includes Haxe Formatter. -Haxe Formatter automatically resolves issues such as intentation style and line breaks, and can be configured in `hxformat.json`. +Haxe Formatter automatically resolves issues such as indentation style and line breaks, and can be configured in `hxformat.json`. Code Quality is handled by the `vshaxe.haxe-checkstyle` extension, which includes Haxe Checkstyle. ### Haxe Checkstyle Notes -* Checks can be escalated to display as different serverities in the Problems window. +* Checks can be escalated to display as different severities in the Problems window. * Checks can be disabled by setting the severity to `IGNORE`. -* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the intentation is. +* `IndentationCharacter` checks what is used to indent, `Indentation` checks how deep the indentation is. * `CommentedOutCode` check is in place because old code should be retrieved via Git history. * TODO items: Enable these one-by-one and fix them to improve the overall code quality. - Reconfigure `MethodLength` diff --git a/docs/COMPILING.md b/docs/COMPILING.md index ce3d89e7b44..6df1232b177 100644 --- a/docs/COMPILING.md +++ b/docs/COMPILING.md @@ -34,10 +34,11 @@ There are several useful build flags you can add to a build to affect how it wor - `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff - `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support. - `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection. - - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game + - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game will break if you try to zip it up and send it to someone, so it's disabled for release builds. - `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence. - `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support. - `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu. +- `-DFEATURE_SCREENSHOTS` or `-DNO_FEATURE_SCREENSHOTS` to forcibly enable or disable the screenshots feature. - `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor. - `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system. diff --git a/docs/Funkin' Debug Hotkeys.md b/docs/Funkin' Debug Hotkeys.md index 1287d5a1bf5..416c897141f 100644 --- a/docs/Funkin' Debug Hotkeys.md +++ b/docs/Funkin' Debug Hotkeys.md @@ -15,8 +15,8 @@ Most of this functionality is only available on debug builds of the game! - `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health. - `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health. - `9`: NEATO! -- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead. -- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead. +- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL***: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead. +- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL***: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead. ## **Freeplay State** - `F` (Freeplay Menu) - Move to Favorites @@ -27,5 +27,5 @@ Most of this functionality is only available on debug builds of the game! - `Y` - WOAH ## **Main Menu** -- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu. +- `~`: ***DEBUG***: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu. - `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds. diff --git a/docs/troubleshooting.md b/docs/TROUBLESHOOTING.md similarity index 100% rename from docs/troubleshooting.md rename to docs/TROUBLESHOOTING.md diff --git a/hmm.json b/hmm.json index 0c0c5571bf2..8ff572b30b9 100644 --- a/hmm.json +++ b/hmm.json @@ -63,14 +63,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "c9d96b168ea2a19274ad7c766ab1a34b57baa793", + "ref": "51c23588614397089a5ce182cddea729f0be6fa0", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "013b9d4e56bfe9a034e028a8d685f0b274cb73c4", + "ref": "da27e833947f32ef007ed11f523aa5524f5a5d54", "url": "https://github.com/haxeui/haxeui-flixel" }, { @@ -205,4 +205,4 @@ "url": "https://github.com/fponticelli/thx.semver" } ] -} \ No newline at end of file +} diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index ff72b1e1256..803b9e1b362 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -413,7 +413,7 @@ class Conductor } // Take into account instrumental and file format song offsets. - songPos += applyOffsets ? (instrumentalOffset + formatOffset + audioVisualOffset) : 0; + songPos += applyOffsets ? (combinedOffset) : 0; var oldMeasure:Float = this.currentMeasure; var oldBeat:Float = this.currentBeat; diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx index 521553527ce..922310309ee 100644 --- a/source/funkin/graphics/FunkinSprite.hx +++ b/source/funkin/graphics/FunkinSprite.hx @@ -238,6 +238,18 @@ class FunkinSprite extends FlxSprite return true; } + /** + * @param id The animation ID to check. + * @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations. + */ + public function isAnimationDynamic(id:String):Bool + { + if (this.animation == null) return false; + var animData = this.animation.getByName(id); + if (animData == null) return false; + return animData.numFrames > 1; + } + /** * Acts similarly to `makeGraphic`, but with improved memory usage, * at the expense of not being able to paint onto the resulting sprite. diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 125882161be..eb0e77fc552 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -282,6 +282,14 @@ class PolymodHandler // System.load() can load malicious DLLs Polymod.blacklistImport('lime.system.System'); + // `lime.utils.Assets` + // Literally just has a private `resolveClass` function for some reason? + Polymod.blacklistImport('lime.utils.Assets'); + Polymod.blacklistImport('openfl.utils.Assets'); + Polymod.blacklistImport('openfl.Lib'); + Polymod.blacklistImport('openfl.system.ApplicationDomain'); + Polymod.blacklistImport('funkin.util.FunkinTypeResolver'); + // `openfl.desktop.NativeProcess` // Can load native processes on the host operating system. Polymod.blacklistImport('openfl.desktop.NativeProcess'); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index fa2b28d54c9..dbacf3fa967 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1457,11 +1457,34 @@ class PlayState extends MusicBeatSubState if (FlxG.sound.music != null) { var correctSync:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset)); + var playerVoicesError:Float = 0; + var opponentVoicesError:Float = 0; - if (!startingSong && (Math.abs(FlxG.sound.music.time - correctSync) > 5 || Math.abs(vocals.checkSyncError(correctSync)) > 5)) + if (vocals != null) + { + @:privateAccess // todo: maybe make the groups public :thinking: + { + vocals.playerVoices.forEachAlive(function(voice:FunkinSound) { + var currentRawVoiceTime:Float = voice.time + vocals.playerVoicesOffset; + if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(playerVoicesError)) playerVoicesError = currentRawVoiceTime - correctSync; + }); + + vocals.opponentVoices.forEachAlive(function(voice:FunkinSound) { + var currentRawVoiceTime:Float = voice.time + vocals.opponentVoicesOffset; + if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(opponentVoicesError)) opponentVoicesError = currentRawVoiceTime - correctSync; + }); + } + } + + if (!startingSong + && (Math.abs(FlxG.sound.music.time - correctSync) > 5 || Math.abs(playerVoicesError) > 5 || Math.abs(opponentVoicesError) > 5)) { trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(correctSync)); + if (vocals != null) + { + trace(playerVoicesError); + trace(opponentVoicesError); + } trace(FlxG.sound.music.time); trace(correctSync); resyncVocals(); @@ -1837,6 +1860,17 @@ class PlayState extends MusicBeatSubState smallImageKey: discordRPCIcon }); #end + + #if FEATURE_DISCORD_RPC + // Updating Discord Rich Presence. + DiscordClient.instance.setPresence( + { + state: buildDiscordRPCState(), + details: buildDiscordRPCDetails(), + largeImageKey: discordRPCAlbum, + smallImageKey: discordRPCIcon + }); + #end } function buildDiscordRPCDetails():String @@ -2528,8 +2562,8 @@ class PlayState extends MusicBeatSubState healthChange = Constants.HEALTH_BAD_BONUS; isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK; case 'shit': - isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; healthChange = Constants.HEALTH_SHIT_BONUS; + isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK; } // Send the note hit event. @@ -2599,8 +2633,6 @@ class PlayState extends MusicBeatSubState } vocals.playerVolume = 0; - if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0); - applyScore(-10, 'miss', healthChange, true); if (playSound) @@ -2819,7 +2851,7 @@ class PlayState extends MusicBeatSubState } } comboPopUps.displayRating(daRating); - if (combo >= 10 || combo == 0) comboPopUps.displayCombo(combo); + if (combo >= 10) comboPopUps.displayCombo(combo); vocals.playerVolume = 1; } diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 16df9f5020b..ebe95e92006 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -2,28 +2,46 @@ package funkin.play.notes; import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; -import flixel.FlxSprite; +import funkin.graphics.FunkinSprite; import funkin.play.notes.NoteSprite; /** * The actual receptor that you see on screen. */ -class StrumlineNote extends FlxSprite +class StrumlineNote extends FunkinSprite { + /** + * Whether this strumline note is on the player's side or the opponent's side. + */ public var isPlayer(default, null):Bool; + /** + * The direction which this strumline note is facing. + */ public var direction(default, set):NoteDirection; - var confirmHoldTimer:Float = -1; - - static final CONFIRM_HOLD_TIME:Float = 0.1; - function set_direction(value:NoteDirection):NoteDirection { this.direction = value; return this.direction; } + /** + * Set this flag to `true` to disable performance optimizations that cause + * the Strumline note sprite to ignore `velocity` and `acceleration`. + */ + public var forceActive:Bool = false; + + /** + * How long to continue the hold note animation after a note is pressed. + */ + static final CONFIRM_HOLD_TIME:Float = 0.1; + + /** + * How long the hold note animation has been playing after a note is pressed. + */ + var confirmHoldTimer:Float = -1; + public function new(noteStyle:NoteStyle, isPlayer:Bool, direction:NoteDirection) { super(0, 0); @@ -41,7 +59,10 @@ class StrumlineNote extends FlxSprite this.active = true; } - function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void {} + function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void + { + // Do nothing. + } function onAnimationFinished(name:String):Void { @@ -103,19 +124,19 @@ class StrumlineNote extends FlxSprite public function playStatic():Void { - this.active = false; + this.active = (forceActive || isAnimationDynamic('static')); this.playAnimation('static', true); } public function playPress():Void { - this.active = true; + this.active = (forceActive || isAnimationDynamic('press')); this.playAnimation('press', true); } public function playConfirm():Void { - this.active = true; + this.active = (forceActive || isAnimationDynamic('confirm')); this.playAnimation('confirm', true); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index a6449bc1034..20d2f75a4c7 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -15,6 +15,7 @@ import funkin.data.song.SongRegistry; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.util.SortUtil; /** @@ -79,7 +80,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - final difficulties:Map; + + /** + * holds the difficulties (as in SongDifficulty) for each variation + * difficulties.get('default').get('easy') would return the easy difficulty for the default variation + */ + final difficulties:Map>; /** * The list of variations a song has. @@ -92,7 +98,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry(); + difficulties = new Map>(); _data = _fetchData(id); @@ -156,7 +162,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function listAlbums(variation:String):Map { var result:Map = new Map(); - for (difficultyId in difficulties.keys()) + for (variationMap in difficulties) { - var meta:Null = difficulties.get(difficultyId); - if (meta != null && meta.album != null) + for (difficultyId in variationMap.keys()) { - result.set(difficultyId, meta.album); + var meta:Null = variationMap.get(difficultyId); + if (meta != null && meta.album != null) + { + result.set(difficultyId, meta.album); + } } } return result; } + /** + * Input a difficulty ID and a variation ID, and get the album ID. + * @param diffId + * @param variation + * @return String + */ + public function getAlbumId(diffId:String, variation:String):String + { + var diff:Null = getDifficulty(diffId, variation); + if (diff == null) return ''; + + return diff.album ?? ''; + } + /** * Populate the difficulty data from the provided metadata. * Does not load chart data (that is triggered later when we want to play the song). @@ -285,6 +309,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = new Map(); + // There may be more difficulties in the chart file than in the metadata, // (i.e. non-playable charts like the one used for Pico on the speaker in Stress) // but all the difficulties in the metadata must be in the chart file. @@ -309,10 +336,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.get('$diffId$variationSuffix'); - if (difficulty == null) + // Retrieve the cached difficulty data. This one could potentially be null. + var nullDiff:Null = getDifficulty(diffId, variation); + + // if the difficulty doesn't exist, create a new one, and then proceed to fill it with data. + // I mostly do this since I don't wanna throw around ? everywhere for null check lol? + var difficulty:SongDifficulty = nullDiff ?? new SongDifficulty(this, diffId, variation); + + if (nullDiff == null) { trace('Fabricated new difficulty for $diffId.'); - difficulty = new SongDifficulty(this, diffId, variation); var metadata = _metadata.get(variation); - difficulties.set('$diffId$variationSuffix', difficulty); + difficulties.get(variation)?.set(diffId, difficulty); if (metadata != null) { @@ -396,29 +425,34 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + */ public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array):Null { if (possibleVariations == null) { possibleVariations = getVariationsByCharacter(currentCharacter); } + if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0]; for (variationId in possibleVariations) { - var variationSuffix = (variationId != Constants.DEFAULT_VARIATION) ? '-$variationId' : ''; - if (difficulties.exists('$diffId$variationSuffix')) return variationId; + if (difficulties.get('$variationId')?.exists(diffId) ?? false) return variationId; } return null; @@ -440,7 +474,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + * @see getVariationsByCharacter + */ + public function getVariationsByCharacterId(?charId:String):Array + { + var charPlayer = PlayerRegistry.instance.fetchEntry(charId ?? ''); + + return getVariationsByCharacter(charPlayer); + } + /** * List all the difficulties in this song. * @@ -501,6 +547,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry, ?showLocked:Bool, ?showHidden:Bool):Array { @@ -529,8 +576,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function getSongScore(songId:String, difficultyId:String = 'normal', ?variation:String):Null { var song = data.scores.songs.get(songId); + trace('Getting song score for $songId $difficultyId $variation'); if (song == null) { + trace('Could not find song data for $songId $difficultyId $variation'); song = []; data.scores.songs.set(songId, song); } + + // 'default' variations are left with no suffix ('easy', 'normal', 'hard'), + // along with 'erect' variations ('erect', 'nightmare') + // otherwise, we want to add a suffix of our current variation to get the save data. + if (variation != null && variation != '' && variation != 'default' && variation != 'erect') + { + difficultyId = '${difficultyId}-${variation}'; + } + return song.get(difficultyId); } - public function getSongRank(songId:String, difficultyId:String = 'normal'):Null + public function getSongRank(songId:String, difficultyId:String = 'normal', ?variation:String):Null { - return Scoring.calculateRank(getSongScore(songId, difficultyId)); + return Scoring.calculateRank(getSongScore(songId, difficultyId, variation)); } /** @@ -678,18 +690,31 @@ class Save /** * Has the provided song been beaten on one of the listed difficulties? + * Note: This function can still take in the 'difficulty-variation' format for the difficultyList parameter + * as it is used in the old save data format. However inputting a variation will append it to the difficulty + * so you can do `hasBeatenSong('dadbattle', ['easy-pico'])` to check if you've beaten the Pico mix on easy. + * or you can do `hasBeatenSong('dadbattle', ['easy'], 'pico')` to check if you've beaten the Pico mix on easy. + * however you should not mix the two as it will append '-pico' to the 'easy-pico' if it's inputted into the array. * @param songId The song ID to check. * @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`. + * @param variation The variation to check. Defaults to empty string. Appended to difficulty list with `-`, e.g. `easy-pico`. + * This is our old format for getting difficulty/variation information, however we don't want to mess around with + * save migration just yet. * @return Whether the song has been beaten on any of the listed difficulties. */ - public function hasBeatenSong(songId:String, ?difficultyList:Array):Bool + public function hasBeatenSong(songId:String, ?difficultyList:Array, ?variation:String):Bool { if (difficultyList == null) { difficultyList = ['easy', 'normal', 'hard']; } + + if (variation == null) variation = ''; + for (difficulty in difficultyList) { + if (variation != '') difficulty = '${difficulty}-${variation}'; + var score:Null = getSongScore(songId, difficulty); if (score != null) { diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx index 83bf15e9d57..4eb975e8af1 100644 --- a/source/funkin/ui/charSelect/CharSelectSubState.hx +++ b/source/funkin/ui/charSelect/CharSelectSubState.hx @@ -885,8 +885,8 @@ class CharSelectSubState extends MusicBeatSubState cursorLocIntended.x += cursorOffsetX; cursorLocIntended.y += cursorOffsetY; - cursor.x = MathUtil.coolLerp(cursor.x, cursorLocIntended.x, lerpAmnt); - cursor.y = MathUtil.coolLerp(cursor.y, cursorLocIntended.y, lerpAmnt); + cursor.x = MathUtil.smoothLerp(cursor.x, cursorLocIntended.x, elapsed, 0.1); + cursor.y = MathUtil.smoothLerp(cursor.y, cursorLocIntended.y, elapsed, 0.1); cursorBlue.x = MathUtil.coolLerp(cursorBlue.x, cursor.x, lerpAmnt * 0.4); cursorBlue.y = MathUtil.coolLerp(cursorBlue.y, cursor.y, lerpAmnt * 0.4); diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx index 4573ffbc7f8..157d20aa1b0 100644 --- a/source/funkin/ui/debug/anim/DebugBoundingState.hx +++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx @@ -109,9 +109,11 @@ class DebugBoundingState extends FlxState offsetEditorDialog.cameras = [hudCam]; add(offsetEditorDialog); + offsetEditorDialog.showDialog(false); - // Anchor to the right side by default - // offsetEditorDialog.x = FlxG.width - offsetEditorDialog.width; + // Anchor to the left side by default + offsetEditorDialog.x = 16; + offsetEditorDialog.y = 16; // sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 6784011fe96..059e4dbe53f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -79,7 +79,6 @@ import funkin.ui.debug.charting.commands.SetItemSelectionCommand; import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; -import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; @@ -5159,7 +5158,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2); if (songPos < 0) songPosMinutes = '-' + songPosMinutes; - var songPosString:String = '${songPosMinutes}:${songPosSeconds}:${songPosMilliseconds}'; + var songPosString:String = '${songPosMinutes}:${songPosSeconds}.${songPosMilliseconds}'; if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString; @@ -5654,7 +5653,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function handleHelpKeybinds():Void { // F1 = Open Help - if (FlxG.keys.justPressed.F1) this.openUserGuideDialog(); + if (FlxG.keys.justPressed.F1 && !isHaxeUIDialogOpen) { + this.openUserGuideDialog(); + } } function handleQuickWatch():Void diff --git a/source/funkin/ui/debug/latency/LatencyState.hx b/source/funkin/ui/debug/latency/LatencyState.hx index 63595655cd5..b980d3e821f 100644 --- a/source/funkin/ui/debug/latency/LatencyState.hx +++ b/source/funkin/ui/debug/latency/LatencyState.hx @@ -199,7 +199,11 @@ class LatencyState extends MusicBeatSubState PreciseInputManager.instance.onInputPressed.remove(preciseInputPressed); PreciseInputManager.instance.onInputReleased.remove(preciseInputReleased); - FlxG.sound.music.volume = previousVolume; + if (FlxG.sound.music != null) + { + FlxG.sound.music.volume = previousVolume; + } + swagSong.stop(); FlxG.sound.list.remove(swagSong); diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx index 7d40e85163d..351de6b977e 100644 --- a/source/funkin/ui/freeplay/FreeplayState.hx +++ b/source/funkin/ui/freeplay/FreeplayState.hx @@ -58,49 +58,6 @@ import funkin.data.song.SongData.SongMusicData; import funkin.api.discord.DiscordClient; #end -/** - * Parameters used to initialize the FreeplayState. - */ -typedef FreeplayStateParams = -{ - ?character:String, - - ?fromCharSelect:Bool, - - ?fromResults:FromResultsParams, -}; - -/** - * A set of parameters for transitioning to the FreeplayState from the ResultsState. - */ -typedef FromResultsParams = -{ - /** - * The previous rank the song hand, if any. Null if it had no score before. - */ - var ?oldRank:ScoringRank; - - /** - * Whether or not to play the rank animation on returning to freeplay. - */ - var playRankAnim:Bool; - - /** - * The new rank the song has. - */ - var newRank:ScoringRank; - - /** - * The song ID to play the animation on. - */ - var songId:String; - - /** - * The difficulty ID to play the animation on. - */ - var difficultyId:String; -}; - /** * The state for the freeplay menu, allowing the player to select any song to play. */ @@ -147,37 +104,17 @@ class FreeplayState extends MusicBeatSubState var songs:Array> = []; - // List of available difficulties for the current song, without `-variation` at the end (no duplicates or nulls). - var diffIdsCurrent:Array = []; - // List of available difficulties for the total song list, without `-variation` at the end (no duplicates or nulls). - var diffIdsTotal:Array = []; - // List of available difficulties for the current song, with `-variation` at the end (no duplicates or nulls). - var suffixedDiffIdsCurrent:Array = []; - // List of available difficulties for the total song list, with `-variation` at the end (no duplicates or nulls). - var suffixedDiffIdsTotal:Array = []; - var curSelected:Int = 0; - var currentSuffixedDifficulty:String = Constants.DEFAULT_DIFFICULTY; - var currentUnsuffixedDifficulty(get, never):String; - - function get_currentUnsuffixedDifficulty():String - { - if (Constants.DEFAULT_DIFFICULTY_LIST_FULL.contains(currentSuffixedDifficulty)) return currentSuffixedDifficulty; - - // Else, we need to strip the suffix. - return currentSuffixedDifficulty.substring(0, currentSuffixedDifficulty.lastIndexOf('-')); - } - - var currentVariation(get, never):String; - function get_currentVariation():String - { - if (Constants.DEFAULT_DIFFICULTY_LIST.contains(currentSuffixedDifficulty)) return Constants.DEFAULT_VARIATION; - if (Constants.DEFAULT_DIFFICULTY_LIST_ERECT.contains(currentSuffixedDifficulty)) return 'erect'; + /** + * Currently selected difficulty, in string form. + */ + var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; - // Else, we need to isolate the suffix. - return currentSuffixedDifficulty.substring(currentSuffixedDifficulty.lastIndexOf('-') + 1, currentSuffixedDifficulty.length); - } + /** + * Current variation: default, erect, pico, bf, etc. + */ + var currentVariation:String = Constants.DEFAULT_VARIATION; public var fp:FreeplayScore; @@ -234,6 +171,11 @@ class FreeplayState extends MusicBeatSubState */ public static var rememberedCharacterId:String = Constants.DEFAULT_CHARACTER; + /** + * The remembered variation we were on when this menu was last accessed. + */ + public static var rememberedVariation:String = Constants.DEFAULT_VARIATION; + var funnyCam:FunkinCamera; var rankCamera:FunkinCamera; var rankBg:FunkinSprite; @@ -241,46 +183,43 @@ class FreeplayState extends MusicBeatSubState var backingCard:Null = null; + /** + * The backing card that has the toned dots, right now we just use that one dad graphic dave cooked up + */ public var bgDad:FlxSprite; - var fromResultsParams:Null = null; + public var angleMaskShader:AngleMask = new AngleMask(); - var prepForNewRank:Bool = false; + var fadeShader:BlueFade = new BlueFade(); + var fromResultsParams:Null = null; + var prepForNewRank:Bool = false; var styleData:Null = null; - - var fromCharSelect:Null = null; + var fromCharSelect:Bool = false; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacterId = params?.character ?? rememberedCharacterId; styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId); + var fetchPlayableCharacter = function():PlayableCharacter { var targetCharId = params?.character ?? rememberedCharacterId; var result = PlayerRegistry.instance.fetchEntry(targetCharId); if (result == null) throw 'No valid playable character with id ${targetCharId}'; return result; }; - currentCharacter = fetchPlayableCharacter(); + currentCharacter = fetchPlayableCharacter(); + currentVariation = rememberedVariation; styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID()); rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER; - - fromCharSelect = params?.fromCharSelect; - + fromCharSelect = params?.fromCharSelect ?? false; fromResultsParams = params?.fromResults; - - if (fromResultsParams?.playRankAnim == true) - { - prepForNewRank = true; - } + prepForNewRank = fromResultsParams?.playRankAnim ?? false; super(FlxColor.TRANSPARENT); - if (stickers?.members != null) - { - stickerSubState = stickers; - } + if (stickers?.members != null) stickerSubState = stickers; switch (currentCharacterId) { @@ -315,16 +254,11 @@ class FreeplayState extends MusicBeatSubState bgDad = new FlxSprite(backingCard.pinkBack.width * 0.74, 0).loadGraphic(styleData == null ? 'freeplay/freeplayBGdad' : styleData.getBgAssetGraphic()); } - var fadeShader:BlueFade = new BlueFade(); - - public var angleMaskShader:AngleMask = new AngleMask(); - override function create():Void { super.create(); FlxG.state.persistentUpdate = false; - FlxTransitionableState.skipNextTransIn = true; var fadeShaderFilter:ShaderFilter = new ShaderFilter(fadeShader); @@ -377,23 +311,7 @@ class FreeplayState extends MusicBeatSubState continue; } - // Only display songs which actually have available difficulties for the current character. - var displayedVariations = song.getVariationsByCharacter(currentCharacter); - trace('Displayed Variations (${songId}): $displayedVariations'); - var availableDifficultiesForSong:Array = song.listSuffixedDifficulties(displayedVariations, false, false); - var unsuffixedDifficulties = song.listDifficulties(displayedVariations, false, false); - trace('Available Difficulties: $availableDifficultiesForSong'); - if (availableDifficultiesForSong.length == 0) continue; - - songs.push(new FreeplaySongData(levelId, songId, song, currentCharacter, displayedVariations)); - for (difficulty in unsuffixedDifficulties) - { - diffIdsTotal.pushUnique(difficulty); - } - for (difficulty in availableDifficultiesForSong) - { - suffixedDiffIdsTotal.pushUnique(difficulty); - } + songs.push(new FreeplaySongData(song, level)); } } @@ -486,30 +404,21 @@ class FreeplayState extends MusicBeatSubState wait: 0.1 }); - for (diffId in suffixedDiffIdsTotal) + for (diffId in Constants.DEFAULT_DIFFICULTY_LIST_FULL) { var diffSprite:DifficultySprite = new DifficultySprite(diffId); diffSprite.difficultyId = diffId; + diffSprite.visible = diffId == Constants.DEFAULT_DIFFICULTY; grpDifficulties.add(diffSprite); } - grpDifficulties.group.forEach(function(spr) { - spr.visible = false; - }); - - for (diffSprite in grpDifficulties.group.members) - { - if (diffSprite == null) continue; - if (diffSprite.difficultyId == currentSuffixedDifficulty) diffSprite.visible = true; - } - albumRoll.albumId = null; add(albumRoll); var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 164, FlxColor.BLACK); overhangStuff.y -= overhangStuff.height; - if (fromCharSelect == true) + if (fromCharSelect) { blackOverlayBullshitLOLXD.x = 387.76; overhangStuff.y = -100; @@ -595,6 +504,7 @@ class FreeplayState extends MusicBeatSubState wait: 0.1 }); + // Reminder, this is a callback function being set, rather than these being called here in create() letterSort.changeSelectionCallback = (str) -> { switch (str) { @@ -643,7 +553,7 @@ class FreeplayState extends MusicBeatSubState add(fnfFreeplay); add(ostName); - if (PlayerRegistry.instance.hasNewCharacter() == true) + if (PlayerRegistry.instance.hasNewCharacter()) { add(charSelectHint); } @@ -656,10 +566,10 @@ class FreeplayState extends MusicBeatSubState // when boyfriend hits dat shiii albumRoll.playIntro(); - var daSong = grpCapsules.members[curSelected].songData; - albumRoll.albumId = daSong?.albumId; + var daSong = grpCapsules.members[curSelected].freeplayData; + albumRoll.albumId = daSong?.data.getAlbumId(currentDifficulty, currentVariation); - if (fromCharSelect == null) + if (!fromCharSelect) { // render optimisation if (_parentState != null) _parentState.persistentDraw = false; @@ -728,6 +638,7 @@ class FreeplayState extends MusicBeatSubState onDJIntroDone(); } + // Generates song list with the starter params (who our current character is, last remembered difficulty, etc.) generateSongList(null, false); // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere @@ -755,7 +666,7 @@ class FreeplayState extends MusicBeatSubState rankCamera.fade(0xFF000000, 0, false, null, true); } - if (fromCharSelect == true) + if (fromCharSelect) { enterFromCharSel(); onDJIntroDone(); @@ -766,7 +677,8 @@ class FreeplayState extends MusicBeatSubState var currentFilteredSongs:Array> = []; /** - * Given the current filter, rebuild the current song list. + * Given the current filter, rebuild the current song list and display it. + * Automatically takes into account currentDifficulty, character, and variation * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param force Whether the capsules should "jump" back in or not using their animation @@ -778,14 +690,18 @@ class FreeplayState extends MusicBeatSubState if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff); - // Filter further by current selected difficulty. - if (currentSuffixedDifficulty != null) - { - tempSongs = tempSongs.filter(song -> { - if (song == null) return true; // Random - return song.suffixedSongDifficulties.contains(currentSuffixedDifficulty); - }); - } + tempSongs = tempSongs.filter(song -> { + if (song == null) return true; // Random + + // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` + // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) + var characterVariations:Array = song.data.getVariationsByCharacter(currentCharacter); + + // Gets all available difficulties for our character, via our available variations + var difficultiesAvailable:Array = song.data.listDifficulties(null, characterVariations); + + return difficultiesAvailable.contains(currentDifficulty); + }); if (onlyIfChanged) { @@ -794,28 +710,20 @@ class FreeplayState extends MusicBeatSubState } // Only now do we know that the filter is actually changing. - - // If curSelected is 0, the result will be null and fall back to the rememberedSongId. - rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId; - - for (cap in grpCapsules.members) - { - cap.songText.resetText(); - cap.kill(); - } - currentFilter = filterStuff; currentFilteredSongs = tempSongs; curSelected = 0; - var hsvShader:HSVShader = new HSVShader(); + // If curSelected is 0, the result will be null and fall back to the rememberedSongId. + // We set this so if we change the filter, we'd remain on the same song if it's still in the list. + rememberedSongId = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? rememberedSongId; + grpCapsules.killMembers(); + + // Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand) var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); randomCapsule.init(FlxG.width, 0, null, styleData); - randomCapsule.onConfirm = function() { - capsuleOnConfirmRandom(randomCapsule); - }; randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; randomCapsule.alpha = 0; @@ -824,14 +732,15 @@ class FreeplayState extends MusicBeatSubState randomCapsule.favIconBlurred.visible = false; randomCapsule.ranking.visible = false; randomCapsule.blurredRanking.visible = false; - if (fromCharSelect == false) - { - randomCapsule.initJumpIn(0, force); - } + randomCapsule.onConfirm = function() { + capsuleOnConfirmRandom(randomCapsule); + }; + + if (fromCharSelect) randomCapsule.forcePosition(); else - { - randomCapsule.forcePosition(); - } + randomCapsule.initJumpIn(0, force); + + var hsvShader:HSVShader = new HSVShader(); randomCapsule.hsvShader = hsvShader; grpCapsules.add(randomCapsule); @@ -850,13 +759,8 @@ class FreeplayState extends MusicBeatSubState funnyMenu.targetPos.x = funnyMenu.x; funnyMenu.ID = i; funnyMenu.capsule.alpha = 0.5; - funnyMenu.songText.visible = false; - funnyMenu.favIcon.visible = tempSong.isFav; - funnyMenu.favIconBlurred.visible = tempSong.isFav; funnyMenu.hsvShader = hsvShader; - funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); - funnyMenu.checkClip(); funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); @@ -865,9 +769,8 @@ class FreeplayState extends MusicBeatSubState FlxG.console.registerFunction('changeSelection', changeSelection); rememberSelection(); - changeSelection(); - changeDiff(0, true); + refreshCapsuleDisplays(); } /** @@ -879,7 +782,7 @@ class FreeplayState extends MusicBeatSubState public function sortSongs(songsToFilter:Array>, songFilter:SongFilter):Array> { var filterAlphabetically = function(a:Null, b:Null):Int { - return SortUtil.alphabetically(a?.songName ?? '', b?.songName ?? ''); + return SortUtil.alphabetically(a?.data.songName ?? '', b?.data.songName ?? ''); }; switch (songFilter.filterType) @@ -891,9 +794,9 @@ class FreeplayState extends MusicBeatSubState // if filterData looks like "A-C", the regex should look something like this: ^[A-C].* // to get every song that starts between A and C var filterRegexp:EReg = new EReg('^[' + songFilter.filterData + '].*', 'i'); - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return filterRegexp.match(str.songName); + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filterRegexp.match(filteredSong.data.songName); }); songsToFilter.sort(filterAlphabetically); @@ -901,16 +804,16 @@ class FreeplayState extends MusicBeatSubState case STARTSWITH: // extra note: this is essentially a "search" - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return str.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filteredSong.data.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); }); case ALL: // no filter! case FAVORITE: - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return str.isFav; + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filteredSong.isFav; }); songsToFilter.sort(filterAlphabetically); @@ -1095,7 +998,7 @@ class FreeplayState extends MusicBeatSubState }); } - function rankAnimSlam(fromResultsParams:Null) + function rankAnimSlam(fromResultsParams:Null):Void { // FlxTween.tween(rankCamera, {"zoom": 1.9}, 0.5, {ease: FlxEase.backOut}); FlxTween.tween(rankBg, {alpha: 0}, 0.5, {ease: FlxEase.expoIn}); @@ -1278,7 +1181,7 @@ class FreeplayState extends MusicBeatSubState } } fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn}); - FlxG.sound.music.fadeOut(0.9, 0); + FlxG.sound.music?.fadeOut(0.9, 0); new FlxTimer().start(0.9, _ -> { FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); }); @@ -1441,7 +1344,7 @@ class FreeplayState extends MusicBeatSubState if (controls.FREEPLAY_FAVORITE && !busy) { - var targetSong = grpCapsules.members[curSelected]?.songData; + var targetSong = grpCapsules.members[curSelected]?.freeplayData; if (targetSong != null) { var realShit:Int = curSelected; @@ -1484,7 +1387,6 @@ class FreeplayState extends MusicBeatSubState busy = true; grpCapsules.members[realShit].doLerp = false; FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1, {ease: FlxEase.expoOut}); - FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1, { ease: FlxEase.expoIn, @@ -1744,7 +1646,9 @@ class FreeplayState extends MusicBeatSubState FunkinSound.playMusic('freakyMenu', { overrideExisting: true, - restartTrack: false + restartTrack: false, + // Continue playing this music between states, until a different music track gets played. + persist: true }); FlxG.sound.music.fadeIn(4.0, 0.0, 1.0); close(); @@ -1772,55 +1676,71 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null = currentFilteredSongs[curSelected]; - if (daSong != null) - { - clearDaCache(daSong.songName); - } - // remove and destroy freeplay camera FlxG.cameras.remove(funnyCam); } + /** + * changeDiff is the root of both difficulty and variation changes/management. + * It will check the difficulty of the current variation, all available variations, and all available difficulties per variation. + * It's generally recommended that after calling this you re-sort the song list, however usually it's already on the way to being sorted. + * @param change + * @param force + */ function changeDiff(change:Int = 0, force:Bool = false):Void { touchTimer = 0; + var previousVariation:String = currentVariation; - var currentDifficultyIndex:Int = suffixedDiffIdsCurrent.indexOf(currentSuffixedDifficulty); + // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` + // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) + var characterVariations:Array = grpCapsules.members[curSelected].freeplayData?.data.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; - if (currentDifficultyIndex == -1) currentDifficultyIndex = suffixedDiffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY); + // Gets all available difficulties for our character, via our available variations + var difficultiesAvailable:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, + characterVariations) ?? Constants.DEFAULT_DIFFICULTY_LIST; - currentDifficultyIndex += change; + var currentDifficultyIndex:Int = difficultiesAvailable.indexOf(currentDifficulty); - if (currentDifficultyIndex < 0) currentDifficultyIndex = suffixedDiffIdsCurrent.length - 1; - if (currentDifficultyIndex >= suffixedDiffIdsCurrent.length) currentDifficultyIndex = 0; + if (currentDifficultyIndex == -1) currentDifficultyIndex = difficultiesAvailable.indexOf(Constants.DEFAULT_DIFFICULTY); + + currentDifficultyIndex += change; - currentSuffixedDifficulty = suffixedDiffIdsCurrent[currentDifficultyIndex]; + if (currentDifficultyIndex < 0) currentDifficultyIndex = Std.int(difficultiesAvailable.length - 1); + if (currentDifficultyIndex >= difficultiesAvailable.length) currentDifficultyIndex = 0; - trace('Switching to difficulty: ${currentSuffixedDifficulty}'); + // Update the current difficulty + currentDifficulty = difficultiesAvailable[currentDifficultyIndex]; + for (variation in characterVariations) + { + if (grpCapsules.members[curSelected].freeplayData?.data.hasDifficulty(currentDifficulty, variation) ?? false) + { + currentVariation = variation; + rememberedVariation = variation; + break; + } + } - var daSong:Null = grpCapsules.members[curSelected].songData; + var daSong:Null = grpCapsules.members[curSelected].freeplayData; if (daSong != null) { - var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.songId); + var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.data.id); if (targetSong == null) { - FlxG.log.warn('WARN: could not find song with id (${daSong.songId})'); + FlxG.log.warn('WARN: could not find song with id (${daSong.data.id})'); return; } - var suffixedDifficulty = suffixedDiffIdsCurrent[currentDifficultyIndex]; - var songScore:Null = Save.instance.getSongScore(daSong.songId, suffixedDifficulty); - trace(songScore); + var songScore:Null = Save.instance.getSongScore(daSong.data.id, currentDifficulty, currentVariation); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); - rememberedDifficulty = suffixedDifficulty; - currentSuffixedDifficulty = suffixedDifficulty; + rememberedDifficulty = currentDifficulty; + grpCapsules.members[curSelected].refreshDisplay(); } else { intendedScore = 0; intendedCompletion = 0.0; - rememberedDifficulty = currentSuffixedDifficulty; + rememberedDifficulty = currentDifficulty; } if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion)) @@ -1828,15 +1748,15 @@ class FreeplayState extends MusicBeatSubState intendedCompletion = 0; } - grpDifficulties.group.forEach(function(diffSprite) { - diffSprite.visible = false; - }); - for (diffSprite in grpDifficulties.group.members) { if (diffSprite == null) continue; - if (diffSprite.difficultyId == currentSuffixedDifficulty) + diffSprite.visible = false; + + if (diffSprite.difficultyId == currentDifficulty) { + diffSprite.visible = true; + if (change != 0) { diffSprite.visible = true; @@ -1847,10 +1767,6 @@ class FreeplayState extends MusicBeatSubState diffSprite.updateHitbox(); }); } - else - { - diffSprite.visible = true; - } } } @@ -1860,26 +1776,20 @@ class FreeplayState extends MusicBeatSubState for (songCapsule in grpCapsules.members) { if (songCapsule == null) continue; - if (songCapsule.songData != null) + + if (songCapsule.freeplayData != null) { - songCapsule.songData.currentVariation = currentVariation; - songCapsule.songData.currentUnsuffixedDifficulty = currentUnsuffixedDifficulty; - songCapsule.songData.currentSuffixedDifficulty = currentSuffixedDifficulty; - songCapsule.init(null, null, songCapsule.songData); + songCapsule.init(null, null, songCapsule.freeplayData); songCapsule.checkClip(); } - else - { - songCapsule.init(null, null, null); - } } // Reset the song preview in case we changed variations (normal->erect etc) - playCurSongPreview(); + if (currentVariation != previousVariation) playCurSongPreview(); } // Set the album graphic and play the animation if relevant. - var newAlbumId:Null = daSong?.albumId; + var newAlbumId:Null = daSong?.data.getAlbumId(currentDifficulty, currentVariation); if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1887,21 +1797,7 @@ class FreeplayState extends MusicBeatSubState } // Set difficulty star count. - albumRoll.setDifficultyStars(daSong?.difficultyRating); - } - - // Clears the cache of songs to free up memory, they'll have to be loaded in later tho - function clearDaCache(actualSongTho:String):Void - { - for (song in songs) - { - if (song == null) continue; - if (song.songName != actualSongTho) - { - trace('trying to remove: ' + song.songName); - // openfl.Assets.cache.clear(Paths.inst(song.songName)); - } - } + albumRoll.setDifficultyStars(daSong?.data.getDifficulty(currentDifficulty, currentVariation)?.difficultyRating ?? 0); } function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void @@ -1913,11 +1809,11 @@ class FreeplayState extends MusicBeatSubState var availableSongCapsules:Array = grpCapsules.members.filter(function(cap:SongMenuItem) { // Dead capsules are ones which were removed from the list when changing filters. - return cap.alive && cap.songData != null; + return cap.alive && cap.freeplayData != null; }); trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap?.songData?.songName; + return cap?.freeplayData?.data.songName; })}'); if (availableSongCapsules.length == 0) @@ -1944,7 +1840,7 @@ class FreeplayState extends MusicBeatSubState */ function capsuleOnOpenDefault(cap:SongMenuItem):Void { - var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongId:String = cap?.freeplayData?.data.id ?? 'unknown'; var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); if (targetSongNullable == null) { @@ -1952,10 +1848,10 @@ class FreeplayState extends MusicBeatSubState return; } var targetSong:Song = targetSongNullable; - var targetDifficultyId:String = currentUnsuffixedDifficulty; + var targetDifficultyId:String = currentDifficulty; var targetVariation:Null = currentVariation; trace('target song: ${targetSongId} (${targetVariation})'); - var targetLevelId:Null = cap?.songData?.levelId; + var targetLevelId:Null = cap?.freeplayData?.levelId; PlayStatePlaylist.campaignId = targetLevelId ?? null; var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); @@ -2026,7 +1922,7 @@ class FreeplayState extends MusicBeatSubState PlayStatePlaylist.isStoryMode = false; - var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongId:String = cap?.freeplayData?.data.id ?? 'unknown'; var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); if (targetSongNullable == null) { @@ -2034,21 +1930,20 @@ class FreeplayState extends MusicBeatSubState return; } var targetSong:Song = targetSongNullable; - var targetDifficultyId:String = currentUnsuffixedDifficulty; var targetVariation:Null = currentVariation; - var targetLevelId:Null = cap?.songData?.levelId; + var targetLevelId:Null = cap?.freeplayData?.levelId; PlayStatePlaylist.campaignId = targetLevelId ?? null; - var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); + var targetDifficulty:Null = targetSong.getDifficulty(currentDifficulty, currentVariation); if (targetDifficulty == null) { - FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); + FlxG.log.warn('WARN: could not find difficulty with id (${currentDifficulty})'); return; } if (targetInstId == null) { - var baseInstrumentalId:String = targetSong?.getBaseInstrumentalId(targetDifficultyId, targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var baseInstrumentalId:String = targetSong?.getBaseInstrumentalId(currentDifficulty, targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? ''; targetInstId = baseInstrumentalId; } @@ -2064,12 +1959,12 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); - Paths.setCurrentLevel(cap?.songData?.levelId); + Paths.setCurrentLevel(cap?.freeplayData?.levelId); LoadingState.loadPlayState( { targetSong: targetSong, - targetDifficulty: targetDifficultyId, - targetVariation: targetVariation, + targetDifficulty: currentDifficulty, + targetVariation: currentVariation, targetInstrumental: targetInstId, practiceMode: false, minimalMode: false, @@ -2087,13 +1982,20 @@ class FreeplayState extends MusicBeatSubState }); } + function refreshCapsuleDisplays():Void + { + grpCapsules.forEachAlive((cap:SongMenuItem) -> { + cap.refreshDisplay(); + }); + } + function rememberSelection():Void { if (rememberedSongId != null) { curSelected = currentFilteredSongs.findIndex(function(song) { if (song == null) return false; - return song.songId == rememberedSongId; + return song.data.id == rememberedSongId; }); if (curSelected == -1) curSelected = 0; @@ -2101,7 +2003,12 @@ class FreeplayState extends MusicBeatSubState if (rememberedDifficulty != null) { - currentSuffixedDifficulty = rememberedDifficulty; + currentDifficulty = rememberedDifficulty; + } + + if (rememberedVariation != null) + { + currentVariation = rememberedVariation; } } @@ -2117,22 +2024,19 @@ class FreeplayState extends MusicBeatSubState if (curSelected >= grpCapsules.countLiving()) curSelected = 0; var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected]; - if (daSongCapsule.songData != null) + if (daSongCapsule.freeplayData != null) { - var songScore:Null = Save.instance.getSongScore(daSongCapsule.songData.songId, currentSuffixedDifficulty); + var songScore:Null = Save.instance.getSongScore(daSongCapsule.freeplayData.data.id, currentDifficulty, currentVariation); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); - diffIdsCurrent = daSongCapsule.songData.songDifficulties; - suffixedDiffIdsCurrent = daSongCapsule.songData.suffixedSongDifficulties; - rememberedSongId = daSongCapsule.songData.songId; + rememberedSongId = daSongCapsule.freeplayData.data.id; changeDiff(); + daSongCapsule.refreshDisplay(); } else { intendedScore = 0; intendedCompletion = 0.0; - diffIdsCurrent = diffIdsTotal; - suffixedDiffIdsCurrent = suffixedDiffIdsTotal; rememberedSongId = null; rememberedDifficulty = Constants.DEFAULT_DIFFICULTY; albumRoll.albumId = null; @@ -2160,7 +2064,6 @@ class FreeplayState extends MusicBeatSubState public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void { if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected]; - if (curSelected == 0) { FunkinSound.playMusic('freeplayRandom', @@ -2173,34 +2076,25 @@ class FreeplayState extends MusicBeatSubState } else { - var previewSongId:Null = daSongCapsule?.songData?.songId; - if (previewSongId == null) return; - - var previewSong:Null = SongRegistry.instance.fetchEntry(previewSongId); + var previewSong:Null = daSongCapsule?.freeplayData?.data; if (previewSong == null) return; - // var currentVariation = previewSong.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; - var targetDifficultyId:String = currentUnsuffixedDifficulty; - var targetVariation:Null = currentVariation; - var songDifficulty:Null = previewSong.getDifficulty(targetDifficultyId, targetVariation ?? Constants.DEFAULT_VARIATION); - var baseInstrumentalId:String = previewSong.getBaseInstrumentalId(targetDifficultyId, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; - var altInstrumentalIds:Array = previewSong.listAltInstrumentalIds(targetDifficultyId, - songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? []; + // Check if character-specific difficulty exists + var songDifficulty:Null = previewSong.getDifficulty(currentDifficulty, currentVariation); + var baseInstrumentalId:String = previewSong.getBaseInstrumentalId(currentDifficulty, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var altInstrumentalIds:Array = previewSong.listAltInstrumentalIds(currentDifficulty, + songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? []; var instSuffix:String = baseInstrumentalId; - #if FEATURE_DEBUG_FUNCTIONS if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) { instSuffix = altInstrumentalIds[0]; } #end - instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - - trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}'); - - FunkinSound.playMusic(previewSongId, + trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); + FunkinSound.playMusic(previewSong.id, { startingVolume: 0.0, overrideExisting: true, @@ -2218,7 +2112,6 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.music.fadeIn(2, 0, 0.4); } }); - if (songDifficulty != null) { Conductor.instance.mapTimeChanges(songDifficulty.timeChanges); @@ -2335,56 +2228,66 @@ enum abstract FilterType(String) class FreeplaySongData { /** - * Whether or not the song has been favorited. + * We used to have a billion fields, but this SongMetadata variable should be all we need + * to be able to get most information about an available song. + * For example, you can get the artist via `data.songArtist` + * + * You can usually get various other particulars of a specific difficulty/variation by + * using data.getDifficulty(), and inputting specifics on your difficulty, variations, etc. + * See the getters here for songCharacter, fullSongName, and songStartingBpm for examples. + * + * @see Song */ - public var isFav:Bool = false; + public var data:Song; - public var isNew:Bool = false; + /** + * The level id of the song, useful for sorting from week1 -> week 7 + weekend1 + * and for properly loading PlayStatePlaylist for preloading on web + */ + public var levelId(get, never):Null; - var song:Song; + function get_levelId():Null + { + return _levelId; + } - public var levelId(default, null):String = ''; - public var songId(default, null):String = ''; + var _levelId:String; - public var songDifficulties(default, null):Array = []; - public var suffixedSongDifficulties(default, null):Array = []; + /** + * Whether or not the song has been favorited. + */ + public var isFav:Bool = false; - public var songName(default, null):String = ''; - public var songCharacter(default, null):String = ''; - public var songStartingBpm(default, null):Float = 0; - public var difficultyRating(default, null):Int = 0; - public var albumId(default, null):Null = null; + /** + * Whether the player has seen/played this song before within freeplay + */ + public var isNew(get, never):Bool; - public var currentCharacter:PlayableCharacter; - public var currentVariation:String = Constants.DEFAULT_VARIATION; - public var currentSuffixedDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; - public var currentUnsuffixedDifficulty:String = Constants.DEFAULT_DIFFICULTY; + /** + * The default opponent for the song. + * Does the getter stuff for you depending on your current (or rather, rememberd) variation and difficulty. + */ + public var songCharacter(get, never):String; - public var scoringRank:Null = null; + /** + * The full song name, dynamically generated depending on your current (or rather, rememberd) variation and difficulty. + */ + public var fullSongName(get, never):String; - var displayedVariations:Array = [Constants.DEFAULT_VARIATION]; + /** + * The starting BPM of the song, dynamically generated depending on your current (or rather, rememberd) variation and difficulty. + */ + public var songStartingBpm(get, never):Float; - function set_currentSuffixedDifficulty(value:String):String - { - if (currentSuffixedDifficulty == value) return value; + public var difficultyRating(get, never):Int; - currentSuffixedDifficulty = value; - updateValues(displayedVariations); - return value; - } + public var scoringRank(get, never):Null; - public function new(levelId:String, songId:String, song:Song, currentCharacter:PlayableCharacter, ?displayedVariations:Array) + public function new(data:Song, levelData:Level) { - this.levelId = levelId; - this.songId = songId; - this.song = song; - - this.isFav = Save.instance.isSongFavorited(songId); - - this.currentCharacter = currentCharacter; - if (displayedVariations != null) this.displayedVariations = displayedVariations; - - updateValues(displayedVariations); + this.data = data; + _levelId = levelData.id; + this.isFav = Save.instance.isSongFavorited(data.songName); } /** @@ -2396,53 +2299,110 @@ class FreeplaySongData isFav = !isFav; if (isFav) { - Save.instance.favoriteSong(this.songId); + Save.instance.favoriteSong(data.songName); } else { - Save.instance.unfavoriteSong(this.songId); + Save.instance.unfavoriteSong(data.songName); } return isFav; } function updateValues(variations:Array):Void { - this.songDifficulties = song.listDifficulties(null, variations, false, false); - this.suffixedSongDifficulties = song.listSuffixedDifficulties(variations, false, false); - if (!this.songDifficulties.contains(currentUnsuffixedDifficulty)) - { - currentSuffixedDifficulty = Constants.DEFAULT_DIFFICULTY; - // This method gets called again by the setter-method - // or the difficulty didn't change, so there's no need to continue. - return; - } + // this.isNew = song.isSongNew(suffixedDifficulty); + } - var targetVariation:Null = currentVariation; + function get_isNew():Bool + { + // We use a slightly different manner to get the new status of a song than the other getters here + // `isSongNew()` only takes a single variation, and it's data that isn't accessible via the Song data/metadata + // it's stored in the song .hxc script in a function that overrides `isSongNew()` + // and is only accessible with the correct valid variation inputs + + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + var variation:String = data.getFirstValidVariation(FreeplayState.rememberedDifficulty, null, variations); + return data.isSongNew(FreeplayState.rememberedDifficulty, variation); + } - var songDifficulty:SongDifficulty = song.getDifficulty(currentUnsuffixedDifficulty, targetVariation); - if (songDifficulty == null) return; - this.songStartingBpm = songDifficulty.getStartingBPM(); - this.songName = songDifficulty.songName; - this.songCharacter = songDifficulty.characters.opponent; - this.difficultyRating = songDifficulty.difficultyRating; - if (songDifficulty.album == null) - { - FlxG.log.warn('No album for: ${songDifficulty.songName}'); - this.albumId = Constants.DEFAULT_ALBUM_ID; - } - else - { - this.albumId = songDifficulty.album; - } + function get_songCharacter():String + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.characters.opponent ?? ''; + } + + function get_fullSongName():String + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.songName ?? data.songName; + } - var suffixedDifficulty = currentSuffixedDifficulty; + function get_songStartingBpm():Float + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); - this.scoringRank = Save.instance.getSongRank(songId, suffixedDifficulty); + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.getStartingBPM() ?? 0; + } - this.isNew = song.isSongNew(suffixedDifficulty); + function get_difficultyRating():Int + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.difficultyRating ?? 0; + } + + function get_scoringRank():Null + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + var variation:String = data.getFirstValidVariation(FreeplayState.rememberedDifficulty, null, variations); + + return Save.instance.getSongRank(data.id, FreeplayState.rememberedDifficulty, variation); } } +/** + * Parameters used to initialize the FreeplayState. + */ +typedef FreeplayStateParams = +{ + ?character:String, + + ?fromCharSelect:Bool, + + ?fromResults:FromResultsParams, +}; + +/** + * A set of parameters for transitioning to the FreeplayState from the ResultsState. + */ +typedef FromResultsParams = +{ + /** + * The previous rank the song hand, if any. Null if it had no score before. + */ + var ?oldRank:ScoringRank; + + /** + * Whether or not to play the rank animation on returning to freeplay. + */ + var playRankAnim:Bool; + + /** + * The new rank the song has. + */ + var newRank:ScoringRank; + + /** + * The song ID to play the animation on. + */ + var songId:String; + + /** + * The difficulty ID to play the animation on. + */ + var difficultyId:String; +}; + /** * The map storing information about the exit movers. */ diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 864fa2d1d26..3db958bb539 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -36,7 +36,7 @@ class SongMenuItem extends FlxSpriteGroup * Modify this by calling `init()` * If `null`, assume this SongMenuItem is for the "Random Song" option. */ - public var songData(default, null):Null = null; + public var freeplayData(default, null):Null = null; public var selected(default, set):Bool; @@ -422,6 +422,34 @@ class SongMenuItem extends FlxSpriteGroup return evilTrail.color; } + public function refreshDisplay():Void + { + if (freeplayData == null) + { + songText.text = 'Random'; + pixelIcon.visible = false; + ranking.visible = false; + blurredRanking.visible = false; + favIcon.visible = false; + favIconBlurred.visible = false; + newText.visible = false; + } + else + { + songText.text = freeplayData.fullSongName; + if (freeplayData.songCharacter != null) pixelIcon.setCharacter(freeplayData.songCharacter); + pixelIcon.visible = true; + updateBPM(Std.int(freeplayData.songStartingBpm) ?? 0); + updateDifficultyRating(freeplayData.difficultyRating ?? 0); + updateScoringRank(freeplayData.scoringRank); + newText.visible = freeplayData.isNew; + favIcon.visible = freeplayData.isFav; + favIconBlurred.visible = freeplayData.isFav; + checkClip(); + } + updateSelected(); + } + function updateDifficultyRating(newRating:Int):Void { var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; @@ -500,11 +528,11 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(?x:Float, ?y:Float, songData:Null, ?styleData:FreeplayStyle = null):Void + public function init(?x:Float, ?y:Float, freeplayData:Null, ?styleData:FreeplayStyle = null):Void { if (x != null) this.x = x; if (y != null) this.y = y; - this.songData = songData; + this.freeplayData = freeplayData; // im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/ // if thats possible someone Please change the new function to be something like @@ -517,21 +545,13 @@ class SongMenuItem extends FlxSpriteGroup songText.applyStyle(styleData); } - // Update capsule text. - songText.text = songData?.songName ?? 'Random'; - // Update capsule character. - if (songData?.songCharacter != null) pixelIcon.setCharacter(songData.songCharacter); - updateBPM(Std.int(songData?.songStartingBpm) ?? 0); - updateDifficultyRating(songData?.difficultyRating ?? 0); - updateScoringRank(songData?.scoringRank); - newText.visible = songData?.isNew; + updateScoringRank(freeplayData?.scoringRank); favIcon.animation.curAnim.curFrame = favIcon.animation.curAnim.numFrames - 1; favIconBlurred.animation.curAnim.curFrame = favIconBlurred.animation.curAnim.numFrames - 1; - // Update opacity, offsets, etc. - updateSelected(); + refreshDisplay(); - checkWeek(songData?.songId); + checkWeek(freeplayData?.data.id); } var frameInTicker:Float = 0; diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx index 685626cbac3..13a7c7395c2 100644 --- a/source/funkin/ui/mainmenu/MainMenuState.hx +++ b/source/funkin/ui/mainmenu/MainMenuState.hx @@ -183,7 +183,9 @@ class MainMenuState extends MusicBeatState FunkinSound.playMusic('freakyMenu', { overrideExisting: true, - restartTrack: false + restartTrack: false, + // Continue playing this music between states, until a different music track gets played. + persist: true }); } diff --git a/source/funkin/ui/options/FunkinSoundTray.hx b/source/funkin/ui/options/FunkinSoundTray.hx index b2fb7fc0443..5a5bf1d6cc1 100644 --- a/source/funkin/ui/options/FunkinSoundTray.hx +++ b/source/funkin/ui/options/FunkinSoundTray.hx @@ -33,6 +33,7 @@ class FunkinSoundTray extends FlxSoundTray var bg:Bitmap = new Bitmap(Assets.getBitmapData(Paths.image("soundtray/volumebox"))); bg.scaleX = graphicScale; bg.scaleY = graphicScale; + bg.smoothing = true; addChild(bg); y = -height; @@ -44,6 +45,7 @@ class FunkinSoundTray extends FlxSoundTray backingBar.y = 5; backingBar.scaleX = graphicScale; backingBar.scaleY = graphicScale; + backingBar.smoothing = true; addChild(backingBar); backingBar.alpha = 0.4; @@ -60,6 +62,7 @@ class FunkinSoundTray extends FlxSoundTray bar.y = 5; bar.scaleX = graphicScale; bar.scaleY = graphicScale; + bar.smoothing = true; addChild(bar); _bars.push(bar); } diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 09af08b22ee..d5a3ce89234 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -242,7 +242,9 @@ class StoryMenuState extends MusicBeatState FunkinSound.playMusic('freakyMenu', { overrideExisting: true, - restartTrack: false + restartTrack: false, + // Continue playing this music between states, until a different music track gets played. + persist: true }); } diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 1b28169367a..839992eea72 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -233,6 +233,7 @@ class TitleState extends MusicBeatState startingVolume: 0.0, overrideExisting: true, restartTrack: false, + // Continue playing this music between states, until a different music track gets played. persist: true }); // Fade from 0.0 to 1 over 4 seconds @@ -270,7 +271,7 @@ class TitleState extends MusicBeatState #if desktop if (FlxG.keys.justPressed.ESCAPE) { - Sys.exit(0); + openfl.Lib.application.window.close(); } #end diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx index 9d256958840..1b39a348271 100644 --- a/source/funkin/ui/transition/preload/FunkinPreloader.hx +++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx @@ -250,18 +250,18 @@ class FunkinPreloader extends FlxBasePreloader dspText.selectable = false; dspText.textColor = 0x000000; dspText.width = this._width; - dspText.height = 20; + dspText.height = 30; dspText.text = 'DSP'; dspText.x = 10; - dspText.y = -5; + dspText.y = -7; box.addChild(dspText); fnfText.selectable = false; fnfText.textColor = 0x000000; fnfText.width = this._width; - fnfText.height = 20; - fnfText.x = 75; - fnfText.y = -5; + fnfText.height = 30; + fnfText.x = 78; + fnfText.y = -7; fnfText.text = 'FNF'; box.addChild(fnfText); diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index 3599e3ace2f..b4e99f1cd50 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -102,6 +102,17 @@ class WindowUtil openfl.Lib.current.stage.addEventListener(openfl.events.KeyboardEvent.KEY_DOWN, (e:openfl.events.KeyboardEvent) -> { for (key in PlayerSettings.player1.controls.getKeysForAction(WINDOW_FULLSCREEN)) { + // FlxG.stage.focus is set to null by the debug console stuff, + // so when that's in focus, we don't want to toggle fullscreen using F + // (annoying when tying "FlxG" in console... lol) + #if FLX_DEBUG + @:privateAccess + if (FlxG.game.debugger.visible) + { + return; + } + #end + if (e.keyCode == key) { openfl.Lib.application.window.fullscreen = !openfl.Lib.application.window.fullscreen; diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index 1b607ddfd6b..4ec578173ea 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -61,6 +61,12 @@ class CrashHandler { trace('Error while handling crash: ' + e); } + + #if sys + Sys.sleep(1); // wait a few moments of margin to process. + // Exit the game. Since it threw an error, we use a non-zero exit code. + openfl.Lib.application.window.close(); + #end } static function onCriticalError(message:String):Void @@ -83,8 +89,9 @@ class CrashHandler } #if sys + Sys.sleep(1); // wait a few moments of margin to process. // Exit the game. Since it threw an error, we use a non-zero exit code. - Sys.exit(1); + openfl.Lib.application.window.close(); #end }