diff --git a/Audio/335571__magntron__gamemusic_120bpm.mp3 b/Audio/335571__magntron__gamemusic_120bpm.mp3
new file mode 100644
index 00000000..de6a511f
Binary files /dev/null and b/Audio/335571__magntron__gamemusic_120bpm.mp3 differ
diff --git a/Audio/335571__magntron__gamemusic_120bpm.mp3.import b/Audio/335571__magntron__gamemusic_120bpm.mp3.import
new file mode 100644
index 00000000..9c209bfe
--- /dev/null
+++ b/Audio/335571__magntron__gamemusic_120bpm.mp3.import
@@ -0,0 +1,19 @@
+[remap]
+
+importer="mp3"
+type="AudioStreamMP3"
+uid="uid://cv6lqjj6lu36h"
+path="res://.godot/imported/335571__magntron__gamemusic_120bpm.mp3-a87b357c4b3c9199709863b47f78bd2a.mp3str"
+
+[deps]
+
+source_file="res://Audio/335571__magntron__gamemusic_120bpm.mp3"
+dest_files=["res://.godot/imported/335571__magntron__gamemusic_120bpm.mp3-a87b357c4b3c9199709863b47f78bd2a.mp3str"]
+
+[params]
+
+loop=true
+loop_offset=0
+bpm=0
+beat_count=0
+bar_beats=4
diff --git a/Classes/Note.cs b/Classes/Note.cs
new file mode 100644
index 00000000..193bbc3f
--- /dev/null
+++ b/Classes/Note.cs
@@ -0,0 +1,18 @@
+using System;
+using Godot;
+
+/**
+ * @class Note
+ * @brief Data structure class for holding data and methods for a battle time note. WIP
+ */
+public partial class Note : Resource
+{
+ public int Beat;
+ public NoteArrow.ArrowType Type;
+
+ public Note(NoteArrow.ArrowType type = NoteArrow.ArrowType.Up, int beat = 0)
+ {
+ Beat = beat;
+ Type = type;
+ }
+}
diff --git a/Funk Engine.sln.DotSettings.user b/Funk Engine.sln.DotSettings.user
new file mode 100644
index 00000000..6125b70c
--- /dev/null
+++ b/Funk Engine.sln.DotSettings.user
@@ -0,0 +1,3 @@
+
+ ForceIncluded
+ ForceIncluded
\ No newline at end of file
diff --git a/README.md b/README.md
index 5e6b8873..727c86ea 100644
--- a/README.md
+++ b/README.md
@@ -13,3 +13,9 @@ Current team members include:
+#### Attributions:
+Note icon: Next icons created by Pixel perfect - Flaticon
+
+First Song: gameMusic by Magntron - freesound.org
+
+
diff --git a/project.godot b/project.godot
index c0855df0..ba8f8e20 100644
--- a/project.godot
+++ b/project.godot
@@ -11,10 +11,14 @@ config_version=5
[application]
config/name="Funk Engine"
-run/main_scene="res://scenes/main.tscn"
+run/main_scene="res://scenes/BattleDirector/test_battle_scene.tscn"
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
config/icon="res://icon.svg"
+[autoload]
+
+TimeKeeper="*res://scripts/TimeKeeper.cs"
+
[display]
window/size/viewport_width=640
@@ -25,3 +29,38 @@ window/stretch/scale_mode="integer"
[dotnet]
project/assembly_name="Funk Engine"
+
+[input]
+
+arrowUp={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+arrowDown={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+arrowLeft={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+arrowRight={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
+]
+}
+
+[rendering]
+
+textures/canvas_textures/default_texture_filter=0
+
+[threading]
+
+worker_pool/canvas_textures/default_texture_filter=1
diff --git a/scenes/BattleDirector/BattleDirector.cs b/scenes/BattleDirector/BattleDirector.cs
new file mode 100644
index 00000000..6de6518c
--- /dev/null
+++ b/scenes/BattleDirector/BattleDirector.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Godot;
+
+/**
+ * @class BattleDirector
+ * @brief Higher priority director to manage battle effects. Can directly access managers, which should signal up to Director WIP
+ */
+public partial class BattleDirector : Node2D
+{
+ #region Declarations
+ private HealthBar Player;
+ private HealthBar Enemy;
+
+ [Export]
+ private ChartManager CM;
+
+ [Export]
+ private InputHandler IH;
+
+ [Export]
+ private NotePlacementBar NotePlacementBar;
+
+ [Export]
+ private AudioStreamPlayer Audio;
+
+ private double _timingInterval = .1; //secs
+
+ [Signal]
+ public delegate void PlayerDamageEventHandler(int damage);
+
+ [Signal]
+ public delegate void EnemyDamageEventHandler(int damage);
+
+ private SongData _curSong;
+
+ public struct SongData
+ {
+ public int Bpm;
+ public double SongLength;
+ public int NumLoops;
+ }
+ #endregion
+
+ #region Note Handling
+ //Assume queue structure for notes in each lane.
+ //Can eventually make this its own structure
+ private NoteArrow[][] _laneData = Array.Empty();
+ private int[] _laneLastBeat = new int[]
+ { //Temporary (hopefully) measure to bridge from note queue structure to ordered array
+ 0,
+ 0,
+ 0,
+ 0,
+ };
+ private Note[] _notes = Array.Empty();
+
+ //Returns first note of lane without modifying lane data
+ private Note GetNoteAt(NoteArrow.ArrowType dir, int beat)
+ {
+ return GetNote(_laneData[(int)dir][beat]);
+ }
+
+ //Get note of a note arrow
+ private Note GetNote(NoteArrow arrow)
+ {
+ return _notes[arrow.NoteIdx];
+ }
+
+ private bool AddNoteToLane(Note note, bool isActive = true)
+ {
+ note.Beat %= CM.BeatsPerLoop;
+ //Don't add dupe notes
+ if (note.Beat == 0 || _notes.Any(nt => nt.Type == note.Type && nt.Beat == note.Beat))
+ {
+ return false; //Beat at 0 is too messy.
+ }
+ _notes = _notes.Append(note).ToArray();
+ //Get noteArrow from CM
+ var arrow = CM.AddArrowToLane(note, _notes.Length - 1);
+ arrow.IsActive = isActive;
+ _laneData[(int)note.Type][note.Beat] = arrow;
+ return true;
+ }
+ #endregion
+
+ //Creeate dummy notes
+ private void AddExampleNotes()
+ {
+ GD.Print(CM.BeatsPerLoop);
+ for (int i = 1; i < 15; i++)
+ {
+ Note exampleNote = new Note(NoteArrow.ArrowType.Up, i * 4);
+ AddNoteToLane(exampleNote);
+ }
+ for (int i = 1; i < 15; i++)
+ {
+ Note exampleNote = new Note(NoteArrow.ArrowType.Left, 4 * i + 1);
+ AddNoteToLane(exampleNote);
+ }
+ for (int i = 0; i < 10; i++)
+ {
+ Note exampleNote = new Note(NoteArrow.ArrowType.Right, 3 * i + 32);
+ AddNoteToLane(exampleNote);
+ }
+ for (int i = 0; i < 3; i++)
+ {
+ Note exampleNote = new Note(NoteArrow.ArrowType.Down, 8 * i + 16);
+ AddNoteToLane(exampleNote);
+ }
+ }
+
+ public override void _Ready()
+ {
+ _curSong = new SongData
+ {
+ Bpm = 120,
+ SongLength = Audio.Stream.GetLength(),
+ NumLoops = 5,
+ };
+
+ var timer = GetTree().CreateTimer(AudioServer.GetTimeToNextMix());
+ timer.Timeout += Begin;
+ }
+
+ private void Begin()
+ {
+ CM.PrepChart(_curSong);
+ _laneData = new NoteArrow[][]
+ {
+ new NoteArrow[CM.BeatsPerLoop],
+ new NoteArrow[CM.BeatsPerLoop],
+ new NoteArrow[CM.BeatsPerLoop],
+ new NoteArrow[CM.BeatsPerLoop],
+ };
+ AddExampleNotes();
+
+ Player = GetNode("PlayerHP");
+ Player.GetNode("Sprite2D").Scale *= .5f; //TEMP
+ Player.GetNode("Sprite2D").Position += Vector2.Down * 30; //TEMP
+ Enemy = GetNode("EnemyHP");
+
+ //TEMP
+ var enemTween = CreateTween();
+ enemTween
+ .TweenProperty(Enemy.GetNode("Sprite2D"), "position", Vector2.Down * 5, 1f)
+ .AsRelative();
+ enemTween
+ .TweenProperty(Enemy.GetNode("Sprite2D"), "position", Vector2.Up * 5, 1f)
+ .AsRelative();
+ enemTween.SetTrans(Tween.TransitionType.Spring);
+ enemTween.SetEase(Tween.EaseType.In);
+ enemTween.SetLoops();
+ enemTween.Play();
+
+ CM.Connect(nameof(InputHandler.NotePressed), new Callable(this, nameof(OnNotePressed)));
+ CM.Connect(nameof(InputHandler.NoteReleased), new Callable(this, nameof(OnNoteReleased)));
+
+ Audio.Play();
+ }
+
+ public override void _Process(double delta)
+ {
+ TimeKeeper.CurrentTime = Audio.GetPlaybackPosition();
+ CheckMiss();
+ }
+
+ #region Input&Timing
+ private void OnNotePressed(NoteArrow.ArrowType type)
+ {
+ CheckNoteTiming(type);
+ }
+
+ private void OnNoteReleased(NoteArrow.ArrowType arrowType) { }
+
+ //Check all lanes for misses from missed inputs
+ private void CheckMiss()
+ {
+ //On current beat, if prev beat is active and not inputted
+ double realBeat = TimeKeeper.CurrentTime / (60 / (double)_curSong.Bpm) % CM.BeatsPerLoop;
+ for (int i = 0; i < _laneData.Length; i++)
+ {
+ if (
+ _laneLastBeat[i] < Math.Floor(realBeat)
+ || (_laneLastBeat[i] == CM.BeatsPerLoop - 1 && Math.Floor(realBeat) == 0)
+ )
+ { //If above, a note has been missed
+ //GD.Print("Last beat " + _laneLastBeat[i]);
+ if (
+ _laneData[i][_laneLastBeat[i]] == null
+ || !_laneData[i][_laneLastBeat[i]].IsActive
+ )
+ {
+ _laneLastBeat[i] = (_laneLastBeat[i] + 1) % CM.BeatsPerLoop;
+ continue;
+ }
+ //Note exists and has been missed
+ _laneData[i][_laneLastBeat[i]].NoteHit();
+ HandleTiming((NoteArrow.ArrowType)i, 1);
+ _laneLastBeat[i] = (_laneLastBeat[i] + 1) % CM.BeatsPerLoop;
+ }
+ }
+ }
+
+ private void CheckNoteTiming(NoteArrow.ArrowType type)
+ {
+ double realBeat = TimeKeeper.CurrentTime / (60 / (double)_curSong.Bpm) % CM.BeatsPerLoop;
+ int curBeat = (int)Math.Round(realBeat);
+ GD.Print("Cur beat " + curBeat + "Real: " + realBeat.ToString("#.###"));
+ if (
+ _laneData[(int)type][curBeat % CM.BeatsPerLoop] == null
+ || !_laneData[(int)type][curBeat % CM.BeatsPerLoop].IsActive
+ )
+ {
+ _laneLastBeat[(int)type] = (curBeat) % CM.BeatsPerLoop;
+ PlayerAddNote(type, curBeat);
+ return;
+ }
+ double beatDif = Math.Abs(realBeat - curBeat);
+ _laneData[(int)type][curBeat % CM.BeatsPerLoop].NoteHit();
+ _laneLastBeat[(int)type] = (curBeat) % CM.BeatsPerLoop;
+ HandleTiming(type, beatDif);
+ }
+
+ private void HandleTiming(NoteArrow.ArrowType type, double beatDif)
+ {
+ if (beatDif < _timingInterval * 1)
+ {
+ GD.Print("Perfect");
+ Enemy.TakeDamage(3);
+ NotePlacementBar.HitNote();
+ NotePlacementBar.ComboText("Perfect!");
+ }
+ else if (beatDif < _timingInterval * 2)
+ {
+ GD.Print("Good");
+ Enemy.TakeDamage(1);
+ NotePlacementBar.HitNote();
+ NotePlacementBar.ComboText("Good");
+ }
+ else if (beatDif < _timingInterval * 3)
+ {
+ GD.Print("Ok");
+ Player.TakeDamage(1);
+ NotePlacementBar.HitNote();
+ NotePlacementBar.ComboText("Okay");
+ }
+ else
+ {
+ GD.Print("Miss");
+ Player.TakeDamage(2);
+ NotePlacementBar.MissNote();
+ NotePlacementBar.ComboText("Miss");
+ }
+ }
+ #endregion
+
+ private void PlayerAddNote(NoteArrow.ArrowType type, int beat)
+ {
+ // can also add some sort of keybind here to also have pressed
+ // in case the user just presses the note too early and spawns a note
+ GD.Print(
+ $"Player trying to place {type} typed note at beat: "
+ + beat
+ + " Verdict: "
+ + NotePlacementBar.CanPlaceNote()
+ );
+ if (NotePlacementBar.CanPlaceNote())
+ {
+ Note exampleNote = new Note(type, beat % CM.BeatsPerLoop);
+ if (AddNoteToLane(exampleNote, false))
+ NotePlacementBar.PlacedNote();
+ }
+ }
+}
diff --git a/scenes/BattleDirector/HealthBar.cs b/scenes/BattleDirector/HealthBar.cs
new file mode 100644
index 00000000..c0ad3e2e
--- /dev/null
+++ b/scenes/BattleDirector/HealthBar.cs
@@ -0,0 +1,48 @@
+using System;
+using Godot;
+
+public partial class HealthBar : Control
+{
+ const int MaxHealth = 100;
+ int _health = MaxHealth;
+
+ [Export]
+ public TextureProgressBar PlayerHealthBar;
+
+ [Export]
+ public Texture2D SpriteText;
+
+ //we can change this to a Texture Progress bar once we have art assets for it
+
+
+ public override void _Ready()
+ {
+ if (PlayerHealthBar != null)
+ {
+ SetHealth(MaxHealth, MaxHealth);
+ }
+ GetNode("Sprite2D").Texture = SpriteText;
+ }
+
+ public void SetHealth(int max, int current)
+ {
+ PlayerHealthBar.MaxValue = max;
+ PlayerHealthBar.Value = current;
+ _updateHealthBar();
+ }
+
+ private void _updateHealthBar()
+ {
+ PlayerHealthBar.Value = _health;
+ }
+
+ public void TakeDamage(int damage)
+ {
+ _health -= damage;
+ if (_health <= 0)
+ {
+ GD.Print("You are dead");
+ }
+ _updateHealthBar();
+ }
+}
diff --git a/scenes/BattleDirector/HealthBar.tscn b/scenes/BattleDirector/HealthBar.tscn
new file mode 100644
index 00000000..0f72abd7
--- /dev/null
+++ b/scenes/BattleDirector/HealthBar.tscn
@@ -0,0 +1,47 @@
+[gd_scene load_steps=8 format=3 uid="uid://bgomxovxs7sr8"]
+
+[ext_resource type="Script" path="res://scenes/BattleDirector/HealthBar.cs" id="1_b1t4i"]
+
+[sub_resource type="Gradient" id="Gradient_ve5ki"]
+colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_ti0cv"]
+gradient = SubResource("Gradient_ve5ki")
+width = 150
+height = 20
+
+[sub_resource type="Gradient" id="Gradient_soqhm"]
+colors = PackedColorArray(0, 1, 0.0999999, 1, 1, 1, 1, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_r4hau"]
+gradient = SubResource("Gradient_soqhm")
+width = 146
+height = 16
+
+[sub_resource type="Gradient" id="Gradient_58kj0"]
+offsets = PackedFloat32Array(1)
+colors = PackedColorArray(1, 1, 1, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_wwca1"]
+gradient = SubResource("Gradient_58kj0")
+
+[node name="Control" type="Control" node_paths=PackedStringArray("PlayerHealthBar")]
+layout_mode = 3
+anchors_preset = 0
+offset_right = 40.0
+offset_bottom = 40.0
+script = ExtResource("1_b1t4i")
+PlayerHealthBar = NodePath("ProgressBar")
+
+[node name="ProgressBar" type="TextureProgressBar" parent="."]
+layout_mode = 0
+offset_right = 150.0
+offset_bottom = 20.0
+texture_under = SubResource("GradientTexture2D_ti0cv")
+texture_progress = SubResource("GradientTexture2D_r4hau")
+texture_progress_offset = Vector2(2, 2)
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+position = Vector2(75, 86)
+scale = Vector2(2, 2)
+texture = SubResource("GradientTexture2D_wwca1")
diff --git a/scenes/BattleDirector/NotePlacementBar.cs b/scenes/BattleDirector/NotePlacementBar.cs
new file mode 100644
index 00000000..d86a3c9a
--- /dev/null
+++ b/scenes/BattleDirector/NotePlacementBar.cs
@@ -0,0 +1,82 @@
+using System;
+using Godot;
+
+public partial class NotePlacementBar : Node
+{
+ const int MaxValue = 80;
+ int currentBarValue;
+ int currentCombo;
+ int comboMult;
+ int notesToIncreaseCombo;
+
+ [Export]
+ TextureProgressBar notePlacementBar;
+
+ [Export]
+ TextEdit currentComboMultText;
+
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ notePlacementBar.MaxValue = MaxValue;
+ currentBarValue = 0;
+ currentCombo = 0;
+ comboMult = 1;
+ notesToIncreaseCombo = 4;
+ }
+
+ public void ComboText(string text)
+ {
+ var feedbackScene = ResourceLoader.Load(
+ "res://scenes/BattleDirector/TextParticle.tscn"
+ );
+ TextParticle newText = feedbackScene.Instantiate();
+ AddChild(newText);
+ newText.Text = text + $" {currentCombo}";
+ }
+
+ // Hitting a note increases combo, combo mult, and note placement bar
+ public void HitNote()
+ {
+ currentCombo++;
+ DetermineComboMult();
+ currentBarValue += comboMult;
+ UpdateNotePlacementBar(currentBarValue);
+ UpdateComboMultText();
+ }
+
+ // Missing a note resets combo
+ public void MissNote()
+ {
+ currentCombo = 0;
+ DetermineComboMult();
+ UpdateComboMultText();
+ }
+
+ // Placing a note resets the note placement bar
+ public void PlacedNote()
+ {
+ currentBarValue = 0;
+ UpdateNotePlacementBar(currentBarValue);
+ }
+
+ public bool CanPlaceNote()
+ {
+ return currentBarValue >= MaxValue;
+ }
+
+ private void DetermineComboMult()
+ {
+ comboMult = currentCombo / notesToIncreaseCombo + 1;
+ }
+
+ public void UpdateNotePlacementBar(int newValue)
+ {
+ notePlacementBar.Value = newValue;
+ }
+
+ public void UpdateComboMultText()
+ {
+ currentComboMultText.Text = $"x{comboMult.ToString()}";
+ }
+}
diff --git a/scenes/BattleDirector/NotePlacementBar.tscn b/scenes/BattleDirector/NotePlacementBar.tscn
new file mode 100644
index 00000000..c8dc9577
--- /dev/null
+++ b/scenes/BattleDirector/NotePlacementBar.tscn
@@ -0,0 +1,56 @@
+[gd_scene load_steps=6 format=3 uid="uid://duhiilcv4tat3"]
+
+[ext_resource type="Script" path="res://scenes/BattleDirector/NotePlacementBar.cs" id="1_456es"]
+
+[sub_resource type="Gradient" id="Gradient_0u6yv"]
+colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_hhds4"]
+gradient = SubResource("Gradient_0u6yv")
+width = 26
+height = 100
+
+[sub_resource type="Gradient" id="Gradient_lyd0l"]
+offsets = PackedFloat32Array(0, 0.485915, 0.739437, 1)
+colors = PackedColorArray(0, 1, 0, 1, 0.68, 0, 0.272, 1, 0, 0.64, 0.608, 1, 0.4, 0, 0, 1)
+
+[sub_resource type="GradientTexture2D" id="GradientTexture2D_k7kvy"]
+gradient = SubResource("Gradient_lyd0l")
+width = 24
+height = 98
+fill_to = Vector2(0, 1)
+
+[node name="NotePlacementBar" type="Control" node_paths=PackedStringArray("notePlacementBar", "currentComboMultText")]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_456es")
+notePlacementBar = NodePath("ProgressBar")
+currentComboMultText = NodePath("TextEdit")
+
+[node name="ProgressBar" type="TextureProgressBar" parent="."]
+layout_mode = 0
+offset_right = 26.0
+offset_bottom = 100.0
+fill_mode = 3
+texture_under = SubResource("GradientTexture2D_hhds4")
+texture_progress = SubResource("GradientTexture2D_k7kvy")
+texture_progress_offset = Vector2(1, 1)
+
+[node name="TextEdit" type="TextEdit" parent="."]
+layout_mode = 0
+offset_top = -41.0
+offset_right = 50.0
+offset_bottom = -6.0
+mouse_filter = 2
+text = "x1"
+context_menu_enabled = false
+shortcut_keys_enabled = false
+selecting_enabled = false
+deselect_on_focus_loss_enabled = false
+drag_and_drop_selection_enabled = false
+virtual_keyboard_enabled = false
+middle_mouse_paste_enabled = false
diff --git a/scenes/BattleDirector/TextParticle.cs b/scenes/BattleDirector/TextParticle.cs
new file mode 100644
index 00000000..273d4967
--- /dev/null
+++ b/scenes/BattleDirector/TextParticle.cs
@@ -0,0 +1,23 @@
+using System;
+using Godot;
+
+public partial class TextParticle : Label
+{
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ Tween tween = GetTree().CreateTween();
+ Position += Vector2.Left * (GD.Randf() * 40 - 20);
+ tween.SetTrans(Tween.TransitionType.Quad);
+ tween.SetEase(Tween.EaseType.Out);
+ tween.TweenProperty(this, "position", Position + Vector2.Up * 10, .5f);
+ tween.TweenProperty(this, "position", Position + Vector2.Down * 20, .5f);
+ tween.SetParallel();
+ tween.TweenProperty(this, "modulate:a", 0, 1f);
+ tween.SetParallel(false);
+ tween.TweenCallback(Callable.From(QueueFree));
+ }
+
+ // Called every frame. 'delta' is the elapsed time since the previous frame.
+ public override void _Process(double delta) { }
+}
diff --git a/scenes/BattleDirector/TextParticle.tscn b/scenes/BattleDirector/TextParticle.tscn
new file mode 100644
index 00000000..6491a671
--- /dev/null
+++ b/scenes/BattleDirector/TextParticle.tscn
@@ -0,0 +1,9 @@
+[gd_scene load_steps=2 format=3 uid="uid://bd23wwbv7i4gg"]
+
+[ext_resource type="Script" path="res://scenes/BattleDirector/TextParticle.cs" id="1_j0ufq"]
+
+[node name="Control" type="Label"]
+anchors_preset = -1
+anchor_right = 0.08
+text = "900000"
+script = ExtResource("1_j0ufq")
diff --git a/scenes/BattleDirector/assets/Character1.png b/scenes/BattleDirector/assets/Character1.png
new file mode 100644
index 00000000..2175831d
Binary files /dev/null and b/scenes/BattleDirector/assets/Character1.png differ
diff --git a/scenes/BattleDirector/assets/Character1.png.import b/scenes/BattleDirector/assets/Character1.png.import
new file mode 100644
index 00000000..8adcfe54
--- /dev/null
+++ b/scenes/BattleDirector/assets/Character1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6fkei0i83vte"
+path="res://.godot/imported/Character1.png-d73d4a980ad79d80867b72bd7a029fb4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://scenes/BattleDirector/assets/Character1.png"
+dest_files=["res://.godot/imported/Character1.png-d73d4a980ad79d80867b72bd7a029fb4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/scenes/BattleDirector/assets/CoolBG.jpg b/scenes/BattleDirector/assets/CoolBG.jpg
new file mode 100644
index 00000000..60734eb2
Binary files /dev/null and b/scenes/BattleDirector/assets/CoolBG.jpg differ
diff --git a/scenes/BattleDirector/assets/CoolBG.jpg.import b/scenes/BattleDirector/assets/CoolBG.jpg.import
new file mode 100644
index 00000000..e4f537c4
--- /dev/null
+++ b/scenes/BattleDirector/assets/CoolBG.jpg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ci0g72j8q4ec2"
+path="res://.godot/imported/CoolBG.jpg-3bad5dc436fec2c6453223cfa1feeeb8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://scenes/BattleDirector/assets/CoolBG.jpg"
+dest_files=["res://.godot/imported/CoolBG.jpg-3bad5dc436fec2c6453223cfa1feeeb8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/scenes/BattleDirector/assets/Enemy1.png b/scenes/BattleDirector/assets/Enemy1.png
new file mode 100644
index 00000000..5a844a04
Binary files /dev/null and b/scenes/BattleDirector/assets/Enemy1.png differ
diff --git a/scenes/BattleDirector/assets/Enemy1.png.import b/scenes/BattleDirector/assets/Enemy1.png.import
new file mode 100644
index 00000000..a6b8781d
--- /dev/null
+++ b/scenes/BattleDirector/assets/Enemy1.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://veedngaorx3l"
+path="res://.godot/imported/Enemy1.png-fb95639d1545a326b6c39640343f3786.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://scenes/BattleDirector/assets/Enemy1.png"
+dest_files=["res://.godot/imported/Enemy1.png-fb95639d1545a326b6c39640343f3786.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/scenes/BattleDirector/test_battle_scene.tscn b/scenes/BattleDirector/test_battle_scene.tscn
new file mode 100644
index 00000000..772d4487
--- /dev/null
+++ b/scenes/BattleDirector/test_battle_scene.tscn
@@ -0,0 +1,61 @@
+[gd_scene load_steps=9 format=3 uid="uid://b0mrgr7h0ty1y"]
+
+[ext_resource type="Script" path="res://scenes/BattleDirector/BattleDirector.cs" id="1_cwqqr"]
+[ext_resource type="PackedScene" uid="uid://dfevfib11kou1" path="res://scenes/ChartViewport/ChartViewport.tscn" id="2_cupb3"]
+[ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://scenes/BattleDirector/HealthBar.tscn" id="3_pp0u0"]
+[ext_resource type="Texture2D" uid="uid://ci0g72j8q4ec2" path="res://scenes/BattleDirector/assets/CoolBG.jpg" id="4_13o87"]
+[ext_resource type="Texture2D" uid="uid://b6fkei0i83vte" path="res://scenes/BattleDirector/assets/Character1.png" id="5_elveq"]
+[ext_resource type="Texture2D" uid="uid://veedngaorx3l" path="res://scenes/BattleDirector/assets/Enemy1.png" id="6_0k4pw"]
+[ext_resource type="PackedScene" uid="uid://duhiilcv4tat3" path="res://scenes/BattleDirector/NotePlacementBar.tscn" id="7_3ko4g"]
+[ext_resource type="AudioStream" uid="uid://cv6lqjj6lu36h" path="res://Audio/335571__magntron__gamemusic_120bpm.mp3" id="8_caqms"]
+
+[node name="ProtoBattleDirector" type="Node2D" node_paths=PackedStringArray("CM", "IH", "NotePlacementBar", "Audio")]
+script = ExtResource("1_cwqqr")
+CM = NodePath("SubViewport")
+IH = NodePath("SubViewport/SubViewport/noteManager")
+NotePlacementBar = NodePath("NotePlacementBar")
+Audio = NodePath("AudioStreamPlayer")
+
+[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
+stream = ExtResource("8_caqms")
+
+[node name="SubViewport" parent="." instance=ExtResource("2_cupb3")]
+offset_left = 80.0
+offset_top = 160.0
+offset_right = 560.0
+offset_bottom = 360.0
+
+[node name="ColorRect" type="TextureRect" parent="."]
+z_index = -1
+offset_right = 640.0
+offset_bottom = 360.0
+texture = ExtResource("4_13o87")
+
+[node name="ColorRect2" type="ColorRect" parent="."]
+self_modulate = Color(0.36, 0.36, 0.36, 0.780392)
+z_index = -1
+offset_left = -70.0
+offset_top = 160.0
+offset_right = 673.0
+offset_bottom = 360.0
+color = Color(0.165656, 0.165656, 0.165656, 1)
+
+[node name="PlayerHP" parent="." instance=ExtResource("3_pp0u0")]
+offset_left = 92.0
+offset_top = 8.0
+offset_right = 132.0
+offset_bottom = 48.0
+SpriteText = ExtResource("5_elveq")
+
+[node name="EnemyHP" parent="." instance=ExtResource("3_pp0u0")]
+offset_left = 403.0
+offset_top = 8.0
+offset_right = 443.0
+offset_bottom = 52.0
+SpriteText = ExtResource("6_0k4pw")
+
+[node name="NotePlacementBar" parent="." instance=ExtResource("7_3ko4g")]
+offset_left = 16.0
+offset_top = 164.0
+offset_right = 16.0
+offset_bottom = 164.0
diff --git a/scenes/ChartViewport/ChartManager.cs b/scenes/ChartViewport/ChartManager.cs
new file mode 100644
index 00000000..4ecae9e4
--- /dev/null
+++ b/scenes/ChartViewport/ChartManager.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Linq;
+using Godot;
+using ArrowType = NoteArrow.ArrowType;
+
+/**
+ * @class ChartManager
+ * @brief Chart Manager is meant to handle the visual aspects of a battle. Setting up the chart background, initial notes, and handle looping. WIP
+ */
+public partial class ChartManager : SubViewportContainer
+{
+ //Nodes from scene
+ [Export]
+ public InputHandler IH;
+
+ [Export]
+ public CanvasGroup ChartLoopables;
+
+ private Node _arrowGroup;
+
+ [Signal]
+ public delegate void NotePressedEventHandler(ArrowType arrowType);
+
+ [Signal]
+ public delegate void NoteReleasedEventHandler(ArrowType arrowType);
+
+ //Arbitrary vars, play with these
+ private double ChartLength = 5000; //Might move this to be song specific?
+ private double _loopLen; //secs
+ public int BeatsPerLoop;
+
+ public void OnNotePressed(ArrowType type)
+ {
+ EmitSignal(nameof(NotePressed), (int)type);
+ }
+
+ public void OnNoteReleased(ArrowType type)
+ {
+ EmitSignal(nameof(NoteReleased), (int)type);
+ }
+
+ public void PrepChart(BattleDirector.SongData songData)
+ {
+ _loopLen = songData.SongLength / songData.NumLoops;
+ TimeKeeper.LoopLength = (float)_loopLen;
+ BeatsPerLoop = (int)(_loopLen / (60f / songData.Bpm));
+ ChartLength = (float)_loopLen * (float)Math.Floor(ChartLength / _loopLen);
+ TimeKeeper.ChartLength = (float)ChartLength;
+ TimeKeeper.Bpm = songData.Bpm;
+
+ InitBackgrounds();
+ _arrowGroup = ChartLoopables.GetNode("ArrowGroup");
+
+ IH.Connect(nameof(InputHandler.NotePressed), new Callable(this, nameof(OnNotePressed)));
+ IH.Connect(nameof(InputHandler.NoteReleased), new Callable(this, nameof(OnNoteReleased)));
+
+ //This could be good as a function to call on something, to have many things animated to the beat.
+ var tween = GetTree().CreateTween();
+ tween
+ .TweenMethod(
+ Callable.From((Vector2 scale) => TweenArrows(scale)),
+ new Vector2(0.07f, 0.07f),
+ new Vector2(0.07f, 0.07f) * 1.25f,
+ 60f / TimeKeeper.Bpm / 2
+ )
+ .SetEase(Tween.EaseType.Out)
+ .SetTrans(Tween.TransitionType.Elastic);
+ tween.TweenMethod(
+ Callable.From((Vector2 scale) => TweenArrows(scale)),
+ new Vector2(0.07f, 0.07f) * 1.25f,
+ new Vector2(0.07f, 0.07f),
+ 60f / TimeKeeper.Bpm / 2
+ );
+ tween.SetLoops().Play();
+ }
+
+ private void InitBackgrounds()
+ {
+ int i = 0;
+ foreach (Node child in ChartLoopables.GetChildren())
+ {
+ if (child is not Loopable)
+ continue;
+ Loopable loopable = (Loopable)child;
+ loopable.Position = Vector2.Zero;
+ loopable.SetSize(new Vector2((float)ChartLength / 2 + 1, Size.Y));
+ loopable.Bounds = (float)ChartLength / 2 * i;
+ i++;
+ }
+ }
+
+ private void TweenArrows(Vector2 scale)
+ {
+ foreach (var node in _arrowGroup.GetChildren())
+ {
+ NoteArrow arrow = (NoteArrow)node;
+ arrow.Scale = scale;
+ }
+ }
+
+ public NoteArrow AddArrowToLane(Note note, int noteIdx)
+ {
+ var newNote = CreateNote(note.Type, note.Beat);
+ CreateNote(note.Type, note.Beat + BeatsPerLoop); //Create a dummy arrow for looping visuals
+ newNote.NoteIdx = noteIdx;
+ return newNote;
+ }
+
+ private NoteArrow CreateNote(ArrowType arrow, int beat = 0)
+ {
+ var noteScene = ResourceLoader.Load("res://scenes/NoteManager/note.tscn");
+ NoteArrow newArrow = noteScene.Instantiate();
+ newArrow.Init(IH.Arrows[(int)arrow]);
+
+ _arrowGroup.AddChild(newArrow);
+ newArrow.Bounds = (float)((double)beat / BeatsPerLoop * (ChartLength / 2));
+ newArrow.Position += Vector2.Right * newArrow.Bounds * 10; //temporary fix for notes spawning and instantly calling loop from originating at 0,0
+ return newArrow;
+ }
+}
diff --git a/scenes/ChartViewport/ChartViewport.tscn b/scenes/ChartViewport/ChartViewport.tscn
new file mode 100644
index 00000000..f6f9fb6a
--- /dev/null
+++ b/scenes/ChartViewport/ChartViewport.tscn
@@ -0,0 +1,50 @@
+[gd_scene load_steps=5 format=3 uid="uid://dfevfib11kou1"]
+
+[ext_resource type="Texture2D" uid="uid://b0tvsewgnf2x7" path="res://icon.svg" id="1_0wnka"]
+[ext_resource type="Script" path="res://scenes/ChartViewport/ChartManager.cs" id="1_ruh2l"]
+[ext_resource type="Script" path="res://scenes/ChartViewport/Loopable.cs" id="3_5u57h"]
+[ext_resource type="PackedScene" uid="uid://bn8txx53xlguw" path="res://scenes/NoteManager/note_manager.tscn" id="4_fd5fw"]
+
+[node name="VPContainer" type="SubViewportContainer" node_paths=PackedStringArray("IH", "ChartLoopables")]
+offset_right = 480.0
+offset_bottom = 200.0
+script = ExtResource("1_ruh2l")
+IH = NodePath("SubViewport/noteManager")
+ChartLoopables = NodePath("SubViewport/ChartLoopables")
+
+[node name="SubViewport" type="SubViewport" parent="."]
+handle_input_locally = false
+size = Vector2i(480, 200)
+render_target_update_mode = 4
+
+[node name="ColorFill" type="ColorRect" parent="SubViewport"]
+z_index = -2
+offset_left = -30.0
+offset_right = 610.0
+offset_bottom = 360.0
+color = Color(0.258824, 0.290196, 0.392157, 1)
+
+[node name="Camera2D" type="Camera2D" parent="SubViewport"]
+position = Vector2(-25, 0)
+anchor_mode = 0
+
+[node name="ChartLoopables" type="CanvasGroup" parent="SubViewport"]
+unique_name_in_owner = true
+
+[node name="ArrowGroup" type="Node" parent="SubViewport/ChartLoopables"]
+
+[node name="ChartBG2" type="TextureRect" parent="SubViewport/ChartLoopables"]
+modulate = Color(2, 2, 2, 1)
+offset_right = 701.0
+offset_bottom = 300.0
+texture = ExtResource("1_0wnka")
+script = ExtResource("3_5u57h")
+
+[node name="ChartBG1" type="TextureRect" parent="SubViewport/ChartLoopables"]
+modulate = Color(2, 2, 2, 1)
+offset_right = 701.0
+offset_bottom = 300.0
+texture = ExtResource("1_0wnka")
+script = ExtResource("3_5u57h")
+
+[node name="noteManager" parent="SubViewport" instance=ExtResource("4_fd5fw")]
diff --git a/scenes/ChartViewport/Loopable.cs b/scenes/ChartViewport/Loopable.cs
new file mode 100644
index 00000000..45e3487e
--- /dev/null
+++ b/scenes/ChartViewport/Loopable.cs
@@ -0,0 +1,26 @@
+using System;
+using Godot;
+
+/**
+ * @class Loopable
+ * @brief A general class fo textures on the chart which should have a position at which point they loop. WIP
+ */
+public partial class Loopable : TextureRect
+{
+ [Export]
+ public float Bounds = 700f; //px Pos to loop or do something at.
+
+ // Called every frame. 'delta' is the elapsed time since the previous frame.
+ public override void _Process(double delta)
+ {
+ Vector2 newPos = Position;
+ //Loop position over the course of time across a loop
+ newPos.X =
+ (float)(
+ (-TimeKeeper.CurrentTime / TimeKeeper.LoopLength * TimeKeeper.ChartLength)
+ % TimeKeeper.ChartLength
+ / 2
+ ) + Bounds;
+ Position = newPos;
+ }
+}
diff --git a/scenes/NoteManager/assets/right-arrow.png b/scenes/NoteManager/assets/right-arrow.png
new file mode 100644
index 00000000..d5c416b7
Binary files /dev/null and b/scenes/NoteManager/assets/right-arrow.png differ
diff --git a/scenes/NoteManager/assets/right-arrow.png.import b/scenes/NoteManager/assets/right-arrow.png.import
new file mode 100644
index 00000000..5a9baa01
--- /dev/null
+++ b/scenes/NoteManager/assets/right-arrow.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bucvj4fquqpkr"
+path="res://.godot/imported/right-arrow.png-b3005485b42777a77a9d39fbba48875d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://scenes/NoteManager/assets/right-arrow.png"
+dest_files=["res://.godot/imported/right-arrow.png-b3005485b42777a77a9d39fbba48875d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/scenes/NoteManager/note.tscn b/scenes/NoteManager/note.tscn
new file mode 100644
index 00000000..e0b2aa76
--- /dev/null
+++ b/scenes/NoteManager/note.tscn
@@ -0,0 +1,9 @@
+[gd_scene load_steps=3 format=3 uid="uid://ck3bfqy30rjbq"]
+
+[ext_resource type="Texture2D" uid="uid://bucvj4fquqpkr" path="res://scenes/NoteManager/assets/right-arrow.png" id="1_kubiy"]
+[ext_resource type="Script" path="res://scenes/NoteManager/scripts/NoteArrow.cs" id="2_lbl4b"]
+
+[node name="Right-arrow" type="Sprite2D"]
+scale = Vector2(0.07, 0.07)
+texture = ExtResource("1_kubiy")
+script = ExtResource("2_lbl4b")
diff --git a/scenes/NoteManager/note_manager.tscn b/scenes/NoteManager/note_manager.tscn
new file mode 100644
index 00000000..82ab15fd
--- /dev/null
+++ b/scenes/NoteManager/note_manager.tscn
@@ -0,0 +1,60 @@
+[gd_scene load_steps=5 format=3 uid="uid://bn8txx53xlguw"]
+
+[ext_resource type="Script" path="res://scenes/NoteManager/scripts/InputHandler.cs" id="1_2oeuf"]
+[ext_resource type="Texture2D" uid="uid://bucvj4fquqpkr" path="res://scenes/NoteManager/assets/right-arrow.png" id="2_ldmuy"]
+[ext_resource type="Script" path="res://scenes/NoteManager/scripts/NoteChecker.cs" id="3_0cioe"]
+[ext_resource type="Texture2D" uid="uid://b0tvsewgnf2x7" path="res://icon.svg" id="4_foklt"]
+
+[node name="noteManager" type="Node2D"]
+script = ExtResource("1_2oeuf")
+
+[node name="noteCheckers" type="Node2D" parent="."]
+
+[node name="arrowUp" type="Sprite2D" parent="noteCheckers"]
+position = Vector2(0, 24)
+rotation = -1.5708
+scale = Vector2(0.09, 0.09)
+texture = ExtResource("2_ldmuy")
+script = ExtResource("3_0cioe")
+
+[node name="arrowLeft" type="Sprite2D" parent="noteCheckers"]
+position = Vector2(0, 76)
+rotation = 3.14159
+scale = Vector2(0.09, 0.09)
+texture = ExtResource("2_ldmuy")
+script = ExtResource("3_0cioe")
+
+[node name="arrowDown" type="Sprite2D" parent="noteCheckers"]
+position = Vector2(0, 129)
+rotation = 1.5708
+scale = Vector2(0.09, 0.09)
+texture = ExtResource("2_ldmuy")
+script = ExtResource("3_0cioe")
+
+[node name="arrowRight" type="Sprite2D" parent="noteCheckers"]
+position = Vector2(0, 181)
+scale = Vector2(0.09, 0.09)
+texture = ExtResource("2_ldmuy")
+script = ExtResource("3_0cioe")
+
+[node name="ui" type="Node2D" parent="."]
+
+[node name="dividers" type="Node2D" parent="ui"]
+
+[node name="DivA" type="Sprite2D" parent="ui/dividers"]
+modulate = Color(0, 0, 0, 1)
+position = Vector2(295, 53)
+scale = Vector2(5.10938, 0.041)
+texture = ExtResource("4_foklt")
+
+[node name="DivB" type="Sprite2D" parent="ui/dividers"]
+modulate = Color(0, 0, 0, 1)
+position = Vector2(295, 100)
+scale = Vector2(5.10938, 0.041)
+texture = ExtResource("4_foklt")
+
+[node name="DivC" type="Sprite2D" parent="ui/dividers"]
+modulate = Color(0, 0, 0, 1)
+position = Vector2(295, 159)
+scale = Vector2(5.10938, 0.041)
+texture = ExtResource("4_foklt")
diff --git a/scenes/NoteManager/scripts/InputHandler.cs b/scenes/NoteManager/scripts/InputHandler.cs
new file mode 100644
index 00000000..466c346a
--- /dev/null
+++ b/scenes/NoteManager/scripts/InputHandler.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using Godot;
+using ArrowType = NoteArrow.ArrowType;
+
+/**
+ * @class InputHandler
+ * @brief InputHandler to handle input, and manage note checkers. WIP
+ */
+public partial class InputHandler : Node2D
+{
+ [Signal]
+ public delegate void NotePressedEventHandler(ArrowType arrowType);
+
+ [Signal]
+ public delegate void NoteReleasedEventHandler(ArrowType arrowType);
+
+ public struct ArrowData
+ {
+ public Color Color;
+ public string Key;
+ public NoteChecker Node;
+ public ArrowType Type;
+ }
+
+ public ArrowData[] Arrows = new ArrowData[]
+ {
+ new ArrowData()
+ {
+ Color = Colors.Green,
+ Key = "arrowUp",
+ Type = ArrowType.Up,
+ },
+ new ArrowData()
+ {
+ Color = Colors.Aqua,
+ Key = "arrowDown",
+ Type = ArrowType.Down,
+ },
+ new ArrowData()
+ {
+ Color = Colors.HotPink,
+ Key = "arrowLeft",
+ Type = ArrowType.Left,
+ },
+ new ArrowData()
+ {
+ Color = Colors.Red,
+ Key = "arrowRight",
+ Type = ArrowType.Right,
+ },
+ };
+
+ private void InitializeArrowCheckers()
+ {
+ //Set the color of the arrows
+ for (int i = 0; i < Arrows.Length; i++)
+ {
+ Arrows[i].Node = GetNode("noteCheckers/" + Arrows[i].Key);
+ Arrows[i].Node.SetColor(Arrows[i].Color);
+ }
+ }
+
+ public override void _Ready()
+ {
+ InitializeArrowCheckers();
+ }
+
+ public override void _Process(double delta)
+ {
+ foreach (var arrow in Arrows)
+ {
+ if (Input.IsActionJustPressed(arrow.Key))
+ {
+ EmitSignal(nameof(NotePressed), (int)arrow.Type);
+ arrow.Node.SetPressed(true);
+ }
+ else if (Input.IsActionJustReleased(arrow.Key))
+ {
+ EmitSignal(nameof(NoteReleased), (int)arrow.Type);
+ arrow.Node.SetPressed(false);
+ }
+ }
+ }
+}
diff --git a/scenes/NoteManager/scripts/NoteArrow.cs b/scenes/NoteManager/scripts/NoteArrow.cs
new file mode 100644
index 00000000..934ed28b
--- /dev/null
+++ b/scenes/NoteManager/scripts/NoteArrow.cs
@@ -0,0 +1,57 @@
+using Godot;
+
+/**
+ * @class NoteArrow
+ * @brief This class represents a visual note that scrolls across the screen to be played by the player. WIP
+ */
+public partial class NoteArrow : Sprite2D
+{ //TextRect caused issues later :)
+ public enum ArrowType
+ {
+ Up = 0,
+ Down = 1,
+ Left = 2,
+ Right = 3,
+ }
+
+ public int NoteIdx;
+ public float Bounds;
+ public bool IsActive;
+
+ public void Init(InputHandler.ArrowData parentArrowData)
+ {
+ ZIndex = 1;
+
+ SelfModulate = parentArrowData.Color;
+ Position += Vector2.Down * (parentArrowData.Node.GlobalPosition.Y);
+ RotationDegrees = parentArrowData.Node.RotationDegrees;
+ }
+
+ public override void _Process(double delta)
+ {
+ Vector2 newPos = Position;
+ newPos.X =
+ (float)(
+ (-TimeKeeper.CurrentTime / TimeKeeper.LoopLength * TimeKeeper.ChartLength)
+ % TimeKeeper.ChartLength
+ / 2
+ ) + Bounds;
+ if (newPos.X > Position.X)
+ {
+ OnLoop();
+ }
+ Position = newPos;
+ }
+
+ public void OnLoop()
+ {
+ Visible = true;
+ IsActive = true;
+ }
+
+ public void NoteHit()
+ {
+ Visible = false;
+ IsActive = false;
+ }
+}
diff --git a/scenes/NoteManager/scripts/NoteChecker.cs b/scenes/NoteManager/scripts/NoteChecker.cs
new file mode 100644
index 00000000..8a32a5dc
--- /dev/null
+++ b/scenes/NoteManager/scripts/NoteChecker.cs
@@ -0,0 +1,31 @@
+using System;
+using Godot;
+
+public partial class NoteChecker : Sprite2D
+{
+ private bool _isPressed;
+ private Color _color;
+ private float _fadeTime = 2.0f;
+
+ public override void _Process(double delta)
+ {
+ SelfModulate = _isPressed
+ ? Modulate.Lerp(_color, _fadeTime)
+ : SelfModulate.Lerp(
+ new Color(_color.R * 0.5f, _color.G * 0.5f, _color.B * 0.5f, 1),
+ (float)delta * _fadeTime
+ );
+ }
+
+ public void SetPressed(bool pressed)
+ {
+ _isPressed = pressed;
+ }
+
+ public void SetColor(Color color)
+ {
+ _color = color;
+
+ SelfModulate = new Color(_color.R * 0.5f, _color.G * 0.5f, _color.B * 0.5f, 1);
+ }
+}
diff --git a/scripts/Main.cs b/scripts/Main.cs
index 54bba923..52278fa9 100644
--- a/scripts/Main.cs
+++ b/scripts/Main.cs
@@ -1,11 +1,5 @@
using System;
using Godot;
+using static InputHandler;
-public partial class Main : Node2D
-{
- // Called when the node enters the scene tree for the first time.
- public override void _Ready() { }
-
- // Called every frame. 'delta' is the elapsed time since the previous frame.
- public override void _Process(double delta) { }
-}
+public partial class Main : Node2D { }
diff --git a/scripts/TimeKeeper.cs b/scripts/TimeKeeper.cs
new file mode 100644
index 00000000..7f09ddff
--- /dev/null
+++ b/scripts/TimeKeeper.cs
@@ -0,0 +1,10 @@
+using System;
+using Godot;
+
+public partial class TimeKeeper : Node
+{
+ public static double CurrentTime = 0;
+ public static float ChartLength;
+ public static float LoopLength;
+ public static float Bpm;
+}