From d686890f763bd7a4db1841464dd7e3559fac1d0f Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Sat, 15 Jun 2024 05:59:58 +0100
Subject: [PATCH] most of the old code

---
 source/funkin/ui/charSelect/CharIcon.hx       |  17 +
 .../funkin/ui/charSelect/CharIconCharacter.hx |  49 ++
 source/funkin/ui/charSelect/CharIconLocked.hx |   3 +
 source/funkin/ui/charSelect/CharSelectGF.hx   | 137 ++++
 .../funkin/ui/charSelect/CharSelectPlayer.hx  |  58 ++
 .../ui/charSelect/CharSelectSubState.hx       | 618 ++++++++++++++++++
 source/funkin/ui/charSelect/Nametag.hx        | 101 +++
 source/funkin/ui/debug/DebugMenuSubState.hx   |   6 +
 8 files changed, 989 insertions(+)
 create mode 100644 source/funkin/ui/charSelect/CharIcon.hx
 create mode 100644 source/funkin/ui/charSelect/CharIconCharacter.hx
 create mode 100644 source/funkin/ui/charSelect/CharIconLocked.hx
 create mode 100644 source/funkin/ui/charSelect/CharSelectGF.hx
 create mode 100644 source/funkin/ui/charSelect/CharSelectPlayer.hx
 create mode 100644 source/funkin/ui/charSelect/CharSelectSubState.hx
 create mode 100644 source/funkin/ui/charSelect/Nametag.hx

