diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000..e8e490865
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,12 @@
+# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
+Documentation:
+- changed-files:
+  - any-glob-to-any-file:
+  - any-glob-to-any-file:
+      - docs/*
+      - '**/*.md'
+
+# Adds Haxe tag to PR's changing haxe code files
+Haxe:
+- changed-files:
+  - any-glob-to-any-file: '**/*.hx'
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 000000000..0bcc420d3
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,14 @@
+name: "Pull Request Labeler"
+on:
+- pull_request_target
+
+jobs:
+  labeler:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/labeler@v5
+      with:
+        sync-labels: true
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 74f72b826..6dc1dc008 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -3,10 +3,17 @@
   "configurations": [
     {
       // Launch in native/CPP on Windows/OSX/Linux
-      "name": "Lime",
+      "name": "Lime Build+Debug",
       "type": "lime",
       "request": "launch"
     },
+    {
+      // Launch in native/CPP on Windows/OSX/Linux
+      "name": "Lime Debug (No Build)",
+      "type": "lime",
+      "request": "launch",
+      "preLaunchTask": null
+    },
     {
       // Launch in browser
       "name": "HTML5 Debug",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a8a67245b..26fe0b042 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -155,6 +155,11 @@
       "target": "hl",
       "args": ["-debug", "-DDIALOGUE"]
     },
+    {
+      "label": "Windows / Debug (Results Screen Test)",
+      "target": "windows",
+      "args": ["-debug", "-DRESULTS"]
+    },
     {
       "label": "Windows / Debug (Straight to Chart Editor)",
       "target": "windows",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a852ca82d..35618dca9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,11 +4,63 @@ 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.4.0] - 2024-06-06
+### Added
+- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
+- Major visual improvements to the Results screen, with additional animations and audio based on your performance.
+- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
+  - Freeplay now plays a preview of songs when you hover over them.
+- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
+  - You can see who charted a song from the Pause menu.
+- Added a new Scroll Speed chart event to change the note speed mid-song (thanks burgerballs!)
+### Changed
+- Tweaked the charts for several songs:
+  - Tutorial (increased the note speed slightly)
+  - Spookeez
+  - Monster
+  - Winter Horrorland
+  - M.I.L.F.
+  - Senpai (increased the note speed)
+  - Roses
+  - Thorns (increased the note speed slightly)
+  - Ugh
+  - Stress
+  - Lit Up
+- Favorite songs marked in Freeplay are now stored between sessions.
+- The Freeplay easter eggs are now easier to see.
+- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
+- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
+- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
+  - Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
+### Fixed
+- Fixed an issue where Nene's visualizer would not play on Desktop builds
+- Fixed a bug where the game would silently fail to load saves on HTML5
+- Fixed some bugs with the props on the Story Menu not bopping properly
+- Improved offsets for Pico and Tankman opponents so they don't slide around as much.
+- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
+- Optimized animation handling for characters (thanks richTrash21!)
+- Made improvements to compiling documentation (thanks gedehari!)
+- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
+- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!)
+- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
+- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
+- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
+- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
+- Improved debug logging for unscripted stages (thanks gamerbross!)
+- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
+- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs (thanks gamerbross!)
+- Fixed a camera bug in the Main Menu (thanks richTrash21!)
+- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
+- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!)
+- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!)
+- Fixed a bug where characters would sometimes use the wrong scale value (thanks PurSnake!)
+- Additional bug fixes and optimizations.
+
 ## [0.3.3] - 2024-05-14
 ### Changed
 - Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
 ### Fixed
-- Fix Web Loading Bar (thanks lemz1!)
+- Fixes to the Loading bar on HTML5 (thanks lemz1!)
 - Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
 - Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
 - Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
@@ -16,11 +68,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
 - Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
 - Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
-- Fix for a game over easter egg so you don't accidentally exit it when viewing
 - Fix a crash when querying FlxG.state in the crash handler
+- Fix for a game over easter egg so you don't accidentally exit it when viewing
 - Fix an issue where the Freeplay menu never displays 100% clear
+- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset.
+- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash
 - Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
-- Hopefully fixed Freeplay crashes on AMD gpu's
+- Fix a crash on Freeplay found on AMD graphics cards
 
 ## [0.3.2] - 2024-05-03
 ### Added
diff --git a/Project.xml b/Project.xml
index 24cdac270..e0e25883d 100644
--- a/Project.xml
+++ b/Project.xml
@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-<project>
+<project xmlns="http://lime.openfl.org/project/1.0.4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+xsi:schemaLocation="http://lime.openfl.org/project/1.0.4 http://lime.openfl.org/xsd/project-1.0.4.xsd">
 	<!-- _________________________ Application Settings _________________________ -->
-	<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.3" company="ninjamuffin99" />
+	<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.4.0" company="ninjamuffin99" />
 	<!--Switch Export with Unique ApplicationID and Icon-->
 	<set name="APP_ID" value="0x0100f6c013bbc000" />
 
@@ -28,7 +29,7 @@
 	<set name="BUILD_DIR" value="export/debug" if="debug" />
 	<set name="BUILD_DIR" value="export/release" unless="debug" />
 	<set name="BUILD_DIR" value="export/32bit" if="32bit" />
-	<classpath name="source" />
+	<source path="source" />
 	<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
 	<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
 	<define name="PRELOAD_ALL" unless="web" />
@@ -125,9 +126,12 @@
 	<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
 	<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
 	<haxelib name="funkin.vis"/>
+	<haxelib name="grig.audio" />
 
+	<haxelib name="FlxPartialSound" /> <!-- Loading partial sound data -->
 
 	<haxelib name="json2object" /> <!-- JSON parsing -->
+	<haxelib name="thx.core" /> <!-- General utility library, "the lodash of Haxe" -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
 
 	<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
diff --git a/README.md b/README.md
index 62794b924..b1e16f6de 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # Friday Night Funkin' 
 
-Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludem Dare 47.
+Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
 
-This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
+This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
 
 - [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
 - [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
@@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is.
 
 ## Programming
 - [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
-- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer
+- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
 - [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
 - [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
 - Our contributors on GitHub
diff --git a/assets b/assets
index 962130b22..3b8235e95 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 962130b2243a839106607d08a11599b1857bf8b3
+Subproject commit 3b8235e953505a6fe7f4ff253f5a99b9a7b9857a
diff --git a/checkstyle.json b/checkstyle.json
index dc89409da..41f0a7998 100644
--- a/checkstyle.json
+++ b/checkstyle.json
@@ -79,7 +79,7 @@
     {
       "props": {
         "ignoreExtern": true,
-        "format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
+        "format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$",
         "tokens": ["INLINE", "NOTINLINE"]
       },
       "type": "ConstantName"
diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index 7278e027c..e4bd8d7dd 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -6,9 +6,10 @@
     - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
     - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
 2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
-3. Install all haxelibs of the current branch by running `hmm install`
-4. Setup lime: `haxelib run lime setup`
-5. Platform setup
+3. Download Git from [git-scm.com](https://www.git-scm.com)
+4. Install all haxelibs of the current branch by running `hmm install`
+5. Setup lime: `haxelib run lime setup`
+6. Platform setup
    - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
         - When prompted, select "Individual Components" and make sure to download the following:
         - MSVC v143 VS 2022 C++ x64/x86 build tools
@@ -16,5 +17,9 @@
     - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
     - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
     - HTML5: Compiles without any extra setup
-6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
-7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
+7. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
+8. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
+
+# Troubleshooting
+
+- During the cloning process, you may experience an error along the lines of `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)` due to poor connectivity. A common fix is to run ` git config --global http.postBuffer 4096M`.
\ No newline at end of file
diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json
index e0b03f1cd..4dc0cd804 100644
--- a/example_mods/introMod/_polymod_meta.json
+++ b/example_mods/introMod/_polymod_meta.json
@@ -3,7 +3,7 @@
   "description": "An introductory mod.",
   "contributors": [
     {
-      "name": "MasterEric"
+      "name": "EliteMasterEric"
     }
   ],
   "api_version": "0.1.0",
diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json
index 4c0f177f9..0a2ed042c 100644
--- a/example_mods/testing123/_polymod_meta.json
+++ b/example_mods/testing123/_polymod_meta.json
@@ -3,7 +3,7 @@
   "description": "Newgrounds? More like OLDGROUNDS lol.",
   "contributors": [
     {
-      "name": "MasterEric"
+      "name": "EliteMasterEric"
     }
   ],
   "api_version": "0.1.0",
diff --git a/hmm.json b/hmm.json
index a6e4467a9..68e0c5cb0 100644
--- a/hmm.json
+++ b/hmm.json
@@ -40,6 +40,13 @@
       "ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49",
       "url": "https://github.com/FunkinCrew/flxanimate"
     },
+    {
+      "name": "FlxPartialSound",
+      "type": "git",
+      "dir": null,
+      "ref": "f986332ba5ab02abd386ce662578baf04904604a",
+      "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
+    },
     {
       "name": "format",
       "type": "haxelib",
@@ -49,9 +56,16 @@
       "name": "funkin.vis",
       "type": "git",
       "dir": null,
-      "ref": "2aa654b974507ab51ab1724d2d97e75726fd7d78",
+      "ref": "38261833590773cb1de34ac5d11e0825696fc340",
       "url": "https://github.com/FunkinCrew/funkVis"
     },
+    {
+      "name": "grig.audio",
+      "type": "git",
+      "dir": "src",
+      "ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
+      "url": "https://gitlab.com/haxe-grig/grig.audio.git"
+    },
     {
       "name": "hamcrest",
       "type": "haxelib",
@@ -80,7 +94,7 @@
       "name": "hxCodec",
       "type": "git",
       "dir": null,
-      "ref": "c0c7f2680cc190c932a549c2e2fdd9b0ba2bd10e",
+      "ref": "61b98a7a353b7f529a8fec84ed9afc919a2dffdd",
       "url": "https://github.com/FunkinCrew/hxCodec"
     },
     {
@@ -153,7 +167,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
+      "ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 0d63cb6cc..e73b2860c 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -430,7 +430,7 @@ class Conductor
     else if (currentTimeChange != null && this.songPosition > 0.0)
     {
       // roundDecimal prevents representing 8 as 7.9999999
-      this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
+      this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
       this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
       this.currentMeasureTime = currentStepTime / stepsPerMeasure;
       this.currentStep = Math.floor(currentStepTime);
@@ -564,7 +564,7 @@ class Conductor
         if (ms >= timeChange.timeStamp)
         {
           lastTimeChange = timeChange;
-          resultStep = lastTimeChange.beatTime * 4;
+          resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
         }
         else
         {
@@ -600,7 +600,7 @@ class Conductor
       var lastTimeChange:SongTimeChange = timeChanges[0];
       for (timeChange in timeChanges)
       {
-        if (stepTime >= timeChange.beatTime * 4)
+        if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
         {
           lastTimeChange = timeChange;
           resultMs = lastTimeChange.timeStamp;
@@ -613,7 +613,7 @@ class Conductor
       }
 
       var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
-      resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
+      resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
 
       return resultMs;
     }
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 00d34fadb..49b15ddf6 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -214,6 +214,32 @@ class InitState extends FlxState
     #elseif STAGEBUILD
     // -DSTAGEBUILD
     FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
+    #elseif RESULTS
+    // -DRESULTS
+    FlxG.switchState(() -> new funkin.play.ResultState(
+      {
+        storyMode: false,
+        title: "Cum Song Erect by Kawai Sprite",
+        songId: "cum",
+        difficultyId: "nightmare",
+        isNewHighscore: true,
+        scoreData:
+          {
+            score: 1_234_567,
+            tallies:
+              {
+                sick: 130,
+                good: 60,
+                bad: 69,
+                shit: 69,
+                missed: 69,
+                combo: 69,
+                maxCombo: 69,
+                totalNotesHit: 140,
+                totalNotes: 200 // 0,
+              }
+          },
+      }));
     #elseif ANIMDEBUG
     // -DANIMDEBUG
     FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index 54a4b7acf..b0a97c4fa 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -123,9 +123,17 @@ class Paths
     return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
   }
 
-  public static function inst(song:String, ?suffix:String = ''):String
+  /**
+   * Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/
+   * @param song name of the song to get instrumental for
+   * @param suffix any suffix to add to end of song name, used for `-erect` variants usually
+   * @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
+   * @return String
+   */
+  public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
   {
-    return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
+    var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
+    return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
   }
 
   public static function image(key:String, ?library:String):String
@@ -153,3 +161,11 @@ class Paths
     return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
   }
 }
+
+enum abstract PathsFunction(String)
+{
+  var MUSIC;
+  var INST;
+  var VOICES;
+  var SOUND;
+}
diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx
index 9616dfe18..77e44bd1d 100644
--- a/source/funkin/api/newgrounds/NGUnsafe.hx
+++ b/source/funkin/api/newgrounds/NGUnsafe.hx
@@ -1,9 +1,5 @@
 package funkin.api.newgrounds;
 
-import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
-import lime.app.Application;
-import openfl.display.Stage;
 #if newgrounds
 import io.newgrounds.NG;
 import io.newgrounds.NGLite;
diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx
index c1f8ad3ba..3f5fc078a 100644
--- a/source/funkin/api/newgrounds/NGio.hx
+++ b/source/funkin/api/newgrounds/NGio.hx
@@ -2,19 +2,11 @@ package funkin.api.newgrounds;
 
 #if newgrounds
 import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
 import io.newgrounds.NG;
 import io.newgrounds.NGLite;
-import io.newgrounds.components.ScoreBoardComponent.Period;
 import io.newgrounds.objects.Error;
-import io.newgrounds.objects.Medal;
 import io.newgrounds.objects.Score;
-import io.newgrounds.objects.ScoreBoard;
-import io.newgrounds.objects.events.Response;
-import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
-import io.newgrounds.objects.events.Result.GetVersionResult;
 import lime.app.Application;
-import openfl.display.Stage;
 #end
 
 /**
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index df05cc3ef..4f61e70c2 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -11,10 +11,14 @@ import funkin.audio.waveform.WaveformDataParser;
 import funkin.data.song.SongData.SongMusicData;
 import funkin.data.song.SongRegistry;
 import funkin.util.tools.ICloneable;
+import funkin.util.flixel.sound.FlxPartialSound;
+import funkin.Paths.PathsFunction;
 import openfl.Assets;
+import lime.app.Future;
+import lime.app.Promise;
 import openfl.media.SoundMixer;
+
 #if (openfl >= "8.0.0")
-import openfl.utils.AssetType;
 #end
 
 /**
@@ -342,23 +346,76 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
         FlxG.log.warn('Tried and failed to find music metadata for $key');
       }
     }
-
-    var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
-    if (music != null)
+    var pathsFunction = params.pathsFunction ?? MUSIC;
+    var suffix = params.suffix ?? '';
+    var pathToUse = switch (pathsFunction)
     {
-      FlxG.sound.music = music;
+      case MUSIC: Paths.music('$key/$key');
+      case INST: Paths.inst('$key', suffix);
+      default: Paths.music('$key/$key');
+    }
 
-      // Prevent repeat update() and onFocus() calls.
-      FlxG.sound.list.remove(FlxG.sound.music);
+    var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
 
-      return true;
+    // even if we arent' trying to partial load a song, we want to error out any songs in progress,
+    // so we don't get overlapping music if someone were to load a new song while a partial one is loading!
+
+    emptyPartialQueue();
+
+    if (shouldLoadPartial)
+    {
+      var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0,
+        params.loop ?? true, false, false, params.onComplete);
+
+      if (music != null)
+      {
+        partialQueue.push(music);
+
+        @:nullSafety(Off)
+        music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
+          FlxG.sound.music = partialMusic;
+          FlxG.sound.list.remove(FlxG.sound.music);
+
+          if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
+        });
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
     else
     {
-      return false;
+      var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
+      if (music != null)
+      {
+        FlxG.sound.music = music;
+
+        // Prevent repeat update() and onFocus() calls.
+        FlxG.sound.list.remove(FlxG.sound.music);
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
     }
   }
 
+  public static function emptyPartialQueue():Void
+  {
+    while (partialQueue.length > 0)
+    {
+      @:nullSafety(Off)
+      partialQueue.pop().error("Cancel loading partial sound");
+    }
+  }
+
+  static var partialQueue:Array<Promise<Null<FunkinSound>>> = [];
+
   /**
    * Creates a new `FunkinSound` object synchronously.
    *
@@ -415,6 +472,49 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
     return sound;
   }
 
+  /**
+   * Will load a section of a sound file, useful for Freeplay where we don't want to load all the bytes of a song
+   * @param path The path to the sound file
+   * @param start The start time of the sound file
+   * @param end The end time of the sound file
+   * @param volume Volume to start at
+   * @param looped Whether the sound file should loop
+   * @param autoDestroy Whether the sound file should be destroyed after it finishes playing
+   * @param autoPlay Whether the sound file should play immediately
+   * @param onComplete Callback when the sound finishes playing
+   * @param onLoad Callback when the sound finishes loading
+   * @return A FunkinSound object
+   */
+  public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,
+      autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Promise<Null<FunkinSound>>
+  {
+    var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
+
+    // split the path and get only after first :
+    // we are bypassing the openfl/lime asset library fuss
+    path = Paths.stripLibrary(path);
+
+    var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
+
+    if (soundRequest == null)
+    {
+      promise.complete(null);
+    }
+    else
+    {
+      promise.future.onError(function(e) {
+        soundRequest.error("Sound loading was errored or cancelled");
+      });
+
+      soundRequest.future.onComplete(function(partialSound) {
+        var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
+        promise.complete(snd);
+      });
+    }
+
+    return promise;
+  }
+
   @:nullSafety(Off)
   public override function destroy():Void
   {
@@ -475,6 +575,12 @@ typedef FunkinSoundPlayMusicParams =
    */
   var ?startingVolume:Float;
 
+  /**
+   * The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file
+   * @default ``
+   */
+  var ?suffix:String;
+
   /**
    * Whether to override music if a different track is already playing.
    * @default `false`
@@ -498,4 +604,22 @@ typedef FunkinSoundPlayMusicParams =
    * @default `true`
    */
   var ?mapTimeChanges:Bool;
+
+  /**
+   * Which Paths function to use to load a song
+   * @default `MUSIC`
+   */
+  var ?pathsFunction:PathsFunction;
+
+  var ?partialParams:PartialSoundParams;
+
+  var ?onComplete:Void->Void;
+  var ?onLoad:Void->Void;
+}
+
+typedef PartialSoundParams =
+{
+  var loadPartial:Bool;
+  var start:Float;
+  var end:Float;
 }
diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx
index 5037ee1d0..9a1e0e0c1 100644
--- a/source/funkin/audio/VoicesGroup.hx
+++ b/source/funkin/audio/VoicesGroup.hx
@@ -1,9 +1,7 @@
 package funkin.audio;
 
-import funkin.audio.FunkinSound;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
 
 class VoicesGroup extends SoundGroup
 {
diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx
index ca77dd58a..1b0463144 100644
--- a/source/funkin/audio/visualize/ABotVis.hx
+++ b/source/funkin/audio/visualize/ABotVis.hx
@@ -1,13 +1,9 @@
 package funkin.audio.visualize;
 
-import funkin.audio.visualize.dsp.FFT;
 import flixel.FlxSprite;
-import flixel.addons.plugin.taskManager.FlxTask;
 import flixel.graphics.frames.FlxAtlasFrames;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
-import flixel.math.FlxMath;
 import flixel.sound.FlxSound;
-import funkin.util.MathUtil;
 import funkin.vis.dsp.SpectralAnalyzer;
 import funkin.vis.audioclip.frontends.LimeAudioClip;
 
@@ -58,8 +54,15 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
   public function initAnalyzer()
   {
     @:privateAccess
-    analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30);
-    analyzer.maxDb = -35;
+    analyzer = new SpectralAnalyzer(snd._channel.__source, 7, 0.1, 30);
+
+    #if desktop
+    // On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
+    // So we want to manually change it!
+    analyzer.fftN = 512;
+    #end
+
+    // analyzer.maxDb = -35;
     // analyzer.fftN = 2048;
   }
 
@@ -83,9 +86,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
 
   override function draw()
   {
-    #if web
     if (analyzer != null) drawFFT();
-    #end
     super.draw();
   }
 
@@ -94,7 +95,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
    */
   function drawFFT():Void
   {
-    var levels = analyzer.getLevels(false);
+    var levels = analyzer.getLevels();
 
     for (i in 0...min(group.members.length, levels.length))
     {
diff --git a/source/funkin/audio/visualize/PolygonVisGroup.hx b/source/funkin/audio/visualize/PolygonVisGroup.hx
index cc68f4ae0..bff845796 100644
--- a/source/funkin/audio/visualize/PolygonVisGroup.hx
+++ b/source/funkin/audio/visualize/PolygonVisGroup.hx
@@ -1,6 +1,5 @@
 package funkin.audio.visualize;
 
-import funkin.audio.visualize.PolygonSpectogram;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.sound.FlxSound;
 
diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx
index 636c0726a..615e80d95 100644
--- a/source/funkin/audio/visualize/SpectogramSprite.hx
+++ b/source/funkin/audio/visualize/SpectogramSprite.hx
@@ -8,8 +8,6 @@ import flixel.sound.FlxSound;
 import flixel.util.FlxColor;
 import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
 import funkin.audio.visualize.VisShit.CurAudioInfo;
-import funkin.audio.visualize.dsp.FFT;
-import lime.system.ThreadPool;
 import lime.utils.Int16Array;
 
 using Lambda;
@@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
     lengthOfShit = amnt;
 
     regenLineShit();
-
-    // makeGraphic(200, 200, FlxColor.BLACK);
   }
 
   public function regenLineShit():Void
@@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
   {
     checkAndSetBuffer();
 
-    // vis.checkAndSetBuffer();
-
     if (setBuffer)
     {
       var samplesToGen:Int = Std.int(sampleRate * seconds);
@@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
           // a value between 10hz and 100Khz
           var hzPicker:Float = Math.pow(10, powedShit);
 
-          // var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen));
           var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
 
           group.members[i].x = prevLine.x;
@@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
           var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
 
           // dont draw a line until i figure out a nicer way to view da spikes and shit idk lol!
-          // group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
-          // group.members[i].angle = line.degrees;
         }
       }
     }
@@ -261,9 +252,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
 
           group.members[Std.int(remappedSample)].x = prevLine.x;
           group.members[Std.int(remappedSample)].y = prevLine.y;
-          // group.members[0].y = prevLine.y;
-
-          // FlxSpriteUtil.drawLine(this, prevLine.x, prevLine.y, width * remappedSample, left * height / 2 + height / 2);
           prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
           prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;
 
diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx
index 204ced1e1..ba235fe89 100644
--- a/source/funkin/audio/visualize/VisShit.hx
+++ b/source/funkin/audio/visualize/VisShit.hx
@@ -3,7 +3,6 @@ package funkin.audio.visualize;
 import flixel.math.FlxMath;
 import flixel.sound.FlxSound;
 import funkin.audio.visualize.dsp.FFT;
-import lime.system.ThreadPool;
 import lime.utils.Int16Array;
 import funkin.util.MathUtil;
 
@@ -73,9 +72,6 @@ class VisShit
 
       freqOutput.push([]);
 
-      // if (FlxG.keys.justPressed.M)
-      // trace(FFT.rfft(chunk).map(z -> z.scale(1 / fs).magnitude));
-
       // find spectral peaks and their instantaneous frequencies
       for (k => s in freqs)
       {
@@ -91,7 +87,6 @@ class VisShit
         if (freq < maxFreq) freqOutput[indexOfArray].push(power);
         //
       }
-      // haxe.Log.trace("", null);
 
       indexOfArray++;
       // move to next (overlapping) chunk
diff --git a/source/funkin/audio/visualize/dsp/FFT.hx b/source/funkin/audio/visualize/dsp/FFT.hx
index dc75acb81..40ee9cb8c 100644
--- a/source/funkin/audio/visualize/dsp/FFT.hx
+++ b/source/funkin/audio/visualize/dsp/FFT.hx
@@ -1,7 +1,5 @@
 package funkin.audio.visualize.dsp;
 
-import funkin.audio.visualize.dsp.Complex;
-
 using funkin.audio.visualize.dsp.OffsetArray;
 using funkin.audio.visualize.dsp.Signal;
 
diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx
index 1f649b472..a939f91bf 100644
--- a/source/funkin/audio/waveform/WaveformData.hx
+++ b/source/funkin/audio/waveform/WaveformData.hx
@@ -1,7 +1,5 @@
 package funkin.audio.waveform;
 
-import funkin.util.MathUtil;
-
 @:nullSafety
 class WaveformData
 {
diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx
index 32ced2fbd..8eaba8117 100644
--- a/source/funkin/audio/waveform/WaveformSprite.hx
+++ b/source/funkin/audio/waveform/WaveformSprite.hx
@@ -1,7 +1,5 @@
 package funkin.audio.waveform;
 
-import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
 import funkin.graphics.rendering.MeshRender;
 import flixel.util.FlxColor;
 
diff --git a/source/funkin/data/dialogue/conversation/ConversationData.hx b/source/funkin/data/dialogue/conversation/ConversationData.hx
index 30e3f451b..650519836 100644
--- a/source/funkin/data/dialogue/conversation/ConversationData.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationData.hx
@@ -1,7 +1,5 @@
 package funkin.data.dialogue.conversation;
 
-import funkin.data.animation.AnimationData;
-
 /**
  * A type definition for the data for a specific conversation.
  * It includes things like what dialogue boxes to use, what text to display, and what animations to play.
diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
index ca072897f..fad1e43ad 100644
--- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
@@ -1,7 +1,6 @@
 package funkin.data.dialogue.conversation;
 
 import funkin.play.cutscene.dialogue.Conversation;
-import funkin.data.dialogue.conversation.ConversationData;
 import funkin.play.cutscene.dialogue.ScriptedConversation;
 
 class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md
index 3cd3af070..4f1c66ade 100644
--- a/source/funkin/data/song/CHANGELOG.md
+++ b/source/funkin/data/song/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to this project 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).
 
+## [2.2.3]
+### Added
+- Added `charter` field to denote authorship of a chart.
+
 ## [2.2.2]
 ### Added
 - Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay.
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 26380947a..769af8f08 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -30,6 +30,9 @@ class SongMetadata implements ICloneable<SongMetadata>
   @:default("Unknown")
   public var artist:String;
 
+  @:optional
+  public var charter:Null<String> = null;
+
   @:optional
   @:default(96)
   public var divisions:Null<Int>; // Optional field
@@ -53,6 +56,8 @@ class SongMetadata implements ICloneable<SongMetadata>
   @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
   public var generatedBy:String;
 
+  @:optional
+  @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
   public var timeFormat:SongTimeFormat;
 
   public var timeChanges:Array<SongTimeChange>;
@@ -112,14 +117,23 @@ class SongMetadata implements ICloneable<SongMetadata>
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var ignoreNullOptionals = true;
     var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
-    // I believe @:jignored should be iggnored by the writer?
+    // I believe @:jignored should be ignored by the writer?
     // var output = this.clone();
     // output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_METADATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   /**
    * Produces a string representation suitable for debugging.
    */
@@ -368,6 +382,12 @@ class SongMusicData implements ICloneable<SongMusicData>
     this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_MUSIC_DATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   public function clone():SongMusicData
   {
     var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
@@ -600,11 +620,20 @@ class SongChartData implements ICloneable<SongChartData>
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var ignoreNullOptionals = true;
     var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = SongRegistry.SONG_CHART_DATA_VERSION;
+    this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+  }
+
   public function clone():SongChartData
   {
     // We have to manually perform the deep clone here because Map.deepClone() doesn't work.
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index 277dcd9e1..a3305c4ec 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
    * Handle breaking changes by incrementing this value
    * and adding migration to the `migrateStageData()` function.
    */
-  public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2";
+  public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.3";
 
   public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
 
diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx
index dd0d28479..04b5a1b69 100644
--- a/source/funkin/data/song/importer/ChartManifestData.hx
+++ b/source/funkin/data/song/importer/ChartManifestData.hx
@@ -61,10 +61,18 @@ class ChartManifestData
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var writer = new json2object.JsonWriter<ChartManifestData>();
     return writer.write(this, pretty ? '  ' : null);
   }
 
+  public function updateVersionToLatest():Void
+  {
+    this.version = CHART_MANIFEST_DATA_VERSION;
+  }
+
   public static function deserialize(contents:String):Null<ChartManifestData>
   {
     var parser = new json2object.JsonParser<ChartManifestData>();
diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ab2abda8e..acbb99342 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -36,7 +36,7 @@ class FNFLegacyImporter
   {
     trace('Migrating song metadata from FNF Legacy.');
 
-    var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
+    var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
 
     var hadError:Bool = false;
 
@@ -65,7 +65,7 @@ class FNFLegacyImporter
 
     songMetadata.timeChanges = rebuildTimeChanges(songData);
 
-    songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
+    songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
 
     return songMetadata;
   }
diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx
index 22b883c75..bebd86d02 100644
--- a/source/funkin/data/stage/StageData.hx
+++ b/source/funkin/data/stage/StageData.hx
@@ -58,9 +58,17 @@ class StageData
    */
   public function serialize(pretty:Bool = true):String
   {
+    // Update generatedBy and version before writing.
+    updateVersionToLatest();
+
     var writer = new json2object.JsonWriter<StageData>();
     return writer.write(this, pretty ? '  ' : null);
   }
+
+  public function updateVersionToLatest():Void
+  {
+    this.version = StageRegistry.STAGE_DATA_VERSION;
+  }
 }
 
 typedef StageDataCharacters =
diff --git a/source/funkin/effects/IntervalShake.hx b/source/funkin/effects/IntervalShake.hx
new file mode 100644
index 000000000..545739cc3
--- /dev/null
+++ b/source/funkin/effects/IntervalShake.hx
@@ -0,0 +1,240 @@
+package funkin.effects;
+
+import flixel.FlxObject;
+import flixel.util.FlxDestroyUtil.IFlxDestroyable;
+import flixel.util.FlxPool;
+import flixel.util.FlxTimer;
+import flixel.math.FlxPoint;
+import flixel.util.FlxAxes;
+import flixel.tweens.FlxEase.EaseFunction;
+import flixel.math.FlxMath;
+
+/**
+ * pretty much a copy of FlxFlicker geared towards making sprites
+ * shake around at a set interval and slow down over time.
+ */
+class IntervalShake implements IFlxDestroyable
+{
+  static var _pool:FlxPool<IntervalShake> = new FlxPool<IntervalShake>(IntervalShake.new);
+
+  /**
+   * Internal map for looking up which objects are currently shaking and getting their shake data.
+   */
+  static var _boundObjects:Map<FlxObject, IntervalShake> = new Map<FlxObject, IntervalShake>();
+
+  /**
+   * An effect that shakes the sprite on a set interval and a starting intensity that goes down over time.
+   *
+   * @param   Object               The object to shake.
+   * @param   Duration             How long to shake for (in seconds). `0` means "forever".
+   * @param   Interval             In what interval to update the shake position. Set to `FlxG.elapsed` if `<= 0`!
+   * @param   StartIntensity       The starting intensity of the shake.
+   * @param   EndIntensity         The ending intensity of the shake.
+   * @param   Ease                 Control the easing of the intensity over the shake.
+   * @param   CompletionCallback   Callback on shake completion
+   * @param   ProgressCallback     Callback on each shake interval
+   * @return The `IntervalShake` object. `IntervalShake`s are pooled internally, so beware of storing references.
+   */
+  public static function shake(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0,
+      Ease:EaseFunction, ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):IntervalShake
+  {
+    if (isShaking(Object))
+    {
+      // if (ForceRestart)
+      // {
+      //   stopShaking(Object);
+      // }
+      // else
+      // {
+      // Ignore this call if object is already flickering.
+      return _boundObjects[Object];
+      // }
+    }
+
+    if (Interval <= 0)
+    {
+      Interval = FlxG.elapsed;
+    }
+
+    var shake:IntervalShake = _pool.get();
+    shake.start(Object, Duration, Interval, StartIntensity, EndIntensity, Ease, CompletionCallback, ProgressCallback);
+    return _boundObjects[Object] = shake;
+  }
+
+  /**
+   * Returns whether the object is shaking or not.
+   *
+   * @param   Object The object to test.
+   */
+  public static function isShaking(Object:FlxObject):Bool
+  {
+    return _boundObjects.exists(Object);
+  }
+
+  /**
+   * Stops shaking the object.
+   *
+   * @param   Object The object to stop shaking.
+   */
+  public static function stopShaking(Object:FlxObject):Void
+  {
+    var boundShake:IntervalShake = _boundObjects[Object];
+    if (boundShake != null)
+    {
+      boundShake.stop();
+    }
+  }
+
+  /**
+   * The shaking object.
+   */
+  public var object(default, null):FlxObject;
+
+  /**
+   * The shaking timer. You can check how many seconds has passed since shaking started etc.
+   */
+  public var timer(default, null):FlxTimer;
+
+  /**
+   * The starting intensity of the shake.
+   */
+  public var startIntensity(default, null):Float;
+
+  /**
+   * The ending intensity of the shake.
+   */
+  public var endIntensity(default, null):Float;
+
+  /**
+   * How long to shake for (in seconds). `0` means "forever".
+   */
+  public var duration(default, null):Float;
+
+  /**
+   * The interval of the shake.
+   */
+  public var interval(default, null):Float;
+
+  /**
+   * Defines on what axes to `shake()`. Default value is `XY` / both.
+   */
+  public var axes(default, null):FlxAxes;
+
+  /**
+   * Defines the initial position of the object at the beginning of the shake effect.
+   */
+  public var initialOffset(default, null):FlxPoint;
+
+  /**
+   * The callback that will be triggered after the shake has completed.
+   */
+  public var completionCallback(default, null):IntervalShake->Void;
+
+  /**
+   * The callback that will be triggered every time the object shakes.
+   */
+  public var progressCallback(default, null):IntervalShake->Void;
+
+  /**
+   * The easing of the intensity over the shake.
+   */
+  public var ease(default, null):EaseFunction;
+
+  /**
+   * Nullifies the references to prepare object for reuse and avoid memory leaks.
+   */
+  public function destroy():Void
+  {
+    object = null;
+    timer = null;
+    ease = null;
+    completionCallback = null;
+    progressCallback = null;
+  }
+
+  /**
+   * Starts shaking behavior.
+   */
+  function start(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0, Ease:EaseFunction,
+      ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):Void
+  {
+    object = Object;
+    duration = Duration;
+    interval = Interval;
+    completionCallback = CompletionCallback;
+    startIntensity = StartIntensity;
+    endIntensity = EndIntensity;
+    initialOffset = new FlxPoint(Object.x, Object.y);
+    ease = Ease;
+    axes = FlxAxes.XY;
+    _secondsSinceStart = 0;
+    timer = new FlxTimer().start(interval, shakeProgress, Std.int(duration / interval));
+  }
+
+  /**
+   * Prematurely ends shaking.
+   */
+  public function stop():Void
+  {
+    timer.cancel();
+    // object.visible = true;
+    object.x = initialOffset.x;
+    object.y = initialOffset.y;
+    release();
+  }
+
+  /**
+   * Unbinds the object from shaking and releases it into pool for reuse.
+   */
+  function release():Void
+  {
+    _boundObjects.remove(object);
+    _pool.put(this);
+  }
+
+  public var _secondsSinceStart(default, null):Float = 0;
+
+  public var scale(default, null):Float = 0;
+
+  /**
+   * Just a helper function for shake() to update object's position.
+   */
+  function shakeProgress(timer:FlxTimer):Void
+  {
+    _secondsSinceStart += interval;
+    scale = _secondsSinceStart / duration;
+    if (ease != null)
+    {
+      scale = 1 - ease(scale);
+      // trace(scale);
+    }
+
+    var curIntensity:Float = 0;
+    curIntensity = FlxMath.lerp(endIntensity, startIntensity, scale);
+
+    if (axes.x) object.x = initialOffset.x + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width);
+    if (axes.y) object.y = initialOffset.y + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width);
+
+    // object.visible = !object.visible;
+
+    if (progressCallback != null) progressCallback(this);
+
+    if (timer.loops > 0 && timer.loopsLeft == 0)
+    {
+      object.x = initialOffset.x;
+      object.y = initialOffset.y;
+      if (completionCallback != null)
+      {
+        completionCallback(this);
+      }
+
+      if (this.timer == timer) release();
+    }
+  }
+
+  /**
+   * Internal constructor. Use static methods.
+   */
+  @:keep
+  function new() {}
+}
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 250de99cb..c8431be33 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker;
 // These are great.
 using Lambda;
 using StringTools;
+using thx.Arrays;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
 using funkin.util.tools.FloatTools;
diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index 548e4edfa..345791eef 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -715,7 +715,7 @@ class Controls extends FlxActionSet
           case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
           case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
           case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
-          case Control.FULLSCREEN: return [FlxKey.F];
+          case Control.FULLSCREEN: return [FlxKey.F11]; // We use F for other things LOL.
 
         }
       case Duo(true):
@@ -997,7 +997,7 @@ class Controls extends FlxActionSet
     for (control in Control.createAll())
     {
       var inputs:Array<Int> = Reflect.field(data, control.getName());
-      inputs = inputs.unique();
+      inputs = inputs.distinct();
       if (inputs != null)
       {
         if (inputs.length == 0) {
@@ -1050,7 +1050,7 @@ class Controls extends FlxActionSet
       if (inputs.length == 0) {
         inputs = [FlxKey.NONE];
       } else {
-        inputs = inputs.unique();
+        inputs = inputs.distinct();
       }
 
       Reflect.setField(data, control.getName(), inputs);
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 8d625290d..dd55de23b 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -140,16 +140,36 @@ class HitNoteScriptEvent extends NoteScriptEvent
    */
   public var score:Int;
 
-  public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void
+  /**
+   * If the hit causes a combo break.
+   */
+  public var isComboBreak:Bool = false;
+
+  /**
+   * The time difference when the player hit the note
+   */
+  public var hitDiff:Float = 0;
+
+  /**
+   * If the hit causes a notesplash
+   */
+  public var doesNotesplash:Bool = false;
+
+  public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
+      doesNotesplash:Bool = false):Void
   {
     super(NOTE_HIT, note, healthChange, comboCount, true);
     this.score = score;
     this.judgement = judgement;
+    this.isComboBreak = isComboBreak;
+    this.doesNotesplash = doesNotesplash;
+    this.hitDiff = hitDiff;
   }
 
   public override function toString():String
   {
-    return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')';
+    return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ', isComboBreak='
+      + isComboBreak + ', hitDiff=' + hitDiff + ', doesNotesplash=' + doesNotesplash + ')';
   }
 }
 
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index c3abbcf3e..4d50d75cc 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -162,6 +162,8 @@ class GameOverSubState extends MusicBeatSubState
   @:nullSafety(Off)
   function setCameraTarget():Void
   {
+    if (PlayState.instance.isMinimalMode || boyfriend == null) return;
+
     // 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;
@@ -254,6 +256,7 @@ class GameOverSubState extends MusicBeatSubState
         this.close();
         if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
         PlayState.instance.close(); // This only works because PlayState is a substate!
+        return;
       }
       else if (PlayStatePlaylist.isStoryMode)
       {
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index fb9d9b4e2..8c45fac65 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -101,6 +101,10 @@ class PauseSubState extends MusicBeatSubState
    */
   static final MUSIC_FINAL_VOLUME:Float = 0.75;
 
+  static final CHARTER_FADE_DELAY:Float = 15.0;
+
+  static final CHARTER_FADE_DURATION:Float = 0.75;
+
   /**
    * Defines which pause music to use.
    */
@@ -163,6 +167,12 @@ class PauseSubState extends MusicBeatSubState
    */
   var metadataDeaths:FlxText;
 
+  /**
+   * A text object which displays the current song's artist.
+   * Fades to the charter after a period before fading back.
+   */
+  var metadataArtist:FlxText;
+
   /**
    * The actual text objects for the menu entries.
    */
@@ -203,6 +213,8 @@ class PauseSubState extends MusicBeatSubState
     regenerateMenu();
 
     transitionIn();
+
+    startCharterTimer();
   }
 
   /**
@@ -222,6 +234,8 @@ class PauseSubState extends MusicBeatSubState
   public override function destroy():Void
   {
     super.destroy();
+    charterFadeTween.cancel();
+    charterFadeTween = null;
     pauseMusic.stop();
   }
 
@@ -270,16 +284,25 @@ class PauseSubState extends MusicBeatSubState
     metadata.scrollFactor.set(0, 0);
     add(metadata);
 
-    var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist');
+    var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name');
     metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     if (PlayState.instance?.currentChart != null)
     {
-      metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
+      metadataSong.text = '${PlayState.instance.currentChart.songName}';
     }
     metadataSong.scrollFactor.set(0, 0);
     metadata.add(metadataSong);
 
-    var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: ');
+    metadataArtist = new FlxText(20, metadataSong.y + 32, FlxG.width - 40, 'Artist: ${Constants.DEFAULT_ARTIST}');
+    metadataArtist.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+    if (PlayState.instance?.currentChart != null)
+    {
+      metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+    }
+    metadataArtist.scrollFactor.set(0, 0);
+    metadata.add(metadataArtist);
+
+    var metadataDifficulty:FlxText = new FlxText(20, metadataArtist.y + 32, FlxG.width - 40, 'Difficulty: ');
     metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     if (PlayState.instance?.currentDifficulty != null)
     {
@@ -288,12 +311,12 @@ class PauseSubState extends MusicBeatSubState
     metadataDifficulty.scrollFactor.set(0, 0);
     metadata.add(metadataDifficulty);
 
-    metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
+    metadataDeaths = new FlxText(20, metadataDifficulty.y + 32, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
     metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     metadataDeaths.scrollFactor.set(0, 0);
     metadata.add(metadataDeaths);
 
-    metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE');
+    metadataPractice = new FlxText(20, metadataDeaths.y + 32, FlxG.width - 40, 'PRACTICE MODE');
     metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
     metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
     metadataPractice.scrollFactor.set(0, 0);
@@ -302,6 +325,62 @@ class PauseSubState extends MusicBeatSubState
     updateMetadataText();
   }
 
+  var charterFadeTween:Null<FlxTween> = null;
+
+  function startCharterTimer():Void
+  {
+    charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+      {
+        startDelay: CHARTER_FADE_DELAY,
+        ease: FlxEase.quartOut,
+        onComplete: (_) -> {
+          if (PlayState.instance?.currentChart != null)
+          {
+            metadataArtist.text = 'Charter: ${PlayState.instance.currentChart.charter ?? 'Unknown'}';
+          }
+          else
+          {
+            metadataArtist.text = 'Charter: ${Constants.DEFAULT_CHARTER}';
+          }
+
+          FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+            {
+              ease: FlxEase.quartOut,
+              onComplete: (_) -> {
+                startArtistTimer();
+              }
+            });
+        }
+      });
+  }
+
+  function startArtistTimer():Void
+  {
+    charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+      {
+        startDelay: CHARTER_FADE_DELAY,
+        ease: FlxEase.quartOut,
+        onComplete: (_) -> {
+          if (PlayState.instance?.currentChart != null)
+          {
+            metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+          }
+          else
+          {
+            metadataArtist.text = 'Artist: ${Constants.DEFAULT_ARTIST}';
+          }
+
+          FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+            {
+              ease: FlxEase.quartOut,
+              onComplete: (_) -> {
+                startCharterTimer();
+              }
+            });
+        }
+      });
+  }
+
   /**
    * Perform additional animations to transition the pause menu in when it is first displayed.
    */
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a9ca09ce8..b3d0a9f8a 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -236,6 +236,11 @@ class PlayState extends MusicBeatSubState
    */
   public var cameraZoomTween:FlxTween;
 
+  /**
+   * An FlxTween that changes the additive speed to the desired amount.
+   */
+  public var scrollSpeedTweens:Array<FlxTween> = [];
+
   /**
    * The camera follow point from the last stage.
    * Used to persist the position of the `cameraFollowPosition` between levels.
@@ -772,19 +777,19 @@ class PlayState extends MusicBeatSubState
       var message:String = 'There was a critical error. Click OK to return to the main menu.';
       if (currentSong == null)
       {
-        message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.';
+        message = 'There was a critical error loading this song\'s chart. Click OK to return to the main menu.';
       }
       else if (currentDifficulty == null)
       {
-        message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
+        message = 'There was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
       }
       else if (currentChart == null)
       {
-        message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
+        message = 'There was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
       }
       else if (currentChart.notes == null)
       {
-        message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
+        message = 'There was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
       }
 
       // Display a popup. This blocks the application until the user clicks OK.
@@ -822,6 +827,8 @@ class PlayState extends MusicBeatSubState
     {
       if (!assertChartExists()) return;
 
+      prevScrollTargets = [];
+
       dispatchEvent(new ScriptEvent(SONG_RETRY));
 
       resetCamera();
@@ -1092,8 +1099,11 @@ class PlayState extends MusicBeatSubState
 
     healthBar.value = healthLerp;
 
-    iconP1.updatePosition();
-    iconP2.updatePosition();
+    if (!isMinimalMode)
+    {
+      iconP1.updatePosition();
+      iconP2.updatePosition();
+    }
 
     // Transition to the game over substate.
     var gameOverSubState = new GameOverSubState(
@@ -1201,6 +1211,18 @@ class PlayState extends MusicBeatSubState
         cameraTweensPausedBySubState.add(cameraZoomTween);
       }
 
+      // Pause camera follow
+      FlxG.camera.followLerp = 0;
+
+      for (tween in scrollSpeedTweens)
+      {
+        if (tween != null && tween.active)
+        {
+          tween.active = false;
+          cameraTweensPausedBySubState.add(tween);
+        }
+      }
+
       // Pause the countdown.
       Countdown.pauseCountdown();
     }
@@ -1236,6 +1258,9 @@ class PlayState extends MusicBeatSubState
       }
       cameraTweensPausedBySubState.clear();
 
+      // Resume camera follow
+      FlxG.camera.followLerp = Constants.DEFAULT_CAMERA_FOLLOW_RATE;
+
       if (currentConversation != null)
       {
         currentConversation.resumeMusic();
@@ -1727,12 +1752,7 @@ class PlayState extends MusicBeatSubState
    */
   function initStrumlines():Void
   {
-    var noteStyleId:String = switch (currentStageId)
-    {
-      case 'school': 'pixel';
-      case 'schoolEvil': 'pixel';
-      default: Constants.DEFAULT_NOTE_STYLE;
-    }
+    var noteStyleId:String = currentChart.noteStyle;
     var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
     if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
 
@@ -2101,7 +2121,8 @@ class PlayState extends MusicBeatSubState
 
         // Call an event to allow canceling the note hit.
         // NOTE: This is what handles the character animations!
-        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+
+        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0);
         dispatchEvent(event);
 
         // Calling event.cancelEvent() skips all the other logic! Neat!
@@ -2197,7 +2218,7 @@ class PlayState extends MusicBeatSubState
 
         // Call an event to allow canceling the note hit.
         // NOTE: This is what handles the character animations!
-        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0);
         dispatchEvent(event);
 
         // Calling event.cancelEvent() skips all the other logic! Neat!
@@ -2255,11 +2276,20 @@ class PlayState extends MusicBeatSubState
       if (holdNote == null || !holdNote.alive) continue;
 
       // While the hold note is being hit, and there is length on the hold note...
-      if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
+      if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
       {
         // Grant the player health.
-        health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
-        songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
+        if (!isBotPlayMode)
+        {
+          health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
+          songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
+        }
+        
+        // Make sure the player keeps singing while the note is held by the bot.
+        if (isBotPlayMode && currentStage != null && currentStage.getBoyfriend() != null && currentStage.getBoyfriend().isSinging())
+        {
+          currentStage.getBoyfriend().holdTimer = 0;
+        }
       }
 
       if (holdNote.missedNote && !holdNote.handledMiss)
@@ -2319,8 +2349,6 @@ class PlayState extends MusicBeatSubState
     var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit();
     var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed();
 
-    // If there are notes in range, pressing a key will cause a ghost miss.
-
     var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []];
 
     for (note in notesInRange)
@@ -2342,17 +2370,27 @@ class PlayState extends MusicBeatSubState
 
         // Play the strumline animation.
         playerStrumline.playPress(input.noteDirection);
+        trace('PENALTY Score: ${songScore}');
       }
-      else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0)
+      else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
       {
-        // Pressed a wrong key with no notes nearby AND with notes in a different direction available.
+        // Pressed a wrong key with notes visible on-screen.
         // Perform a ghost miss (anti-spam).
         ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
 
         // Play the strumline animation.
         playerStrumline.playPress(input.noteDirection);
+        trace('PENALTY Score: ${songScore}');
       }
-      else if (notesInDirection.length > 0)
+      else if (notesInDirection.length == 0)
+      {
+        // Press a key with no penalty.
+
+        // Play the strumline animation.
+        playerStrumline.playPress(input.noteDirection);
+        trace('NO PENALTY Score: ${songScore}');
+      }
+      else
       {
         // Choose the first note, deprioritizing low priority notes.
         var targetNote:Null<NoteSprite> = notesInDirection.find((note) -> !note.lowPriority);
@@ -2362,17 +2400,13 @@ class PlayState extends MusicBeatSubState
         // Judge and hit the note.
         trace('Hit note! ${targetNote.noteData}');
         goodNoteHit(targetNote, input);
+        trace('Score: ${songScore}');
 
         notesInDirection.remove(targetNote);
 
         // Play the strumline animation.
         playerStrumline.playConfirm(input.noteDirection);
       }
-      else
-      {
-        // Play the strumline animation.
-        playerStrumline.playPress(input.noteDirection);
-      }
     }
 
     while (inputReleaseQueue.length > 0)
