diff --git a/assets b/assets
index 69ebdb6a7..518349369 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120
+Subproject commit 518349369ea3237504e8100c901313185bb5b80f
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index b009aea41..5f2ff2b9e 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -56,7 +56,20 @@ interface IStateStageProp extends IScriptedClass
  */
 interface INoteScriptedClass extends IScriptedClass
 {
-  public function onNoteHit(event:NoteScriptEvent):Void;
+  /**
+   * Called when a note enters the field of view and approaches the strumline.
+   */
+  public function onNoteIncoming(event:NoteScriptEvent):Void;
+
+  /**
+   * Called when EITHER player hits a note.
+   * Query the note attached to the event to determine if it was hit by the player or CPU.
+   */
+  public function onNoteHit(event:HitNoteScriptEvent):Void;
+
+  /**
+   * Called when EITHER player (usually the player) misses a note.
+   */
   public function onNoteMiss(event:NoteScriptEvent):Void;
 }
 
@@ -73,7 +86,7 @@ interface INoteScriptedClass extends IScriptedClass
 /**
  * Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
  */
-interface IPlayStateScriptedClass extends IScriptedClass
+interface IPlayStateScriptedClass extends INoteScriptedClass
 {
   /**
    * Called when the game is paused.
@@ -113,17 +126,6 @@ interface IPlayStateScriptedClass extends IScriptedClass
    */
   public function onSongRetry(event:ScriptEvent):Void;
 
-  /**
-   * Called when EITHER player hits a note.
-   * Query the note attached to the event to determine if it was hit by the player or CPU.
-   */
-  public function onNoteHit(event:NoteScriptEvent):Void;
-
-  /**
-   * Called when EITHER player (usually the player) misses a note.
-   */
-  public function onNoteMiss(event:NoteScriptEvent):Void;
-
   /**
    * Called when the player presses a key when no note is on the strumline.
    */
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index f5d797ea4..fd58d0fad 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -71,17 +71,29 @@ class ScriptEventDispatcher
       }
     }
 
-    if (Std.isOfType(target, IPlayStateScriptedClass))
+    if (Std.isOfType(target, INoteScriptedClass))
     {
-      var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+      var t:INoteScriptedClass = cast(target, INoteScriptedClass);
       switch (event.type)
       {
+        case NOTE_INCOMING:
+          t.onNoteIncoming(cast event);
+          return;
         case NOTE_HIT:
           t.onNoteHit(cast event);
           return;
         case NOTE_MISS:
           t.onNoteMiss(cast event);
           return;
+        default: // Continue;
+      }
+    }
+
+    if (Std.isOfType(target, IPlayStateScriptedClass))
+    {
+      var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+      switch (event.type)
+      {
         case NOTE_GHOST_MISS:
           t.onNoteGhostMiss(cast event);
           return;
diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx
index e06b5ad24..eeeb8ef29 100644
--- a/source/funkin/modding/events/ScriptEventType.hx
+++ b/source/funkin/modding/events/ScriptEventType.hx
@@ -63,6 +63,13 @@ enum abstract ScriptEventType(String) from String to String
    */
   var SONG_STEP_HIT = 'STEP_HIT';
 
+  /**
+   * Called when a note comes on screen and starts approaching the strumline.
+   *
+   * This event is not cancelable.
+   */
+  var NOTE_INCOMING = 'NOTE_INCOMING';
+
   /**
    * Called when a character hits a note.
    * Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index f50c9936a..be9b7146b 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -83,7 +83,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
 
   public function onGameOver(event:ScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e7ad326d2..5564c46c2 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1614,8 +1614,10 @@ 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);
+    playerStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
     opponentStrumline = new Strumline(noteStyle, false);
+    opponentStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
     add(playerStrumline);
     add(opponentStrumline);
 
@@ -1751,6 +1753,13 @@ class PlayState extends MusicBeatSubState
     opponentStrumline.applyNoteData(opponentNoteData);
   }
 
+  function onStrumlineNoteIncoming(noteSprite:NoteSprite):Void
+  {
+    var event:NoteScriptEvent = new NoteScriptEvent(NOTE_INCOMING, noteSprite, 0, false);
+
+    dispatchEvent(event);
+  }
+
   /**
    * Prepares to start the countdown.
    * Ends any running cutscenes, creates the strumlines, and starts the countdown.
@@ -1942,7 +1951,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 NoteScriptEvent(NOTE_HIT, note, 0, true);
+        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
         dispatchEvent(event);
 
         // Calling event.cancelEvent() skips all the other logic! Neat!
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index cf5311bdc..d39f19b76 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -485,7 +485,7 @@ class BaseCharacter extends Bopper
    * Every time a note is hit, check if the note is from the same strumline.
    * If it is, then play the sing animation.
    */
-  public override function onNoteHit(event:NoteScriptEvent)
+  public override function onNoteHit(event:HitNoteScriptEvent)
   {
     super.onNoteHit(event);
 
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 190aa3ee0..1ba5dcfc5 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -1,5 +1,6 @@
 package funkin.play.notes;
 
+import flixel.util.FlxSignal.FlxTypedSignal;
 import flixel.FlxG;
 import funkin.play.notes.notestyle.NoteStyle;
 import flixel.group.FlxSpriteGroup;
@@ -49,6 +50,8 @@ class Strumline extends FlxSpriteGroup
 
   public var holdNotes:FlxTypedSpriteGroup<SustainTrail>;
 
+  public var onNoteIncoming:FlxTypedSignal<NoteSprite->Void>;
+
   var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
   var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
   var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
@@ -106,6 +109,8 @@ class Strumline extends FlxSpriteGroup
 
     this.refresh();
 
+    this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
+
     for (i in 0...KEY_COUNT)
     {
       var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
@@ -311,6 +316,8 @@ class Strumline extends FlxSpriteGroup
       }
 
       nextNoteIndex = noteIndex + 1; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
+
+      onNoteIncoming.dispatch(noteSprite);
     }
 
     // Update rendering of notes.
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 61f83d1ed..3997692c2 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -364,7 +364,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
 
   public function onSongRetry(event:ScriptEvent):Void {};
 
-  public function onNoteHit(event:NoteScriptEvent):Void {};
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent):Void {};
 
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 7974900d8..3a57072e6 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -353,7 +353,9 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
 
   public function onGameOver(event:ScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index 32c0509a5..9605c6989 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -870,7 +870,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
 
   public function onCountdownEnd(event:CountdownScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 78e73bf27..4449134b8 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -5928,7 +5928,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 NoteScriptEvent(NOTE_HIT, tempNote, 1, true);
+      var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
       dispatchEvent(event);
 
       // Calling event.cancelEvent() skips all the other logic! Neat!
diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
index c7171fac7..77b23d68a 100644
--- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx
+++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
@@ -2,6 +2,7 @@ package funkin.ui.haxeui.components;
 
 import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent;
 import funkin.modding.events.ScriptEvent.NoteScriptEvent;
+import funkin.modding.events.ScriptEvent.HitNoteScriptEvent;
 import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
 import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
 import haxe.ui.core.IDataComponent;
@@ -216,12 +217,17 @@ class CharacterPlayer extends Box
     if (character != null) character.onStepHit(event);
   }
 
+  public function onNoteIncoming(event:NoteScriptEvent)
+  {
+    if (character != null) character.onNoteIncoming(event);
+  }
+
   /**
    * Called when a note is hit in the song
    * Used to play character animations.
    * @param event The event.
    */
-  public function onNoteHit(event:NoteScriptEvent):Void
+  public function onNoteHit(event:HitNoteScriptEvent):Void
   {
     if (character != null) character.onNoteHit(event);
   }