From e7079452fb4e54cc26d8c1cd132d70d0103a5d37 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Thu, 30 May 2024 04:34:00 +0100
Subject: [PATCH] freeplay visual changes + base rank visuals

---
 source/funkin/ui/freeplay/CapsuleText.hx   | 135 +++++++
 source/funkin/ui/freeplay/DJBoyfriend.hx   |  35 ++
 source/funkin/ui/freeplay/FreeplayState.hx | 396 +++++++++++++++++-
 source/funkin/ui/freeplay/SongMenuItem.hx  | 450 ++++++++++++++++++++-
 4 files changed, 992 insertions(+), 24 deletions(-)

diff --git a/source/funkin/ui/freeplay/CapsuleText.hx b/source/funkin/ui/freeplay/CapsuleText.hx
index 3a520e015..c3fd51d1f 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,30 @@ class CapsuleText extends FlxSpriteGroup
     return text;
   }
 
+  // ???? none
+  // 255, 27 normal
+  // 220, 27 favourited
+
+  function set_clipWidth(value:Int):Int
+  {
+    resetText();
+    if (whiteText.width > value)
+    {
+      tooLong = true;
+
+      blurredText.clipRect = new FlxRect(0, 0, value, blurredText.height);
+      whiteText.clipRect = new FlxRect(0, 0, value, whiteText.height);
+    }
+    else
+    {
+      tooLong = false;
+
+      blurredText.clipRect = null;
+      whiteText.clipRect = null;
+    }
+    return clipWidth = value;
+  }
+
   function set_text(value:String):String
   {
     if (value == null) return value;
@@ -51,6 +90,102 @@ class CapsuleText extends FlxSpriteGroup
       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..248526aaf 100644
--- a/source/funkin/ui/freeplay/DJBoyfriend.hx
+++ b/source/funkin/ui/freeplay/DJBoyfriend.hx
@@ -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/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 7b7543845..a665f0756 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1,5 +1,6 @@
 package funkin.ui.freeplay;
 
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
 import flixel.addons.transition.FlxTransitionableState;
 import flixel.addons.ui.FlxInputText;
 import flixel.FlxCamera;
@@ -10,6 +11,7 @@ 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;
@@ -38,6 +40,8 @@ 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;
 
 /**
  * Parameters used to initialize the FreeplayState.
@@ -135,6 +139,29 @@ 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;
+
   public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
   {
     currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
@@ -216,17 +243,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 +268,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 +302,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 +315,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 +324,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 +335,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 +346,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 +357,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 +387,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 +404,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);
 
@@ -527,18 +593,35 @@ class FreeplayState extends MusicBeatSubState
       orangeBackShit.visible = true;
       alsoOrangeLOL.visible = true;
       grpTxtScrolls.visible = true;
+
+      cardGlow.visible = true;
+      FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
     });
 
     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;
   }
 
   var currentFilter:SongFilter = null;
@@ -585,6 +668,7 @@ class FreeplayState extends MusicBeatSubState
 
     for (cap in grpCapsules.members)
     {
+      cap.songText.resetText();
       cap.kill();
     }
 
@@ -602,9 +686,11 @@ 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.ranking.visible = false;
+    randomCapsule.blurredRanking.visible = false;
     randomCapsule.initJumpIn(0, force);
     randomCapsule.hsvShader = hsvShader;
     grpCapsules.add(randomCapsule);
@@ -627,8 +713,12 @@ class FreeplayState extends MusicBeatSubState
       funnyMenu.favIcon.visible = tempSongs[i].isFav;
       funnyMenu.hsvShader = hsvShader;
 
+      funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45);
+
       funnyMenu.forcePosition();
 
+      funnyMenu.checkClip();
+
       grpCapsules.add(funnyMenu);
     }
 
@@ -682,6 +772,210 @@ class FreeplayState extends MusicBeatSubState
     return songsToFilter;
   }
 
+  function rankAnimStart()
+  {
+    dj.fistPump();
+    // rankCamera.fade(FlxColor.BLACK, 0.5, true);
+    rankCamera.fade(0xFF000000, 0.5, true, null, true);
+    FlxG.sound.music.volume = 0;
+    rankBg.alpha = 1;
+
+    originalPos.x = grpCapsules.members[curSelected].x;
+    originalPos.y = grpCapsules.members[curSelected].y;
+
+    grpCapsules.members[curSelected].ranking.alpha = 0;
+    grpCapsules.members[curSelected].blurredRanking.alpha = 0;
+
+    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));
+
+    new FlxTimer().start(0.5, _ -> {
+      grpCapsules.members[curSelected].doLerp = false;
+      rankDisplayNew();
+    });
+  }
+
+  function rankDisplayNew()
+  {
+    grpCapsules.members[curSelected].ranking.alpha = 1;
+    grpCapsules.members[curSelected].blurredRanking.alpha = 1;
+
+    grpCapsules.members[curSelected].ranking.scale.set(20, 20);
+    grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20);
+    // var tempr:Int = FlxG.random.int(0, 4);
+
+    // grpCapsules.members[curSelected].ranking.rank = tempr;
+    grpCapsules.members[curSelected].ranking.animation.play(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(grpCapsules.members[curSelected].blurredRanking.animation.curAnim.name, 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);
+      switch (grpCapsules.members[curSelected].tempr)
+      {
+        case 0:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinbad'));
+        case 4:
+          FunkinSound.playOnce(Paths.sound('ranks/rankinperfect'));
+        case 5:
+          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();
+      // IntervalShake.shake(grpCapsules.members[curSelected].capsule, 0.3, 1 / 30, 0, 0.3, FlxEase.quartIn);
+    });
+  }
+
+  function rankAnimSlam()
+  {
+    // 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 (grpCapsules.members[curSelected].tempr)
+    {
+      case 0:
+        FunkinSound.playOnce(Paths.sound('ranks/loss'));
+      case 1:
+        FunkinSound.playOnce(Paths.sound('ranks/good'));
+      case 2:
+        FunkinSound.playOnce(Paths.sound('ranks/great'));
+      case 3:
+        FunkinSound.playOnce(Paths.sound('ranks/excellent'));
+      case 4:
+        FunkinSound.playOnce(Paths.sound('ranks/perfect'));
+      case 5:
+        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 (grpCapsules.members[curSelected].tempr == 0)
+      {
+        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];
+            }, 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();
+      FlxG.sound.music.fadeIn(4.0, 0.0, 1.0);
+    });
+  }
+
   var touchY:Float = 0;
   var touchX:Float = 0;
   var dxTouch:Float = 0;
@@ -698,10 +992,48 @@ class FreeplayState extends MusicBeatSubState
 
   var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
 
+  var originalPos:FlxPoint = new FlxPoint();
+
   override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
+    if (FlxG.keys.justPressed.T)
+    {
+      rankAnimStart();
+    }
+
+    if (FlxG.keys.justPressed.H)
+    {
+      rankDisplayNew();
+    }
+
+    if (FlxG.keys.justPressed.G)
+    {
+      rankAnimSlam();
+    }
+
+    if (FlxG.keys.justPressed.I)
+    {
+      confirmTextGlow.y -= 1;
+      trace(confirmTextGlow.x, confirmTextGlow.y);
+    }
+    if (FlxG.keys.justPressed.J)
+    {
+      confirmTextGlow.x -= 1;
+      trace(confirmTextGlow.x, confirmTextGlow.y);
+    }
+    if (FlxG.keys.justPressed.L)
+    {
+      confirmTextGlow.x += 1;
+      trace(confirmTextGlow.x, confirmTextGlow.y);
+    }
+    if (FlxG.keys.justPressed.K)
+    {
+      confirmTextGlow.y += 1;
+      trace(confirmTextGlow.x, confirmTextGlow.y);
+    }
+
     if (FlxG.keys.justPressed.F)
     {
       var targetSong = grpCapsules.members[curSelected]?.songData;
@@ -1145,6 +1477,42 @@ class FreeplayState extends MusicBeatSubState
     FunkinSound.playOnce(Paths.sound('confirmMenu'));
     dj.confirm();
 
+    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) {
       Paths.setCurrentLevel(cap.songData.levelId);
       LoadingState.loadPlayState(
@@ -1383,6 +1751,7 @@ class FreeplaySongData
 
   public var songName(default, null):String = '';
   public var songCharacter(default, null):String = '';
+  public var songStartingBpm(default, null):Float = 0;
   public var songRating(default, null):Int = 0;
   public var albumId(default, null):Null<String> = null;
 
@@ -1415,6 +1784,7 @@ class FreeplaySongData
 
     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;
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index f6d85e56e..0f72199ba 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -14,6 +14,13 @@ 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 flixel.util.FlxColor;
 
 class SongMenuItem extends FlxSpriteGroup
 {
@@ -31,9 +38,10 @@ class SongMenuItem extends FlxSpriteGroup
 
   public var songText:CapsuleText;
   public var favIcon:FlxSprite;
-  public var ranking:FlxSprite;
+  public var ranking:FreeplayRank;
+  public var blurredRanking:FreeplayRank;
 
-  var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
+  var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect", "perfectsick"];
 
   public var targetPos:FlxPoint = new FlxPoint();
   public var doLerp:Bool = false;
@@ -47,6 +55,22 @@ 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 tempr:Int;
 
   public function new(x:Float, y:Float)
   {
@@ -59,12 +83,64 @@ 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);
 
-    ranking = new FlxSprite(capsule.width * 0.84, 30);
+    tempr = FlxG.random.int(0, 5);
+    ranking = new FreeplayRank(420, 41, tempr);
+    add(ranking);
+
+    blurredRanking = new FreeplayRank(ranking.x, ranking.y, tempr);
+    blurredRanking.shader = new GaussianBlurShader(1);
+    add(blurredRanking);
     // ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
     // ranking.scale.x = ranking.scale.y = realScaled;
     // ranking.alpha = 0.75;
@@ -73,11 +149,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 +169,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,21 +179,216 @@ class SongMenuItem extends FlxSpriteGroup
     add(pixelIcon);
     grpHide.add(pixelIcon);
 
-    favIcon = new FlxSprite(400, 40);
+    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);
   }
 
+  // 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 += 1;
+    switch (clipType)
+    {
+      case 2:
+        clipSize = 220;
+      case 1:
+        clipSize = 255;
+    }
+    songText.clipWidth = clipSize;
+  }
+
+  function updateBPM(newBPM:Int):Void
+  {
+    trace(newBPM);
+
+    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()
+  {
+    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 (tempr)
+    {
+      case 0:
+        evilTrail.color = 0xFF6044FF;
+      case 1:
+        evilTrail.color = 0xFFEF8764;
+      case 2:
+        evilTrail.color = 0xFFEAF6FF;
+      case 3:
+        evilTrail.color = 0xFFFDCB42;
+      case 4:
+        evilTrail.color = 0xFFFF58B4;
+      case 5:
+        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;
   }
@@ -169,8 +440,11 @@ class SongMenuItem extends FlxSpriteGroup
     // Update capsule character.
     if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
     updateDifficultyRating(songData?.songRating ?? 0);
+    updateBPM(Std.int(songData?.songStartingBpm) ?? 0);
     // Update opacity, offsets, etc.
     updateSelected();
+
+    checkWeek(songData?.songId);
   }
 
   /**
@@ -289,6 +563,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;
@@ -358,5 +654,137 @@ class SongMenuItem extends FlxSpriteGroup
     capsule.animation.play(this.selected ? "selected" : "unselected");
     ranking.alpha = this.selected ? 1 : 0.7;
     ranking.color = this.selected ? 0xFFFFFFFF : 0xFFAAAAAA;
+
+    if (selected)
+    {
+      if (songText.tooLong == true) songText.initMove();
+    }
+    else
+    {
+      if (songText.tooLong == true) songText.resetText();
+    }
+  }
+}
+
+class FreeplayRank extends FlxSprite
+{
+  public var rank(default, set):Int = 0;
+
+  var numToRank:Array<String> = ["LOSS", "GOOD", "GREAT", "EXCELLENT", "PERFECT", "PERFECTSICK"];
+
+  function set_rank(val):Int
+  {
+    animation.play(numToRank[val], true, false);
+
+    centerOffsets(false);
+
+    switch (val)
+    {
+      case 0:
+      // offset.x -= 1;
+      case 1:
+        // offset.x -= 1;
+        offset.y -= 8;
+      case 2:
+        // offset.x -= 1;
+        offset.y -= 8;
+      case 3:
+      // offset.y += 5;
+      case 4:
+      // offset.y += 5;
+      default:
+        centerOffsets(false);
+    }
+    updateHitbox();
+    return val;
+  }
+
+  public var baseY:Float = 0;
+  public var baseX:Float = 0;
+
+  public function new(x:Float, y:Float, ?initRank:Int = 0)
+  {
+    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 = initRank;
+
+    animation.play(numToRank[initRank], true);
+
+    // 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();
   }
 }