@@ -2402,27 +2436,41 @@ class PlayState extends MusicBeatSubState
     var daRating = Scoring.judgeNote(noteDiff, PBOT1);
 
     var healthChange = 0.0;
+    var isComboBreak = false;
     switch (daRating)
     {
       case 'sick':
         healthChange = Constants.HEALTH_SICK_BONUS;
+        isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
       case 'good':
         healthChange = Constants.HEALTH_GOOD_BONUS;
+        isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
       case 'bad':
         healthChange = Constants.HEALTH_BAD_BONUS;
+        isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
       case 'shit':
+        isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
         healthChange = Constants.HEALTH_SHIT_BONUS;
     }
 
     // Send the note hit event.
-    var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1);
+    var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, isComboBreak, Highscore.tallies.combo + 1, noteDiff,
+      daRating == 'sick');
     dispatchEvent(event);
 
     // Calling event.cancelEvent() skips all the other logic! Neat!
     if (event.eventCanceled) return;
 
+    Highscore.tallies.totalNotesHit++;
+    // Display the hit on the strums
+    playerStrumline.hitNote(note, !isComboBreak);
+    if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection());
+    if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite);
+    vocals.playerVolume = 1;
+
     // Display the combo meter and add the calculation to the score.
-    popUpScore(note, event.score, event.judgement, event.healthChange);
+    applyScore(event.score, event.judgement, event.healthChange, event.isComboBreak);
+    popUpScore(event.judgement);
   }
 
   /**
@@ -2433,9 +2481,6 @@ class PlayState extends MusicBeatSubState
   {
     // If we are here, we already CALLED the onNoteMiss script hook!
 
-    health += healthChange;
-    songScore -= 10;
-
     if (!isPracticeMode)
     {
       // messy copy paste rn lol
@@ -2475,14 +2520,9 @@ class PlayState extends MusicBeatSubState
     }
     vocals.playerVolume = 0;
 
-    Highscore.tallies.missed++;
+    if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
 
-    if (Highscore.tallies.combo != 0)
-    {
-      // Break the combo.
-      if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
-      Highscore.tallies.combo = 0;
-    }
+    applyScore(-10, 'miss', healthChange, true);
 
     if (playSound)
     {
@@ -2570,20 +2610,12 @@ class PlayState extends MusicBeatSubState
     // Redirect to the chart editor playing the current song.
     if (controls.DEBUG_CHART)
     {
-      if (isChartingMode)
-      {
-        if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
-        this.close(); // This only works because PlayState is a substate!
-      }
-      else
-      {
-        disableKeys = true;
-        persistentUpdate = false;
-        FlxG.switchState(() -> new ChartEditorState(
-          {
-            targetSongId: currentSong.id,
-          }));
-      }
+      disableKeys = true;
+      persistentUpdate = false;
+      FlxG.switchState(() -> new ChartEditorState(
+        {
+          targetSongId: currentSong.id,
+        }));
     }
     #end
 
@@ -2614,46 +2646,24 @@ class PlayState extends MusicBeatSubState
   }
 
   /**
-   * Handles health, score, and rating popups when a note is hit.
+   * Handles applying health, score, and ratings.
    */
-  function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
+  function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool)
   {
-    if (daRating == 'miss')
-    {
-      // If daRating is 'miss', that means we made a mistake and should not continue.
-      FlxG.log.warn('popUpScore judged a note as a miss!');
-      // TODO: Remove this.
-      // comboPopUps.displayRating('miss');
-      return;
-    }
-
-    vocals.playerVolume = 1;
-
-    var isComboBreak = false;
     switch (daRating)
     {
       case 'sick':
         Highscore.tallies.sick += 1;
-        Highscore.tallies.totalNotesHit++;
-        isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
       case 'good':
         Highscore.tallies.good += 1;
-        Highscore.tallies.totalNotesHit++;
-        isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
       case 'bad':
         Highscore.tallies.bad += 1;
-        Highscore.tallies.totalNotesHit++;
-        isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
       case 'shit':
         Highscore.tallies.shit += 1;
-        Highscore.tallies.totalNotesHit++;
-        isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
-      default:
-        FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
+      case 'miss':
+        Highscore.tallies.missed += 1;
     }
-
     health += healthChange;
-
     if (isComboBreak)
     {
       // Break the combo, but don't increment tallies.misses.
@@ -2665,15 +2675,23 @@ class PlayState extends MusicBeatSubState
       Highscore.tallies.combo++;
       if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
     }
-
-    playerStrumline.hitNote(daNote, !isComboBreak);
-
-    if (daRating == 'sick')
-    {
-      playerStrumline.playNoteSplash(daNote.noteData.getDirection());
-    }
-
     songScore += score;
+  }
+
+  /**
+   * Handles rating popups when a note is hit.
+   */
+  function popUpScore(daRating:String, ?combo:Int):Void
+  {
+    if (daRating == 'miss')
+    {
+      // If daRating is 'miss', that means we made a mistake and should not continue.
+      FlxG.log.warn('popUpScore judged a note as a miss!');
+      // TODO: Remove this.
+      // comboPopUps.displayRating('miss');
+      return;
+    }
+    if (combo == null) combo = Highscore.tallies.combo;
 
     if (!isPracticeMode)
     {
@@ -2713,12 +2731,7 @@ class PlayState extends MusicBeatSubState
       }
     }
     comboPopUps.displayRating(daRating);
-    if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
-
-    if (daNote.isHoldNote && daNote.holdNoteSprite != null)
-    {
-      playerStrumline.playNoteHoldCover(daNote.holdNoteSprite);
-    }
+    if (combo >= 10 || combo == 0) comboPopUps.displayCombo(combo);
 
     vocals.playerVolume = 1;
   }
@@ -2806,6 +2819,7 @@ class PlayState extends MusicBeatSubState
     deathCounter = 0;
 
     var isNewHighscore = false;
+    var prevScoreData:Null<SaveScoreData> = Save.instance.getSongScore(currentSong.id, currentDifficulty);
 
     if (currentSong != null && currentSong.validScore)
     {
@@ -2825,7 +2839,6 @@ class PlayState extends MusicBeatSubState
               totalNotesHit: Highscore.tallies.totalNotesHit,
               totalNotes: Highscore.tallies.totalNotes,
             },
-          accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
         };
 
       // adds current song data into the tallies for the level (story levels)
@@ -2862,7 +2875,7 @@ class PlayState extends MusicBeatSubState
               score: PlayStatePlaylist.campaignScore,
               tallies:
                 {
-                  // TODO: Sum up the values for the whole level!
+                  // TODO: Sum up the values for the whole week!
                   sick: 0,
                   good: 0,
                   bad: 0,
@@ -2873,7 +2886,6 @@ class PlayState extends MusicBeatSubState
                   totalNotesHit: 0,
                   totalNotes: 0,
                 },
-              accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
             };
 
           if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
@@ -2959,11 +2971,11 @@ class PlayState extends MusicBeatSubState
       {
         if (rightGoddamnNow)
         {
-          moveToResultsScreen(isNewHighscore);
+          moveToResultsScreen(isNewHighscore, prevScoreData);
         }
         else
         {
-          zoomIntoResultsScreen(isNewHighscore);
+          zoomIntoResultsScreen(isNewHighscore, prevScoreData);
         }
       }
     }
@@ -3037,15 +3049,16 @@ class PlayState extends MusicBeatSubState
   /**
    * Play the camera zoom animation and then move to the results screen once it's done.
    */
-  function zoomIntoResultsScreen(isNewHighscore:Bool):Void
+  function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
   {
     trace('WENT TO RESULTS SCREEN!');
 
     // Stop camera zooming on beat.
     cameraZoomRate = 0;
 
-    // Cancel camera tweening if it's active.
+    // Cancel camera and scroll tweening if it's active.
     cancelAllCameraTweens();
+    cancelScrollSpeedTweens();
 
     // If the opponent is GF, zoom in on the opponent.
     // Else, if there is no GF, zoom in on BF.
@@ -3072,12 +3085,12 @@ class PlayState extends MusicBeatSubState
     FlxG.camera.targetOffset.x += 20;
 
     // Replace zoom animation with a fade out for now.
-    camGame.fade(FlxColor.BLACK, 0.6);
+    FlxG.camera.fade(FlxColor.BLACK, 0.6);
 
     FlxTween.tween(camHUD, {alpha: 0}, 0.6,
       {
         onComplete: function(_) {
-          moveToResultsScreen(isNewHighscore);
+          moveToResultsScreen(isNewHighscore, prevScoreData);
         }
       });
 
@@ -3110,7 +3123,7 @@ class PlayState extends MusicBeatSubState
   /**
    * Move to the results screen right goddamn now.
    */
-  function moveToResultsScreen(isNewHighscore:Bool):Void
+  function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
   {
     persistentUpdate = false;
     vocals.stop();
@@ -3121,7 +3134,10 @@ class PlayState extends MusicBeatSubState
     var res:ResultState = new ResultState(
       {
         storyMode: PlayStatePlaylist.isStoryMode,
+        songId: currentChart.song.id,
+        difficultyId: currentDifficulty,
         title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+        prevScoreData: prevScoreData,
         scoreData:
           {
             score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
@@ -3137,11 +3153,10 @@ class PlayState extends MusicBeatSubState
                 totalNotesHit: talliesToUse.totalNotesHit,
                 totalNotes: talliesToUse.totalNotes,
               },
-            accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
           },
         isNewHighscore: isNewHighscore
       });
-    res.camera = camHUD;
+    this.persistentDraw = false;
     openSubState(res);
   }
 
