diff --git a/assets b/assets
index 69ebdb6a7..51b02f0d4 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120
+Subproject commit 51b02f0d47e5b34bf8589065c092953c10c5040d
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e7ad326d2..46c09090d 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -111,6 +111,11 @@ typedef PlayStateParams =
    * @default `false`
    */
   ?practiceMode:Bool,
+  /**
+   * Whether the song should start in Bot Play Mode.
+   * @default `false`
+   */
+  ?botPlayMode:Bool,
   /**
    * Whether the song should be in minimal mode.
    * @default `false`
@@ -282,6 +287,12 @@ class PlayState extends MusicBeatSubState
    */
   public var isPracticeMode:Bool = false;
 
+  /**
+   * Whether the game is currently in Bot Play Mode.
+   * If true, player will not lose gain or lose score from notes.
+   */
+  public var isBotPlayMode:Bool = false;
+
   /**
    * Whether the player has dropped below zero health,
    * and we are just waiting for an animation to play out before transitioning.
@@ -566,6 +577,7 @@ class PlayState extends MusicBeatSubState
     if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
     if (params.targetVariation != null) currentVariation = params.targetVariation;
     isPracticeMode = params.practiceMode ?? false;
+    isBotPlayMode = params.botPlayMode ?? false;
     isMinimalMode = params.minimalMode ?? false;
     startTimestamp = params.startTimestamp ?? 0.0;
     playbackRate = params.playbackRate ?? 1.0;
@@ -1614,7 +1626,7 @@ class PlayState extends MusicBeatSubState
     var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
     if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
 
-    playerStrumline = new Strumline(noteStyle, true);
+    playerStrumline = new Strumline(noteStyle, !isBotPlayMode);
     opponentStrumline = new Strumline(noteStyle, false);
     add(playerStrumline);
     add(opponentStrumline);
@@ -1876,7 +1888,14 @@ class PlayState extends MusicBeatSubState
   function updateScoreText():Void
   {
     // TODO: Add functionality for modules to update the score text.
-    scoreText.text = 'Score:' + songScore;
+    if (isBotPlayMode)
+    {
+      scoreText.text = 'Bot Play Enabled';
+    }
+    else
+    {
+      scoreText.text = 'Score:' + songScore;
+    }
   }
 
   /**
@@ -1884,7 +1903,14 @@ class PlayState extends MusicBeatSubState
    */
   function updateHealthBar():Void
   {
-    healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+    if (isBotPlayMode)
+    {
+      healthLerp = Constants.HEALTH_MAX;
+    }
+    else
+    {
+      healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+    }
   }
 
   /**
@@ -1928,13 +1954,16 @@ class PlayState extends MusicBeatSubState
 
       if (Conductor.instance.songPosition > hitWindowEnd)
       {
-        if (note.hasMissed) continue;
+        if (note.hasMissed || note.hasBeenHit) continue;
 
         note.tooEarly = false;
         note.mayHit = false;
         note.hasMissed = true;
 
-        if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+        if (note.holdNoteSprite != null)
+        {
+          note.holdNoteSprite.missedNote = true;
+        }
       }
       else if (Conductor.instance.songPosition > hitWindowCenter)
       {
@@ -2021,10 +2050,38 @@ class PlayState extends MusicBeatSubState
 
       if (Conductor.instance.songPosition > hitWindowEnd)
       {
+        if (note.hasMissed || note.hasBeenHit) continue;
         note.tooEarly = false;
         note.mayHit = false;
         note.hasMissed = true;
-        if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+        if (note.holdNoteSprite != null)
+        {
+          note.holdNoteSprite.missedNote = true;
+        }
+      }
+      else if (isBotPlayMode && Conductor.instance.songPosition > hitWindowCenter)
+      {
+        if (note.hasBeenHit) continue;
+
+        // We call onHitNote to play the proper animations,
+        // but not goodNoteHit! This means zero score and zero notes hit for the results screen!
+
+        // 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);
+        dispatchEvent(event);
+
+        // Calling event.cancelEvent() skips all the other logic! Neat!
+        if (event.eventCanceled) continue;
+
+        // Command the bot to hit the note on time.
+        // NOTE: This is what handles the strumline and cleaning up the note itself!
+        playerStrumline.hitNote(note);
+
+        if (note.holdNoteSprite != null)
+        {
+          playerStrumline.playNoteHoldCover(note.holdNoteSprite);
+        }
       }
       else if (Conductor.instance.songPosition > hitWindowStart)
       {
@@ -2069,7 +2126,7 @@ 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 (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
+      if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
       {
         // Grant the player health.
         health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 190aa3ee0..efe1c707a 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -38,6 +38,10 @@ class Strumline extends FlxSpriteGroup
     return FlxG.height / 0.45;
   }
 
+  /**
+   * Whether this strumline is controlled by the player's inputs.
+   * False means it's controlled by the opponent or Bot Play.
+   */
   public var isPlayer:Bool;
 
   /**
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 78e73bf27..85a2396b9 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -592,6 +592,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var playtestPracticeMode:Bool = false;
 
+  /**
+   * If true, playtesting a chart will make the computer do it for you!
+   */
+  var playtestBotPlayMode:Bool = false;
+
   /**
    * Enables or disables the "debugger" popup that appears when you run into a flixel error.
    */
@@ -5359,6 +5364,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         targetDifficulty: selectedDifficulty,
         targetVariation: selectedVariation,
         practiceMode: playtestPracticeMode,
+        botPlayMode: playtestBotPlayMode,
         minimalMode: minimal,
         startTimestamp: startTimestamp,
         playbackRate: playbackRate,
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index f32cc2bfb..3b32edf5d 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -299,6 +299,15 @@ class ChartEditorToolboxHandler
       state.playtestStartTime = checkboxStartTime.selected;
     };
 
+    var checkboxBotPlay:Null<CheckBox> = toolbox.findComponent('playtestBotPlayCheckbox', CheckBox);
+    if (checkboxBotPlay == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestBotPlayCheckbox component.';
+
+    checkboxBotPlay.selected = state.playtestBotPlayMode;
+
+    checkboxBotPlay.onClick = _ -> {
+      state.playtestBotPlayMode = checkboxBotPlay.selected;
+    };
+
     var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
 
     if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 50f85571b..45f9a4d27 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1143,12 +1143,12 @@ class FreeplayState extends MusicBeatSubState
           targetSong: targetSong,
           targetDifficulty: targetDifficulty,
           targetVariation: targetVariation,
-          // TODO: Make this an option!
-          // startTimestamp: 0.0,
-          // TODO: Make this an option!
-          // playbackRate: 0.5,
           practiceMode: false,
           minimalMode: false,
+          // TODO: Make these an option! It's currently only accessible via chart editor.
+          // startTimestamp: 0.0,
+          // playbackRate: 0.5,
+          // botPlayMode: true,
         }, true);
     });
   }