diff --git a/source/funkin/ui/charSelect/CharIcon.hx b/source/funkin/ui/charSelect/CharIcon.hx
new file mode 100644
index 000000000..6d6274286
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharIcon.hx
@@ -0,0 +1,17 @@
+package funkin.ui.charSelect;
+
+import flixel.FlxSprite;
+
+class CharIcon extends FlxSprite
+{
+  public var locked:Bool = false;
+
+  public function new(x:Float, y:Float, locked:Bool = false)
+  {
+    super(x, y);
+
+    this.locked = locked;
+
+    makeGraphic(128, 128);
+  }
+}
diff --git a/source/funkin/ui/charSelect/CharIconCharacter.hx b/source/funkin/ui/charSelect/CharIconCharacter.hx
new file mode 100644
index 000000000..7f7b5c212
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharIconCharacter.hx
@@ -0,0 +1,49 @@
+package funkin.ui.charSelect;
+
+import openfl.display.BitmapData;
+import openfl.filters.DropShadowFilter;
+import openfl.filters.ConvolutionFilter;
+import funkin.graphics.shaders.StrokeShader;
+
+class CharIconCharacter extends CharIcon
+{
+  public var dropShadowFilter:DropShadowFilter;
+
+  var matrixFilter:Array<Float> = [
+    1, 1, 1,
+    1, 1, 1,
+    1, 1, 1
+  ];
+
+  var divisor:Int = 1;
+  var bias:Int = 0;
+  var convolutionFilter:ConvolutionFilter;
+
+  public var noDropShadow:BitmapData;
+  public var withDropShadow:BitmapData;
+
+  var strokeShader:StrokeShader;
+
+  public function new(path:String)
+  {
+    super(0, 0, false);
+
+    loadGraphic(Paths.image('freeplay/icons/' + path + 'pixel'));
+    setGraphicSize(128, 128);
+    updateHitbox();
+    antialiasing = false;
+
+    strokeShader = new StrokeShader();
+    // shader = strokeShader;
+
+    // noDropShadow = pixels.clone();
+
+    // dropShadowFilter = new DropShadowFilter(5, 45, 0, 1, 0, 0);
+    // convolutionFilter = new ConvolutionFilter(3, 3, matrixFilter, divisor, bias);
+    // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), dropShadowFilter);
+    // pixels.applyFilter(pixels, pixels.rect, new openfl.geom.Point(0, 0), convolutionFilter);
+    // withDropShadow = pixels.clone();
+
+    // pixels = noDropShadow.clone();
+  }
+}
diff --git a/source/funkin/ui/charSelect/CharIconLocked.hx b/source/funkin/ui/charSelect/CharIconLocked.hx
new file mode 100644
index 000000000..dbe84a6ce
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharIconLocked.hx
@@ -0,0 +1,3 @@
+package funkin.ui.charSelect;
+
+class CharIconLocked extends CharIcon {}
diff --git a/source/funkin/ui/charSelect/CharSelectGF.hx b/source/funkin/ui/charSelect/CharSelectGF.hx
new file mode 100644
index 000000000..6d8e3e657
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharSelectGF.hx
@@ -0,0 +1,137 @@
+package funkin.ui.charSelect;
+
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import flixel.tweens.FlxTween;
+import flixel.tweens.FlxEase;
+import flixel.math.FlxMath;
+import funkin.util.FramesJSFLParser;
+import funkin.util.FramesJSFLParser.FramesJSFLInfo;
+import funkin.util.FramesJSFLParser.FramesJSFLFrame;
+import flixel.math.FlxMath;
+
+class CharSelectGF extends FlxAtlasSprite
+{
+  var fadeTimer:Float = 0;
+  var fadingStatus:FadeStatus = OFF;
+  var fadeAnimIndex:Int = 0;
+
+  var animInInfo:FramesJSFLInfo;
+  var animOutInfo:FramesJSFLInfo;
+
+  var intendedYPos:Float = 0;
+  var intendedAlpha:Float = 0;
+
+  public function new()
+  {
+    super(0, 0, Paths.animateAtlas("charSelect/gfChill"));
+    anim.play("");
+    switchGF("bf");
+  }
+
+  override public function update(elapsed:Float)
+  {
+    super.update(elapsed);
+
+    switch (fadingStatus)
+    {
+      case OFF:
+        // do nothing if it's off!
+        // or maybe force position to be 0,0?
+        // maybe reset timers?
+        resetFadeAnimParams();
+      case FADE_OUT:
+        doFade(animOutInfo);
+      case FADE_IN:
+        doFade(animInInfo);
+      default:
+    }
+
+    if (FlxG.keys.justPressed.J)
+    {
+      alpha = 1;
+      x = y = 0;
+      fadingStatus = FADE_OUT;
+    }
+    if (FlxG.keys.justPressed.K)
+    {
+      alpha = 0;
+      fadingStatus = FADE_IN;
+    }
+  }
+
+  /**
+   * @param animInfo Should not be confused with animInInfo!
+   *                 This is merely a local var for the function!
+   */
+  function doFade(animInfo:FramesJSFLInfo)
+  {
+    fadeTimer += FlxG.elapsed;
+    if (fadeTimer >= 1 / 24)
+    {
+      fadeTimer = 0;
+      // only inc the index for the first frame, used for reference of where to "start"
+      if (fadeAnimIndex == 0)
+      {
+        fadeAnimIndex++;
+        return;
+      }
+
+      var curFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex];
+      var prevFrame:FramesJSFLFrame = animInfo.frames[fadeAnimIndex - 1];
+
+      var xDiff:Float = curFrame.x - prevFrame.x;
+      var yDiff:Float = curFrame.y - prevFrame.y;
+      var alphaDiff:Float = curFrame.alpha - prevFrame.alpha;
+      alphaDiff /= 100; // flash exports alpha as a whole number
+
+      alpha += alphaDiff;
+      alpha = FlxMath.bound(alpha, 0, 1);
+      x += xDiff;
+      y += yDiff;
+
+      fadeAnimIndex++;
+    }
+
+    if (fadeAnimIndex >= animInfo.frames.length) fadingStatus = OFF;
+  }
+
+  function resetFadeAnimParams()
+  {
+    fadeTimer = 0;
+    fadeAnimIndex = 0;
+  }
+
+  public function switchGF(str:String)
+  {
+    str = switch (str)
+    {
+      case "pico":
+        "nene";
+      case "bf":
+        "gf";
+      default:
+        "gf";
+    }
+
+    switch str
+    {
+      default:
+        loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill"));
+    }
+
+    animInInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "In.txt"));
+    animOutInfo = FramesJSFLParser.parse(Paths.file("images/charSelect/" + str + "AnimInfo/" + str + "Out.txt"));
+
+    anim.play("");
+    playAnimation("idle", true, false, true);
+
+    updateHitbox();
+  }
+}
+
+enum FadeStatus
+{
+  OFF;
+  FADE_OUT;
+  FADE_IN;
+}
diff --git a/source/funkin/ui/charSelect/CharSelectPlayer.hx b/source/funkin/ui/charSelect/CharSelectPlayer.hx
new file mode 100644
index 000000000..9322369ba
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharSelectPlayer.hx
@@ -0,0 +1,58 @@
+package funkin.ui.charSelect;
+
+import flixel.FlxSprite;
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
+
+class CharSelectPlayer extends FlxAtlasSprite
+{
+  public function new(x:Float, y:Float)
+  {
+    super(x, y, Paths.animateAtlas("charSelect/bfChill"));
+
+    onAnimationFinish.add(function(animLabel:String) {
+      switch (animLabel)
+      {
+        case "slidein":
+          if (hasAnimation("slidein idle point")) playAnimation("slidein idle point", true, false, false);
+          else
+            playAnimation("idle", true, false, true);
+        case "slidein idle point":
+          playAnimation("idle", true, false, true);
+        case "select":
+          anim.pause();
+        case "deselect":
+          playAnimation("deselect loop start", true, false, true);
+      }
+    });
+  }
+
+  public function updatePosition(str:String)
+  {
+    switch (str)
+    {
+      case "bf":
+        x = 0;
+        y = 0;
+      case "pico":
+        x = 0;
+        y = 0;
+      case "random":
+    }
+  }
+
+  public function switchChar(str:String)
+  {
+    switch str
+    {
+      default:
+        loadAtlas(Paths.animateAtlas("charSelect/" + str + "Chill"));
+    }
+
+    anim.play("");
+    playAnimation("slidein", true, false, false);
+
+    updateHitbox();
+
+    updatePosition(str);
+  }
+}
diff --git a/source/funkin/ui/charSelect/CharSelectSubState.hx b/source/funkin/ui/charSelect/CharSelectSubState.hx
new file mode 100644
index 000000000..a360328a2
--- /dev/null
+++ b/source/funkin/ui/charSelect/CharSelectSubState.hx
@@ -0,0 +1,618 @@
+package funkin.ui.charSelect;
+
+import flixel.text.FlxText;
+import flixel.FlxSprite;
+import flixel.system.debug.watch.Tracker.TrackerProfile;
+import flixel.math.FlxPoint;
+import flixel.tweens.FlxTween;
+import openfl.display.BlendMode;
+import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.group.FlxSpriteGroup;
+import funkin.play.stage.Stage;
+import funkin.play.stage.StageData.StageDataParser;
+import funkin.modding.events.ScriptEvent;
+import funkin.modding.events.ScriptEventDispatcher;
+import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import flixel.FlxObject;
+import openfl.display.BlendMode;
+import flixel.group.FlxGroup;
+import funkin.util.MathUtil;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxEase;
+import flixel.sound.FlxSound;
+import funkin.audio.FunkinSound;
+
+class CharSelectSubState extends MusicBeatSubState
+{
+  var cursor:FlxSprite;
+  var cursorBlue:FlxSprite;
+  var cursorDarkBlue:FlxSprite;
+
+  var grpCursors:FlxTypedGroup<FlxSprite>;
+
+  var cursorConfirmed:FlxSprite;
+  var cursorDenied:FlxSprite;
+
+  var cursorX:Int = 0;
+  var cursorY:Int = 0;
+
+  var cursorFactor:Float = 110;
+  var cursorOffsetX:Float = -16;
+  var cursorOffsetY:Float = -48;
+
+  var cursorLocIntended:FlxPoint = new FlxPoint(0, 0);
+  var lerpAmnt:Float = 0.95;
+
+  var tmrFrames:Int = 60;
+
+  var currentStage:Stage;
+
+  var playerChill:CharSelectPlayer;
+  var playerChillOut:CharSelectPlayer;
+  var gfChill:CharSelectGF;
+  var gfChillOut:CharSelectGF;
+
+  var curChar(default, set):String = "pico";
+  var nametag:Nametag;
+  var camFollow:FlxObject;
+
+  var availableChars:Map<Int, String> = new Map<Int, String>();
+  var pressedSelect:Bool = false;
+
+  var selectTimer:FlxTimer = new FlxTimer();
+  var selectSound:FunkinSound;
+
+  public function new()
+  {
+    super();
+
+    availableChars.set(4, "bf");
+    availableChars.set(3, "pico");
+  }
+
+  override public function create()
+  {
+    super.create();
+
+    selectSound = new FunkinSound();
+    selectSound.loadEmbedded(Paths.sound('CS_select'));
+    selectSound.pitch = 1;
+    selectSound.volume = 0.7;
+    FlxG.sound.defaultSoundGroup.add(selectSound);
+
+    Conductor.forceBPM(90);
+
+    var bg:FlxSprite = new FlxSprite(-153, -140);
+    bg.loadGraphic(Paths.image('charSelect/charSelectBG'));
+    bg.scrollFactor.set(0.1, 0.1);
+    add(bg);
+
+    var crowd:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/crowd"));
+    crowd.anim.play("");
+    crowd.scrollFactor.set(0.3, 0.3);
+    add(crowd);
+
+    var stageSpr:FlxSprite = new FlxSprite(-40, 391);
+    stageSpr.frames = Paths.getSparrowAtlas("charSelect/charSelectStage");
+    stageSpr.animation.addByPrefix("idle", "stage", 24, true);
+    stageSpr.animation.play("idle");
+    add(stageSpr);
+
+    var curtains:FlxSprite = new FlxSprite(-47, -49);
+    curtains.loadGraphic(Paths.image('charSelect/curtains'));
+    curtains.scrollFactor.set(1.4, 1.4);
+    add(curtains);
+
+    var barthing:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/barThing"));
+    barthing.anim.play("");
+    barthing.blend = BlendMode.MULTIPLY;
+    barthing.scrollFactor.set(0, 0);
+    add(barthing);
+
+    var charLight:FlxSprite = new FlxSprite(800, 250);
+    charLight.loadGraphic(Paths.image('charSelect/charLight'));
+    add(charLight);
+
+    var charLightGF:FlxSprite = new FlxSprite(180, 240);
+    charLightGF.loadGraphic(Paths.image('charSelect/charLight'));
+    add(charLightGF);
+
+    gfChill = new CharSelectGF();
+    gfChill.switchGF("bf");
+    add(gfChill);
+
+    playerChill = new CharSelectPlayer(0, 0);
+    playerChill.switchChar("bf");
+    add(playerChill);
+
+    playerChillOut = new CharSelectPlayer(0, 0);
+    playerChillOut.switchChar("bf");
+    add(playerChillOut);
+
+    var speakers:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/charSelectSpeakers"));
+    speakers.anim.play("");
+    speakers.scrollFactor.set(1.8, 1.8);
+    add(speakers);
+
+    var fgBlur:FlxSprite = new FlxSprite(-125, 170);
+    fgBlur.loadGraphic(Paths.image('charSelect/foregroundBlur'));
+    fgBlur.blend = openfl.display.BlendMode.MULTIPLY;
+    add(fgBlur);
+
+    var dipshitBlur:FlxSprite = new FlxSprite(419, -65);
+    dipshitBlur.frames = Paths.getSparrowAtlas("charSelect/dipshitBlur");
+    dipshitBlur.animation.addByPrefix('idle', "CHOOSE vertical", 24, true);
+    dipshitBlur.blend = BlendMode.ADD;
+    dipshitBlur.animation.play("idle");
+    add(dipshitBlur);
+
+    var dipshitBacking:FlxSprite = new FlxSprite(423, -17);
+    dipshitBacking.frames = Paths.getSparrowAtlas("charSelect/dipshitBacking");
+    dipshitBacking.animation.addByPrefix('idle', "CHOOSE horizontal", 24, true);
+    dipshitBacking.blend = BlendMode.ADD;
+    dipshitBacking.animation.play("idle");
+    add(dipshitBacking);
+
+    var chooseDipshit:FlxSprite = new FlxSprite(426, -13);
+    chooseDipshit.loadGraphic(Paths.image('charSelect/chooseDipshit'));
+    add(chooseDipshit);
+
+    chooseDipshit.scrollFactor.set();
+    dipshitBacking.scrollFactor.set();
+    dipshitBlur.scrollFactor.set();
+
+    nametag = new Nametag();
+    add(nametag);
+
+    nametag.scrollFactor.set();
+
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSprite, ["x", "y", "alpha", "scale", "blend"]));
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxAtlasSprite, ["x", "y"]));
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSound, ["pitch", "volume"]));
+
+    // FlxG.debugger.track(crowd);
+    // FlxG.debugger.track(stageSpr, "stageSpr");
+    // FlxG.debugger.track(bfChill, "bf chill");
+    // FlxG.debugger.track(playerChill, "player");
+    // FlxG.debugger.track(nametag, "nametag");
+    FlxG.debugger.track(selectSound, "selectSound");
+    // FlxG.debugger.track(chooseDipshit, "choose dipshit");
+    // FlxG.debugger.track(barthing, "barthing");
+    // FlxG.debugger.track(fgBlur, "fgBlur");
+    // FlxG.debugger.track(dipshitBlur, "dipshitBlur");
+    // FlxG.debugger.track(dipshitBacking, "dipshitBacking");
+    // FlxG.debugger.track(charLightGF, "charLight");
+    // FlxG.debugger.track(gfChill, "gfChill");
+
+    grpCursors = new FlxTypedGroup<FlxSprite>();
+    add(grpCursors);
+
+    cursor = new FlxSprite(0, 0);
+    cursor.loadGraphic(Paths.image('charSelect/charSelector'));
+    cursor.color = 0xFFFFFF00;
+
+    // FFCC00
+
+    cursorBlue = new FlxSprite(0, 0);
+    cursorBlue.loadGraphic(Paths.image('charSelect/charSelector'));
+    cursorBlue.color = 0xFF3EBBFF;
+
+    cursorDarkBlue = new FlxSprite(0, 0);
+    cursorDarkBlue.loadGraphic(Paths.image('charSelect/charSelector'));
+    cursorDarkBlue.color = 0xFF3C74F7;
+
+    cursorBlue.blend = BlendMode.SCREEN;
+    cursorDarkBlue.blend = BlendMode.SCREEN;
+
+    cursorConfirmed = new FlxSprite(0, 0);
+    cursorConfirmed.scrollFactor.set();
+    cursorConfirmed.frames = Paths.getSparrowAtlas("charSelect/charSelectorConfirm");
+    cursorConfirmed.animation.addByPrefix("idle", "cursor ACCEPTED", 24, true);
+    cursorConfirmed.visible = false;
+    add(cursorConfirmed);
+
+    cursorDenied = new FlxSprite(0, 0);
+    cursorDenied.scrollFactor.set();
+    cursorDenied.frames = Paths.getSparrowAtlas("charSelect/charSelectorDenied");
+    cursorDenied.animation.addByPrefix("idle", "cursor DENIED", 24, false);
+    cursorDenied.visible = false;
+    add(cursorDenied);
+
+    grpCursors.add(cursorDarkBlue);
+    grpCursors.add(cursorBlue);
+    grpCursors.add(cursor);
+
+    initLocks();
+
+    cursor.scrollFactor.set();
+    cursorBlue.scrollFactor.set();
+    cursorDarkBlue.scrollFactor.set();
+
+    FlxTween.color(cursor, 0.2, 0xFFFFFF00, 0xFFFFCC00, {type: FlxTween.PINGPONG});
+
+    // FlxG.debugger.track(cursor);
+
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(CharSelectSubState, ["curChar", "grpXSpread", "grpYSpread"]));
+    FlxG.debugger.track(this);
+
+    FlxG.sound.playMusic(Paths.music('charSelect/charSelectMusic'));
+
+    camFollow = new FlxObject(0, 0, 1, 1);
+    add(camFollow);
+    camFollow.screenCenter();
+
+    FlxG.camera.follow(camFollow, LOCKON, 0.01);
+
+    var temp:FlxSprite = new FlxSprite();
+    temp.loadGraphic(Paths.image('charSelect/placement'));
+    add(temp);
+    temp.alpha = 0.0;
+    Conductor.stepHit.add(spamOnStep);
+    // FlxG.debugger.track(temp, "tempBG");
+  }
+
+  var grpIcons:FlxSpriteGroup;
+
+  var grpXSpread(default, set):Float = 107;
+  var grpYSpread(default, set):Float = 127;
+
+  function initLocks()
+  {
+    grpIcons = new FlxSpriteGroup();
+    add(grpIcons);
+
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(FlxSpriteGroup, ["x", "y"]));
+    // FlxG.debugger.track(grpIcons, "iconGrp");
+
+    for (i in 0...9)
+    {
+      if (availableChars.exists(i))
+      {
+        var path:String = availableChars.get(i);
+        var temp:CharIconCharacter = new CharIconCharacter(path);
+        temp.ID = 0;
+
+        var idleAnimPrefix:String = switch (path)
+        {
+          case "pico":
+            "Pico Icon";
+          case "bf":
+            "boyfriend icon instance 1";
+          case _:
+            "Pico Icon";
+        };
+
+        // temp.animation.addByPrefix("idle", idleAnimPrefix, 0, false);
+        // temp.animation.play("idle");
+        grpIcons.add(temp);
+      }
+      else
+      {
+        var temp:FlxSprite = new FlxSprite();
+        temp.ID = 1;
+        temp.frames = Paths.getSparrowAtlas("charSelect/locks");
+
+        var lockIndex:Int = i + 1;
+
+        if (i == 3) lockIndex = 3;
+
+        if (i >= 4) lockIndex = i - 2;
+
+        temp.animation.addByIndices("idle", "LOCK FULL " + lockIndex + " instance 1", [0], "", 24);
+        temp.animation.addByIndices("selected", "LOCK FULL " + lockIndex + " instance 1", [3, 4, 5], "", 24, false);
+        temp.animation.addByIndices("clicked", "LOCK FULL " + lockIndex + " instance 1", [9, 10, 11, 12, 13, 14, 15], "", 24, false);
+
+        temp.animation.play("idle");
+
+        grpIcons.add(temp);
+      }
+    }
+
+    updateIconPositions();
+
+    grpIcons.scrollFactor.set();
+  }
+
+  function updateIconPositions()
+  {
+    grpIcons.x = 450;
+    grpIcons.y = 120;
+    for (index => member in grpIcons.members)
+    {
+      var posX:Float = (index % 3);
+      var posY:Float = Math.floor(index / 3);
+
+      member.x = posX * grpXSpread;
+      member.y = posY * grpYSpread;
+
+      member.x += grpIcons.x;
+      member.y += grpIcons.y;
+    }
+  }
+
+  var holdTmrUp:Float = 0;
+  var holdTmrDown:Float = 0;
+  var holdTmrLeft:Float = 0;
+  var holdTmrRight:Float = 0;
+  var spamUp:Bool = false;
+  var spamDown:Bool = false;
+  var spamLeft:Bool = false;
+  var spamRight:Bool = false;
+
+  override public function update(elapsed:Float)
+  {
+    super.update(elapsed);
+
+    Conductor.update();
+
+    if (controls.UI_UP_R || controls.UI_DOWN_R || controls.UI_LEFT_R || controls.UI_RIGHT_R) selectSound.pitch = 1;
+
+    if (controls.UI_UP) holdTmrUp += elapsed;
+    if (controls.UI_UP_R)
+    {
+      holdTmrUp = 0;
+      spamUp = false;
+    }
+
+    if (controls.UI_DOWN) holdTmrDown += elapsed;
+    if (controls.UI_DOWN_R)
+    {
+      holdTmrDown = 0;
+      spamDown = false;
+    }
+
+    if (controls.UI_LEFT) holdTmrLeft += elapsed;
+    if (controls.UI_LEFT_R)
+    {
+      holdTmrLeft = 0;
+      spamLeft = false;
+    }
+
+    if (controls.UI_RIGHT) holdTmrRight += elapsed;
+    if (controls.UI_RIGHT_R)
+    {
+      holdTmrRight = 0;
+      spamRight = false;
+    }
+
+    var initSpam = 0.5;
+
+    if (holdTmrUp >= initSpam) spamUp = true;
+    if (holdTmrDown >= initSpam) spamDown = true;
+    if (holdTmrLeft >= initSpam) spamLeft = true;
+    if (holdTmrRight >= initSpam) spamRight = true;
+
+    if (controls.UI_UP_P)
+    {
+      cursorY -= 1;
+      holdTmrUp = 0;
+
+      selectSound.play(true);
+    }
+    if (controls.UI_DOWN_P)
+    {
+      cursorY += 1;
+      holdTmrDown = 0;
+      selectSound.play(true);
+    }
+    if (controls.UI_LEFT_P)
+    {
+      cursorX -= 1;
+      holdTmrLeft = 0;
+      selectSound.play(true);
+    }
+    if (controls.UI_RIGHT_P)
+    {
+      cursorX += 1;
+      holdTmrRight = 0;
+      selectSound.play(true);
+    }
+
+    if (cursorX < -1)
+    {
+      cursorX = 1;
+    }
+    if (cursorX > 1)
+    {
+      cursorX = -1;
+    }
+    if (cursorY < -1)
+    {
+      cursorY = 1;
+    }
+    if (cursorY > 1)
+    {
+      cursorY = -1;
+    }
+
+    if (availableChars.exists(getCurrentSelected()))
+    {
+      curChar = availableChars.get(getCurrentSelected());
+
+      if (controls.ACCEPT)
+      {
+        cursorConfirmed.visible = true;
+        cursorConfirmed.x = cursor.x - 2;
+        cursorConfirmed.y = cursor.y - 4;
+        cursorConfirmed.animation.play("idle", true);
+
+        grpCursors.visible = false;
+
+        FlxG.sound.play(Paths.sound('CS_confirm'));
+
+        FlxTween.tween(FlxG.sound.music, {pitch: 0.1}, 1.5, {ease: FlxEase.quadInOut});
+        playerChill.playAnimation("select");
+        pressedSelect = true;
+        selectTimer.start(1.5, (_) -> {
+          pressedSelect = false;
+          close();
+        });
+      }
+
+      if (pressedSelect && controls.BACK)
+      {
+        cursorConfirmed.visible = false;
+        grpCursors.visible = true;
+
+        FlxTween.globalManager.cancelTweensOf(FlxG.sound.music);
+        FlxTween.tween(FlxG.sound.music, {pitch: 1.0}, 1, {ease: FlxEase.quartInOut});
+        playerChill.playAnimation("deselect");
+        pressedSelect = false;
+        selectTimer.cancel();
+      }
+    }
+    else
+    {
+      curChar = "locked";
+
+      if (controls.ACCEPT)
+      {
+        cursorDenied.visible = true;
+        cursorDenied.x = cursor.x - 2;
+        cursorDenied.y = cursor.y - 4;
+        cursorDenied.animation.play("idle", true);
+        cursorDenied.animation.finishCallback = (_) -> {
+          cursorDenied.visible = false;
+        };
+      }
+    }
+
+    updateLockAnims();
+
+    camFollow.screenCenter();
+    camFollow.x += cursorX * 10;
+    camFollow.y += cursorY * 10;
+
+    cursorLocIntended.x = (cursorFactor * cursorX) + (FlxG.width / 2) - cursor.width / 2;
+    cursorLocIntended.y = (cursorFactor * cursorY) + (FlxG.height / 2) - cursor.height / 2;
+
+    cursorLocIntended.x += cursorOffsetX;
+    cursorLocIntended.y += cursorOffsetY;
+
+    cursor.x = MathUtil.coolLerp(cursor.x, cursorLocIntended.x, lerpAmnt);
+    cursor.y = MathUtil.coolLerp(cursor.y, cursorLocIntended.y, lerpAmnt);
+
+    cursorBlue.x = MathUtil.coolLerp(cursorBlue.x, cursor.x, lerpAmnt * 0.4);
+    cursorBlue.y = MathUtil.coolLerp(cursorBlue.y, cursor.y, lerpAmnt * 0.4);
+
+    cursorDarkBlue.x = MathUtil.coolLerp(cursorDarkBlue.x, cursorLocIntended.x, lerpAmnt * 0.2);
+    cursorDarkBlue.y = MathUtil.coolLerp(cursorDarkBlue.y, cursorLocIntended.y, lerpAmnt * 0.2);
+  }
+
+  function spamOnStep()
+  {
+    if (spamUp || spamDown || spamLeft || spamRight)
+    {
+      selectSound.changePitchBySemitone(1);
+      if (selectSound.pitch > 5) selectSound.pitch = 5;
+      selectSound.play(true);
+
+      if (spamUp)
+      {
+        cursorY -= 1;
+        holdTmrUp = 0;
+      }
+      if (spamDown)
+      {
+        cursorY += 1;
+        holdTmrDown = 0;
+      }
+      if (spamLeft)
+      {
+        cursorX -= 1;
+        holdTmrLeft = 0;
+      }
+      if (spamRight)
+      {
+        cursorX += 1;
+        holdTmrRight = 0;
+      }
+    }
+  }
+
+  private function updateLockAnims()
+  {
+    for (index => member in grpIcons.group.members)
+    {
+      switch (member.ID)
+      {
+        case 1:
+          if (index == getCurrentSelected())
+          {
+            switch (member.animation.curAnim.name)
+            {
+              case "idle":
+                member.animation.play("selected");
+              case "selected" | "clicked":
+                if (controls.ACCEPT) member.animation.play("clicked", true);
+            }
+          }
+          else
+          {
+            member.animation.play("idle");
+          }
+        case 0:
+          var memb:CharIconCharacter = cast member;
+
+          if (index == getCurrentSelected())
+          {
+            // memb.pixels = memb.withDropShadow.clone();
+            memb.setGraphicSize(128 * 1.3);
+          }
+          else
+          {
+            // memb.pixels = memb.noDropShadow.clone();
+            memb.setGraphicSize(128);
+          }
+      }
+    }
+  }
+
+  function getCurrentSelected()
+  {
+    var tempX:Int = cursorX + 1;
+    var tempY:Int = cursorY + 1;
+    var gridPosition:Int = tempX + tempY * 3;
+    return gridPosition;
+  }
+
+  function set_curChar(value:String):String
+  {
+    if (curChar == value) return value;
+
+    curChar = value;
+
+    nametag.switchChar(value);
+    playerChill.visible = false;
+    playerChillOut.visible = true;
+    playerChillOut.anim.goToFrameLabel("slideout");
+    playerChillOut.anim.callback = (_, frame:Int) -> {
+      if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 1)
+      {
+        playerChill.visible = true;
+        playerChill.switchChar(value);
+        gfChill.switchGF(value);
+      }
+      if (frame == playerChillOut.anim.getFrameLabel("slideout").index + 2)
+      {
+        playerChillOut.switchChar(value);
+        playerChillOut.visible = false;
+      }
+    };
+    return value;
+  }
+
+  function set_grpXSpread(value:Float):Float
+  {
+    grpXSpread = value;
+    updateIconPositions();
+    return value;
+  }
+
+  function set_grpYSpread(value:Float):Float
+  {
+    grpYSpread = value;
+    updateIconPositions();
+    return value;
+  }
+}
diff --git a/source/funkin/ui/charSelect/Nametag.hx b/source/funkin/ui/charSelect/Nametag.hx
new file mode 100644
index 000000000..b6cedb0c7
--- /dev/null
+++ b/source/funkin/ui/charSelect/Nametag.hx
@@ -0,0 +1,101 @@
+package funkin.ui.charSelect;
+
+import flixel.FlxSprite;
+import funkin.graphics.shaders.MosaicEffect;
+import flixel.util.FlxTimer;
+
+class Nametag extends FlxSprite
+{
+  var midpointX(default, set):Float = 1008;
+  var midpointY(default, set):Float = 100;
+  var mosaicShader:MosaicEffect;
+
+  public function new(?x:Float = 0, ?y:Float = 0)
+  {
+    super(x, y);
+
+    mosaicShader = new MosaicEffect();
+    shader = mosaicShader;
+
+    switchChar("bf");
+
+    FlxG.debugger.addTrackerProfile(new TrackerProfile(Nametag, ["midpointX", "midpointY"]));
+    FlxG.debugger.track(this, "Nametag");
+  }
+
+  public function updatePosition():Void
+  {
+    var offsetX:Float = getMidpoint().x - midpointX;
+    var offsetY:Float = getMidpoint().y - midpointY;
+
+    x -= offsetX;
+    y -= offsetY;
+  }
+
+  public function switchChar(str:String):Void
+  {
+    shaderEffect();
+
+    new FlxTimer().start(4 / 30, _ -> {
+      var path:String = str;
+      switch str
+      {
+        case "bf":
+          path = "boyfriend";
+      }
+
+      loadGraphic(Paths.image('charSelect/' + path + "Nametag"));
+      updateHitbox();
+      scale.x = scale.y = 0.77;
+
+      updatePosition();
+      shaderEffect(true);
+    });
+  }
+
+  function shaderEffect(fadeOut:Bool = false):Void
+  {
+    if (fadeOut)
+    {
+      setBlockTimer(0, 1, 1);
+      setBlockTimer(1, width / 27, height / 26);
+      setBlockTimer(2, width / 10, height / 10);
+
+      setBlockTimer(3, 1, 1);
+    }
+    else
+    {
+      setBlockTimer(0, (width / 10), (height / 10));
+      setBlockTimer(1, width / 73, height / 6);
+      setBlockTimer(2, width / 10, height / 10);
+    }
+  }
+
+  function setBlockTimer(frame:Int, ?forceX:Float, ?forceY:Float)
+  {
+    var daX:Float = 10 * FlxG.random.int(1, 4);
+    var daY:Float = 10 * FlxG.random.int(1, 4);
+
+    if (forceX != null) daX = forceX;
+
+    if (forceY != null) daY = forceY;
+
+    new FlxTimer().start(frame / 30, _ -> {
+      mosaicShader.setBlockSize(daX, daY);
+    });
+  }
+
+  function set_midpointX(val:Float):Float
+  {
+    this.midpointX = val;
+    updatePosition();
+    return val;
+  }
+
+  function set_midpointY(val:Float):Float
+  {
+    this.midpointY = val;
+    updatePosition();
+    return val;
+  }
+}
diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index f8b1be9d2..22dc5dba6 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -56,6 +56,7 @@ class DebugMenuSubState extends MusicBeatSubState
     // Call onMenuChange when the first item is created to move the camera .
     onMenuChange(createItem("CHART EDITOR", openChartEditor));
     // createItem("Input Offset Testing", openInputOffsetTesting);
+    createItem("CHARACTER SELECT", openCharSelect, true);
     createItem("ANIMATION EDITOR", openAnimationEditor);
     // createItem("STAGE EDITOR", openStageEditor);
     // createItem("TEST STICKERS", testStickers);
@@ -102,6 +103,11 @@ class DebugMenuSubState extends MusicBeatSubState
     trace('Input Offset Testing');
   }
 
+  function openCharSelect()
+  {
+    FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
+  }
+
   function openAnimationEditor()
   {
     FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());