@@ -3165,7 +3180,7 @@ class PlayState extends MusicBeatSubState
       cancelAllCameraTweens();
     }
 
-    FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
+    FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE);
     FlxG.camera.targetOffset.set();
 
     if (resetZoom)
@@ -3265,6 +3280,60 @@ class PlayState extends MusicBeatSubState
     cancelCameraZoomTween();
   }
 
+  var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
+
+  /**
+   * The magical function that shall tween the scroll speed.
+   */
+  public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:Null<Float->Float>, strumlines:Array<String>):Void
+  {
+    // Cancel the current tween if it's active.
+    cancelScrollSpeedTweens();
+
+    // Snap to previous event value to prevent the tween breaking when another event cancels the previous tween.
+    for (i in prevScrollTargets)
+    {
+      var value:Float = i[0];
+      var strum:Strumline = Reflect.getProperty(this, i[1]);
+      strum.scrollSpeed = value;
+    }
+
+    // for next event, clean array.
+    prevScrollTargets = [];
+
+    for (i in strumlines)
+    {
+      var value:Float = speed;
+      var strum:Strumline = Reflect.getProperty(this, i);
+
+      if (duration == 0)
+      {
+        strum.scrollSpeed = value;
+      }
+      else
+      {
+        scrollSpeedTweens.push(FlxTween.tween(strum,
+          {
+            'scrollSpeed': value
+          }, duration, {ease: ease}));
+      }
+      // make sure charts dont break if the charter is dumb and stupid
+      prevScrollTargets.push([value, i]);
+    }
+  }
+
+  public function cancelScrollSpeedTweens()
+  {
+    for (tween in scrollSpeedTweens)
+    {
+      if (tween != null)
+      {
+        tween.cancel();
+      }
+    }
+    scrollSpeedTweens = [];
+  }
+
   #if (debug || FORCE_DEBUG_VERSION)
   /**
    * Jumps forward or backward a number of sections in the song.
diff --git a/source/funkin/play/ResultScore.hx b/source/funkin/play/ResultScore.hx
index d5d5a6567..23e6c8d32 100644
--- a/source/funkin/play/ResultScore.hx
+++ b/source/funkin/play/ResultScore.hx
@@ -2,11 +2,16 @@ package funkin.play;
 
 import flixel.FlxSprite;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxEase;
 
 class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 {
   public var scoreShit(default, set):Int = 0;
 
+  public var scoreStart:Int = 0;
+
   function set_scoreShit(val):Int
   {
     if (group == null || group.members == null) return val;
@@ -16,7 +21,8 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 
     while (dumbNumb > 0)
     {
-      group.members[loopNum].digit = dumbNumb % 10;
+      scoreStart += 1;
+      group.members[loopNum].finalDigit = dumbNumb % 10;
 
       // var funnyNum = group.members[loopNum];
       // prevNum = group.members[loopNum + 1];
@@ -44,9 +50,15 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 
   public function animateNumbers():Void
   {
-    for (i in group.members)
+    for (i in group.members.length-scoreStart...group.members.length)
     {
-      i.playAnim();
+     // if(i.finalDigit == 10) continue;
+
+      new FlxTimer().start((i-1)/24, _ -> {
+        group.members[i].finalDelay = scoreStart - (i-1);
+        group.members[i].playAnim();
+        group.members[i].shuffle();
+      });
     }
   }
 
@@ -71,12 +83,26 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
 class ScoreNum extends FlxSprite
 {
   public var digit(default, set):Int = 10;
+  public var finalDigit(default, set):Int = 10;
+  public var glow:Bool = true;
+
+  function set_finalDigit(val):Int
+  {
+    animation.play('GONE', true, false, 0);
+
+    return finalDigit = val;
+  }
 
   function set_digit(val):Int
   {
     if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
     {
-      animation.play(numToString[val], true, false, 0);
+      if(glow){
+        animation.play(numToString[val], true, false, 0);
+        glow = false;
+      }else{
+        animation.play(numToString[val], true, false, 4);
+      }
       updateHitbox();
 
       switch (val)
@@ -107,6 +133,10 @@ class ScoreNum extends FlxSprite
     animation.play(numToString[digit], true, false, 0);
   }
 
+  public var shuffleTimer:FlxTimer;
+  public var finalTween:FlxTween;
+  public var finalDelay:Float = 0;
+
   public var baseY:Float = 0;
   public var baseX:Float = 0;
 
@@ -114,6 +144,47 @@ class ScoreNum extends FlxSprite
     "ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED"
   ];
 
+  function finishShuffleTween():Void{
+
+    var tweenFunction = function(x) {
+      var digitRounded = Math.floor(x);
+      //if(digitRounded == finalDigit) glow = true;
+      digit = digitRounded;
+    };
+
+    finalTween = FlxTween.num(0.0, finalDigit, 23/24, {
+      ease: FlxEase.quadOut,
+      onComplete: function (input) {
+        new FlxTimer().start((finalDelay)/24, _ -> {
+          animation.play(animation.curAnim.name, true, false, 0);
+        });
+        // fuck
+      }
+    }, tweenFunction);
+  }
+
+
+  function shuffleProgress(shuffleTimer:FlxTimer):Void
+  {
+    var tempDigit:Int = digit;
+    tempDigit += 1;
+    if(tempDigit > 9) tempDigit = 0;
+    if(tempDigit < 0) tempDigit = 0;
+    digit = tempDigit;
+
+    if (shuffleTimer.loops > 0 && shuffleTimer.loopsLeft == 0)
+    {
+      //digit = finalDigit;
+      finishShuffleTween();
+    }
+  }
+
+  public function shuffle():Void{
+    var duration:Float = 41/24;
+    var interval:Float = 1/24;
+    shuffleTimer = new FlxTimer().start(interval, shuffleProgress, Std.int(duration / interval));
+  }
+
   public function new(x:Float, y:Float)
   {
     super(x, y);
@@ -130,6 +201,7 @@ class ScoreNum extends FlxSprite
     }
 
     animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
+    animation.addByPrefix('GONE', 'GONE', 24, false);
 
     this.digit = 10;
 
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 56dd1e80f..48fb3b04e 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -5,6 +5,7 @@ import funkin.ui.story.StoryMenuState;
 import funkin.graphics.adobeanimate.FlxAtlasSprite;
 import flixel.FlxSprite;
 import funkin.graphics.FunkinSprite;
+import flixel.effects.FlxFlicker;
 import flixel.graphics.frames.FlxBitmapFont;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.math.FlxPoint;
@@ -12,163 +13,275 @@ import funkin.ui.MusicBeatSubState;
 import flixel.math.FlxRect;
 import flixel.text.FlxBitmapText;
 import funkin.ui.freeplay.FreeplayScore;
+import flixel.text.FlxText;
+import flixel.util.FlxColor;
 import flixel.tweens.FlxEase;
+import funkin.graphics.FunkinCamera;
 import funkin.ui.freeplay.FreeplayState;
 import flixel.tweens.FlxTween;
+import flixel.addons.display.FlxBackdrop;
 import funkin.audio.FunkinSound;
 import flixel.util.FlxGradient;
 import flixel.util.FlxTimer;
 import funkin.save.Save;
+import funkin.play.scoring.Scoring;
 import funkin.save.Save.SaveScoreData;
 import funkin.graphics.shaders.LeftMaskShader;
 import funkin.play.components.TallyCounter;
+import funkin.play.components.ClearPercentCounter;
 
 /**
  * The state for the results screen after a song or week is finished.
  */
+@:nullSafety
 class ResultState extends MusicBeatSubState
 {
   final params:ResultsStateParams;
 
-  var resultsVariation:ResultVariations;
-  var songName:FlxBitmapText;
-  var difficulty:FlxSprite;
+  final rank:ScoringRank;
+  final songName:FlxBitmapText;
+  final difficulty:FlxSprite;
+  final clearPercentSmall:ClearPercentCounter;
 
-  var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
-  var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+  final maskShaderSongName:LeftMaskShader = new LeftMaskShader();
+  final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+
+  final resultsAnim:FunkinSprite;
+  final ratingsPopin:FunkinSprite;
+  final scorePopin:FunkinSprite;
+
+  final bgFlash:FlxSprite;
+
+  final highscoreNew:FlxSprite;
+  final score:ResultScore;
+
+  var bfPerfect:Null<FlxAtlasSprite> = null;
+  var heartsPerfect:Null<FlxAtlasSprite> = null;
+  var bfExcellent:Null<FlxAtlasSprite> = null;
+  var bfGreat:Null<FlxAtlasSprite> = null;
+  var gfGreat:Null<FlxAtlasSprite> = null;
+  var bfGood:Null<FlxSprite> = null;
+  var gfGood:Null<FlxSprite> = null;
+  var bfShit:Null<FlxAtlasSprite> = null;
+
+  var rankBg:FunkinSprite;
+  final cameraBG:FunkinCamera;
+  final cameraScroll:FunkinCamera;
+  final cameraEverything:FunkinCamera;
 
   public function new(params:ResultsStateParams)
   {
     super();
 
     this.params = params;
-  }
 
-  override function create():Void
-  {
-    /*
-      if (params.scoreData.sick == params.scoreData.totalNotesHit
-        && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
-      else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
-        resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
-      else
-        resultsVariation = NORMAL;
-     */
-    resultsVariation = NORMAL;
+    rank = Scoring.calculateRank(params.scoreData) ?? SHIT;
 
-    FunkinSound.playMusic('results$resultsVariation',
-      {
-        startingVolume: 1.0,
-        overrideExisting: true,
-        restartTrack: true,
-        loop: resultsVariation != SHIT
-      });
+    cameraBG = new FunkinCamera('resultsBG', 0, 0, FlxG.width, FlxG.height);
+    cameraScroll = new FunkinCamera('resultsScroll', 0, 0, FlxG.width, FlxG.height);
+    cameraEverything = new FunkinCamera('resultsEverything', 0, 0, FlxG.width, FlxG.height);
 
-    // Reset the camera zoom on the results screen.
-    FlxG.camera.zoom = 1.0;
-
-    // TEMP-ish, just used to sorta "cache" the 3000x3000 image!
-    var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
-    add(cacheBullShit);
-
-    var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
-    add(dumb);
-
-    var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
-    bg.scrollFactor.set();
-    add(bg);
-
-    var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
-    bgFlash.scrollFactor.set();
-    bgFlash.visible = false;
-    add(bgFlash);
-
-    // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
-    // bfGfExcellent.visible = false;
-    // add(bfGfExcellent);
-    //
-    // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
-    // bfPerfect.visible = false;
-    // add(bfPerfect);
-    //
-    // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
-    // bfSHIT.visible = false;
-    // add(bfSHIT);
-    //
-    // bfGfExcellent.anim.onComplete = () -> {
-    // bfGfExcellent.anim.curFrame = 28;
-    // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-    //
-    // bfPerfect.anim.onComplete = () -> {
-    //  bfPerfect.anim.curFrame = 136;
-    //  bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-    //
-    // bfSHIT.anim.onComplete = () -> {
-    //  bfSHIT.anim.curFrame = 150;
-    //  bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
-    // };
-
-    var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
-    gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
-    gf.visible = false;
-    gf.animation.finishCallback = _ -> {
-      gf.animation.play('clap', true, false, 9);
-    };
-    add(gf);
-
-    var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
-    boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
-    boyfriend.visible = false;
-    boyfriend.animation.finishCallback = function(_) {
-      boyfriend.animation.play('fall', true, false, 14);
-    };
-
-    add(boyfriend);
-
-    var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
-    soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
-    soundSystem.visible = false;
-    new FlxTimer().start(0.4, _ -> {
-      soundSystem.animation.play("idle");
-      soundSystem.visible = true;
-    });
-    add(soundSystem);
-
-    difficulty = new FlxSprite(555);
-
-    var diffSpr:String = switch (PlayState.instance.currentDifficulty)
-    {
-      case 'easy':
-        'difEasy';
-      case 'normal':
-        'difNormal';
-      case 'hard':
-        'difHard';
-      case 'erect':
-        'difErect';
-      case 'nightmare':
-        'difNightmare';
-      case _:
-        'difNormal';
-    }
-
-    difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
-    add(difficulty);
+    // We build a lot of this stuff in the constructor, then place it in create().
+    // This prevents having to do `null` checks everywhere.
 
     var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
     songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
     songName.text = params.title;
     songName.letterSpacing = -15;
     songName.angle = -4.4;
+    songName.zIndex = 1000;
+
+    difficulty = new FlxSprite(555);
+    difficulty.zIndex = 1000;
+
+    clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true);
+    clearPercentSmall.zIndex = 1000;
+    clearPercentSmall.visible = false;
+
+    bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFF1A6, 0xFFFFF1BE], 90);
+
+    resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
+
+    ratingsPopin = FunkinSprite.createSparrow(-135, 135, "resultScreen/ratingsPopin");
+
+    scorePopin = FunkinSprite.createSparrow(-180, 515, "resultScreen/scorePopin");
+
+    highscoreNew = new FlxSprite(44, 557);
+
+    score = new ResultScore(35, 305, 10, params.scoreData.score);
+
+    rankBg = new FunkinSprite(0, 0);
+  }
+
+  override function create():Void
+  {
+    if (FlxG.sound.music != null) FlxG.sound.music.stop();
+
+    // We need multiple cameras so we can put one at an angle.
+    cameraScroll.angle = -3.8;
+
+    cameraBG.bgColor = FlxColor.MAGENTA;
+    cameraScroll.bgColor = FlxColor.TRANSPARENT;
+    cameraEverything.bgColor = FlxColor.TRANSPARENT;
+
+    FlxG.cameras.add(cameraBG, false);
+    FlxG.cameras.add(cameraScroll, false);
+    FlxG.cameras.add(cameraEverything, false);
+
+    FlxG.cameras.setDefaultDrawTarget(cameraEverything, true);
+    this.camera = cameraEverything;
+
+    // Reset the camera zoom on the results screen.
+    FlxG.camera.zoom = 1.0;
+
+    var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
+    bg.scrollFactor.set();
+    bg.zIndex = 10;
+    bg.cameras = [cameraBG];
+    add(bg);
+
+    bgFlash.scrollFactor.set();
+    bgFlash.visible = false;
+    bgFlash.zIndex = 20;
+    // bgFlash.cameras = [cameraBG];
+    add(bgFlash);
+
+    // The sound system which falls into place behind the score text. Plays every time!
+    var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
+    soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
+    soundSystem.visible = false;
+    new FlxTimer().start(8 / 24, _ -> {
+      soundSystem.animation.play("idle");
+      soundSystem.visible = true;
+    });
+    soundSystem.zIndex = 1100;
+    add(soundSystem);
+
+    switch (rank)
+    {
+      case PERFECT | PERFECT_GOLD:
+        heartsPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT/hearts", "shared"));
+        heartsPerfect.visible = false;
+        heartsPerfect.zIndex = 501;
+        add(heartsPerfect);
+
+        heartsPerfect.anim.onComplete = () -> {
+          if (heartsPerfect != null)
+          {
+            // bfPerfect.anim.curFrame = 137;
+            heartsPerfect.anim.curFrame = 43;
+            heartsPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+        bfPerfect = new FlxAtlasSprite(1342, 370, Paths.animateAtlas("resultScreen/results-bf/resultsPERFECT", "shared"));
+        bfPerfect.visible = false;
+        bfPerfect.zIndex = 500;
+        add(bfPerfect);
+
+        bfPerfect.anim.onComplete = () -> {
+          if (bfPerfect != null)
+          {
+            // bfPerfect.anim.curFrame = 137;
+            bfPerfect.anim.curFrame = 137;
+            bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+      case EXCELLENT:
+        bfExcellent = new FlxAtlasSprite(1329, 429, Paths.animateAtlas("resultScreen/results-bf/resultsEXCELLENT", "shared"));
+        bfExcellent.visible = false;
+        bfExcellent.zIndex = 500;
+        add(bfExcellent);
+
+        bfExcellent.anim.onComplete = () -> {
+          if (bfExcellent != null)
+          {
+            bfExcellent.anim.curFrame = 28;
+            bfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+      case GREAT:
+        gfGreat = new FlxAtlasSprite(802, 331, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/gf", "shared"));
+        gfGreat.visible = false;
+        gfGreat.zIndex = 499;
+        add(gfGreat);
+
+        gfGreat.scale.set(0.93, 0.93);
+
+        gfGreat.anim.onComplete = () -> {
+          if (gfGreat != null)
+          {
+            gfGreat.anim.curFrame = 9;
+            gfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+        bfGreat = new FlxAtlasSprite(929, 363, Paths.animateAtlas("resultScreen/results-bf/resultsGREAT/bf", "shared"));
+        bfGreat.visible = false;
+        bfGreat.zIndex = 500;
+        add(bfGreat);
+
+        bfGreat.scale.set(0.93, 0.93);
+
+        bfGreat.anim.onComplete = () -> {
+          if (bfGreat != null)
+          {
+            bfGreat.anim.curFrame = 15;
+            bfGreat.anim.play(); // unpauses this anim, since it's on PlayOnce!
+          }
+        };
+
+      case GOOD:
+        gfGood = FunkinSprite.createSparrow(625, 325, 'resultScreen/results-bf/resultsGOOD/resultGirlfriendGOOD');
+        gfGood.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
+        gfGood.visible = false;
+        gfGood.zIndex = 500;
+        gfGood.animation.finishCallback = _ -> {
+          if (gfGood != null)
+          {
+            gfGood.animation.play('clap', true, false, 9);
+          }
+        };
+        add(gfGood);
+
+        bfGood = FunkinSprite.createSparrow(640, -200, 'resultScreen/results-bf/resultsGOOD/resultBoyfriendGOOD');
+        bfGood.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
+        bfGood.visible = false;
+        bfGood.zIndex = 501;
+        bfGood.animation.finishCallback = function(_) {
+          if (bfGood != null)
+          {
+            bfGood.animation.play('fall', true, false, 14);
+          }
+        };
+        add(bfGood);
+
+      case SHIT:
+        bfShit = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/results-bf/resultsSHIT", "shared"));
+        bfShit.visible = false;
+        bfShit.zIndex = 500;
+        add(bfShit);
+        bfShit.onAnimationFinish.add((animName) -> {
+          if (bfShit != null)
+          {
+            bfShit.playAnimation('Loop Start');
+          }
+        });
+    }
+
+    var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}';
+    difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
+    add(difficulty);
+
     add(songName);
 
     var angleRad = songName.angle * Math.PI / 180;
     speedOfTween.x = -1.0 * Math.cos(angleRad);
     speedOfTween.y = -1.0 * Math.sin(angleRad);
 
-    timerThenSongName();
+    timerThenSongName(1.0, false);
 
     songName.shader = maskShaderSongName;
     difficulty.shader = maskShaderDifficulty;
@@ -178,35 +291,77 @@ class ResultState extends MusicBeatSubState
 
     var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
     blackTopBar.y = -blackTopBar.height;
-    FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
+    FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24});
+    blackTopBar.zIndex = 1010;
     add(blackTopBar);
 
-    var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
     resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false);
-    resultsAnim.animation.play("result");
+    resultsAnim.visible = false;
+    resultsAnim.zIndex = 1200;
     add(resultsAnim);
+    new FlxTimer().start(6 / 24, _ -> {
+      resultsAnim.visible = true;
+      resultsAnim.animation.play("result");
+    });
 
-    var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
     ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
     ratingsPopin.visible = false;
+    ratingsPopin.zIndex = 1200;
     add(ratingsPopin);
+    new FlxTimer().start(21 / 24, _ -> {
+      ratingsPopin.visible = true;
+      ratingsPopin.animation.play("idle");
+    });
 
-    var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
     scorePopin.animation.addByPrefix("score", "tally score", 24, false);
     scorePopin.visible = false;
+    scorePopin.zIndex = 1200;
     add(scorePopin);
+    new FlxTimer().start(36 / 24, _ -> {
+      scorePopin.visible = true;
+      scorePopin.animation.play("score");
+      scorePopin.animation.finishCallback = anim -> {};
+    });
+
+    new FlxTimer().start(37 / 24, _ -> {
+      score.visible = true;
+      score.animateNumbers();
+      startRankTallySequence();
+    });
+
+    new FlxTimer().start(rank.getBFDelay(), _ -> {
+      afterRankTallySequence();
+    });
+
+    new FlxTimer().start(rank.getFlashDelay(), _ -> {
+      displayRankText();
+    });
 
-    var highscoreNew:FlxSprite = new FlxSprite(310, 570);
     highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
-    highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
+    highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false);
     highscoreNew.visible = false;
-    highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
+    // highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
     highscoreNew.updateHitbox();
+    highscoreNew.zIndex = 1200;
     add(highscoreNew);
 
+    new FlxTimer().start(rank.getHighscoreDelay(), _ -> {
+      if (params.isNewHighscore ?? false)
+      {
+        highscoreNew.visible = true;
+        highscoreNew.animation.play("new");
+        highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16);
+      }
+      else
+      {
+        highscoreNew.visible = false;
+      }
+    });
+
     var hStuf:Int = 50;
 
     var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
+    ratingGrp.zIndex = 1200;
     add(ratingGrp);
 
     /**
@@ -220,7 +375,10 @@ class ResultState extends MusicBeatSubState
     ratingGrp.add(maxCombo);
 
     hStuf += 2;
-    var extraYOffset:Float = 5;
+    var extraYOffset:Float = 7;
+
+    hStuf += 2;
+
     var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E);
     ratingGrp.add(tallySick);
 
@@ -236,83 +394,289 @@ class ResultState extends MusicBeatSubState
     var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
     ratingGrp.add(tallyMissed);
 
-    var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
     score.visible = false;
+    score.zIndex = 1200;
     add(score);
 
     for (ind => rating in ratingGrp.members)
     {
       rating.visible = false;
-      new FlxTimer().start((0.3 * ind) + 0.55, _ -> {
+      new FlxTimer().start((0.3 * ind) + 1.20, _ -> {
         rating.visible = true;
         FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut});
       });
     }
 
-    new FlxTimer().start(0.5, _ -> {
-      ratingsPopin.animation.play("idle");
-      ratingsPopin.visible = true;
+    // if (params.isNewHighscore ?? false)
+    // {
+    //   highscoreNew.visible = true;
+    //   highscoreNew.animation.play("new");
+    //   //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
+    // }
+    // else
+    // {
+    //   highscoreNew.visible = false;
+    // }
+
+    new FlxTimer().start(rank.getMusicDelay(), _ -> {
+      if (rank.hasMusicIntro())
+      {
+        // Play the intro music.
+        var introMusic:String = Paths.music(rank.getMusicPath() + '/' + rank.getMusicPath() + '-intro');
+        FunkinSound.load(introMusic, 1.0, false, true, true, () -> {
+          FunkinSound.playMusic(rank.getMusicPath(),
+            {
+              startingVolume: 1.0,
+              overrideExisting: true,
+              restartTrack: true,
+              loop: rank.shouldMusicLoop()
+            });
+        });
+      }
+      else
+      {
+        FunkinSound.playMusic(rank.getMusicPath(),
+          {
+            startingVolume: 1.0,
+            overrideExisting: true,
+            restartTrack: true,
+            loop: rank.shouldMusicLoop()
+          });
+      }
+    });
+
+    rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000);
+    rankBg.zIndex = 99999;
+    add(rankBg);
+
+    rankBg.alpha = 0;
+
+    refresh();
+
+    super.create();
+  }
+
+  var rankTallyTimer:Null<FlxTimer> = null;
+  var clearPercentTarget:Int = 100;
+  var clearPercentLerp:Int = 0;
+
+  function startRankTallySequence():Void
+  {
+    bgFlash.visible = true;
+    FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24);
+    var clearPercentFloat = (params.scoreData.tallies.sick + params.scoreData.tallies.good) / params.scoreData.tallies.totalNotes * 100;
+    clearPercentTarget = Math.floor(clearPercentFloat);
+    // Prevent off-by-one errors.
+
+    clearPercentLerp = Std.int(Math.max(0, clearPercentTarget - 36));
+
+    trace('Clear percent target: ' + clearPercentFloat + ', round: ' + clearPercentTarget);
+
+    var clearPercentCounter:ClearPercentCounter = new ClearPercentCounter(FlxG.width / 2 + 190, FlxG.height / 2 - 70, clearPercentLerp);
+    FlxTween.tween(clearPercentCounter, {curNumber: clearPercentTarget}, 58 / 24,
+      {
+        ease: FlxEase.quartOut,
+        onUpdate: _ -> {
+          // Only play the tick sound if the number increased.
+          if (clearPercentLerp != clearPercentCounter.curNumber)
+          {
+            clearPercentLerp = clearPercentCounter.curNumber;
+            FunkinSound.playOnce(Paths.sound('scrollMenu'));
+          }
+        },
+        onComplete: _ -> {
+          // Play confirm sound.
+          FunkinSound.playOnce(Paths.sound('confirmMenu'));
+
+          // Just to be sure that the lerp didn't mess things up.
+          clearPercentCounter.curNumber = clearPercentTarget;
+
+          clearPercentCounter.flash(true);
+          new FlxTimer().start(0.4, _ -> {
+            clearPercentCounter.flash(false);
+          });
+
+          // displayRankText();
+
+          // previously 2.0 seconds
+          new FlxTimer().start(0.25, _ -> {
+            FlxTween.tween(clearPercentCounter, {alpha: 0}, 0.5,
+              {
+                startDelay: 0.5,
+                ease: FlxEase.quartOut,
+                onComplete: _ -> {
+                  remove(clearPercentCounter);
+                }
+              });
+
+            // afterRankTallySequence();
+          });
+        }
+      });
+    clearPercentCounter.zIndex = 450;
+    add(clearPercentCounter);
+
+    if (ratingsPopin == null)
+    {
+      trace("Could not build ratingsPopin!");
+    }
+    else
+    {
+      // ratingsPopin.animation.play("idle");
+      // ratingsPopin.visible = true;
 
       ratingsPopin.animation.finishCallback = anim -> {
-        scorePopin.animation.play("score");
-        scorePopin.animation.finishCallback = anim -> {
-          score.visible = true;
-          score.animateNumbers();
-        };
-        scorePopin.visible = true;
+        // scorePopin.animation.play("score");
 
-        if (params.isNewHighscore)
+        // scorePopin.visible = true;
+
+        if (params.isNewHighscore ?? false)
         {
           highscoreNew.visible = true;
           highscoreNew.animation.play("new");
-          FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
         }
         else
         {
           highscoreNew.visible = false;
         }
       };
+    }
 
-      switch (resultsVariation)
-      {
-        // case SHIT:
-        // bfSHIT.visible = true;
-        // bfSHIT.playAnimation("");
-
-        case NORMAL:
-          boyfriend.animation.play('fall');
-          boyfriend.visible = true;
-
-          new FlxTimer().start((1 / 24) * 12, _ -> {
-            bgFlash.visible = true;
-            FlxTween.tween(bgFlash, {alpha: 0}, 0.4);
-            new FlxTimer().start((1 / 24) * 2, _ ->
-              {
-                // bgFlash.alpha = 0.5;
-
-                // bgFlash.visible = false;
-              });
-          });
-
-          new FlxTimer().start((1 / 24) * 22, _ -> {
-            // plays about 22 frames (at 24fps timing) after bf spawns in
-            gf.animation.play('clap', true);
-            gf.visible = true;
-          });
-        // case PERFECT:
-        //          bfPerfect.visible = true;
-        //          bfPerfect.playAnimation("");
-
-        // bfGfExcellent.visible = true;
-        // bfGfExcellent.playAnimation("");
-        default:
-      }
-    });
-
-    super.create();
+    refresh();
   }
 
-  function timerThenSongName():Void
+  function displayRankText():Void
+  {
+    bgFlash.visible = true;
+    bgFlash.alpha = 1;
+    FlxTween.tween(bgFlash, {alpha: 0}, 14 / 24);
+
+    var rankTextVert:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getVerTextAsset()), Y, 0, 30);
+    rankTextVert.x = FlxG.width - 44;
+    rankTextVert.y = 100;
+    rankTextVert.zIndex = 990;
+    add(rankTextVert);
+
+    FlxFlicker.flicker(rankTextVert, 2 / 24 * 3, 2 / 24, true);
+
+    // Scrolling.
+    new FlxTimer().start(30 / 24, _ -> {
+      rankTextVert.velocity.y = -80;
+    });
+
+    for (i in 0...12)
+    {
+      var rankTextBack:FlxBackdrop = new FlxBackdrop(Paths.image(rank.getHorTextAsset()), X, 10, 0);
+      rankTextBack.x = FlxG.width / 2 - 320;
+      rankTextBack.y = 50 + (135 * i / 2) + 10;
+      // rankTextBack.angle = -3.8;
+      rankTextBack.zIndex = 100;
+      rankTextBack.cameras = [cameraScroll];
+      add(rankTextBack);
+
+      // Scrolling.
+      rankTextBack.velocity.x = (i % 2 == 0) ? -7.0 : 7.0;
+    }
+
+    refresh();
+  }
+
+  function afterRankTallySequence():Void
+  {
+    showSmallClearPercent();
+
+    switch (rank)
+    {
+      case PERFECT | PERFECT_GOLD:
+        if (bfPerfect == null)
+        {
+          trace("Could not build PERFECT animation!");
+        }
+        else
+        {
+          bfPerfect.visible = true;
+          bfPerfect.playAnimation('');
+        }
+        new FlxTimer().start(106 / 24, _ -> {
+          if (heartsPerfect == null)
+          {
+            trace("Could not build heartsPerfect animation!");
+          }
+          else
+          {
+            heartsPerfect.visible = true;
+            heartsPerfect.playAnimation('');
+          }
+        });
+      case EXCELLENT:
+        if (bfExcellent == null)
+        {
+          trace("Could not build EXCELLENT animation!");
+        }
+        else
+        {
+          bfExcellent.visible = true;
+          bfExcellent.playAnimation('');
+        }
+      case GREAT:
+        if (bfGreat == null)
+        {
+          trace("Could not build GREAT animation!");
+        }
+        else
+        {
+          bfGreat.visible = true;
+          bfGreat.playAnimation('');
+        }
+
+        new FlxTimer().start(6 / 24, _ -> {
+          if (gfGreat == null)
+          {
+            trace("Could not build GREAT animation for gf!");
+          }
+          else
+          {
+            gfGreat.visible = true;
+            gfGreat.playAnimation('');
+          }
+        });
+      case SHIT:
+        if (bfShit == null)
+        {
+          trace("Could not build SHIT animation!");
+        }
+        else
+        {
+          bfShit.visible = true;
+          bfShit.playAnimation('Intro');
+        }
+      case GOOD:
+        if (bfGood == null)
+        {
+          trace("Could not build GOOD animation!");
+        }
+        else
+        {
+          bfGood.animation.play('fall');
+          bfGood.visible = true;
+          new FlxTimer().start((1 / 24) * 22, _ -> {
+            // plays about 22 frames (at 24fps timing) after bf spawns in
+            if (gfGood != null)
+            {
+              gfGood.animation.play('clap', true);
+              gfGood.visible = true;
+            }
+            else
+            {
+              trace("Could not build GOOD animation!");
+            }
+          });
+        }
+      default:
+    }
+  }
+
+  function timerThenSongName(timerLength:Float = 3.0, autoScroll:Bool = true):Void
   {
     movingSongStuff = false;
 
@@ -323,17 +687,45 @@ class ResultState extends MusicBeatSubState
     difficulty.y = -difficulty.height;
     FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.8});
 
+    if (clearPercentSmall != null)
+    {
+      clearPercentSmall.x = (difficulty.x + difficulty.width) + 60;
+      clearPercentSmall.y = -clearPercentSmall.height;
+      FlxTween.tween(clearPercentSmall, {y: 122 - 5}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.85});
+    }
+
     songName.y = -songName.height;
     var fuckedupnumber = (10) * (songName.text.length / 15);
-    FlxTween.tween(songName, {y: diffYTween - 35 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
-    songName.x = (difficulty.x + difficulty.width) + 20;
+    FlxTween.tween(songName, {y: diffYTween - 25 - fuckedupnumber}, 0.5, {ease: FlxEase.expoOut, startDelay: 0.9});
+    songName.x = clearPercentSmall.x + 94;
 
-    new FlxTimer().start(3, _ -> {
+    new FlxTimer().start(timerLength, _ -> {
       var tempSpeed = FlxPoint.get(speedOfTween.x, speedOfTween.y);
 
       speedOfTween.set(0, 0);
       FlxTween.tween(speedOfTween, {x: tempSpeed.x, y: tempSpeed.y}, 0.7, {ease: FlxEase.quadIn});
 
+      movingSongStuff = (autoScroll);
+    });
+  }
+
+  function showSmallClearPercent():Void
+  {
+    if (clearPercentSmall != null)
+    {
+      add(clearPercentSmall);
+      clearPercentSmall.visible = true;
+      clearPercentSmall.flash(true);
+      new FlxTimer().start(0.4, _ -> {
+        clearPercentSmall.flash(false);
+      });
+
+      clearPercentSmall.curNumber = clearPercentTarget;
+      clearPercentSmall.zIndex = 1000;
+      refresh();
+    }
+
+    new FlxTimer().start(2.5, _ -> {
       movingSongStuff = true;
     });
   }
@@ -345,11 +737,9 @@ class ResultState extends MusicBeatSubState
   {
     super.draw();
 
-    if (songName != null)
-    {
-      songName.clipRect = FlxRect.get(Math.max(0, 540 - songName.x), 0, FlxG.width, songName.height);
-      // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
-    }
+    songName.clipRect = FlxRect.get(Math.max(0, 520 - songName.x), 0, FlxG.width, songName.height);
+
+    // PROBABLY SHOULD FIX MEMORY FREE OR WHATEVER THE PUT() FUNCTION DOES !!!! FEELS LIKE IT STUTTERS!!!
 
     // if (songName != null && songName.frame != null)
     // maskShaderSongName.frameUV = songName.frame.uv;
@@ -357,6 +747,79 @@ class ResultState extends MusicBeatSubState
 
   override function update(elapsed:Float):Void
   {
+    // if(FlxG.keys.justPressed.R){
+    //   FlxG.switchState(() -> new funkin.play.ResultState(
+    //   {
+    //     storyMode: false,
+    //     title: "Cum Song Erect by Kawai Sprite",
+    //     songId: "cum",
+    //     difficultyId: "nightmare",
+    //     isNewHighscore: true,
+    //     scoreData:
+    //       {
+    //         score: 1_234_567,
+    //         tallies:
+    //           {
+    //             sick: 200,
+    //             good: 0,
+    //             bad: 0,
+    //             shit: 0,
+    //             missed: 0,
+    //             combo: 0,
+    //             maxCombo: 69,
+    //             totalNotesHit: 200,
+    //             totalNotes: 200 // 0,
+    //           }
+    //       },
+    //   }));
+    // }
+
+    // if(heartsPerfect != null){
+    // if (FlxG.keys.justPressed.I)
+    // {
+    //   heartsPerfect.y -= 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.J)
+    // {
+    //   heartsPerfect.x -= 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.L)
+    // {
+    //   heartsPerfect.x += 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // if (FlxG.keys.justPressed.K)
+    // {
+    //   heartsPerfect.y += 1;
+    //   trace(heartsPerfect.x, heartsPerfect.y);
+    // }
+    // }
+
+    // if(bfGreat != null){
+    // if (FlxG.keys.justPressed.W)
+    // {
+    //   bfGreat.y -= 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.A)
+    // {
+    //   bfGreat.x -= 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.D)
+    // {
+    //   bfGreat.x += 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // if (FlxG.keys.justPressed.S)
+    // {
+    //   bfGreat.y += 1;
+    //   trace(bfGreat.x, bfGreat.y);
+    // }
+    // }
+
     // maskShaderSongName.swagSprX = songName.x;
     maskShaderDifficulty.swagSprX = difficulty.x;
 
@@ -364,8 +827,10 @@ class ResultState extends MusicBeatSubState
     {
       songName.x += speedOfTween.x;
       difficulty.x += speedOfTween.x;
+      clearPercentSmall.x += speedOfTween.x;
       songName.y += speedOfTween.y;
       difficulty.y += speedOfTween.y;
+      clearPercentSmall.y += speedOfTween.y;
 
       if (songName.x + songName.width < 100)
       {
@@ -382,20 +847,64 @@ class ResultState extends MusicBeatSubState
 
     if (controls.PAUSE)
     {
-      FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
-      FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1,
-        {
-          onComplete: _ -> {
-            FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
-          }
-        });
+      if (FlxG.sound.music != null)
+      {
+        FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
+        FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1,
+          {
+            onComplete: _ -> {
+              FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
+            }
+          });
+      }
       if (params.storyMode)
       {
         openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
       }
       else
       {
-        openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(null, sticker)));
+        var rigged:Bool = true;
+        if (rank > Scoring.calculateRank(params?.prevScoreData)) // if (rigged)
+        {
+          trace('THE RANK IS Higher.....');
+
+          FlxTween.tween(rankBg, {alpha: 1}, 0.5,
+            {
+              ease: FlxEase.expoOut,
+              onComplete: function(_) {
+                FlxG.switchState(FreeplayState.build(
+                  {
+                    {
+                      fromResults:
+                        {
+                          oldRank: Scoring.calculateRank(params?.prevScoreData),
+                          newRank: rank,
+                          songId: params.songId,
+                          difficultyId: params.difficultyId,
+                          playRankAnim: true
+                        }
+                    }
+                  }));
+              }
+            });
+        }
+        else
+        {
+          trace('rank is lower...... and/or equal');
+          openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> FreeplayState.build(
+            {
+              {
+                fromResults:
+                  {
+                    oldRank: null,
+                    playRankAnim: false,
+                    newRank: rank,
+                    songId: params.songId,
+                    difficultyId: params.difficultyId
+                  }
+              }
+            }, sticker)));
+        }
       }
     }
 
@@ -403,14 +912,6 @@ class ResultState extends MusicBeatSubState
   }
 }
 
-enum abstract ResultVariations(String)
-{
-  var PERFECT;
-  var EXCELLENT;
-  var NORMAL;
-  var SHIT;
-}
-
 typedef ResultsStateParams =
 {
   /**
@@ -423,13 +924,26 @@ typedef ResultsStateParams =
    */
   var title:String;
 
+  var songId:String;
+
   /**
    * Whether the displayed score is a new highscore
    */
-  var isNewHighscore:Bool;
+  var ?isNewHighscore:Bool;
+
+  /**
+   * The difficulty ID of the song/week we just played.
+   * @default Normal
+   */
+  var ?difficultyId:String;
 
   /**
    * The score, accuracy, and judgements.
    */
   var scoreData:SaveScoreData;
+
+  /**
+   * The previous score data, used for rank comparision.
+   */
+  var ?prevScoreData:SaveScoreData;
 };
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 2796f8123..4ef86c6a9 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -420,7 +420,8 @@ class BaseCharacter extends Bopper
     {
       if (isSinging()) return;
 
-      if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return;
+      var currentAnimation:String = getCurrentAnimation();
+      if ((currentAnimation == 'hey' || currentAnimation == 'cheer') && !isAnimationFinished()) return;
     }
 
     // Prevent dancing while another animation is playing.
@@ -441,19 +442,15 @@ class BaseCharacter extends Bopper
     switch (player)
     {
       case 1:
-        return [
-          PlayerSettings.player1.controls.NOTE_LEFT_P,
-          PlayerSettings.player1.controls.NOTE_DOWN_P,
-          PlayerSettings.player1.controls.NOTE_UP_P,
-          PlayerSettings.player1.controls.NOTE_RIGHT_P,
-        ].contains(true);
+        return PlayerSettings.player1.controls.NOTE_LEFT_P
+          || PlayerSettings.player1.controls.NOTE_DOWN_P
+          || PlayerSettings.player1.controls.NOTE_UP_P
+          || PlayerSettings.player1.controls.NOTE_RIGHT_P;
       case 2:
-        return [
-          PlayerSettings.player2.controls.NOTE_LEFT_P,
-          PlayerSettings.player2.controls.NOTE_DOWN_P,
-          PlayerSettings.player2.controls.NOTE_UP_P,
-          PlayerSettings.player2.controls.NOTE_RIGHT_P,
-        ].contains(true);
+        return PlayerSettings.player2.controls.NOTE_LEFT_P
+          || PlayerSettings.player2.controls.NOTE_DOWN_P
+          || PlayerSettings.player2.controls.NOTE_UP_P
+          || PlayerSettings.player2.controls.NOTE_RIGHT_P;
     }
     return false;
   }
@@ -469,19 +466,15 @@ class BaseCharacter extends Bopper
     switch (player)
     {
       case 1:
-        return [
-          PlayerSettings.player1.controls.NOTE_LEFT,
-          PlayerSettings.player1.controls.NOTE_DOWN,
-          PlayerSettings.player1.controls.NOTE_UP,
-          PlayerSettings.player1.controls.NOTE_RIGHT,
-        ].contains(true);
+        return PlayerSettings.player1.controls.NOTE_LEFT
+          || PlayerSettings.player1.controls.NOTE_DOWN
+          || PlayerSettings.player1.controls.NOTE_UP
+          || PlayerSettings.player1.controls.NOTE_RIGHT;
       case 2:
-        return [
-          PlayerSettings.player2.controls.NOTE_LEFT,
-          PlayerSettings.player2.controls.NOTE_DOWN,
-          PlayerSettings.player2.controls.NOTE_UP,
-          PlayerSettings.player2.controls.NOTE_RIGHT,
-        ].contains(true);
+        return PlayerSettings.player2.controls.NOTE_LEFT
+          || PlayerSettings.player2.controls.NOTE_DOWN
+          || PlayerSettings.player2.controls.NOTE_UP
+          || PlayerSettings.player2.controls.NOTE_RIGHT;
     }
     return false;
   }
diff --git a/source/funkin/play/components/ClearPercentCounter.hx b/source/funkin/play/components/ClearPercentCounter.hx
new file mode 100644
index 000000000..e3d3795d9
--- /dev/null
+++ b/source/funkin/play/components/ClearPercentCounter.hx
@@ -0,0 +1,141 @@
+package funkin.play.components;
+
+import funkin.graphics.FunkinSprite;
+import funkin.graphics.shaders.PureColor;
+import flixel.FlxSprite;
+import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.math.FlxMath;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.text.FlxText.FlxTextAlign;
+import funkin.util.MathUtil;
+import flixel.util.FlxColor;
+
+/**
+ * Numerical counters used to display the clear percent.
+ */
+class ClearPercentCounter extends FlxTypedSpriteGroup<FlxSprite>
+{
+  public var curNumber(default, set):Int = 0;
+
+  var numberChanged:Bool = false;
+
+  function set_curNumber(val:Int):Int
+  {
+    numberChanged = true;
+    return curNumber = val;
+  }
+
+  var small:Bool = false;
+  var flashShader:PureColor;
+
+  public function new(x:Float, y:Float, startingNumber:Int = 0, small:Bool = false)
+  {
+    super(x, y);
+
+    flashShader = new PureColor(FlxColor.WHITE);
+    flashShader.colorSet = false;
+
+    curNumber = startingNumber;
+
+    this.small = small;
+
+    var clearPercentText:FunkinSprite = FunkinSprite.create(0, 0, 'resultScreen/clearPercent/clearPercentText${small ? 'Small' : ''}');
+    clearPercentText.x = small ? 40 : 0;
+    add(clearPercentText);
+
+    drawNumbers();
+  }
+
+  /**
+   * Make the counter flash turn white or stop being all white.
+   * @param enabled Whether the counter should be white.
+   */
+  public function flash(enabled:Bool):Void
+  {
+    flashShader.colorSet = enabled;
+  }
+
+  var tmr:Float = 0;
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (numberChanged) drawNumbers();
+  }
+
+  function drawNumbers():Void
+  {
+    var seperatedScore:Array<Int> = [];
+    var tempCombo:Int = Math.round(curNumber);
+
+    while (tempCombo != 0)
+    {
+      seperatedScore.push(tempCombo % 10);
+      tempCombo = Math.floor(tempCombo / 10);
+    }
+
+    if (seperatedScore.length == 0) seperatedScore.push(0);
+
+    seperatedScore.reverse();
+
+    for (ind => num in seperatedScore)
+    {
+      var digitIndex:Int = ind + 1;
+      // If there's only one digit, move it to the right
+      // If there's three digits, move them all to the left
+      var digitOffset = (seperatedScore.length == 1) ? 1 : (seperatedScore.length == 3) ? -1 : 0;
+      var digitSize = small ? 32 : 72;
+      var digitHeightOffset = small ? -4 : 0;
+
+      var xPos = (digitIndex - 1 + digitOffset) * (digitSize * this.scale.x);
+      xPos += small ? -24 : 0;
+      var yPos = (digitIndex - 1 + digitOffset) * (digitHeightOffset * this.scale.y);
+      yPos += small ? 0 : 72;
+
+      if (digitIndex >= members.length)
+      {
+        // Three digits = LLR because the 1 and 0 won't be the same anyway.
+        var variant:Bool = (seperatedScore.length == 3) ? (digitIndex >= 2) : (digitIndex >= 1);
+        // var variant:Bool = (seperatedScore.length % 2 != 0) ? (digitIndex % 2 == 0) : (digitIndex % 2 == 1);
+        var numb:ClearPercentNumber = new ClearPercentNumber(xPos, yPos, num, variant, this.small);
+        numb.scale.set(this.scale.x, this.scale.y);
+        numb.shader = flashShader;
+        numb.visible = true;
+        add(numb);
+      }
+      else
+      {
+        members[digitIndex].animation.play(Std.string(num));
+        // Reset the position of the number
+        members[digitIndex].x = xPos + this.x;
+        members[digitIndex].y = yPos + this.y;
+        members[digitIndex].visible = true;
+      }
+    }
+    for (ind in (seperatedScore.length + 1)...(members.length))
+    {
+      members[ind].visible = false;
+    }
+  }
+}
+
+class ClearPercentNumber extends FlxSprite
+{
+  public function new(x:Float, y:Float, digit:Int, variant:Bool, small:Bool)
+  {
+    super(x, y);
+
+    frames = Paths.getSparrowAtlas('resultScreen/clearPercent/clearPercentNumber${small ? 'Small' : variant ? 'Right' : 'Left'}');
+
+    for (i in 0...10)
+    {
+      animation.addByPrefix('$i', 'number $i 0', 24, false);
+    }
+
+    animation.play('$digit');
+    updateHitbox();
+  }
+}
diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 957daa43c..a3204329a 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -24,7 +24,7 @@ import funkin.util.MathUtil;
  *     - i.e. `PlayState.instance.iconP1.playAnimation("losing")`
  *   - Scripts can also utilize all functionality that a normal FlxSprite would have access to, such as adding supplimental animations.
  *     - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
- * @author MasterEric
+ * @author EliteMasterEric
  */
 @:nullSafety
 class HealthIcon extends FunkinSprite
@@ -373,6 +373,10 @@ class HealthIcon extends FunkinSprite
     // Don't flip BF's icon here! That's done later.
     this.animation.add(Idle, [0], 0, false, false);
     this.animation.add(Losing, [1], 0, false, false);
+    if (animation.numFrames >= 3)
+    {
+      this.animation.add(Winning, [2], 0, false, false);
+    }
   }
 
   function correctCharacterId(charId:Null<String>):String
diff --git a/source/funkin/play/event/ScrollSpeedEvent.hx b/source/funkin/play/event/ScrollSpeedEvent.hx
new file mode 100644
index 000000000..c752d2f6d
--- /dev/null
+++ b/source/funkin/play/event/ScrollSpeedEvent.hx
@@ -0,0 +1,176 @@
+package funkin.play.event;
+
+import flixel.tweens.FlxTween;
+import flixel.FlxCamera;
+import flixel.tweens.FlxEase;
+// Data from the chart
+import funkin.data.song.SongData;
+import funkin.data.song.SongData.SongEventData;
+// Data from the event schema
+import funkin.play.event.SongEvent;
+import funkin.data.event.SongEventSchema;
+import funkin.data.event.SongEventSchema.SongEventFieldType;
+
+/**
+ * This class represents a handler for scroll speed events.
+ *
+ * Example: Scroll speed change of both strums from 1x to 1.3x:
+ * ```
+ * {
+ *   'e': 'ScrollSpeed',
+ *   "v": {
+ *      "scroll": "1.3",
+ *      "duration": "4",
+ *      "ease": "linear",
+ *      "strumline": "both",
+ *      "absolute": false
+ *    }
+ * }
+ * ```
+ */
+class ScrollSpeedEvent extends SongEvent
+{
+  public function new()
+  {
+    super('ScrollSpeed');
+  }
+
+  static final DEFAULT_SCROLL:Float = 1;
+  static final DEFAULT_DURATION:Float = 4.0;
+  static final DEFAULT_EASE:String = 'linear';
+  static final DEFAULT_ABSOLUTE:Bool = false;
+  static final DEFAULT_STRUMLINE:String = 'both'; // my special little trick
+
+  public override function handleEvent(data:SongEventData):Void
+  {
+    // Does nothing if there is no PlayState.
+    if (PlayState.instance == null) return;
+
+    var scroll:Float = data.getFloat('scroll') ?? DEFAULT_SCROLL;
+
+    var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
+
+    var ease:String = data.getString('ease') ?? DEFAULT_EASE;
+
+    var strumline:String = data.getString('strumline') ?? DEFAULT_STRUMLINE;
+
+    var absolute:Bool = data.getBool('absolute') ?? DEFAULT_ABSOLUTE;
+
+    var strumlineNames:Array<String> = [];
+
+    if (!absolute)
+    {
+      // If absolute is set to false, do the awesome multiplicative thing
+      scroll = scroll * (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0);
+    }
+
+    switch (strumline)
+    {
+      case 'both':
+        strumlineNames = ['playerStrumline', 'opponentStrumline'];
+      default:
+        strumlineNames = [strumline + 'Strumline'];
+    }
+    // If it's a string, check the value.
+    switch (ease)
+    {
+      case 'INSTANT':
+        PlayState.instance.tweenScrollSpeed(scroll, 0, null, strumlineNames);
+      default:
+        var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
+        var easeFunction:Null<Float->Float> = Reflect.field(FlxEase, ease);
+        if (easeFunction == null)
+        {
+          trace('Invalid ease function: $ease');
+          return;
+        }
+
+        PlayState.instance.tweenScrollSpeed(scroll, durSeconds, easeFunction, strumlineNames);
+    }
+  }
+
+  public override function getTitle():String
+  {
+    return 'Scroll Speed';
+  }
+
+  /**
+   * ```
+   * {
+   *   'scroll': FLOAT, // Target scroll level.
+   *   'duration': FLOAT, // Duration in steps.
+   *   'ease': ENUM, // Easing function.
+   *   'strumline': ENUM, // Which strumline to change
+   *   'absolute': BOOL, // True to set the scroll speed to the target level, false to set the scroll speed to (target level x base scroll speed)
+   * }
+   * @return SongEventSchema
+   */
+  public override function getEventSchema():SongEventSchema
+  {
+    return new SongEventSchema([
+      {
+        name: 'scroll',
+        title: 'Target Value',
+        defaultValue: 1.0,
+        step: 0.1,
+        type: SongEventFieldType.FLOAT,
+        units: 'x'
+      },
+      {
+        name: 'duration',
+        title: 'Duration',
+        defaultValue: 4.0,
+        step: 0.5,
+        type: SongEventFieldType.FLOAT,
+        units: 'steps'
+      },
+      {
+        name: 'ease',
+        title: 'Easing Type',
+        defaultValue: 'linear',
+        type: SongEventFieldType.ENUM,
+        keys: [
+          'Linear' => 'linear',
+          'Instant (Ignores Duration)' => 'INSTANT',
+          'Sine In' => 'sineIn',
+          'Sine Out' => 'sineOut',
+          'Sine In/Out' => 'sineInOut',
+          'Quad In' => 'quadIn',
+          'Quad Out' => 'quadOut',
+          'Quad In/Out' => 'quadInOut',
+          'Cube In' => 'cubeIn',
+          'Cube Out' => 'cubeOut',
+          'Cube In/Out' => 'cubeInOut',
+          'Quart In' => 'quartIn',
+          'Quart Out' => 'quartOut',
+          'Quart In/Out' => 'quartInOut',
+          'Quint In' => 'quintIn',
+          'Quint Out' => 'quintOut',
+          'Quint In/Out' => 'quintInOut',
+          'Expo In' => 'expoIn',
+          'Expo Out' => 'expoOut',
+          'Expo In/Out' => 'expoInOut',
+          'Smooth Step In' => 'smoothStepIn',
+          'Smooth Step Out' => 'smoothStepOut',
+          'Smooth Step In/Out' => 'smoothStepInOut',
+          'Elastic In' => 'elasticIn',
+          'Elastic Out' => 'elasticOut',
+          'Elastic In/Out' => 'elasticInOut'
+        ]
+      },
+      {
+        name: 'strumline',
+        title: 'Target Strumline',
+        defaultValue: 'both',
+        type: SongEventFieldType.ENUM,
+        keys: ['Both' => 'both', 'Player' => 'player', 'Opponent' => 'opponent']
+      },
+      {
+        name: 'absolute',
+        title: 'Absolute',
+        defaultValue: false,
+        type: SongEventFieldType.BOOL,
+      }
+    ]);
+  }
+}
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 95e0668be..0e4b6645f 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -52,6 +52,14 @@ class Strumline extends FlxSpriteGroup
    */
   public var conductorInUse(get, set):Conductor;
 
+  // Used in-game to control the scroll speed within a song
+  public var scrollSpeed:Float = 1.0;
+
+  public function resetScrollSpeed():Void
+  {
+    scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
+  }
+
   var _conductorInUse:Null<Conductor>;
 
   function get_conductorInUse():Conductor
@@ -134,6 +142,7 @@ class Strumline extends FlxSpriteGroup
     this.refresh();
 
     this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
+    resetScrollSpeed();
 
     for (i in 0...KEY_COUNT)
     {
@@ -171,6 +180,20 @@ class Strumline extends FlxSpriteGroup
     updateNotes();
   }
 
+  /**
+   * Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
+   */
+  public function mayGhostTap():Bool
+  {
+    // TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose.
+    // Also, if you just hit a note, there should be a (short) period where this is off so you can't spam.
+
+    // If there are any notes on screen, we can't ghost tap.
+    return notes.members.filter(function(note:NoteSprite) {
+      return note != null && note.alive && !note.hasBeenHit;
+    }).length == 0;
+  }
+
   /**
    * Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
    * @return An array of `NoteSprite` objects.
@@ -283,7 +306,6 @@ class Strumline extends FlxSpriteGroup
     // var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
     // ^^^ commented this out... do NOT make it move faster as it moves offscreen!
     var vwoosh:Float = 1.0;
-    var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
 
     return
       Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
@@ -406,7 +428,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
@@ -435,7 +457,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
@@ -450,7 +472,7 @@ class Strumline extends FlxSpriteGroup
 
         if (Preferences.downscroll)
         {
-          holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
+          holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
         }
         else
         {
@@ -539,6 +561,7 @@ class Strumline extends FlxSpriteGroup
     {
       playStatic(dir);
     }
+    resetScrollSpeed();
   }
 
   public function applyNoteData(data:Array<SongNoteData>):Void
@@ -705,6 +728,7 @@ class Strumline extends FlxSpriteGroup
 
     if (holdNoteSprite != null)
     {
+      holdNoteSprite.parentStrumline = this;
       holdNoteSprite.noteData = note;
       holdNoteSprite.strumTime = note.time;
       holdNoteSprite.noteDirection = note.getDirection();
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index 056a6a5a9..b358d7f03 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -32,6 +32,7 @@ class SustainTrail extends FlxSprite
   public var sustainLength(default, set):Float = 0; // millis
   public var fullSustainLength:Float = 0;
   public var noteData:Null<SongNoteData>;
+  public var parentStrumline:Strumline;
 
   public var cover:NoteHoldCover = null;
 
@@ -119,7 +120,7 @@ class SustainTrail extends FlxSprite
 
     // CALCULATE SIZE
     graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
-    graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
+    graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0);
     // instead of scrollSpeed, PlayState.SONG.speed
 
     flipY = Preferences.downscroll;
@@ -135,9 +136,21 @@ class SustainTrail extends FlxSprite
     this.active = true; // This NEEDS to be true for the note to be drawn!
   }
 
-  function getScrollSpeed():Float
+  function getBaseScrollSpeed()
   {
-    return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0;
+    return (PlayState.instance?.currentChart?.scrollSpeed ?? 1.0);
+  }
+
+  var previousScrollSpeed:Float = 1;
+
+  override function update(elapsed)
+  {
+    super.update(elapsed);
+    if (previousScrollSpeed != (parentStrumline?.scrollSpeed ?? 1.0))
+    {
+      triggerRedraw();
+    }
+    previousScrollSpeed = parentStrumline?.scrollSpeed ?? 1.0;
   }
 
   /**
@@ -155,12 +168,16 @@ class SustainTrail extends FlxSprite
     if (s < 0.0) s = 0.0;
 
     if (sustainLength == s) return s;
-
-    graphicHeight = sustainHeight(s, getScrollSpeed());
     this.sustainLength = s;
+    triggerRedraw();
+    return this.sustainLength;
+  }
+
+  function triggerRedraw()
+  {
+    graphicHeight = sustainHeight(sustainLength, parentStrumline?.scrollSpeed ?? 1.0);
     updateClipping();
     updateHitbox();
-    return this.sustainLength;
   }
 
   public override function updateHitbox():Void
@@ -178,7 +195,7 @@ class SustainTrail extends FlxSprite
    */
   public function updateClipping(songTime:Float = 0):Void
   {
-    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
+    var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
     if (clipHeight <= 0.1)
     {
       visible = false;
diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx
index 744091b44..d6f71fc7e 100644
--- a/source/funkin/play/scoring/Scoring.hx
+++ b/source/funkin/play/scoring/Scoring.hx
@@ -1,5 +1,7 @@
 package funkin.play.scoring;
 
+import funkin.save.Save.SaveScoreData;
+
 /**
  * Which system to use when scoring and judging notes.
  */
@@ -344,4 +346,303 @@ class Scoring
       return 'miss';
     }
   }
+
+  public static function calculateRank(scoreData:Null<SaveScoreData>):Null<ScoringRank>
+  {
+    if (scoreData?.tallies.totalNotes == 0 || scoreData == null) return null;
+
+    // we can return null here, meaning that the player hasn't actually played and finished the song (thus has no data)
+    if (scoreData.tallies.totalNotes == 0) return null;
+
+    // Perfect (Platinum) is a Sick Full Clear
+    var isPerfectGold = scoreData.tallies.sick == scoreData.tallies.totalNotes;
+    if (isPerfectGold) return ScoringRank.PERFECT_GOLD;
+
+    // Else, use the standard grades
+
+    // Grade % (only good and sick), 1.00 is a full combo
+    var grade = (scoreData.tallies.sick + scoreData.tallies.good) / scoreData.tallies.totalNotes;
+    // Clear % (including bad and shit). 1.00 is a full clear but not a full combo
+    var clear = (scoreData.tallies.totalNotesHit) / scoreData.tallies.totalNotes;
+
+    if (grade == Constants.RANK_PERFECT_THRESHOLD)
+    {
+      return ScoringRank.PERFECT;
+    }
+    else if (grade >= Constants.RANK_EXCELLENT_THRESHOLD)
+    {
+      return ScoringRank.EXCELLENT;
+    }
+    else if (grade >= Constants.RANK_GREAT_THRESHOLD)
+    {
+      return ScoringRank.GREAT;
+    }
+    else if (grade >= Constants.RANK_GOOD_THRESHOLD)
+    {
+      return ScoringRank.GOOD;
+    }
+    else
+    {
+      return ScoringRank.SHIT;
+    }
+  }
+}
+
+enum abstract ScoringRank(String)
+{
+  var PERFECT_GOLD;
+  var PERFECT;
+  var EXCELLENT;
+  var GREAT;
+  var GOOD;
+  var SHIT;
+
+  @:op(A > B) static function compare(a:Null<ScoringRank>, b:Null<ScoringRank>):Bool
+  {
+    if (a != null && b == null) return true;
+    if (a == null || b == null) return false;
+
+    var temp1:Int = 0;
+    var temp2:Int = 0;
+
+    // temp 1
+    switch (a)
+    {
+      case PERFECT_GOLD:
+        temp1 = 5;
+      case PERFECT:
+        temp1 = 4;
+      case EXCELLENT:
+        temp1 = 3;
+      case GREAT:
+        temp1 = 2;
+      case GOOD:
+        temp1 = 1;
+      case SHIT:
+        temp1 = 0;
+      default:
+        temp1 = -1;
+    }
+
+    // temp 2
+    switch (b)
+    {
+      case PERFECT_GOLD:
+        temp2 = 5;
+      case PERFECT:
+        temp2 = 4;
+      case EXCELLENT:
+        temp2 = 3;
+      case GREAT:
+        temp2 = 2;
+      case GOOD:
+        temp2 = 1;
+      case SHIT:
+        temp2 = 0;
+      default:
+        temp2 = -1;
+    }
+
+    if (temp1 > temp2)
+    {
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+  /**
+   * Delay in seconds
+   */
+  public function getMusicDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 95/24;
+      case EXCELLENT:
+        return 0;
+      case GREAT:
+        return 5/24;
+      case GOOD:
+        return 3/24;
+      case SHIT:
+        return 2/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getBFDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 95/24;
+      case EXCELLENT:
+        return 97/24;
+      case GREAT:
+        return 95/24;
+      case GOOD:
+        return 95/24;
+      case SHIT:
+        return 95/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getFlashDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 129/24;
+      case EXCELLENT:
+        return 122/24;
+      case GREAT:
+        return 109/24;
+      case GOOD:
+        return 107/24;
+      case SHIT:
+        return 186/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getHighscoreDelay():Float
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT:
+        // return 2.5;
+        return 140/24;
+      case EXCELLENT:
+        return 140/24;
+      case GREAT:
+        return 129/24;
+      case GOOD:
+        return 127/24;
+      case SHIT:
+        return 207/24;
+      default:
+        return 3.5;
+    }
+  }
+
+  public function getMusicPath():String
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultsPERFECT';
+      case PERFECT:
+        return 'resultsPERFECT';
+      case EXCELLENT:
+        return 'resultsEXCELLENT';
+      case GREAT:
+        return 'resultsNORMAL';
+      case GOOD:
+        return 'resultsNORMAL';
+      case SHIT:
+        return 'resultsSHIT';
+      default:
+        return 'resultsNORMAL';
+    }
+  }
+
+  public function hasMusicIntro():Bool
+  {
+    switch (abstract)
+    {
+      case EXCELLENT:
+        return true;
+      case SHIT:
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  public function getFreeplayRankIconAsset():Null<String>
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'PERFECTSICK';
+      case PERFECT:
+        return 'PERFECT';
+      case EXCELLENT:
+        return 'EXCELLENT';
+      case GREAT:
+        return 'GREAT';
+      case GOOD:
+        return 'GOOD';
+      case SHIT:
+        return 'LOSS';
+      default:
+        return null;
+    }
+  }
+
+  public function shouldMusicLoop():Bool
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD | PERFECT | EXCELLENT | GREAT | GOOD:
+        return true;
+      case SHIT:
+        return false;
+      default:
+        return false;
+    }
+  }
+
+  public function getHorTextAsset()
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultScreen/rankText/rankScrollPERFECT';
+      case PERFECT:
+        return 'resultScreen/rankText/rankScrollPERFECT';
+      case EXCELLENT:
+        return 'resultScreen/rankText/rankScrollEXCELLENT';
+      case GREAT:
+        return 'resultScreen/rankText/rankScrollGREAT';
+      case GOOD:
+        return 'resultScreen/rankText/rankScrollGOOD';
+      case SHIT:
+        return 'resultScreen/rankText/rankScrollLOSS';
+      default:
+        return 'resultScreen/rankText/rankScrollGOOD';
+    }
+  }
+
+  public function getVerTextAsset()
+  {
+    switch (abstract)
+    {
+      case PERFECT_GOLD:
+        return 'resultScreen/rankText/rankTextPERFECT';
+      case PERFECT:
+        return 'resultScreen/rankText/rankTextPERFECT';
+      case EXCELLENT:
+        return 'resultScreen/rankText/rankTextEXCELLENT';
+      case GREAT:
+        return 'resultScreen/rankText/rankTextGREAT';
+      case GOOD:
+        return 'resultScreen/rankText/rankTextGOOD';
+      case SHIT:
+        return 'resultScreen/rankText/rankTextLOSS';
+      default:
+        return 'resultScreen/rankText/rankTextGOOD';
+    }
+  }
 }
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index e71ae3213..df3e343e2 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -91,6 +91,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return _metadata.keys().array();
   }
 
+  // this returns false so that any new song can override this and return true when needed
+  public function isSongNew(currentDifficulty:String):Bool
+  {
+    return false;
+  }
+
   /**
    * Set to false if the song was edited in the charter and should not be saved as a high score.
    */
@@ -120,6 +126,18 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return DEFAULT_ARTIST;
   }
 
+  /**
+   * The artist of the song.
+   */
+  public var charter(get, never):String;
+
+  function get_charter():String
+  {
+    if (_data != null) return _data?.charter ?? 'Unknown';
+    if (_metadata.size() > 0) return _metadata.get(Constants.DEFAULT_VARIATION)?.charter ?? 'Unknown';
+    return Constants.DEFAULT_CHARTER;
+  }
+
   /**
    * @param id The ID of the song to load.
    * @param ignoreErrors If false, an exception will be thrown if the song data could not be loaded.
@@ -270,6 +288,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
 
         difficulty.songName = metadata.songName;
         difficulty.songArtist = metadata.artist;
+        difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
         difficulty.timeFormat = metadata.timeFormat;
         difficulty.divisions = metadata.divisions;
         difficulty.timeChanges = metadata.timeChanges;
@@ -334,6 +353,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
         {
           difficulty.songName = metadata.songName;
           difficulty.songArtist = metadata.artist;
+          difficulty.charter = metadata.charter ?? Constants.DEFAULT_CHARTER;
           difficulty.timeFormat = metadata.timeFormat;
           difficulty.divisions = metadata.divisions;
           difficulty.timeChanges = metadata.timeChanges;
@@ -364,7 +384,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
    */
   public function getDifficulty(?diffId:String, ?variation:String, ?variations:Array<String>):Null<SongDifficulty>
   {
-    if (diffId == null) diffId = listDifficulties(variation)[0];
+    if (diffId == null) diffId = listDifficulties(variation, variations)[0];
     if (variation == null) variation = Constants.DEFAULT_VARIATION;
     if (variations == null) variations = [variation];
 
@@ -399,6 +419,27 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return null;
   }
 
+  /**
+   * Given that this character is selected in the Freeplay menu,
+   * which variations should be available?
+   * @param charId The character ID to query.
+   * @return An array of available variations.
+   */
+  public function getVariationsByCharId(?charId:String):Array<String>
+  {
+    if (charId == null) charId = Constants.DEFAULT_CHARACTER;
+
+    if (variations.contains(charId))
+    {
+      return [charId];
+    }
+    else
+    {
+      // TODO: How to exclude character variations while keeping other custom variations?
+      return variations;
+    }
+  }
+
   /**
    * List all the difficulties in this song.
    *
@@ -418,12 +459,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     // so we have to map it to the actual difficulty names.
     // We also filter out difficulties that don't match the variation or that don't exist.
 
-    var diffFiltered:Array<String> = difficulties.keys().array().map(function(diffId:String):Null<String> {
-      var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
-      if (difficulty == null) return null;
-      if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
-      return difficulty.difficulty;
-    }).nonNull().unique();
+    var diffFiltered:Array<String> = difficulties.keys()
+      .array()
+      .map(function(diffId:String):Null<String> {
+        var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
+        if (difficulty == null) return null;
+        if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
+        return difficulty.difficulty;
+      })
+      .filterNull()
+      .distinct();
 
     diffFiltered = diffFiltered.filter(function(diffId:String):Bool {
       if (showHidden) return true;
@@ -565,6 +610,7 @@ class SongDifficulty
 
   public var songName:String = Constants.DEFAULT_SONGNAME;
   public var songArtist:String = Constants.DEFAULT_ARTIST;
+  public var charter:String = Constants.DEFAULT_CHARTER;
   public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT;
   public var divisions:Null<Int> = null;
   public var looped:Bool = false;
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index eb9eb1810..4f8ab4434 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -124,7 +124,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
       getGirlfriend().resetCharacter(true);
       // Reapply the camera offsets.
       var stageCharData:StageDataCharacter = _data.characters.gf;
-      var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
+      var finalScale:Float = getGirlfriend().getBaseScale() * stageCharData.scale;
       getGirlfriend().setScale(finalScale);
       getGirlfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
       getGirlfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
@@ -134,7 +134,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
       getDad().resetCharacter(true);
       // Reapply the camera offsets.
       var stageCharData:StageDataCharacter = _data.characters.dad;
-      var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
+      var finalScale:Float = getDad().getBaseScale() * stageCharData.scale;
       getDad().setScale(finalScale);
       getDad().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
       getDad().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
@@ -852,6 +852,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
     }
   }
 
+  public override function toString():String
+  {
+    return 'Stage($id)';
+  }
+
   static function _fetchData(id:String):Null<StageData>
   {
     return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index acbe59edd..2ff6b96cc 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -1,21 +1,22 @@
 package funkin.save;
 
 import flixel.util.FlxSave;
-import funkin.save.migrator.SaveDataMigrator;
-import thx.semver.Version;
 import funkin.input.Controls.Device;
+import funkin.play.scoring.Scoring;
+import funkin.play.scoring.Scoring.ScoringRank;
 import funkin.save.migrator.RawSaveData_v1_0_0;
 import funkin.save.migrator.SaveDataMigrator;
+import funkin.save.migrator.SaveDataMigrator;
 import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
 import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
-import thx.semver.Version;
 import funkin.util.SerializerUtil;
+import thx.semver.Version;
+import thx.semver.Version;
 
 @:nullSafety
 class Save
 {
-  // Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
-  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.3";
+  public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.5";
   public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
 
   // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@@ -53,7 +54,11 @@ class Save
   public function new(?data:RawSaveData)
   {
     if (data == null) this.data = Save.getDefault();
-    else this.data = data;
+    else
+      this.data = data;
+
+    // Make sure the verison number is up to date before we flush.
+    this.data.version = Save.SAVE_DATA_VERSION;
   }
 
   public static function getDefault():RawSaveData
@@ -77,6 +82,9 @@ class Save
           levels: [],
           songs: [],
         },
+
+      favoriteSongs: [],
+
       options:
         {
           // Reasonable defaults.
@@ -489,6 +497,11 @@ class Save
     return song.get(difficultyId);
   }
 
+  public function getSongRank(songId:String, difficultyId:String = 'normal'):Null<ScoringRank>
+  {
+    return Scoring.calculateRank(getSongScore(songId, difficultyId));
+  }
+
   /**
    * Apply the score the user achieved for a given song on a given difficulty.
    */
@@ -554,6 +567,35 @@ class Save
     return false;
   }
 
+  public function isSongFavorited(id:String):Bool
+  {
+    if (data.favoriteSongs == null)
+    {
+      data.favoriteSongs = [];
+      flush();
+    };
+
+    return data.favoriteSongs.contains(id);
+  }
+
+  public function favoriteSong(id:String):Void
+  {
+    if (!isSongFavorited(id))
+    {
+      data.favoriteSongs.push(id);
+      flush();
+    }
+  }
+
+  public function unfavoriteSong(id:String):Void
+  {
+    if (isSongFavorited(id))
+    {
+      data.favoriteSongs.remove(id);
+      flush();
+    }
+  }
+
   public function getControls(playerId:Int, inputType:Device):Null<SaveControlsData>
   {
     switch (inputType)
@@ -674,7 +716,6 @@ class Save
       {
         trace('[SAVE] Found legacy save data, converting...');
         var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData);
-        @:privateAccess
         FlxG.save.mergeData(gameSave.data, true);
       }
       else
@@ -686,13 +727,94 @@ class Save
     }
     else
     {
-      trace('[SAVE] Loaded save data.');
-      @:privateAccess
+      trace('[SAVE] Found existing save data.');
       var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
       FlxG.save.mergeData(gameSave.data, true);
     }
   }
 
+  public static function archiveBadSaveData(data:Dynamic):Int
+  {
+    // We want to save this somewhere so we can try to recover it for the user in the future!
+
+    final RECOVERY_SLOT_START = 1000;
+
+    return writeToAvailableSlot(RECOVERY_SLOT_START, data);
+  }
+
+  public static function debug_queryBadSaveData():Void
+  {
+    final RECOVERY_SLOT_START = 1000;
+    final RECOVERY_SLOT_END = 1100;
+    var firstBadSaveData = querySlotRange(RECOVERY_SLOT_START, RECOVERY_SLOT_END);
+    if (firstBadSaveData > 0)
+    {
+      trace('[SAVE] Found bad save data in slot ${firstBadSaveData}!');
+      trace('We should look into recovery...');
+
+      trace(haxe.Json.stringify(fetchFromSlotRaw(firstBadSaveData)));
+    }
+  }
+
+  static function fetchFromSlotRaw(slot:Int):Null<Dynamic>
+  {
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    if (targetSaveData.isEmpty()) return null;
+    return targetSaveData.data;
+  }
+
+  static function writeToAvailableSlot(slot:Int, data:Dynamic):Int
+  {
+    trace('[SAVE] Finding slot to write data to (starting with ${slot})...');
+
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    while (!targetSaveData.isEmpty())
+    {
+      // Keep trying to bind to slots until we find an empty slot.
+      trace('[SAVE] Slot ${slot} is taken, continuing...');
+      slot++;
+      targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    }
+
+    trace('[SAVE] Writing data to slot ${slot}...');
+    targetSaveData.mergeData(data, true);
+
+    trace('[SAVE] Data written to slot ${slot}!');
+    return slot;
+  }
+
+  /**
+   * Return true if the given save slot is not empty.
+   * @param slot The slot number to check.
+   * @return Whether the slot is not empty.
+   */
+  static function querySlot(slot:Int):Bool
+  {
+    var targetSaveData = new FlxSave();
+    targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH);
+    return !targetSaveData.isEmpty();
+  }
+
+  /**
+   * Return true if any of the slots in the given range is not empty.
+   * @param start The starting slot number to check.
+   * @param end The ending slot number to check.
+   * @return The first slot in the range that is not empty, or `-1` if none are.
+   */
+  static function querySlotRange(start:Int, end:Int):Int
+  {
+    for (i in start...end)
+    {
+      if (querySlot(i))
+      {
+        return i;
+      }
+    }
+    return -1;
+  }
+
   static function fetchLegacySaveData():Null<RawSaveData_v1_0_0>
   {
     trace("[SAVE] Checking for legacy save data...");
@@ -714,6 +836,7 @@ class Save
 
 /**
  * An anonymous structure containingg all the user's save data.
+ * Isn't stored with JSON, stored with some sort of Haxe built-in serialization?
  */
 typedef RawSaveData =
 {
@@ -724,8 +847,6 @@ typedef RawSaveData =
   /**
    * A semantic versioning string for the save data format.
    */
-  @:jcustomparse(funkin.data.DataParse.semverVersion)
-  @:jcustomwrite(funkin.data.DataWrite.semverVersion)
   var version:Version;
 
   var api:SaveApiData;
@@ -740,6 +861,12 @@ typedef RawSaveData =
    */
   var options:SaveDataOptions;
 
+  /**
+   * The user's favorited songs in the Freeplay menu,
+   * as a list of song IDs.
+   */
+  var favoriteSongs:Array<String>;
+
   var mods:SaveDataMods;
 
   /**
@@ -809,11 +936,6 @@ typedef SaveScoreData =
    * The count of each judgement hit.
    */
   var tallies:SaveScoreTallyData;
-
-  /**
-   * The accuracy percentage.
-   */
-  var accuracy:Float;
 }
 
 typedef SaveScoreTallyData =
diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md
index 3fa9839d1..e3038373d 100644
--- a/source/funkin/save/changelog.md
+++ b/source/funkin/save/changelog.md
@@ -5,6 +5,13 @@ All notable changes to this project 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).
 
+## [2.0.5] - 2024-05-21
+### Fixed
+- Resolved an issue where HTML5 wouldn't store the semantic version properly, causing the game to fail to load the save.
+
+## [2.0.4] - 2024-05-21
+### Added
+- `favoriteSongs:Array<String>` to `Save`
 
 ## [2.0.3] - 2024-01-09
 ### Added
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 3ed59e726..7a929322a 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -3,7 +3,6 @@ package funkin.save.migrator;
 import funkin.save.Save;
 import funkin.save.migrator.RawSaveData_v1_0_0;
 import thx.semver.Version;
-import funkin.util.StructureUtil;
 import funkin.util.VersionUtil;
 
 @:nullSafety
@@ -24,16 +23,21 @@ class SaveDataMigrator
     }
     else
     {
+      // Sometimes the Haxe serializer has issues with the version so we fix it here.
+      version = VersionUtil.repairVersion(version);
       if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
       {
-        // Simply import the structured data.
-        var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
+        // Import the structured data.
+        var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
+        var save:Save = new Save(saveDataWithDefaults);
         return save;
       }
       else
       {
-        trace('[SAVE] Invalid save data version! Returning blank data.');
-        trace(inputData);
+        var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
+        var slot:Int = Save.archiveBadSaveData(inputData);
+        var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.';
+        lime.app.Application.current.window.alert(fullMessage, "Save Data Failure");
         return new Save(Save.getDefault());
       }
     }
@@ -118,7 +122,7 @@ class SaveDataMigrator
     var scoreDataEasy:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}-easy') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -137,7 +141,7 @@ class SaveDataMigrator
     var scoreDataNormal:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -156,7 +160,7 @@ class SaveDataMigrator
     var scoreDataHard:SaveScoreData =
       {
         score: inputSaveData.songScores.get('${levelId}-hard') ?? 0,
-        accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
+        // accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
         tallies:
           {
             sick: 0,
@@ -178,7 +182,6 @@ class SaveDataMigrator
     var scoreDataEasy:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -196,14 +199,13 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0));
-      scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
+      // scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'easy', scoreDataEasy);
 
     var scoreDataNormal:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -221,14 +223,13 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0));
-      scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
+      // scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'normal', scoreDataNormal);
 
     var scoreDataHard:SaveScoreData =
       {
         score: 0,
-        accuracy: 0,
         tallies:
           {
             sick: 0,
@@ -246,7 +247,7 @@ class SaveDataMigrator
     for (songId in songIds)
     {
       scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0));
-      scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
+      // scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
     }
     result.setSongScore(songIds[0], 'hard', scoreDataHard);
   }
diff --git a/source/funkin/ui/credits/CreditsDataHandler.hx b/source/funkin/ui/credits/CreditsDataHandler.hx
index 2240ec50e..844d0f4db 100644
--- a/source/funkin/ui/credits/CreditsDataHandler.hx
+++ b/source/funkin/ui/credits/CreditsDataHandler.hx
@@ -54,7 +54,7 @@ class CreditsDataHandler
           body: [
             {line: 'ninjamuffin99'},
             {line: 'PhantomArcade'},
-            {line: 'KawaiSprite'},
+            {line: 'Kawai Sprite'},
             {line: 'evilsk8r'},
           ]
         }
diff --git a/source/funkin/ui/credits/CreditsState.hx b/source/funkin/ui/credits/CreditsState.hx
index 6be1fecf7..44769e9b3 100644
--- a/source/funkin/ui/credits/CreditsState.hx
+++ b/source/funkin/ui/credits/CreditsState.hx
@@ -4,6 +4,7 @@ import flixel.text.FlxText;
 import flixel.util.FlxColor;
 import funkin.audio.FunkinSound;
 import flixel.FlxSprite;
+import funkin.ui.mainmenu.MainMenuState;
 import flixel.group.FlxSpriteGroup;
 
 /**
@@ -199,7 +200,7 @@ class CreditsState extends MusicBeatState
 
   function exit():Void
   {
-    FlxG.switchState(funkin.ui.mainmenu.MainMenuState.new);
+    FlxG.switchState(() -> new MainMenuState());
   }
 
   public override function destroy():Void
diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index 6d6e73e80..f8b1be9d2 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -62,7 +62,6 @@ class DebugMenuSubState extends MusicBeatSubState
     #if sys
     createItem("OPEN CRASH LOG FOLDER", openLogFolder);
     #end
-    FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y));
     FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500));
   }
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index b75cd8bf1..1e3f011c1 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -137,7 +137,7 @@ using Lambda;
  *
  * Some functionality is split into handler classes to help maintain my sanity.
  *
- * @author MasterEric
+ * @author EliteMasterEric
  */
 // @:nullSafety
 
@@ -1270,7 +1270,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     var result:Null<SongMetadata> = songMetadata.get(selectedVariation);
     if (result == null)
     {
-      result = new SongMetadata('DadBattle', 'Kawai Sprite', selectedVariation);
+      result = new SongMetadata('Default Song Name', Constants.DEFAULT_ARTIST, selectedVariation);
       songMetadata.set(selectedVariation, result);
     }
     return result;
@@ -4566,8 +4566,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             }
 
             gridGhostHoldNote.visible = true;
-            gridGhostHoldNote.noteData = gridGhostNote.noteData;
-            gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
+            gridGhostHoldNote.noteData = currentPlaceNoteData;
+            gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
 
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@@ -6304,7 +6304,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
       tempNote.noteData = noteData;
       tempNote.scrollFactor.set(0, 0);
-      var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
+      var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', false, 0);
       dispatchEvent(event);
 
       // Calling event.cancelEvent() skips all the other logic! Neat!
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index aeb6dd0e4..ded48abe3 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -36,6 +36,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
     zoom *= 0.7;
     zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
 
+    flipY = false;
+
     setup();
   }
 
@@ -58,11 +60,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
   {
     if (lerp)
     {
-      sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
+      sustainLength = FlxMath.lerp(sustainLength, h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
     }
     else
     {
-      sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
+      sustainLength = h / (getBaseScrollSpeed() * Constants.PIXELS_PER_MS);
     }
 
     fullSustainLength = sustainLength;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 0308cd871..e84f7ec43 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -384,17 +384,34 @@ class ChartEditorImportExportHandler
       if (variationId == '')
       {
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
-        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
+        if (variationMetadata != null)
+        {
+          variationMetadata.version = funkin.data.song.SongRegistry.SONG_METADATA_VERSION;
+          variationMetadata.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
+        }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
-        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
+        if (variationChart != null)
+        {
+          variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION;
+          variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
+        }
       }
       else
       {
         var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
-        if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json',
-          variationMetadata.serialize()));
+        if (variationMetadata != null)
+        {
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', variationMetadata.serialize()));
+        }
         var variationChart:Null<SongChartData> = state.songChartData.get(variation);
-        if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
+        if (variationChart != null)
+        {
+          variationChart.version = funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION;
+          variationChart.generatedBy = funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY;
+          zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize()));
+        }
       }
     }
 
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index f85307c64..c97e8142d 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -29,6 +29,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 {
   var inputSongName:TextField;
   var inputSongArtist:TextField;
+  var inputSongCharter:TextField;
   var inputStage:DropDown;
   var inputNoteStyle:DropDown;
   var buttonCharacterPlayer:Button;
@@ -89,6 +90,20 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       }
     };
 
+    inputSongCharter.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
+
+      if (valid)
+      {
+        inputSongCharter.removeClass('invalid-value');
+        chartEditorState.currentSongMetadata.charter = event.target.text;
+      }
+      else
+      {
+        chartEditorState.currentSongMetadata.charter = null;
+      }
+    };
+
     inputStage.onChange = function(event:UIEvent) {
       var valid:Bool = event.data != null && event.data.id != null;
 
@@ -104,6 +119,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       if (event.data?.id == null) return;
       chartEditorState.currentSongNoteStyle = event.data.id;
     };
+    var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle);
+    inputNoteStyle.value = startingValueNoteStyle;
 
     inputBPM.onChange = function(event:UIEvent) {
       if (event.value == null || event.value <= 0) return;
@@ -176,6 +193,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 
     inputSongName.value = chartEditorState.currentSongMetadata.songName;
     inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
+    inputSongCharter.value = chartEditorState.currentSongMetadata.charter;
     inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
     inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
     inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
index 35facf131..49c588722 100644
--- a/source/funkin/ui/freeplay/AlbumRoll.hx
+++ b/source/funkin/ui/freeplay/AlbumRoll.hx
@@ -38,7 +38,7 @@ class AlbumRoll extends FlxSpriteGroup
 
   var newAlbumArt:FlxAtlasSprite;
 
-  // var difficultyStars:DifficultyStars;
+  var difficultyStars:DifficultyStars;
   var _exitMovers:Null<FreeplayState.ExitMoverData>;
 
   var albumData:Album;
@@ -65,9 +65,9 @@ class AlbumRoll extends FlxSpriteGroup
 
     add(newAlbumArt);
 
-    // difficultyStars = new DifficultyStars(140, 39);
-    // difficultyStars.stars.visible = false;
-    // add(difficultyStars);
+    difficultyStars = new DifficultyStars(140, 39);
+    difficultyStars.visible = false;
+    add(difficultyStars);
   }
 
   function onAlbumFinish(animName:String):Void
@@ -86,9 +86,14 @@ class AlbumRoll extends FlxSpriteGroup
   {
     if (albumId == null)
     {
-      // difficultyStars.stars.visible = false;
+      this.visible = false;
+      difficultyStars.stars.visible = false;
       return;
     }
+    else
+    {
+      this.visible = true;
+    }
 
     albumData = AlbumRegistry.instance.fetchEntry(albumId);
 
@@ -126,7 +131,7 @@ class AlbumRoll extends FlxSpriteGroup
 
     if (exitMovers == null) return;
 
-    exitMovers.set([newAlbumArt],
+    exitMovers.set([newAlbumArt, difficultyStars],
       {
         x: FlxG.width,
         speed: 0.4,
@@ -144,10 +149,10 @@ class AlbumRoll extends FlxSpriteGroup
     newAlbumArt.visible = true;
     newAlbumArt.playAnimation(animNames.get('$albumId-active'), false, false, false);
 
-    // difficultyStars.stars.visible = false;
+    difficultyStars.visible = false;
     new FlxTimer().start(0.75, function(_) {
       // showTitle();
-      // showStars();
+      showStars();
     });
   }
 
@@ -156,16 +161,18 @@ class AlbumRoll extends FlxSpriteGroup
     newAlbumArt.playAnimation(animNames.get('$albumId-trans'), false, false, false);
   }
 
-  // public function setDifficultyStars(?difficulty:Int):Void
-  // {
-  //   if (difficulty == null) return;
-  //   difficultyStars.difficulty = difficulty;
-  // }
-  // /**
-  //  * Make the album stars visible.
-  //  */
-  // public function showStars():Void
-  // {
-  //   difficultyStars.stars.visible = false; // true;
-  // }
+  public function setDifficultyStars(?difficulty:Int):Void
+  {
+    if (difficulty == null) return;
+    difficultyStars.difficulty = difficulty;
+  }
+
+  /**
+   * Make the album stars visible.
+   */
+  public function showStars():Void
+  {
+    difficultyStars.visible = true; // true;
+    difficultyStars.flameCheck();
+  }
 }
diff --git a/source/funkin/ui/freeplay/CapsuleText.hx b/source/funkin/ui/freeplay/CapsuleText.hx
index 3a520e015..c3bcdb09b 100644
--- a/source/funkin/ui/freeplay/CapsuleText.hx
+++ b/source/funkin/ui/freeplay/CapsuleText.hx
@@ -4,6 +4,12 @@ import openfl.filters.BitmapFilterQuality;
 import flixel.text.FlxText;
 import flixel.group.FlxSpriteGroup;
 import funkin.graphics.shaders.GaussianBlurShader;
+import funkin.graphics.shaders.LeftMaskShader;
+import flixel.math.FlxRect;
+import flixel.tweens.FlxEase;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxTween;
+import openfl.display.BlendMode;
 
 class CapsuleText extends FlxSpriteGroup
 {
@@ -13,6 +19,15 @@ class CapsuleText extends FlxSpriteGroup
 
   public var text(default, set):String;
 
+  var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
+
+  public var clipWidth(default, set):Int = 255;
+
+  public var tooLong:Bool = false;
+
+  // 255, 27 normal
+  // 220, 27 favourited
+
   public function new(x:Float, y:Float, songTitle:String, size:Float)
   {
     super(x, y);
@@ -36,6 +51,41 @@ class CapsuleText extends FlxSpriteGroup
     return text;
   }
 
+  // ???? none
+  // 255, 27 normal
+  // 220, 27 favourited
+
+  function set_clipWidth(value:Int):Int
+  {
+    resetText();
+    checkClipWidth(value);
+    return clipWidth = value;
+  }
+
+  /**
+   * Checks if the text if it's too long, and clips if it is
+   * @param wid
+   */
+  function checkClipWidth(?wid:Int):Void
+  {
+    if (wid == null) wid = clipWidth;
+
+    if (whiteText.width > wid)
+    {
+      tooLong = true;
+
+      blurredText.clipRect = new FlxRect(0, 0, wid, blurredText.height);
+      whiteText.clipRect = new FlxRect(0, 0, wid, whiteText.height);
+    }
+    else
+    {
+      tooLong = false;
+
+      blurredText.clipRect = null;
+      whiteText.clipRect = null;
+    }
+  }
+
   function set_text(value:String):String
   {
     if (value == null) return value;
@@ -47,10 +97,107 @@ class CapsuleText extends FlxSpriteGroup
 
     blurredText.text = value;
     whiteText.text = value;
+    checkClipWidth();
     whiteText.textField.filters = [
       new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
       // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
     ];
+
     return text = value;
   }
+
+  var moveTimer:FlxTimer = new FlxTimer();
+  var moveTween:FlxTween;
+
+  public function initMove():Void
+  {
+    moveTimer.start(0.6, (timer) -> {
+      moveTextRight();
+    });
+  }
+
+  function moveTextRight():Void
+  {
+    var distToMove:Float = whiteText.width - clipWidth;
+    moveTween = FlxTween.tween(whiteText.offset, {x: distToMove}, 2,
+      {
+        onUpdate: function(_) {
+          whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height);
+          blurredText.offset = whiteText.offset;
+          blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, blurredText.height);
+        },
+        onComplete: function(_) {
+          moveTimer.start(0.3, (timer) -> {
+            moveTextLeft();
+          });
+        },
+        ease: FlxEase.sineInOut
+      });
+  }
+
+  function moveTextLeft():Void
+  {
+    moveTween = FlxTween.tween(whiteText.offset, {x: 0}, 2,
+      {
+        onUpdate: function(_) {
+          whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height);
+          blurredText.offset = whiteText.offset;
+          blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, blurredText.height);
+        },
+        onComplete: function(_) {
+          moveTimer.start(0.3, (timer) -> {
+            moveTextRight();
+          });
+        },
+        ease: FlxEase.sineInOut
+      });
+  }
+
+  public function resetText():Void
+  {
+    if (moveTween != null) moveTween.cancel();
+    if (moveTimer != null) moveTimer.cancel();
+    whiteText.offset.x = 0;
+    whiteText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height);
+    blurredText.clipRect = new FlxRect(whiteText.offset.x, 0, clipWidth, whiteText.height);
+  }
+
+  var flickerState:Bool = false;
+  var flickerTimer:FlxTimer;
+
+  public function flickerText():Void
+  {
+    resetText();
+    flickerTimer = new FlxTimer().start(1 / 24, flickerProgress, 19);
+  }
+
+  function flickerProgress(timer:FlxTimer):Void
+  {
+    if (flickerState == true)
+    {
+      whiteText.blend = BlendMode.ADD;
+      blurredText.blend = BlendMode.ADD;
+      blurredText.color = 0xFFFFFFFF;
+      whiteText.color = 0xFFFFFFFF;
+      whiteText.textField.filters = [
+        new openfl.filters.GlowFilter(0xFFFFFF, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
+        // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
+      ];
+    }
+    else
+    {
+      blurredText.color = 0xFF00aadd;
+      whiteText.color = 0xFFDDDDDD;
+      whiteText.textField.filters = [
+        new openfl.filters.GlowFilter(0xDDDDDD, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
+        // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
+      ];
+    }
+    flickerState = !flickerState;
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+  }
 }
diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx
index 5f1144fab..bd2f73e42 100644
--- a/source/funkin/ui/freeplay/DJBoyfriend.hx
+++ b/source/funkin/ui/freeplay/DJBoyfriend.hx
@@ -27,8 +27,8 @@ class DJBoyfriend extends FlxAtlasSprite
 
   var gotSpooked:Bool = false;
 
-  static final SPOOK_PERIOD:Float = 120.0;
-  static final TV_PERIOD:Float = 180.0;
+  static final SPOOK_PERIOD:Float = 60.0;
+  static final TV_PERIOD:Float = 120.0;
 
   // Time since dad last SPOOKED you.
   var timeSinceSpook:Float = 0;
@@ -82,6 +82,8 @@ class DJBoyfriend extends FlxAtlasSprite
     return anims;
   }
 
+  var lowPumpLoopPoint:Int = 4;
+
   public override function update(elapsed:Float):Void
   {
     super.update(elapsed);
@@ -114,6 +116,14 @@ class DJBoyfriend extends FlxAtlasSprite
       case Confirm:
         if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false);
         timeSinceSpook = 0;
+      case PumpIntro:
+        if (getCurrentAnimation() != 'Boyfriend DJ fist pump') playFlashAnimation('Boyfriend DJ fist pump', false);
+        if (getCurrentAnimation() == 'Boyfriend DJ fist pump' && anim.curFrame >= 4)
+        {
+          anim.play("Boyfriend DJ fist pump", true, false, 0);
+        }
+      case FistPump:
+
       case Spook:
         if (getCurrentAnimation() != 'bf dj afk')
         {
@@ -174,6 +184,12 @@ class DJBoyfriend extends FlxAtlasSprite
         currentState = Idle;
       case "Boyfriend DJ confirm":
 
+      case "Boyfriend DJ fist pump":
+        currentState = Idle;
+
+      case "Boyfriend DJ loss reaction 1":
+        currentState = Idle;
+
       case "Boyfriend DJ watchin tv OG":
         var frame:Int = FlxG.random.bool(33) ? 112 : 166;
 
@@ -275,6 +291,23 @@ class DJBoyfriend extends FlxAtlasSprite
     currentState = Confirm;
   }
 
+  public function fistPump():Void
+  {
+    currentState = PumpIntro;
+  }
+
+  public function pumpFist():Void
+  {
+    currentState = FistPump;
+    anim.play("Boyfriend DJ fist pump", true, false, 4);
+  }
+
+  public function pumpFistBad():Void
+  {
+    currentState = FistPump;
+    anim.play("Boyfriend DJ loss reaction 1", true, false, 4);
+  }
+
   public inline function addOffset(name:String, x:Float = 0, y:Float = 0)
   {
     animOffsets[name] = [x, y];
@@ -331,6 +364,8 @@ enum DJBoyfriendState
   Intro;
   Idle;
   Confirm;
+  PumpIntro;
+  FistPump;
   Spook;
   TV;
 }
diff --git a/source/funkin/ui/freeplay/DifficultyStars.hx b/source/funkin/ui/freeplay/DifficultyStars.hx
new file mode 100644
index 000000000..e7a2b8888
--- /dev/null
+++ b/source/funkin/ui/freeplay/DifficultyStars.hx
@@ -0,0 +1,111 @@
+package funkin.ui.freeplay;
+
+import flixel.group.FlxSpriteGroup;
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import funkin.graphics.shaders.HSVShader;
+
+class DifficultyStars extends FlxSpriteGroup
+{
+  /**
+   * Internal handler var for difficulty... ranges from 0... to 15
+   * 0 is 1 star... 15 is 0 stars!
+   */
+  var curDifficulty(default, set):Int = 0;
+
+  /**
+   * Range between 0 and 15
+   */
+  public var difficulty(default, set):Int = 1;
+
+  public var stars:FlxAtlasSprite;
+
+  public var flames:FreeplayFlames;
+
+  var hsvShader:HSVShader;
+
+  public function new(x:Float, y:Float)
+  {
+    super(x, y);
+
+    hsvShader = new HSVShader();
+
+    flames = new FreeplayFlames(0, 0);
+    add(flames);
+
+    stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
+    stars.anim.play("diff stars");
+    add(stars);
+
+    stars.shader = hsvShader;
+
+    for (memb in flames.members)
+      memb.shader = hsvShader;
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // "loops" the current animation
+    // for clarity, the animation file looks like
+    // frame : stars
+    // 0-99: 1 star
+    // 100-199: 2 stars
+    // ......
+    // 1300-1499: 15 stars
+    // 1500 : 0 stars
+    if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
+    {
+      stars.anim.play("diff stars", true, false, curDifficulty * 100);
+    }
+  }
+
+  function set_difficulty(value:Int):Int
+  {
+    difficulty = value;
+
+    if (difficulty <= 0)
+    {
+      difficulty = 0;
+      curDifficulty = 15;
+    }
+    else if (difficulty <= 15)
+    {
+      difficulty = value;
+      curDifficulty = difficulty - 1;
+    }
+    else
+    {
+      difficulty = 15;
+      curDifficulty = difficulty - 1;
+    }
+
+    flameCheck();
+
+    return difficulty;
+  }
+
+  public function flameCheck():Void
+  {
+    if (difficulty > 10) flames.flameCount = difficulty - 10;
+    else
+      flames.flameCount = 0;
+  }
+
+  function set_curDifficulty(value:Int):Int
+  {
+    curDifficulty = value;
+    if (curDifficulty == 15)
+    {
+      stars.anim.play("diff stars", true, false, 1500);
+      stars.anim.pause();
+    }
+    else
+    {
+      stars.anim.curFrame = Std.int(curDifficulty * 100);
+      stars.anim.play("diff stars", true, false, curDifficulty * 100);
+    }
+
+    return curDifficulty;
+  }
+}
diff --git a/source/funkin/ui/freeplay/FreeplayFlames.hx b/source/funkin/ui/freeplay/FreeplayFlames.hx
index c20d85898..f6b6f5c3d 100644
--- a/source/funkin/ui/freeplay/FreeplayFlames.hx
+++ b/source/funkin/ui/freeplay/FreeplayFlames.hx
@@ -50,8 +50,19 @@ class FreeplayFlames extends FlxSpriteGroup
     }
   }
 
+  var timers:Array<FlxTimer> = [];
+
   function set_flameCount(value:Int):Int
   {
+    // Stop all existing timers.
+    // This fixes a bug where quickly switching difficulties would show flames.
+    for (timer in timers)
+    {
+      timer.active = false;
+      timer.destroy();
+      timers.remove(timer);
+    }
+
     this.flameCount = value;
     var visibleCount:Int = 0;
     for (i in 0...5)
@@ -62,10 +73,18 @@ class FreeplayFlames extends FlxSpriteGroup
       {
         if (!flame.visible)
         {
-          new FlxTimer().start(flameTimer * visibleCount, function(_) {
+          var nextTimer:FlxTimer = new FlxTimer().start(flameTimer * visibleCount, function(currentTimer:FlxTimer) {
+            if (i >= this.flameCount)
+            {
+              trace('EARLY EXIT');
+              return;
+            }
+            timers.remove(currentTimer);
             flame.animation.play("flame", true);
             flame.visible = true;
           });
+          timers.push(nextTimer);
+
           visibleCount++;
         }
       }
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 1c7926f62..5e07fb396 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1,15 +1,18 @@
 package funkin.ui.freeplay;
 
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
 import flixel.addons.transition.FlxTransitionableState;
 import flixel.addons.ui.FlxInputText;
 import flixel.FlxCamera;
 import flixel.FlxSprite;
 import flixel.group.FlxGroup;
+import funkin.graphics.shaders.GaussianBlurShader;
 import flixel.group.FlxGroup.FlxTypedGroup;
 import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
 import flixel.input.touch.FlxTouch;
 import flixel.math.FlxAngle;
 import flixel.math.FlxPoint;
+import openfl.display.BlendMode;
 import flixel.system.debug.watch.Tracker.TrackerProfile;
 import flixel.text.FlxText;
 import flixel.tweens.FlxEase;
@@ -29,15 +32,21 @@ import funkin.graphics.shaders.StrokeShader;
 import funkin.input.Controls;
 import funkin.play.PlayStatePlaylist;
 import funkin.play.song.Song;
+import funkin.ui.story.Level;
 import funkin.save.Save;
 import funkin.save.Save.SaveScoreData;
 import funkin.ui.AtlasText;
+import funkin.play.scoring.Scoring;
+import funkin.play.scoring.Scoring.ScoringRank;
 import funkin.ui.mainmenu.MainMenuState;
 import funkin.ui.MusicBeatSubState;
 import funkin.ui.transition.LoadingState;
 import funkin.ui.transition.StickerSubState;
 import funkin.util.MathUtil;
 import lime.utils.Assets;
+import flixel.tweens.misc.ShakeTween;
+import funkin.effects.IntervalShake;
+import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
 
 /**
  * Parameters used to initialize the FreeplayState.
@@ -45,6 +54,39 @@ import lime.utils.Assets;
 typedef FreeplayStateParams =
 {
   ?character:String,
+
+  ?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;
 };
 
 /**
@@ -69,6 +111,7 @@ class FreeplayState extends MusicBeatSubState
 
   /**
    * For the audio preview, the duration of the fade-out effect.
+   *
    */
   public static final FADE_OUT_DURATION:Float = 0.25;
 
@@ -120,8 +163,6 @@ class FreeplayState extends MusicBeatSubState
   var curCapsule:SongMenuItem;
   var curPlaying:Bool = false;
 
-  var displayedVariations:Array<String>;
-
   var dj:DJBoyfriend;
 
   var ostName:FlxText;
@@ -135,10 +176,44 @@ class FreeplayState extends MusicBeatSubState
   public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
   public static var rememberedSongId:Null<String> = 'tutorial';
 
+  var funnyCam:FunkinCamera;
+  var rankCamera:FunkinCamera;
+  var rankBg:FunkinSprite;
+  var rankVignette:FlxSprite;
+
+  var backingTextYeah:FlxAtlasSprite;
+  var orangeBackShit:FunkinSprite;
+  var alsoOrangeLOL:FunkinSprite;
+  var pinkBack:FunkinSprite;
+  var confirmGlow:FlxSprite;
+  var confirmGlow2:FlxSprite;
+  var confirmTextGlow:FlxSprite;
+
+  var moreWays:BGScrollingText;
+  var funnyScroll:BGScrollingText;
+  var txtNuts:BGScrollingText;
+  var funnyScroll2:BGScrollingText;
+  var moreWays2:BGScrollingText;
+  var funnyScroll3:BGScrollingText;
+
+  var bgDad:FlxSprite;
+  var cardGlow:FlxSprite;
+
+  var fromResultsParams:Null<FromResultsParams> = null;
+
+  var prepForNewRank:Bool = false;
+
   public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
   {
     currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
 
+    fromResultsParams = params?.fromResults;
+
+    if (fromResultsParams?.playRankAnim == true)
+    {
+      prepForNewRank = true;
+    }
+
     if (stickers != null)
     {
       stickerSubState = stickers;
@@ -155,6 +230,12 @@ class FreeplayState extends MusicBeatSubState
 
     FlxTransitionableState.skipNextTransIn = true;
 
+    // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere
+    funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
+    funnyCam.bgColor = FlxColor.TRANSPARENT;
+    FlxG.cameras.add(funnyCam, false);
+    this.cameras = [funnyCam];
+
     if (stickerSubState != null)
     {
       this.persistentUpdate = true;
@@ -175,27 +256,41 @@ class FreeplayState extends MusicBeatSubState
     isDebug = true;
     #end
 
-    FunkinSound.playMusic('freakyMenu',
-      {
-        overrideExisting: true,
-        restartTrack: false
-      });
+    if (prepForNewRank == false)
+    {
+      FunkinSound.playMusic('freakyMenu',
+        {
+          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];
-
     // programmatically adds the songs via LevelRegistry and SongRegistry
     for (levelId in LevelRegistry.instance.listSortedLevelIds())
     {
-      for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
+      var level:Level = LevelRegistry.instance.fetchEntry(levelId);
+
+      if (level == null)
+      {
+        trace('[WARN] Could not find level with id (${levelId})');
+        continue;
+      }
+
+      for (songId in level.getSongs())
       {
         var song:Song = SongRegistry.instance.fetchEntry(songId);
 
-        // Only display songs which actually have available charts for the current character.
+        if (song == null)
+        {
+          trace('[WARN] Could not find song with id (${songId})');
+          continue;
+        }
+
+        // Only display songs which actually have available difficulties for the current character.
+        var displayedVariations = song.getVariationsByCharId(currentCharacter);
         var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
         if (availableDifficultiesForSong.length == 0) continue;
 
@@ -216,17 +311,17 @@ class FreeplayState extends MusicBeatSubState
     trace(FlxG.camera.initialZoom);
     trace(FlxCamera.defaultZoom);
 
-    var pinkBack:FunkinSprite = FunkinSprite.create('freeplay/pinkBack');
+    pinkBack = FunkinSprite.create('freeplay/pinkBack');
     pinkBack.color = 0xFFFFD4E9; // sets it to pink!
     pinkBack.x -= pinkBack.width;
 
     FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
     add(pinkBack);
 
-    var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
+    orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
     add(orangeBackShit);
 
-    var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
+    alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
     add(alsoOrangeLOL);
 
     exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL],
@@ -241,13 +336,30 @@ class FreeplayState extends MusicBeatSubState
     orangeBackShit.visible = false;
     alsoOrangeLOL.visible = false;
 
+    confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText'));
+    confirmTextGlow.blend = BlendMode.ADD;
+    confirmTextGlow.visible = false;
+
+    confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow'));
+    confirmGlow.blend = BlendMode.ADD;
+
+    confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2'));
+
+    confirmGlow.visible = false;
+    confirmGlow2.visible = false;
+
+    add(confirmGlow2);
+    add(confirmGlow);
+
+    add(confirmTextGlow);
+
     var grpTxtScrolls:FlxGroup = new FlxGroup();
     add(grpTxtScrolls);
     grpTxtScrolls.visible = false;
 
     FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size']));
 
-    var moreWays:BGScrollingText = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
+    moreWays = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
     moreWays.funnyColor = 0xFFFFF383;
     moreWays.speed = 6.8;
     grpTxtScrolls.add(moreWays);
@@ -258,7 +370,7 @@ class FreeplayState extends MusicBeatSubState
         speed: 0.4,
       });
 
-    var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60);
+    funnyScroll = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60);
     funnyScroll.funnyColor = 0xFFFF9963;
     funnyScroll.speed = -3.8;
     grpTxtScrolls.add(funnyScroll);
@@ -271,7 +383,7 @@ class FreeplayState extends MusicBeatSubState
         wait: 0
       });
 
-    var txtNuts:BGScrollingText = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43);
+    txtNuts = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43);
     txtNuts.speed = 3.5;
     grpTxtScrolls.add(txtNuts);
     exitMovers.set([txtNuts],
@@ -280,7 +392,7 @@ class FreeplayState extends MusicBeatSubState
         speed: 0.4,
       });
 
-    var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60);
+    funnyScroll2 = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60);
     funnyScroll2.funnyColor = 0xFFFF9963;
     funnyScroll2.speed = -3.8;
     grpTxtScrolls.add(funnyScroll2);
@@ -291,7 +403,7 @@ class FreeplayState extends MusicBeatSubState
         speed: 0.5,
       });
 
-    var moreWays2:BGScrollingText = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
+    moreWays2 = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
     moreWays2.funnyColor = 0xFFFFF383;
     moreWays2.speed = 6.8;
     grpTxtScrolls.add(moreWays2);
@@ -302,7 +414,7 @@ class FreeplayState extends MusicBeatSubState
         speed: 0.4
       });
 
-    var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60);
+    funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60);
     funnyScroll3.funnyColor = 0xFFFEA400;
     funnyScroll3.speed = -3.8;
     grpTxtScrolls.add(funnyScroll3);
@@ -313,6 +425,24 @@ class FreeplayState extends MusicBeatSubState
         speed: 0.3
       });
 
+    backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"),
+      {
+        FrameRate: 24.0,
+        Reversed: false,
+        // ?OnComplete:Void -> Void,
+        ShowPivot: false,
+        Antialiasing: true,
+        ScrollFactor: new FlxPoint(1, 1),
+      });
+
+    add(backingTextYeah);
+
+    cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow'));
+    cardGlow.blend = BlendMode.ADD;
+    cardGlow.visible = false;
+
+    add(cardGlow);
+
     dj = new DJBoyfriend(640, 366);
     exitMovers.set([dj],
       {
@@ -325,7 +455,7 @@ class FreeplayState extends MusicBeatSubState
 
     add(dj);
 
-    var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
+    bgDad = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
     bgDad.setGraphicSize(0, FlxG.height);
     bgDad.updateHitbox();
     bgDad.shader = new AngleMask();
@@ -342,10 +472,14 @@ class FreeplayState extends MusicBeatSubState
       });
 
     add(bgDad);
-    FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.75}, 0.7, {ease: FlxEase.quintOut});
+    FlxTween.tween(blackOverlayBullshitLOLXD, {x: pinkBack.width * 0.76}, 0.7, {ease: FlxEase.quintOut});
 
     blackOverlayBullshitLOLXD.shader = bgDad.shader;
 
+    rankBg = new FunkinSprite(0, 0);
+    rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xD3000000);
+    add(rankBg);
+
     grpSongs = new FlxTypedGroup<Alphabet>();
     add(grpSongs);
 
@@ -488,10 +622,6 @@ class FreeplayState extends MusicBeatSubState
 
       albumRoll.playIntro();
 
-      new FlxTimer().start(0.75, function(_) {
-        // albumRoll.showTitle();
-      });
-
       FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
 
       diffSelLeft.visible = true;
@@ -527,18 +657,48 @@ class FreeplayState extends MusicBeatSubState
       orangeBackShit.visible = true;
       alsoOrangeLOL.visible = true;
       grpTxtScrolls.visible = true;
+
+      // render optimisation
+      if (_parentState != null) _parentState.persistentDraw = false;
+
+      cardGlow.visible = true;
+      FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
+
+      if (prepForNewRank)
+      {
+        rankAnimStart(fromResultsParams);
+      }
     });
 
     generateSongList(null, false);
 
     // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere
-    var funnyCam:FunkinCamera = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
+    funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
     funnyCam.bgColor = FlxColor.TRANSPARENT;
     FlxG.cameras.add(funnyCam, false);
 
+    rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette'));
+    rankVignette.scale.set(2, 2);
+    rankVignette.updateHitbox();
+    rankVignette.blend = BlendMode.ADD;
+    // rankVignette.cameras = [rankCamera];
+    add(rankVignette);
+    rankVignette.alpha = 0;
+
     forEach(function(bs) {
       bs.cameras = [funnyCam];
     });
+
+    rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height);
+    rankCamera.bgColor = FlxColor.TRANSPARENT;
+    FlxG.cameras.add(rankCamera, false);
+    rankBg.cameras = [rankCamera];
+    rankBg.alpha = 0;
+
+    if (prepForNewRank)
+    {
+      rankCamera.fade(0xFF000000, 0, false, null, true);
+    }
   }
 
   var currentFilter:SongFilter = null;
@@ -585,6 +745,7 @@ class FreeplayState extends MusicBeatSubState
 
     for (cap in grpCapsules.members)
     {
+      cap.songText.resetText();
       cap.kill();
     }
 
@@ -602,9 +763,12 @@ class FreeplayState extends MusicBeatSubState
     };
     randomCapsule.y = randomCapsule.intendedY(0) + 10;
     randomCapsule.targetPos.x = randomCapsule.x;
-    randomCapsule.alpha = 0.5;
+    randomCapsule.alpha = 0;
     randomCapsule.songText.visible = false;
     randomCapsule.favIcon.visible = false;
+    randomCapsule.favIconBlurred.visible = false;
+    randomCapsule.ranking.visible = false;
+    randomCapsule.blurredRanking.visible = false;
     randomCapsule.initJumpIn(0, force);
     randomCapsule.hsvShader = hsvShader;
     grpCapsules.add(randomCapsule);
@@ -625,8 +789,11 @@ class FreeplayState extends MusicBeatSubState
       funnyMenu.capsule.alpha = 0.5;
       funnyMenu.songText.visible = false;
       funnyMenu.favIcon.visible = tempSongs[i].isFav;
+      funnyMenu.favIconBlurred.visible = tempSongs[i].isFav;
       funnyMenu.hsvShader = hsvShader;
 
+      funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45);
+      funnyMenu.checkClip();
       funnyMenu.forcePosition();
 
       grpCapsules.add(funnyMenu);
@@ -648,6 +815,13 @@ class FreeplayState extends MusicBeatSubState
    */
   public function sortSongs(songsToFilter:Array<FreeplaySongData>, songFilter:SongFilter):Array<FreeplaySongData>
   {
+    var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int {
+      if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1;
+      else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1;
+      else
+        return 0;
+    };
+
     switch (songFilter.filterType)
     {
       case REGEXP:
@@ -662,6 +836,8 @@ class FreeplayState extends MusicBeatSubState
           return filterRegexp.match(str.songName);
         });
 
+        songsToFilter.sort(filterAlphabetically);
+
       case STARTSWITH:
         // extra note: this is essentially a "search"
 
@@ -676,12 +852,306 @@ class FreeplayState extends MusicBeatSubState
           if (str == null) return true; // Random
           return str.isFav;
         });
+
+        songsToFilter.sort(filterAlphabetically);
+
       default:
         // return all on default
     }
+
     return songsToFilter;
   }
 
+  var sparks:FlxSprite;
+  var sparksADD:FlxSprite;
+
+  function rankAnimStart(fromResults:Null<FromResultsParams>):Void
+  {
+    busy = true;
+    grpCapsules.members[curSelected].sparkle.alpha = 0;
+    // grpCapsules.members[curSelected].forcePosition();
+
+    if (fromResults != null)
+    {
+      rememberedSongId = fromResults.songId;
+      rememberedDifficulty = fromResults.difficultyId;
+      changeSelection();
+      changeDiff();
+    }
+
+    dj.fistPump();
+    // rankCamera.fade(FlxColor.BLACK, 0.5, true);
+    rankCamera.fade(0xFF000000, 0.5, true, null, true);
+    if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
+    rankBg.alpha = 1;
+
+    if (fromResults?.oldRank != null)
+    {
+      grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank;
+      grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank;
+
+      sparks = new FlxSprite(0, 0);
+      sparks.frames = Paths.getSparrowAtlas('freeplay/sparks');
+      sparks.animation.addByPrefix('sparks', 'sparks', 24, false);
+      sparks.visible = false;
+      sparks.blend = BlendMode.ADD;
+      sparks.setPosition(517, 134);
+      sparks.scale.set(0.5, 0.5);
+      add(sparks);
+      sparks.cameras = [rankCamera];
+
+      sparksADD = new FlxSprite(0, 0);
+      sparksADD.visible = false;
+      sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd');
+      sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false);
+      sparksADD.setPosition(498, 116);
+      sparksADD.blend = BlendMode.ADD;
+      sparksADD.scale.set(0.5, 0.5);
+      add(sparksADD);
+      sparksADD.cameras = [rankCamera];
+
+      switch (fromResults.oldRank)
+      {
+        case SHIT:
+          sparksADD.color = 0xFF6044FF;
+        case GOOD:
+          sparksADD.color = 0xFFEF8764;
+        case GREAT:
+          sparksADD.color = 0xFFEAF6FF;
+        case EXCELLENT:
+          sparksADD.color = 0xFFFDCB42;
+        case PERFECT:
+          sparksADD.color = 0xFFFF58B4;
+        case PERFECT_GOLD:
+          sparksADD.color = 0xFFFFB619;
+      }
+      // sparksADD.color = sparks.color;
+    }
+
+    grpCapsules.members[curSelected].doLerp = false;
+
+    // originalPos.x = grpCapsules.members[curSelected].x;
+    // originalPos.y = grpCapsules.members[curSelected].y;
+
+    originalPos.x = 320.488;
+    originalPos.y = 235.6;
+    trace(originalPos);
+
+    grpCapsules.members[curSelected].ranking.visible = false;
+    grpCapsules.members[curSelected].blurredRanking.visible = false;
+
+    rankCamera.zoom = 1.85;
+    FlxTween.tween(rankCamera, {"zoom": 1.8}, 0.6, {ease: FlxEase.sineIn});
+
+    funnyCam.zoom = 1.15;
+    FlxTween.tween(funnyCam, {"zoom": 1.1}, 0.6, {ease: FlxEase.sineIn});
+
+    grpCapsules.members[curSelected].cameras = [rankCamera];
+    // grpCapsules.members[curSelected].targetPos.set((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2),
+    //  (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2));
+
+    grpCapsules.members[curSelected].setPosition((FlxG.width / 2) - (grpCapsules.members[curSelected].width / 2),
+      (FlxG.height / 2) - (grpCapsules.members[curSelected].height / 2));
+
+    new FlxTimer().start(0.5, _ -> {
+      rankDisplayNew(fromResults);
+    });
+  }
+
+  function rankDisplayNew(fromResults:Null<FromResultsParams>):Void
+  {
+    grpCapsules.members[curSelected].ranking.visible = true;
+    grpCapsules.members[curSelected].blurredRanking.visible = true;
+    grpCapsules.members[curSelected].ranking.scale.set(20, 20);
+    grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20);
+
+    grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
+    // grpCapsules.members[curSelected].ranking.animation.curAnim.name, true);
+
+    FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1);
+
+    grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
+    FlxTween.tween(grpCapsules.members[curSelected].blurredRanking, {"scale.x": 1, "scale.y": 1}, 0.1);
+
+    new FlxTimer().start(0.1, _ -> {
+      // trace(grpCapsules.members[curSelected].ranking.rank);
+      if (fromResults?.oldRank != null)
+      {
+        grpCapsules.members[curSelected].fakeRanking.visible = false;
+        grpCapsules.members[curSelected].fakeBlurredRanking.visible = false;
+
+        sparks.visible = true;
+        sparksADD.visible = true;
+        sparks.animation.play('sparks', true);
+        sparksADD.animation.play('sparks add', true);
+
+        sparks.animation.finishCallback = anim -> {
+          sparks.visible = false;
+          sparksADD.visible = false;
+        };
+      }
+
+      switch (fromResultsParams?.newRank)
+      {
+        case SHIT:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinbad'));
+        case PERFECT:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinperfect'));
+        case PERFECT_GOLD:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinperfect'));
+        default:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinnormal'));
+      }
+      rankCamera.zoom = 1.3;
+      // FlxTween.tween(rankCamera, {"zoom": 1.4}, 0.3, {ease: FlxEase.elasticOut});
+
+      FlxTween.tween(rankCamera, {"zoom": 1.5}, 0.3, {ease: FlxEase.backInOut});
+
+      grpCapsules.members[curSelected].x -= 10;
+      grpCapsules.members[curSelected].y -= 20;
+
+      FlxTween.tween(funnyCam, {"zoom": 1.05}, 0.3, {ease: FlxEase.elasticOut});
+
+      grpCapsules.members[curSelected].capsule.angle = -3;
+      FlxTween.tween(grpCapsules.members[curSelected].capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut});
+
+      IntervalShake.shake(grpCapsules.members[curSelected].capsule, 0.3, 1 / 30, 0.1, 0, FlxEase.quadOut);
+    });
+
+    new FlxTimer().start(0.4, _ -> {
+      FlxTween.tween(funnyCam, {"zoom": 1}, 0.8, {ease: FlxEase.sineIn});
+      FlxTween.tween(rankCamera, {"zoom": 1.2}, 0.8, {ease: FlxEase.backIn});
+      // IntervalShake.shake(grpCapsules.members[curSelected], 0.8 + 0.5, 1 / 24, 0, 2, FlxEase.quadIn);
+      FlxTween.tween(grpCapsules.members[curSelected], {x: originalPos.x - 7, y: originalPos.y - 80}, 0.8 + 0.5, {ease: FlxEase.quartIn});
+    });
+
+    new FlxTimer().start(0.6, _ -> {
+      rankAnimSlam(fromResults);
+      // IntervalShake.shake(grpCapsules.members[curSelected].capsule, 0.3, 1 / 30, 0, 0.3, FlxEase.quartIn);
+    });
+  }
+
+  function rankAnimSlam(fromResultsParams:Null<FromResultsParams>)
+  {
+    // FlxTween.tween(rankCamera, {"zoom": 1.9}, 0.5, {ease: FlxEase.backOut});
+    FlxTween.tween(rankBg, {alpha: 0}, 0.5, {ease: FlxEase.expoIn});
+
+    // FlxTween.tween(grpCapsules.members[curSelected], {angle: 5}, 0.5, {ease: FlxEase.backIn});
+
+    switch (fromResultsParams?.newRank)
+    {
+      case SHIT:
+        FunkinSound.playOnce(Paths.sound('ranks/loss'));
+      case GOOD:
+        FunkinSound.playOnce(Paths.sound('ranks/good'));
+      case GREAT:
+        FunkinSound.playOnce(Paths.sound('ranks/great'));
+      case EXCELLENT:
+        FunkinSound.playOnce(Paths.sound('ranks/excellent'));
+      case PERFECT:
+        FunkinSound.playOnce(Paths.sound('ranks/perfect'));
+      case PERFECT_GOLD:
+        FunkinSound.playOnce(Paths.sound('ranks/perfect'));
+      default:
+        FunkinSound.playOnce(Paths.sound('ranks/loss'));
+    }
+
+    FlxTween.tween(grpCapsules.members[curSelected], {"targetPos.x": originalPos.x, "targetPos.y": originalPos.y}, 0.5, {ease: FlxEase.expoOut});
+    new FlxTimer().start(0.5, _ -> {
+      funnyCam.shake(0.0045, 0.35);
+
+      if (fromResultsParams?.newRank == SHIT)
+      {
+        dj.pumpFistBad();
+      }
+      else
+      {
+        dj.pumpFist();
+      }
+
+      rankCamera.zoom = 0.8;
+      funnyCam.zoom = 0.8;
+      FlxTween.tween(rankCamera, {"zoom": 1}, 1, {ease: FlxEase.elasticOut});
+      FlxTween.tween(funnyCam, {"zoom": 1}, 0.8, {ease: FlxEase.elasticOut});
+
+      for (index => capsule in grpCapsules.members)
+      {
+        var distFromSelected:Float = Math.abs(index - curSelected) - 1;
+
+        if (distFromSelected < 5)
+        {
+          if (index == curSelected)
+          {
+            FlxTween.cancelTweensOf(capsule);
+            // capsule.targetPos.x += 50;
+            capsule.fadeAnim();
+
+            rankVignette.color = capsule.getTrailColor();
+            rankVignette.alpha = 1;
+            FlxTween.tween(rankVignette, {alpha: 0}, 0.6, {ease: FlxEase.expoOut});
+
+            capsule.doLerp = false;
+            capsule.setPosition(originalPos.x, originalPos.y);
+            IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12, 0, FlxEase.quadOut, function(_) {
+              capsule.doLerp = true;
+              capsule.cameras = [funnyCam];
+
+              // NOW we can interact with the menu
+              busy = false;
+              grpCapsules.members[curSelected].sparkle.alpha = 0.7;
+              playCurSongPreview(capsule);
+            }, null);
+
+            // FlxTween.tween(capsule, {"targetPos.x": capsule.targetPos.x - 50}, 0.6,
+            //   {
+            //     ease: FlxEase.backInOut,
+            //     onComplete: function(_) {
+            //       capsule.cameras = [funnyCam];
+            //     }
+            //   });
+            FlxTween.tween(capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut});
+          }
+          if (index > curSelected)
+          {
+            // capsule.color = FlxColor.RED;
+            new FlxTimer().start(distFromSelected / 20, _ -> {
+              capsule.doLerp = false;
+
+              capsule.capsule.angle = FlxG.random.float(-10 + (distFromSelected * 2), 10 - (distFromSelected * 2));
+              FlxTween.tween(capsule.capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut});
+
+              IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12 / (distFromSelected + 1), 0, FlxEase.quadOut, function(_) {
+                capsule.doLerp = true;
+              });
+            });
+          }
+
+          if (index < curSelected)
+          {
+            // capsule.color = FlxColor.BLUE;
+            new FlxTimer().start(distFromSelected / 20, _ -> {
+              capsule.doLerp = false;
+
+              capsule.capsule.angle = FlxG.random.float(-10 + (distFromSelected * 2), 10 - (distFromSelected * 2));
+              FlxTween.tween(capsule.capsule, {angle: 0}, 0.5, {ease: FlxEase.backOut});
+
+              IntervalShake.shake(capsule, 0.6, 1 / 24, 0.12 / (distFromSelected + 1), 0, FlxEase.quadOut, function(_) {
+                capsule.doLerp = true;
+              });
+            });
+          }
+        }
+
+        index += 1;
+      }
+    });
+
+    new FlxTimer().start(2, _ -> {
+      // dj.fistPump();
+      prepForNewRank = false;
+    });
+  }
+
   var touchY:Float = 0;
   var touchX:Float = 0;
   var dxTouch:Float = 0;
@@ -696,39 +1166,131 @@ class FreeplayState extends MusicBeatSubState
   var spamTimer:Float = 0;
   var spamming:Bool = false;
 
-  var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
+  /**
+   * If true, disable interaction with the interface.
+   */
+  var busy:Bool = false;
+
+  var originalPos:FlxPoint = new FlxPoint();
 
   override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
-    if (FlxG.keys.justPressed.F)
+    #if debug
+    if (FlxG.keys.justPressed.T)
+    {
+      rankAnimStart(fromResultsParams);
+    }
+
+    // if (FlxG.keys.justPressed.H)
+    // {
+    //   rankDisplayNew(fromResultsParams);
+    // }
+
+    // if (FlxG.keys.justPressed.G)
+    // {
+    //   rankAnimSlam(fromResultsParams);
+    // }
+
+    if (FlxG.keys.justPressed.G)
+    {
+      sparks.y -= 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.V)
+    {
+      sparks.x -= 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.N)
+    {
+      sparks.x += 2;
+      trace(sparks.x, sparks.y);
+    }
+    if (FlxG.keys.justPressed.B)
+    {
+      sparks.y += 2;
+      trace(sparks.x, sparks.y);
+    }
+
+    if (FlxG.keys.justPressed.I)
+    {
+      sparksADD.y -= 2;
+      trace(sparksADD.x, sparksADD.y);
+    }
+    if (FlxG.keys.justPressed.J)
+    {
+      sparksADD.x -= 2;
+      trace(sparksADD.x, sparksADD.y);
+    }
+    if (FlxG.keys.justPressed.L)
+    {
+      sparksADD.x += 2;
+      trace(sparksADD.x, sparksADD.y);
+    }
+    if (FlxG.keys.justPressed.K)
+    {
+      sparksADD.y += 2;
+      trace(sparksADD.x, sparksADD.y);
+    }
+    #end
+
+    if (FlxG.keys.justPressed.F && !busy)
     {
       var targetSong = grpCapsules.members[curSelected]?.songData;
       if (targetSong != null)
       {
         var realShit:Int = curSelected;
-        targetSong.isFav = !targetSong.isFav;
-        if (targetSong.isFav)
+        var isFav = targetSong.toggleFavorite();
+        if (isFav)
         {
-          FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
+          grpCapsules.members[realShit].favIcon.visible = true;
+          grpCapsules.members[realShit].favIconBlurred.visible = true;
+          grpCapsules.members[realShit].favIcon.animation.play('fav');
+          grpCapsules.members[realShit].favIconBlurred.animation.play('fav');
+          FunkinSound.playOnce(Paths.sound('fav'), 1);
+          grpCapsules.members[realShit].checkClip();
+          grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement
+          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.elasticOut,
-              onComplete: _ -> {
-                grpCapsules.members[realShit].favIcon.visible = true;
-                grpCapsules.members[realShit].favIcon.animation.play('fav');
+              ease: FlxEase.expoIn,
+              startDelay: 0.1,
+              onComplete: function(_) {
+                grpCapsules.members[realShit].doLerp = true;
+                busy = false;
               }
             });
         }
         else
         {
-          grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
-          new FlxTimer().start((1 / 24) * 14, _ -> {
+          grpCapsules.members[realShit].favIcon.animation.play('fav', true, true, 9);
+          grpCapsules.members[realShit].favIconBlurred.animation.play('fav', true, true, 9);
+          FunkinSound.playOnce(Paths.sound('unfav'), 1);
+          new FlxTimer().start(0.2, _ -> {
             grpCapsules.members[realShit].favIcon.visible = false;
+            grpCapsules.members[realShit].favIconBlurred.visible = false;
+            grpCapsules.members[realShit].checkClip();
           });
-          new FlxTimer().start((1 / 24) * 24, _ -> {
-            FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
-          });
+
+          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,
+              startDelay: 0.1,
+              onComplete: function(_) {
+                grpCapsules.members[realShit].doLerp = true;
+                busy = false;
+              }
+            });
         }
       }
     }
@@ -932,6 +1494,24 @@ class FreeplayState extends MusicBeatSubState
 
       var longestTimer:Float = 0;
 
+      // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut});
+      FlxTween.color(pinkBack, 0.25, 0xFFFFD863, 0xFFFFD0D5, {ease: FlxEase.quadOut});
+
+      cardGlow.visible = true;
+      cardGlow.alpha = 1;
+      cardGlow.scale.set(1, 1);
+      FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.25, {ease: FlxEase.sineOut});
+
+      orangeBackShit.visible = false;
+      alsoOrangeLOL.visible = false;
+
+      moreWays.visible = false;
+      funnyScroll.visible = false;
+      txtNuts.visible = false;
+      funnyScroll2.visible = false;
+      moreWays2.visible = false;
+      funnyScroll3.visible = false;
+
       for (grpSpr in exitMovers.keys())
       {
         var moveData:MoveData = exitMovers.get(grpSpr);
@@ -976,6 +1556,7 @@ class FreeplayState extends MusicBeatSubState
               overrideExisting: true,
               restartTrack: false
             });
+          FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
           close();
         }
         else
@@ -999,6 +1580,8 @@ class FreeplayState extends MusicBeatSubState
     {
       clearDaCache(daSong.songName);
     }
+    // remove and destroy freeplay camera
+    FlxG.cameras.remove(funnyCam);
   }
 
   function changeDiff(change:Int = 0, force:Bool = false):Void
@@ -1021,7 +1604,7 @@ class FreeplayState extends MusicBeatSubState
     {
       var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, currentDifficulty);
       intendedScore = songScore?.score ?? 0;
-      intendedCompletion = songScore?.accuracy ?? 0.0;
+      intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
       rememberedDifficulty = currentDifficulty;
     }
     else
@@ -1071,6 +1654,7 @@ class FreeplayState extends MusicBeatSubState
         {
           songCapsule.songData.currentDifficulty = currentDifficulty;
           songCapsule.init(null, null, songCapsule.songData);
+          songCapsule.checkClip();
         }
         else
         {
@@ -1086,6 +1670,9 @@ class FreeplayState extends MusicBeatSubState
       albumRoll.albumId = newAlbumId;
       albumRoll.skipIntro();
     }
+
+    // Set difficulty star count.
+    albumRoll.setDifficultyStars(daSong?.difficultyRating);
   }
 
   // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@@ -1159,7 +1746,46 @@ class FreeplayState extends MusicBeatSubState
     FunkinSound.playOnce(Paths.sound('confirmMenu'));
     dj.confirm();
 
+    grpCapsules.members[curSelected].forcePosition();
+    grpCapsules.members[curSelected].songText.flickerText();
+
+    // FlxTween.color(bgDad, 0.33, 0xFFFFFFFF, 0xFF555555, {ease: FlxEase.quadOut});
+    FlxTween.color(pinkBack, 0.33, 0xFFFFD0D5, 0xFF171831, {ease: FlxEase.quadOut});
+    orangeBackShit.visible = false;
+    alsoOrangeLOL.visible = false;
+
+    confirmGlow.visible = true;
+    confirmGlow2.visible = true;
+
+    backingTextYeah.anim.play("BF back card confirm raw", false, false, 0);
+    confirmGlow2.alpha = 0;
+    confirmGlow.alpha = 0;
+
+    FlxTween.tween(confirmGlow2, {alpha: 0.5}, 0.33,
+      {
+        ease: FlxEase.quadOut,
+        onComplete: function(_) {
+          confirmGlow2.alpha = 0.6;
+          confirmGlow.alpha = 1;
+          confirmTextGlow.visible = true;
+          confirmTextGlow.alpha = 1;
+          FlxTween.tween(confirmTextGlow, {alpha: 0.4}, 0.5);
+          FlxTween.tween(confirmGlow, {alpha: 0}, 0.5);
+        }
+      });
+
+    // confirmGlow
+
+    moreWays.visible = false;
+    funnyScroll.visible = false;
+    txtNuts.visible = false;
+    funnyScroll2.visible = false;
+    moreWays2.visible = false;
+    funnyScroll3.visible = false;
+
     new FlxTimer().start(1, function(tmr:FlxTimer) {
+      FunkinSound.emptyPartialQueue();
+
       Paths.setCurrentLevel(cap.songData.levelId);
       LoadingState.loadPlayState(
         {
@@ -1202,7 +1828,7 @@ class FreeplayState extends MusicBeatSubState
 
   function changeSelection(change:Int = 0):Void
   {
-    FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
+    if (!prepForNewRank) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
 
     var prevSelected:Int = curSelected;
 
@@ -1216,7 +1842,7 @@ class FreeplayState extends MusicBeatSubState
     {
       var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
       intendedScore = songScore?.score ?? 0;
-      intendedCompletion = songScore?.accuracy ?? 0.0;
+      intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
       diffIdsCurrent = daSongCapsule.songData.songDifficulties;
       rememberedSongId = daSongCapsule.songData.songId;
       changeDiff();
@@ -1243,49 +1869,58 @@ class FreeplayState extends MusicBeatSubState
       if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
     }
 
-    if (grpCapsules.countLiving() > 0)
+    if (grpCapsules.countLiving() > 0 && !prepForNewRank)
     {
-      if (curSelected == 0)
-      {
-        FunkinSound.playMusic('freeplayRandom',
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: true
-          });
-        FlxG.sound.music.fadeIn(2, 0, 0.8);
-      }
-      else
-      {
-        // TODO: Stream the instrumental of the selected song?
-        var didReplace:Bool = FunkinSound.playMusic('freakyMenu',
-          {
-            startingVolume: 0.0,
-            overrideExisting: true,
-            restartTrack: false
-          });
-        if (didReplace)
-        {
-          FunkinSound.playMusic('freakyMenu',
-            {
-              startingVolume: 0.0,
-              overrideExisting: true,
-              restartTrack: false
-            });
-          FlxG.sound.music.fadeIn(2, 0, 0.8);
-        }
-      }
+      playCurSongPreview(daSongCapsule);
       grpCapsules.members[curSelected].selected = true;
     }
   }
 
+  public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
+  {
+    if (curSelected == 0)
+    {
+      FunkinSound.playMusic('freeplayRandom',
+        {
+          startingVolume: 0.0,
+          overrideExisting: true,
+          restartTrack: false
+        });
+      FlxG.sound.music.fadeIn(2, 0, 0.8);
+    }
+    else
+    {
+      var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
+      FunkinSound.playMusic(daSongCapsule.songData.songId,
+        {
+          startingVolume: 0.0,
+          overrideExisting: true,
+          restartTrack: false,
+          pathsFunction: INST,
+          suffix: potentiallyErect,
+          partialParams:
+            {
+              loadPartial: true,
+              start: 0.05,
+              end: 0.25
+            },
+          onLoad: function() {
+            FlxG.sound.music.fadeIn(2, 0, 0.4);
+          }
+        });
+    }
+  }
+
   /**
    * Build an instance of `FreeplayState` that is above the `MainMenuState`.
    * @return The MainMenuState with the FreeplayState as a substate.
    */
   public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
   {
-    var result = new MainMenuState();
+    var result:MainMenuState;
+    if (params?.fromResults.playRankAnim) result = new MainMenuState(true);
+    else
+      result = new MainMenuState(false);
 
     result.openSubState(new FreeplayState(params, stickers));
     result.persistentUpdate = false;
@@ -1388,6 +2023,8 @@ class FreeplaySongData
    */
   public var isFav:Bool = false;
 
+  public var isNew:Bool = false;
+
   var song:Song;
 
   public var levelId(default, null):String = '';
@@ -1397,16 +2034,18 @@ class FreeplaySongData
 
   public var songName(default, null):String = '';
   public var songCharacter(default, null):String = '';
-  public var songRating(default, null):Int = 0;
+  public var songStartingBpm(default, null):Float = 0;
+  public var difficultyRating(default, null):Int = 0;
   public var albumId(default, null):Null<String> = null;
 
   public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
-  public var displayedVariations(default, null):Array<String> = [Constants.DEFAULT_VARIATION];
+
+  public var scoringRank:Null<ScoringRank> = null;
+
+  var displayedVariations:Array<String> = [Constants.DEFAULT_VARIATION];
 
   function set_currentDifficulty(value:String):String
   {
-    if (currentDifficulty == value) return value;
-
     currentDifficulty = value;
     updateValues(displayedVariations);
     return value;
@@ -1417,21 +2056,43 @@ class FreeplaySongData
     this.levelId = levelId;
     this.songId = songId;
     this.song = song;
+
+    this.isFav = Save.instance.isSongFavorited(songId);
+
     if (displayedVariations != null) this.displayedVariations = displayedVariations;
 
     updateValues(displayedVariations);
   }
 
+  /**
+   * Toggle whether or not the song is favorited, then flush to save data.
+   * @return Whether or not the song is now favorited.
+   */
+  public function toggleFavorite():Bool
+  {
+    isFav = !isFav;
+    if (isFav)
+    {
+      Save.instance.favoriteSong(this.songId);
+    }
+    else
+    {
+      Save.instance.unfavoriteSong(this.songId);
+    }
+    return isFav;
+  }
+
   function updateValues(variations:Array<String>):Void
   {
-    this.songDifficulties = song.listDifficulties(variations, false, false);
+    this.songDifficulties = song.listDifficulties(null, variations, false, false);
     if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
 
     var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
     if (songDifficulty == null) return;
+    this.songStartingBpm = songDifficulty.getStartingBPM();
     this.songName = songDifficulty.songName;
     this.songCharacter = songDifficulty.characters.opponent;
-    this.songRating = songDifficulty.difficultyRating;
+    this.difficultyRating = songDifficulty.difficultyRating;
     if (songDifficulty.album == null)
     {
       FlxG.log.warn('No album for: ${songDifficulty.songName}');
@@ -1441,6 +2102,10 @@ class FreeplaySongData
     {
       this.albumId = songDifficulty.album;
     }
+
+    this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty);
+
+    this.isNew = song.isSongNew(currentDifficulty);
   }
 }
 
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index f6d85e56e..dc30b4345 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -14,6 +14,16 @@ import flixel.text.FlxText;
 import flixel.util.FlxTimer;
 import funkin.util.MathUtil;
 import funkin.graphics.shaders.Grayscale;
+import funkin.graphics.shaders.GaussianBlurShader;
+import openfl.display.BlendMode;
+import funkin.graphics.FunkinSprite;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.addons.effects.FlxTrail;
+import funkin.play.scoring.Scoring.ScoringRank;
+import funkin.save.Save;
+import funkin.save.Save.SaveScoreData;
+import flixel.util.FlxColor;
 
 class SongMenuItem extends FlxSpriteGroup
 {
@@ -30,10 +40,16 @@ class SongMenuItem extends FlxSpriteGroup
   public var selected(default, set):Bool;
 
   public var songText:CapsuleText;
+  public var favIconBlurred:FlxSprite;
   public var favIcon:FlxSprite;
-  public var ranking:FlxSprite;
 
-  var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
+  public var ranking:FreeplayRank;
+  public var blurredRanking:FreeplayRank;
+
+  public var fakeRanking:FreeplayRank;
+  public var fakeBlurredRanking:FreeplayRank;
+
+  var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect", "perfectsick"];
 
   public var targetPos:FlxPoint = new FlxPoint();
   public var doLerp:Bool = false;
@@ -47,6 +63,24 @@ class SongMenuItem extends FlxSpriteGroup
   public var hsvShader(default, set):HSVShader;
 
   // var diffRatingSprite:FlxSprite;
+  public var bpmText:FlxSprite;
+  public var difficultyText:FlxSprite;
+  public var weekType:FlxSprite;
+
+  public var newText:FlxSprite;
+
+  // public var weekType:FlxSprite;
+  public var bigNumbers:Array<CapsuleNumber> = [];
+
+  public var smallNumbers:Array<CapsuleNumber> = [];
+
+  public var weekNumbers:Array<CapsuleNumber> = [];
+
+  var impactThing:FunkinSprite;
+
+  public var sparkle:FlxSprite;
+
+  var sparkleTimer:FlxTimer;
 
   public function new(x:Float, y:Float)
   {
@@ -59,12 +93,84 @@ class SongMenuItem extends FlxSpriteGroup
     // capsule.animation
     add(capsule);
 
+    bpmText = new FlxSprite(144, 87).loadGraphic(Paths.image('freeplay/freeplayCapsule/bpmtext'));
+    bpmText.setGraphicSize(Std.int(bpmText.width * 0.9));
+    add(bpmText);
+
+    difficultyText = new FlxSprite(414, 87).loadGraphic(Paths.image('freeplay/freeplayCapsule/difficultytext'));
+    difficultyText.setGraphicSize(Std.int(difficultyText.width * 0.9));
+    add(difficultyText);
+
+    weekType = new FlxSprite(291, 87);
+    weekType.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/weektypes');
+
+    weekType.animation.addByPrefix('WEEK', 'WEEK text instance 1', 24, false);
+    weekType.animation.addByPrefix('WEEKEND', 'WEEKEND text instance 1', 24, false);
+
+    weekType.setGraphicSize(Std.int(weekType.width * 0.9));
+    add(weekType);
+
+    newText = new FlxSprite(454, 9);
+    newText.frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/new');
+    newText.animation.addByPrefix('newAnim', 'NEW notif', 24, true);
+    newText.animation.play('newAnim', true);
+    newText.setGraphicSize(Std.int(newText.width * 0.9));
+
+    // newText.visible = false;
+
+    add(newText);
+
+    // var debugNumber2:CapsuleNumber = new CapsuleNumber(0, 0, true, 2);
+    // add(debugNumber2);
+
+    for (i in 0...2)
+    {
+      var bigNumber:CapsuleNumber = new CapsuleNumber(466 + (i * 30), 32, true, 0);
+      add(bigNumber);
+
+      bigNumbers.push(bigNumber);
+    }
+
+    for (i in 0...3)
+    {
+      var smallNumber:CapsuleNumber = new CapsuleNumber(185 + (i * 11), 88.5, false, 0);
+      add(smallNumber);
+
+      smallNumbers.push(smallNumber);
+    }
+
     // doesn't get added, simply is here to help with visibility of things for the pop in!
     grpHide = new FlxGroup();
 
-    var rank:String = FlxG.random.getObject(ranks);
+    fakeRanking = new FreeplayRank(420, 41);
+    add(fakeRanking);
+
+    fakeBlurredRanking = new FreeplayRank(fakeRanking.x, fakeRanking.y);
+    fakeBlurredRanking.shader = new GaussianBlurShader(1);
+    add(fakeBlurredRanking);
+
+    fakeRanking.visible = false;
+    fakeBlurredRanking.visible = false;
+
+    ranking = new FreeplayRank(420, 41);
+    add(ranking);
+
+    blurredRanking = new FreeplayRank(ranking.x, ranking.y);
+    blurredRanking.shader = new GaussianBlurShader(1);
+    add(blurredRanking);
+
+    sparkle = new FlxSprite(ranking.x, ranking.y);
+    sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle');
+    sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false);
+    sparkle.animation.play('sparkle', true);
+    sparkle.scale.set(0.8, 0.8);
+    sparkle.blend = BlendMode.ADD;
+
+    sparkle.visible = false;
+    sparkle.alpha = 0.7;
+
+    add(sparkle);
 
-    ranking = new FlxSprite(capsule.width * 0.84, 30);
     // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
     // ranking.scale.x = ranking.scale.y = realScaled;
     // ranking.alpha = 0.75;
@@ -73,11 +179,11 @@ class SongMenuItem extends FlxSpriteGroup
     // add(ranking);
     // grpHide.add(ranking);
 
-    switch (rank)
-    {
-      case 'perfect':
-        ranking.x -= 10;
-    }
+    // switch (rank)
+    // {
+    //   case 'perfect':
+    //     ranking.x -= 10;
+    // }
 
     grayscaleShader = new Grayscale(1);
 
@@ -93,7 +199,7 @@ class SongMenuItem extends FlxSpriteGroup
     grpHide.add(songText);
 
     // TODO: Use value from metadata instead of random.
-    updateDifficultyRating(FlxG.random.int(0, 15));
+    updateDifficultyRating(FlxG.random.int(0, 20));
 
     pixelIcon = new FlxSprite(160, 35);
 
@@ -103,25 +209,250 @@ class SongMenuItem extends FlxSpriteGroup
     add(pixelIcon);
     grpHide.add(pixelIcon);
 
-    favIcon = new FlxSprite(400, 40);
+    favIconBlurred = new FlxSprite(380, 40);
+    favIconBlurred.frames = Paths.getSparrowAtlas('freeplay/favHeart');
+    favIconBlurred.animation.addByPrefix('fav', 'favorite heart', 24, false);
+    favIconBlurred.animation.play('fav');
+    favIconBlurred.setGraphicSize(50, 50);
+    favIconBlurred.blend = BlendMode.ADD;
+    favIconBlurred.shader = new GaussianBlurShader(1.2);
+    favIconBlurred.visible = false;
+    add(favIconBlurred);
+
+    favIcon = new FlxSprite(380, 40);
     favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
     favIcon.animation.addByPrefix('fav', 'favorite heart', 24, false);
     favIcon.animation.play('fav');
     favIcon.setGraphicSize(50, 50);
     favIcon.visible = false;
+    favIcon.blend = BlendMode.ADD;
     add(favIcon);
-    // grpHide.add(favIcon);
+
+    var weekNumber:CapsuleNumber = new CapsuleNumber(355, 88.5, false, 0);
+    add(weekNumber);
+
+    weekNumbers.push(weekNumber);
 
     setVisibleGrp(false);
   }
 
+  function sparkleEffect(timer:FlxTimer):Void
+  {
+    sparkle.setPosition(FlxG.random.float(ranking.x - 20, ranking.x + 3), FlxG.random.float(ranking.y - 29, ranking.y + 4));
+    sparkle.animation.play('sparkle', true);
+    sparkleTimer = new FlxTimer().start(FlxG.random.float(1.2, 4.5), sparkleEffect);
+  }
+
+  // no way to grab weeks rn, so this needs to be done :/
+  // negative values mean weekends
+  function checkWeek(name:String):Void
+  {
+    // trace(name);
+    var weekNum:Int = 0;
+    switch (name)
+    {
+      case 'bopeebo' | 'fresh' | 'dadbattle':
+        weekNum = 1;
+      case 'spookeez' | 'south' | 'monster':
+        weekNum = 2;
+      case 'pico' | 'philly-nice' | 'blammed':
+        weekNum = 3;
+      case "satin-panties" | 'high' | 'milf':
+        weekNum = 4;
+      case "cocoa" | 'eggnog' | 'winter-horrorland':
+        weekNum = 5;
+      case 'senpai' | 'roses' | 'thorns':
+        weekNum = 6;
+      case 'ugh' | 'guns' | 'stress':
+        weekNum = 7;
+      case 'darnell' | 'lit-up' | '2hot' | 'blazin':
+        weekNum = -1;
+      default:
+        weekNum = 0;
+    }
+
+    weekNumbers[0].digit = Std.int(Math.abs(weekNum));
+
+    if (weekNum == 0)
+    {
+      weekType.visible = false;
+      weekNumbers[0].visible = false;
+    }
+    else
+    {
+      weekType.visible = true;
+      weekNumbers[0].visible = true;
+    }
+    if (weekNum > 0)
+    {
+      weekType.animation.play('WEEK', true);
+    }
+    else
+    {
+      weekType.animation.play('WEEKEND', true);
+      weekNumbers[0].offset.x -= 35;
+    }
+  }
+
+  // 255, 27 normal
+  // 220, 27 favourited
+  public function checkClip():Void
+  {
+    var clipSize:Int = 290;
+    var clipType:Int = 0;
+
+    if (ranking.visible == true) clipType += 1;
+    if (favIcon.visible == true) clipType = 2;
+    switch (clipType)
+    {
+      case 2:
+        clipSize = 220;
+      case 1:
+        clipSize = 255;
+    }
+    songText.clipWidth = clipSize;
+  }
+
+  function updateBPM(newBPM:Int):Void
+  {
+    var shiftX:Float = 191;
+    var tempShift:Float = 0;
+
+    if (Math.floor(newBPM / 100) == 1)
+    {
+      shiftX = 186;
+    }
+
+    for (i in 0...smallNumbers.length)
+    {
+      smallNumbers[i].x = this.x + (shiftX + (i * 11));
+      switch (i)
+      {
+        case 0:
+          if (newBPM < 100)
+          {
+            smallNumbers[i].digit = 0;
+          }
+          else
+          {
+            smallNumbers[i].digit = Math.floor(newBPM / 100) % 10;
+          }
+
+        case 1:
+          if (newBPM < 10)
+          {
+            smallNumbers[i].digit = 0;
+          }
+          else
+          {
+            smallNumbers[i].digit = Math.floor(newBPM / 10) % 10;
+
+            if (Math.floor(newBPM / 10) % 10 == 1) tempShift = -4;
+          }
+        case 2:
+          smallNumbers[i].digit = newBPM % 10;
+        default:
+          trace('why the fuck is this being called');
+      }
+      smallNumbers[i].x += tempShift;
+    }
+    // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
+    // diffRatingSprite.visible = false;
+  }
+
+  var evilTrail:FlxTrail;
+
+  public function fadeAnim():Void
+  {
+    impactThing = new FunkinSprite(0, 0);
+    impactThing.frames = capsule.frames;
+    impactThing.frame = capsule.frame;
+    impactThing.updateHitbox();
+    // impactThing.x = capsule.x;
+    // impactThing.y = capsule.y;
+    // picoFade.stamp(this, 0, 0);
+    impactThing.alpha = 0;
+    impactThing.zIndex = capsule.zIndex - 3;
+    add(impactThing);
+    FlxTween.tween(impactThing.scale, {x: 2.5, y: 2.5}, 0.5);
+    // FlxTween.tween(impactThing, {alpha: 0}, 0.5);
+
+    evilTrail = new FlxTrail(impactThing, null, 15, 2, 0.01, 0.069);
+    evilTrail.blend = BlendMode.ADD;
+    evilTrail.zIndex = capsule.zIndex - 5;
+    FlxTween.tween(evilTrail, {alpha: 0}, 0.6,
+      {
+        ease: FlxEase.quadOut,
+        onComplete: function(_) {
+          remove(evilTrail);
+        }
+      });
+    add(evilTrail);
+
+    switch (ranking.rank)
+    {
+      case SHIT:
+        evilTrail.color = 0xFF6044FF;
+      case GOOD:
+        evilTrail.color = 0xFFEF8764;
+      case GREAT:
+        evilTrail.color = 0xFFEAF6FF;
+      case EXCELLENT:
+        evilTrail.color = 0xFFFDCB42;
+      case PERFECT:
+        evilTrail.color = 0xFFFF58B4;
+      case PERFECT_GOLD:
+        evilTrail.color = 0xFFFFB619;
+    }
+  }
+
+  public function getTrailColor():FlxColor
+  {
+    return evilTrail.color;
+  }
+
   function updateDifficultyRating(newRating:Int):Void
   {
     var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
+
+    for (i in 0...bigNumbers.length)
+    {
+      switch (i)
+      {
+        case 0:
+          if (newRating < 10)
+          {
+            bigNumbers[i].digit = 0;
+          }
+          else
+          {
+            bigNumbers[i].digit = Math.floor(newRating / 10);
+          }
+        case 1:
+          bigNumbers[i].digit = newRating % 10;
+        default:
+          trace('why the fuck is this being called');
+      }
+    }
     // diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
     // diffRatingSprite.visible = false;
   }
 
+  function updateScoringRank(newRank:Null<ScoringRank>):Void
+  {
+    if (sparkleTimer != null) sparkleTimer.cancel();
+    sparkle.visible = false;
+
+    this.ranking.rank = newRank;
+    this.blurredRanking.rank = newRank;
+
+    if (newRank == PERFECT_GOLD)
+    {
+      sparkleTimer = new FlxTimer().start(1, sparkleEffect);
+      sparkle.visible = true;
+    }
+  }
+
   function set_hsvShader(value:HSVShader):HSVShader
   {
     this.hsvShader = value;
@@ -168,9 +499,14 @@ class SongMenuItem extends FlxSpriteGroup
     songText.text = songData?.songName ?? 'Random';
     // Update capsule character.
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
-    updateDifficultyRating(songData?.songRating ?? 0);
+    updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
+    updateDifficultyRating(songData?.difficultyRating ?? 0);
+    updateScoringRank(songData?.scoringRank);
+    newText.visible = songData?.isNew;
     // Update opacity, offsets, etc.
     updateSelected();
+
+    checkWeek(songData?.songId);
   }
 
   /**
@@ -289,6 +625,28 @@ class SongMenuItem extends FlxSpriteGroup
 
   override function update(elapsed:Float):Void
   {
+    if (impactThing != null) impactThing.angle = capsule.angle;
+
+    // if (FlxG.keys.justPressed.I)
+    // {
+    //   newText.y -= 1;
+    //   trace(this.x - newText.x, this.y - newText.y);
+    // }
+    // if (FlxG.keys.justPressed.J)
+    // {
+    //   newText.x -= 1;
+    //   trace(this.x - newText.x, this.y - newText.y);
+    // }
+    // if (FlxG.keys.justPressed.L)
+    // {
+    //   newText.x += 1;
+    //   trace(this.x - newText.x, this.y - newText.y);
+    // }
+    // if (FlxG.keys.justPressed.K)
+    // {
+    //   newText.y += 1;
+    //   trace(this.x - newText.x, this.y - newText.y);
+    // }
     if (doJumpIn)
     {
       frameInTicker += elapsed;
@@ -357,6 +715,146 @@ class SongMenuItem extends FlxSpriteGroup
     capsule.offset.x = this.selected ? 0 : -5;
     capsule.animation.play(this.selected ? "selected" : "unselected");
     ranking.alpha = this.selected ? 1 : 0.7;
+    favIcon.alpha = this.selected ? 1 : 0.6;
+    favIconBlurred.alpha = this.selected ? 1 : 0;
     ranking.color = this.selected ? 0xFFFFFFFF : 0xFFAAAAAA;
+
+    if (songText.tooLong) songText.resetText();
+
+    if (selected && songText.tooLong) songText.initMove();
+  }
+}
+
+class FreeplayRank extends FlxSprite
+{
+  public var rank(default, set):Null<ScoringRank> = null;
+
+  function set_rank(val:Null<ScoringRank>):Null<ScoringRank>
+  {
+    rank = val;
+
+    if (rank == null || val == null)
+    {
+      this.visible = false;
+    }
+    else
+    {
+      this.visible = true;
+
+      animation.play(val.getFreeplayRankIconAsset(), true, false);
+
+      centerOffsets(false);
+
+      switch (val)
+      {
+        case SHIT:
+        // offset.x -= 1;
+        case GOOD:
+          // offset.x -= 1;
+          offset.y -= 8;
+        case GREAT:
+          // offset.x -= 1;
+          offset.y -= 8;
+        case EXCELLENT:
+        // offset.y += 5;
+        case PERFECT:
+        // offset.y += 5;
+        case PERFECT_GOLD:
+        // offset.y += 5;
+        default:
+          centerOffsets(false);
+          this.visible = false;
+      }
+      updateHitbox();
+    }
+
+    return rank = val;
+  }
+
+  public var baseX:Float = 0;
+  public var baseY:Float = 0;
+
+  public function new(x:Float, y:Float)
+  {
+    super(x, y);
+
+    frames = Paths.getSparrowAtlas('freeplay/rankbadges');
+
+    animation.addByPrefix('PERFECT', 'PERFECT rank0', 24, false);
+    animation.addByPrefix('EXCELLENT', 'EXCELLENT rank0', 24, false);
+    animation.addByPrefix('GOOD', 'GOOD rank0', 24, false);
+    animation.addByPrefix('PERFECTSICK', 'PERFECT rank GOLD', 24, false);
+    animation.addByPrefix('GREAT', 'GREAT rank0', 24, false);
+    animation.addByPrefix('LOSS', 'LOSS rank0', 24, false);
+
+    blend = BlendMode.ADD;
+
+    this.rank = null;
+
+    // setGraphicSize(Std.int(width * 0.9));
+    scale.set(0.9, 0.9);
+    updateHitbox();
+  }
+}
+
+class CapsuleNumber extends FlxSprite
+{
+  public var digit(default, set):Int = 0;
+
+  function set_digit(val):Int
+  {
+    animation.play(numToString[val], true, false, 0);
+
+    centerOffsets(false);
+
+    switch (val)
+    {
+      case 1:
+        offset.x -= 4;
+      case 3:
+        offset.x -= 1;
+
+      case 6:
+
+      case 4:
+      // offset.y += 5;
+      case 9:
+      // offset.y += 5;
+      default:
+        centerOffsets(false);
+    }
+    return val;
+  }
+
+  public var baseY:Float = 0;
+  public var baseX:Float = 0;
+
+  var numToString:Array<String> = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"];
+
+  public function new(x:Float, y:Float, big:Bool = false, ?initDigit:Int = 0)
+  {
+    super(x, y);
+
+    if (big)
+    {
+      frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/bignumbers');
+    }
+    else
+    {
+      frames = Paths.getSparrowAtlas('freeplay/freeplayCapsule/smallnumbers');
+    }
+
+    for (i in 0...10)
+    {
+      var stringNum:String = numToString[i];
+      animation.addByPrefix(stringNum, '$stringNum', 24, false);
+    }
+
+    this.digit = initDigit;
+
+    animation.play(numToString[initDigit], true);
+
+    setGraphicSize(Std.int(width * 0.9));
+    updateHitbox();
   }
 }
diff --git a/source/funkin/ui/mainmenu/MainMenuState.hx b/source/funkin/ui/mainmenu/MainMenuState.hx
index 7a21a6e8f..b6ec25e61 100644
--- a/source/funkin/ui/mainmenu/MainMenuState.hx
+++ b/source/funkin/ui/mainmenu/MainMenuState.hx
@@ -42,6 +42,16 @@ class MainMenuState extends MusicBeatState
   var magenta:FlxSprite;
   var camFollow:FlxObject;
 
+  var overrideMusic:Bool = false;
+
+  static var rememberedSelectedIndex:Int = 0;
+
+  public function new(?_overrideMusic:Bool = false)
+  {
+    super();
+    overrideMusic = _overrideMusic;
+  }
+
   override function create():Void
   {
     #if discord_rpc
@@ -49,10 +59,12 @@ class MainMenuState extends MusicBeatState
     DiscordClient.changePresence("In the Menus", null);
     #end
 
+    FlxG.cameras.reset(new FunkinCamera('mainMenu'));
+
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
-    playMenuMusic();
+    if (overrideMusic == false) playMenuMusic();
 
     // We want the state to always be able to begin with being able to accept inputs and show the anims of the menu items.
     persistentUpdate = true;
@@ -137,6 +149,8 @@ class MainMenuState extends MusicBeatState
       menuItem.scrollFactor.y = 0.4;
     }
 
+    menuItems.selectItem(rememberedSelectedIndex);
+
     resetCamStuff();
 
     subStateOpened.add(sub -> {
@@ -170,7 +184,6 @@ class MainMenuState extends MusicBeatState
 
   function resetCamStuff():Void
   {
-    FlxG.cameras.reset(new FunkinCamera('mainMenu'));
     FlxG.camera.follow(camFollow, null, 0.06);
     FlxG.camera.snapToTarget();
   }
@@ -285,6 +298,8 @@ class MainMenuState extends MusicBeatState
   function startExitState(state:NextState):Void
   {
     menuItems.enabled = false; // disable for exit
+    rememberedSelectedIndex = menuItems.selectedIndex;
+
     var duration = 0.4;
     menuItems.forEach(function(item) {
       if (menuItems.selectedIndex != item.ID)
@@ -329,6 +344,8 @@ class MainMenuState extends MusicBeatState
       persistentUpdate = false;
 
       FlxG.state.openSubState(new DebugMenuSubState());
+      // reset camera when debug menu is closed
+      subStateClosed.addOnce(_ -> resetCamStuff());
     }
     #end
 
@@ -351,8 +368,7 @@ class MainMenuState extends MusicBeatState
               maxCombo: 0,
               totalNotesHit: 0,
               totalNotes: 0,
-            },
-          accuracy: 0,
+            }
         });
     }
     #end
diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index ffc756e1c..0547404a1 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -11,11 +11,13 @@ class LevelProp extends Bopper
   function set_propData(value:LevelPropData):LevelPropData
   {
     // Only reset the prop if the asset path has changed.
-    if (propData == null || value?.assetPath != propData?.assetPath)
+    if (propData == null || !(thx.Dynamics.equals(value, propData)))
     {
-      this.visible = (value != null);
       this.propData = value;
+
+      this.visible = this.propData != null;
       danceEvery = this.propData?.danceEvery ?? 0;
+
       applyData();
     }
 
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 0c2214529..c1a001e5d 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -306,7 +306,7 @@ class StoryMenuState extends MusicBeatState
   {
     Conductor.instance.update();
 
-    highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.5));
+    highScoreLerp = Std.int(MathUtil.smoothLerp(highScoreLerp, highScore, elapsed, 0.25));
 
     scoreText.text = 'LEVEL SCORE: ${Math.round(highScoreLerp)}';
 
@@ -466,6 +466,9 @@ class StoryMenuState extends MusicBeatState
       // Disable the funny music thing for now.
       // funnyMusicThing();
     }
+
+    updateText();
+    refresh();
   }
 
   final FADE_OUT_TIME:Float = 1.5;
diff --git a/source/funkin/ui/title/AttractState.hx b/source/funkin/ui/title/AttractState.hx
index 3ecb756df..c5a3d0504 100644
--- a/source/funkin/ui/title/AttractState.hx
+++ b/source/funkin/ui/title/AttractState.hx
@@ -89,7 +89,7 @@ class AttractState extends MusicBeatState
     super.update(elapsed);
 
     // If the user presses any button, skip the video.
-    if (FlxG.keys.justPressed.ANY)
+    if (FlxG.keys.justPressed.ANY && !controls.VOLUME_MUTE && !controls.VOLUME_UP && !controls.VOLUME_DOWN)
     {
       onAttractEnd();
     }
diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx
index 49bef5e4a..c6dbcd505 100644
--- a/source/funkin/ui/title/TitleState.hx
+++ b/source/funkin/ui/title/TitleState.hx
@@ -67,9 +67,11 @@ class TitleState extends MusicBeatState
     // DEBUG BULLSHIT
 
     // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
-    new FlxTimer().start(1, function(tmr:FlxTimer) {
+    if (!initialized) new FlxTimer().start(1, function(tmr:FlxTimer) {
       startIntro();
     });
+    else
+      startIntro();
   }
 
   function client_onMetaData(metaData:Dynamic)
@@ -118,11 +120,11 @@ class TitleState extends MusicBeatState
 
   function startIntro():Void
   {
-    playMenuMusic();
+    if (!initialized || FlxG.sound.music == null) playMenuMusic();
 
     persistentUpdate = true;
 
-    var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
+    var bg:FunkinSprite = new FunkinSprite(-1).makeSolidColor(FlxG.width + 2, FlxG.height, FlxColor.BLACK);
     bg.screenCenter();
     add(bg);
 
@@ -231,7 +233,7 @@ class TitleState extends MusicBeatState
         overrideExisting: true,
         restartTrack: true
       });
-    // Fade from 0.0 to 0.7 over 4 seconds
+    // Fade from 0.0 to 1 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 95c378b24..bc26ad97a 100644
--- a/source/funkin/ui/transition/LoadingState.hx
+++ b/source/funkin/ui/transition/LoadingState.hx
@@ -57,8 +57,7 @@ class LoadingState extends MusicBeatSubState
     funkay.scrollFactor.set();
     funkay.screenCenter();
 
-    loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(FlxG.width, 10, 0xFFff16d2);
-    loadBar.screenCenter(X);
+    loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(0, 10, 0xFFff16d2);
     add(loadBar);
 
     initSongsManifest().onComplete(function(lib) {
@@ -163,8 +162,15 @@ class LoadingState extends MusicBeatSubState
       targetShit = FlxMath.remapToRange(callbacks.numRemaining / callbacks.length, 1, 0, 0, 1);
 
       var lerpWidth:Int = Std.int(FlxMath.lerp(loadBar.width, FlxG.width * targetShit, 0.2));
-      loadBar.setGraphicSize(lerpWidth, loadBar.height);
-      loadBar.updateHitbox();
+      // this if-check prevents the setGraphicSize function
+      // from setting the width of the loadBar to the height of the loadBar
+      // this is a behaviour that is implemented in the setGraphicSize function
+      // if the width parameter is equal to 0
+      if (lerpWidth > 0)
+      {
+        loadBar.setGraphicSize(lerpWidth, loadBar.height);
+        loadBar.updateHitbox();
+      }
       FlxG.watch.addQuick('percentage?', callbacks.numRemaining / callbacks.length);
     }
 
diff --git a/source/funkin/ui/transition/preload/FunkinPreloader.hx b/source/funkin/ui/transition/preload/FunkinPreloader.hx
index b71af2b3b..9d2569588 100644
--- a/source/funkin/ui/transition/preload/FunkinPreloader.hx
+++ b/source/funkin/ui/transition/preload/FunkinPreloader.hx
@@ -136,6 +136,8 @@ class FunkinPreloader extends FlxBasePreloader
     // We can't even call trace() yet, until Flixel loads.
     trace('Initializing custom preloader...');
 
+    funkin.util.CLIUtil.resetWorkingDir();
+
     this.siteLockTitleText = Constants.SITE_LOCK_TITLE;
     this.siteLockBodyText = Constants.SITE_LOCK_DESC;
   }
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index c50f17697..1e0978839 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -248,6 +248,11 @@ class Constants
    */
   public static final DEFAULT_ARTIST:String = 'Unknown';
 
+  /**
+   * The default charter for songs.
+   */
+  public static final DEFAULT_CHARTER:String = 'Unknown';
+
   /**
    * The default note style for songs.
    */
@@ -455,6 +460,17 @@ class Constants
   public static final JUDGEMENT_BAD_COMBO_BREAK:Bool = true;
   public static final JUDGEMENT_SHIT_COMBO_BREAK:Bool = true;
 
+  // % Sick
+  public static final RANK_PERFECT_PLAT_THRESHOLD:Float = 1.0; // % Sick
+  public static final RANK_PERFECT_GOLD_THRESHOLD:Float = 0.85; // % Sick
+
+  // % Hit
+  public static final RANK_PERFECT_THRESHOLD:Float = 1.00;
+  public static final RANK_EXCELLENT_THRESHOLD:Float = 0.90;
+  public static final RANK_GREAT_THRESHOLD:Float = 0.80;
+  public static final RANK_GOOD_THRESHOLD:Float = 0.60;
+
+  // public static final RANK_SHIT_THRESHOLD:Float = 0.00;
   /**
    * FILE EXTENSIONS
    */
diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx
deleted file mode 100644
index 2f0c3818a..000000000
--- a/source/funkin/util/StructureUtil.hx
+++ /dev/null
@@ -1,136 +0,0 @@
-package funkin.util;
-
-import funkin.util.tools.MapTools;
-import haxe.DynamicAccess;
-
-/**
- * Utilities for working with anonymous structures.
- */
-class StructureUtil
-{
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a SHALLOW clone, where child structures are not merged.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function merge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      result.set(field, Reflect.field(b, field));
-    }
-
-    return result;
-  }
-
-  public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic>
-  {
-    var result:haxe.ds.Map<String, Dynamic> = [];
-
-    for (field in Reflect.fields(a))
-    {
-      result.set(field, Reflect.field(a, field));
-    }
-
-    return result;
-  }
-
-  public static function isMap(a:Dynamic):Bool
-  {
-    return Std.isOfType(a, haxe.Constraints.IMap);
-  }
-
-  public static function isObject(a:Dynamic):Bool
-  {
-    switch (Type.typeof(a))
-    {
-      case TObject:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  public static function isPrimitive(a:Dynamic):Bool
-  {
-    switch (Type.typeof(a))
-    {
-      case TInt | TFloat | TBool:
-        return true;
-      case TClass(c):
-        return false;
-      case TEnum(e):
-        return false;
-      case TObject:
-        return false;
-      case TFunction:
-        return false;
-      case TNull:
-        return true;
-      case TUnknown:
-        return false;
-      default:
-        return false;
-    }
-  }
-
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a DEEP clone, where child structures are also merged recursively.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    if (a == null) return b;
-    if (b == null) return null;
-    if (isPrimitive(a) && isPrimitive(b)) return b;
-    if (isMap(b))
-    {
-      if (isMap(a))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-    if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b;
-    if (Std.isOfType(b, haxe.ds.StringMap))
-    {
-      if (Std.isOfType(a, haxe.ds.StringMap))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      if (Reflect.isObject(b))
-      {
-        // Note that isObject also returns true for class instances,
-        // but we just assume that's not a problem here.
-        result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
-      }
-      else
-      {
-        // If we're here, b[field] is a primitive.
-        result.set(field, Reflect.field(b, field));
-      }
-    }
-
-    return result;
-  }
-}
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 247ba19db..832ce008a 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -23,6 +23,8 @@ class VersionUtil
   {
     try
     {
+      var versionRaw:thx.semver.Version.SemVer = version;
+      trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}');
       return version.satisfies(versionRule);
     }
     catch (e)
@@ -32,6 +34,40 @@ class VersionUtil
     }
   }
 
+  public static function repairVersion(version:thx.semver.Version):thx.semver.Version
+  {
+    var versionData:thx.semver.Version.SemVer = version;
+
+    if (thx.Types.isAnonymousObject(versionData.version))
+    {
+      // This is bad! versionData.version should be an array!
+      trace('[SAVE] Version data repair required! (got ${versionData.version})');
+      // Turn the objects back into arrays.
+      // I'd use DynamicsT.values but IDK if it maintains order
+      versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
+
+      // This is so jank but it should work.
+      var buildData:Dynamic<String> = cast versionData.build;
+      var buildDataFixed:Array<thx.semver.Version.Identifier> = thx.Dynamics.DynamicsT.values(buildData)
+        .map(function(d:Dynamic) return StringId(d.toString()));
+      versionData.build = buildDataFixed;
+
+      var preData:Dynamic<String> = cast versionData.pre;
+      var preDataFixed:Array<thx.semver.Version.Identifier> = thx.Dynamics.DynamicsT.values(preData).map(function(d:Dynamic) return StringId(d.toString()));
+      versionData.pre = preDataFixed;
+
+      var fixedVersion:thx.semver.Version = versionData;
+      trace('[SAVE] Fixed version: ${fixedVersion}');
+      return fixedVersion;
+    }
+    else
+    {
+      trace('[SAVE] Version data repair not required (got ${version})');
+      // No need for repair.
+      return version;
+    }
+  }
+
   /**
    * Checks that a given verison number satisisfies a given version rule.
    * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx
index 763d84853..07f6bc13a 100644
--- a/source/funkin/util/WindowUtil.hx
+++ b/source/funkin/util/WindowUtil.hx
@@ -24,7 +24,7 @@ class WindowUtil
   {
     #if CAN_OPEN_LINKS
     #if linux
-    Sys.command('/usr/bin/xdg-open', [targetUrl, '&']);
+    Sys.command('/usr/bin/xdg-open $targetUrl &');
     #else
     // This should work on Windows and HTML5.
     FlxG.openURL(targetUrl);
diff --git a/source/funkin/util/macro/InlineMacro.hx b/source/funkin/util/macro/InlineMacro.hx
index b0e7ed184..c40257409 100644
--- a/source/funkin/util/macro/InlineMacro.hx
+++ b/source/funkin/util/macro/InlineMacro.hx
@@ -23,7 +23,7 @@ class InlineMacro
     var fields:Array<haxe.macro.Expr.Field> = haxe.macro.Context.getBuildFields();
 
     // Find the field with the given name.
-    var targetField:Null<haxe.macro.Expr.Field> = fields.find(function(f) return f.name == field
+    var targetField:Null<haxe.macro.Expr.Field> = thx.Arrays.find(fields, function(f) return f.name == field
       && (MacroUtil.isFieldStatic(f) == isStatic));
 
     // If the field was not found, throw an error.
diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx
index caf8e8aab..0fe245e3a 100644
--- a/source/funkin/util/tools/ArrayTools.hx
+++ b/source/funkin/util/tools/ArrayTools.hx
@@ -5,72 +5,6 @@ package funkin.util.tools;
  */
 class ArrayTools
 {
-  /**
-   * Returns a copy of the array with all duplicate elements removed.
-   * @param array The array to remove duplicates from.
-   * @return A copy of the array with all duplicate elements removed.
-   */
-  public static function unique<T>(array:Array<T>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (!result.contains(element))
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Returns a copy of the array with all `null` elements removed.
-   * @param array The array to remove `null` elements from.
-   * @return A copy of the array with all `null` elements removed.
-   */
-  public static function nonNull<T>(array:Array<Null<T>>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (element != null)
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Return the first element of the array that satisfies the predicate, or null if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The result
-   */
-  public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T>
-  {
-    for (element in input)
-    {
-      if (predicate(element)) return element;
-    }
-    return null;
-  }
-
-  /**
-   * Return the index of the first element of the array that satisfies the predicate, or `-1` if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The index of the result
-   */
-  public static function findIndex<T>(input:Array<T>, predicate:T->Bool):Int
-  {
-    for (index in 0...input.length)
-    {
-      if (predicate(input[index])) return index;
-    }
-    return -1;
-  }
-
   /*
    * Push an element to the array if it is not already present.
    * @param input The array to push to
diff --git a/tests/unit/assets/shared/images/arrows.png b/tests/unit/assets/shared/images/arrows.png
deleted file mode 100644
index a44368432..000000000
Binary files a/tests/unit/assets/shared/images/arrows.png and /dev/null differ
diff --git a/tests/unit/assets/shared/images/arrows.xml b/tests/unit/assets/shared/images/arrows.xml
deleted file mode 100644
index 96a73a388..000000000
--- a/tests/unit/assets/shared/images/arrows.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<TextureAtlas imagePath="arrows.png">
-	<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17" />
-	<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17" />
-	<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17" />
-	<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17" />
-	<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17" />
-	<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17" />
-	<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17" />
-	<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17" />
-	<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17" />
-	<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17" />
-	<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17" />
-	<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17" />
-	<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17" />
-	<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17" />
-	<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17" />
-	<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17" />
-	<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17" />
-	<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17" />
-	<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17" />
-	<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17" />
-	<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17" />
-	<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17" />
-	<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17" />
-	<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17" />
-</TextureAtlas>