commit 8e6966bfcd8f35c615231af44aacd7fee7804c98
Author: muskit <15199219+muskit@users.noreply.github.com>
Date: Sun Jul 30 00:22:03 2023 -0700
Initial commit
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8ad74f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4709183
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# Godot 4+ specific ignores
+.godot/
diff --git a/Scenes/DebugChartLoader.tscn b/Scenes/DebugChartLoader.tscn
new file mode 100644
index 0000000..c37d76d
--- /dev/null
+++ b/Scenes/DebugChartLoader.tscn
@@ -0,0 +1,73 @@
+[gd_scene load_steps=2 format=3 uid="uid://bqhdw2o5vq7ny"]
+
+[ext_resource type="Script" path="res://Scripts/Scenes/DebugChartLoader.cs" id="1_hjgpd"]
+
+[node name="DebugChartLoader" type="MarginContainer" node_paths=PackedStringArray("inputField", "soundField", "difficultyButton", "playButton")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 32
+theme_override_constants/margin_top = 32
+theme_override_constants/margin_right = 32
+theme_override_constants/margin_bottom = 32
+script = ExtResource("1_hjgpd")
+inputField = NodePath("VBoxContainer/HBoxContainer/PathLineEdit")
+soundField = NodePath("VBoxContainer/HBoxContainer3/SoundLineEdit")
+difficultyButton = NodePath("VBoxContainer/HBoxContainer/DifficultyOptionButton")
+playButton = NodePath("VBoxContainer/HBoxContainer2/PlayButton")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 8
+
+[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 1
+
+[node name="PathLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
+size_flags_stretch_ratio = 5.97
+text = "AkiraComplex - Ether Strike"
+placeholder_text = "Folder path relative to \"user://songs/\""
+
+[node name="DifficultyOptionButton" type="OptionButton" parent="VBoxContainer/HBoxContainer"]
+custom_minimum_size = Vector2(0, 35)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 8
+item_count = 4
+selected = 0
+popup/item_0/text = "0. Normal"
+popup/item_0/id = 0
+popup/item_1/text = "1. Hard"
+popup/item_1/id = 1
+popup/item_2/text = "2. Expert"
+popup/item_2/id = 2
+popup/item_3/text = "3. Inferno"
+popup/item_3/id = 3
+
+[node name="HBoxContainer3" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+alignment = 1
+
+[node name="SoundLineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer3"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "music.wav"
+placeholder_text = "Name of audio file in the folder specified above with extension"
+
+[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+alignment = 1
+
+[node name="PlayButton" type="Button" parent="VBoxContainer/HBoxContainer2"]
+layout_mode = 2
+size_flags_vertical = 0
+text = "Play"
diff --git a/Scenes/Play.tscn b/Scenes/Play.tscn
new file mode 100644
index 0000000..82c6330
--- /dev/null
+++ b/Scenes/Play.tscn
@@ -0,0 +1,13 @@
+[gd_scene format=3 uid="uid://bqh00ot0csqmk"]
+
+[node name="Play" type="Node"]
+
+[node name="Node3D" type="Node3D" parent="."]
+
+[node name="Camera3D" type="Camera3D" parent="Node3D"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 4.09161)
+fov = 60.0
+near = 0.001
+far = 100.0
+
+[node name="Node2D" type="Node2D" parent="."]
diff --git a/Scenes/_STARTUP.tscn b/Scenes/_STARTUP.tscn
new file mode 100644
index 0000000..64d3de3
--- /dev/null
+++ b/Scenes/_STARTUP.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://bef4fdycrfvls"]
+
+[ext_resource type="Script" path="res://Scripts/Scenes/Startup.cs" id="1_yd1au"]
+
+[node name="Control" type="Node"]
+script = ExtResource("1_yd1au")
diff --git a/Scripts/Configuration/PlaySettings.cs b/Scripts/Configuration/PlaySettings.cs
new file mode 100644
index 0000000..6508a69
--- /dev/null
+++ b/Scripts/Configuration/PlaySettings.cs
@@ -0,0 +1,7 @@
+namespace WacK.Configuration
+{
+ public class PlaySettings
+ {
+ public static float playSpeedMultiplier = 2f;
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Configuration/PlaySettings.cs.meta b/Scripts/Configuration/PlaySettings.cs.meta
new file mode 100644
index 0000000..6bde4d4
--- /dev/null
+++ b/Scripts/Configuration/PlaySettings.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dee0701f4dec1e53681db1444842f032
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Constants.cs b/Scripts/Constants.cs
new file mode 100644
index 0000000..bd3adb8
--- /dev/null
+++ b/Scripts/Constants.cs
@@ -0,0 +1,8 @@
+namespace WacK
+{
+ public class Constants
+ {
+ public const float SCROLL_MULT = 3f;
+ public const float NOTE_DRAW_DISTANCE = 10;
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Chart.meta b/Scripts/Data/Chart.meta
new file mode 100644
index 0000000..f507915
--- /dev/null
+++ b/Scripts/Data/Chart.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 25140b91ea8636ea5acb3ff90421f11a
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/Chart.cs b/Scripts/Data/Chart/Chart.cs
new file mode 100644
index 0000000..fe95ec8
--- /dev/null
+++ b/Scripts/Data/Chart/Chart.cs
@@ -0,0 +1,332 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+
+using WacK.Data.Mer;
+
+using Godot;
+
+namespace WacK.Data.Chart
+{
+ ///
+ /// Chart data.
+ ///
+ public class Chart
+ {
+ public static bool doneLoading { get; private set; } = false;
+ // [ms] = List (list for chord marking creation)
+ public SortedList> playNotes { get; private set; }
+ public SortedList> timeSigChgs { get; private set; }
+ public SortedList> tempoChgs { get; private set; }
+ public SortedList> events { get; private set; }
+
+ public Chart()
+ {
+ doneLoading = false;
+ }
+
+ // place notes and events relative to the previous
+ public void Load(Mer.Mer chart)
+ {
+ playNotes = new SortedList>();
+
+ List tempo = new List();
+ List tempoChangeMeasures = new List();
+ List tempoChangeBeats = new List();
+
+ // TODO: switch to MeasureBeat
+ List beatsPerMeasure = new List();
+ List bpmChangeMeasures = new List();
+
+ tempo.Add(0);
+ tempoChangeMeasures.Add(0);
+ tempoChangeBeats.Add(0);
+
+ beatsPerMeasure.Add(0);
+ bpmChangeMeasures.Add(0);
+
+ float queuedTempo = -1;
+ int queuedBPM = -1;
+
+ // timing info of the previous beat
+ float prevTime = 0;
+ int prevMeasure = 0;
+ int prevBeat = 0; // (/1920 beats per measure)
+
+ Note prevNote = null;
+ Note curNote = null;
+ var prevHoldPoint = new System.Collections.Generic.Dictionary(); //
+ var curHoldNote = new System.Collections.Generic.Dictionary(); //
+
+ // Notes and Events //
+ foreach (var measure in chart.notes) // `measure` = measure: List
+ {
+ foreach (var chartNote in measure.Value) // `chartNote` = beat, ChartNote
+ {
+ var curTime = prevTime + Util.NoteTime(measure.Key - prevMeasure, chartNote.Item1 - prevBeat, tempo.Last(), beatsPerMeasure.Last());
+ var mb = new MeasureBeat(measure.Key, chartNote.Item1);
+
+ if (prevMeasure != measure.Key && prevBeat != chartNote.Item1)
+ {
+ if (queuedTempo != -1)
+ {
+ tempo.Add(queuedTempo);
+ tempoChangeMeasures.Add(measure.Key);
+ tempoChangeBeats.Add(chartNote.Item1);
+ queuedTempo = -1;
+ }
+
+ if (queuedBPM != -1)
+ {
+ beatsPerMeasure.Add(queuedBPM);
+ bpmChangeMeasures.Add(measure.Key);
+ queuedBPM = -1;
+ }
+ }
+
+ // notetype-dependent operations
+ switch (chartNote.Item2.noteType)
+ {
+ // Beat map data
+ case MerType.Tempo:
+ if (tempo.Count == 1)
+ {
+ tempo.Add(float.Parse(chartNote.Item2.value));
+ tempoChangeMeasures.Add(measure.Key);
+ tempoChangeBeats.Add(chartNote.Item1);
+ }
+ else
+ queuedTempo = float.Parse(chartNote.Item2.value);
+ this.tempoChgs.Add(
+ curTime,
+ new NoteEvent (
+ curTime, mb,
+ NoteEventType.Tempo,
+ value: float.Parse(chartNote.Item2.value)
+ ));
+ break;
+ case MerType.TimeSignature:
+ var words = chartNote.Item2.value.Split();
+ var nu = int.Parse(words[0]);
+ var de = int.Parse(words[1]);
+ if (beatsPerMeasure.Count == 1)
+ {
+ beatsPerMeasure.Add(int.Parse(chartNote.Item2.value));
+ bpmChangeMeasures.Add(measure.Key);
+ }
+ else
+ queuedBPM = int.Parse(chartNote.Item2.value);
+ this.timeSigChgs.Add(
+ curTime,
+ new NoteEvent<(int, int)> (
+ curTime, mb,
+ NoteEventType.TimeSignature,
+ value: (nu, de)
+ ));
+ break;
+ // Playable notes
+ case MerType.Touch:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size
+ );
+ break;
+ case MerType.HoldStart:
+ curNote = new NoteHold(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ holdIndex: chartNote.Item2.holdIdx,
+ holdNext: chartNote.Item2.holdNextIdx
+ );
+ var nh = curNote as NoteHold;
+ prevHoldPoint[chartNote.Item2.holdNextIdx] = (NotePlay) curNote;
+ curHoldNote[chartNote.Item2.holdNextIdx] = nh;
+ break;
+ case MerType.HoldMid:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.HoldMid,
+ holdIndex: chartNote.Item2.holdIdx,
+ holdNext: chartNote.Item2.holdNextIdx
+ );
+ prevHoldPoint[chartNote.Item2.holdNextIdx] = (NotePlay) curNote;
+ curHoldNote[chartNote.Item2.holdNextIdx] = curHoldNote[chartNote.Item2.holdIdx];
+ break;
+ case MerType.HoldEnd: // TODO: draw end note on cone texture
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.HoldEnd,
+ holdIndex: chartNote.Item2.holdIdx
+ );
+ break;
+ case MerType.Untimed:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.Untimed
+ );
+ break;
+ case MerType.SwipeIn:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.SwipeIn
+ );
+ break;
+ case MerType.SwipeOut:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.SwipeOut
+ );
+ break;
+ case MerType.SwipeCW:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.SwipeCW
+ );
+ break;
+ case MerType.SwipeCCW:
+ curNote = new NotePlay(
+ curTime, mb,
+ chartNote.Item2.position, chartNote.Item2.size,
+ type: NotePlayType.SwipeCCW
+ );
+ break;
+ // Events (invisible modifier notes)
+ case MerType.BGAdd:
+ curNote = new NoteEvent(
+ curTime, mb,
+ NoteEventType.BGAdd,
+ chartNote.Item2.position, chartNote.Item2.size,
+ value: int.Parse(chartNote.Item2.value)
+ );
+ break;
+ case MerType.BGRem:
+ curNote = new NoteEvent(
+ curTime, mb,
+ NoteEventType.BGRem,
+ chartNote.Item2.position, chartNote.Item2.size,
+ value: int.Parse(chartNote.Item2.value)
+ );
+ break;
+ }
+
+ if (curNote == null || curNote == prevNote) continue;
+
+ /* Handle our crafted curNote, storing it somewhere in this Chart */
+ // NotePlay
+ var np = curNote as NotePlay;
+ if (np != null)
+ {
+ // hold point handling
+ if (np.type == NotePlayType.HoldMid || np.type == NotePlayType.HoldEnd)
+ {
+ curHoldNote[np.holdIdx].points[curTime] = np;
+ }
+
+ // add note
+ if (!playNotes.ContainsKey(curTime))
+ {
+ playNotes[curTime] = new List();
+ }
+ playNotes[curTime].Add(np);
+ }
+
+ // NoteEvent -- tempo changes
+ var nef = curNote as NoteEvent;
+ if (nef != null)
+ {
+ if (nef.type == NoteEventType.Tempo)
+ this.tempoChgs[curTime] = nef;
+ else
+ GD.Print($"Didn't add NoteEvent of type {nef.type}");
+ }
+
+ // NoteEvent<(int, int)> -- time signature changes
+ var neii = curNote as NoteEvent<(int, int)>;
+ if (neii != null)
+ {
+ this.timeSigChgs[curTime] = neii;
+ }
+
+ // NoteEvent
+ var nei = curNote as NoteEvent;
+ if (nei != null)
+ {
+ this.events[curTime] = nei;
+ }
+
+ // update previous states
+ prevNote = curNote;
+ prevTime = curTime;
+ prevBeat = chartNote.Item1;
+ prevMeasure = measure.Key;
+ }
+ }
+
+ // chords
+ foreach (KeyValuePair> pair in playNotes)
+ {
+ List chordableNotes = new List();
+ foreach (NotePlay n in pair.Value)
+ {
+ if (n.type != NotePlayType.HoldEnd && n.type != NotePlayType.Untimed)
+ if (!(new NotePlayType[] { NotePlayType.HoldEnd, NotePlayType.Untimed, NotePlayType.HoldMid }).Contains(n.type))
+ chordableNotes.Add(n);
+ }
+ if (chordableNotes.Count >= 2)
+ {
+ GD.Print($"Found chord: {string.Join(", ", chordableNotes)}");
+ // TODO: draw chord indicators "Chordify"
+ }
+ }
+
+ // Measure Lines //
+ // TODO: adapt to tempo changes in the middle of a measure
+ // int tempoIdx = 1;
+ // int bpmIdx = 1;
+ // for (int curMeasure = 0; curMeasure < chart.notes.Count; curMeasure++)
+ // {
+ // while (curMeasure >= bpmChangeMeasures[bpmIdx] && bpmIdx < bpmChangeMeasures.Count - 1)
+ // ++bpmIdx;
+ // GD.Print($"{curMeasure}: {bpmIdx}");
+
+ // // last tempo change / only one tempo change exists
+ // if (tempoIdx == tempoChangeMeasures.Count - 1)
+ // {
+ // float pos = tempoChangePositions[tempoIdx] + Util.NotePosition(curMeasure - tempoChangeMeasures[tempoIdx], 0, tempo.Last(), beatsPerMeasure[bpmIdx]);
+ // var ml = measureLine.Instance();
+ // measureScroll.AddChild(ml);
+ // ml.Translation = new Vector3(0, 0, pos);
+ // ml.Text = $"{curMeasure}";
+ // }
+ // else if (tempoIdx < tempoChangeMeasures.Count)
+ // {
+ // // TODO: adapt to key signature changes
+ // while (curMeasure == tempoChangeMeasures[tempoIdx])
+ // {
+ // int measuresToCreate = tempoChangeMeasures[tempoIdx] - tempoChangeMeasures[tempoIdx - 1];
+ // for (int i = 0; i < measuresToCreate; ++i)
+ // {
+ // int measureNum = tempoChangeMeasures[tempoIdx - 1] + i;
+ // // GD.Print($"{tempoIdx} / {tempoChangePositions.Count}, {tempo.Count}");
+ // float pos = Util.InterpFloat(tempoChangePositions[tempoIdx - 1], tempoChangePositions[tempoIdx], (float)i/measuresToCreate);
+
+ // var ml = measureLine.Instance();
+ // measureScroll.AddChild(ml);
+ // ml.Translation = new Vector3(0, 0, pos);
+ // ml.Text = $"{curMeasure}";
+ // }
+ // tempoIdx = Mathf.Clamp(tempoIdx + 1, 0, tempo.Count - 1);
+ // }
+ // }
+ // }
+
+ doneLoading = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Chart/Chart.cs.meta b/Scripts/Data/Chart/Chart.cs.meta
new file mode 100644
index 0000000..dc3ce32
--- /dev/null
+++ b/Scripts/Data/Chart/Chart.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 562cc671f5a30839c8769855d8d0483f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/Difficulty.cs b/Scripts/Data/Chart/Difficulty.cs
new file mode 100644
index 0000000..22294fd
--- /dev/null
+++ b/Scripts/Data/Chart/Difficulty.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace WacK.Data.Chart
+{
+ public enum DifficultyLevel
+ {
+ Normal, Hard, Expert, Inferno
+ }
+}
diff --git a/Scripts/Data/Chart/Difficulty.cs.meta b/Scripts/Data/Chart/Difficulty.cs.meta
new file mode 100644
index 0000000..96aa7a8
--- /dev/null
+++ b/Scripts/Data/Chart/Difficulty.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 02342af085b6384489297bdfaaff13f1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/MeasureBeat.cs b/Scripts/Data/Chart/MeasureBeat.cs
new file mode 100755
index 0000000..d7171c2
--- /dev/null
+++ b/Scripts/Data/Chart/MeasureBeat.cs
@@ -0,0 +1,121 @@
+using System;
+
+namespace WacK.Data.Chart
+{
+ ///
+ /// Represents a non-timed chart position in measure and beat as a fraction of the measure.
+ ///
+ public class MeasureBeat
+ {
+ public const int DENOMINATOR = 1920;
+
+ public int measure { get; private set; }
+ ///
+ /// Beat in the measure, represented as beat/1920 of a measure.
+ ///
+ public int beat { get; private set; }
+
+ public MeasureBeat(int measure, int beat)
+ {
+ this.measure = measure;
+ this.beat = beat;
+ Normalize();
+ }
+
+ public void Normalize()
+ {
+ // beats larger than denominator
+ if (Math.Abs(beat) >= DENOMINATOR)
+ {
+ measure += beat / DENOMINATOR;
+ beat %= DENOMINATOR;
+ }
+ // positive measure, negative beats
+ if (beat < 0 && measure > 0)
+ {
+ measure--;
+ beat += DENOMINATOR;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"MeasureBeat({this.measure}, {this.beat}/{DENOMINATOR})";
+ }
+
+ public override int GetHashCode()
+ {
+ return measure*DENOMINATOR + beat;
+ }
+
+ public override bool Equals(object tgt)
+ {
+ if (tgt == null) return false;
+
+ return this == (MeasureBeat)tgt;
+ }
+
+ /* STATIC CONSTANTS */
+ public readonly static MeasureBeat ZERO = new MeasureBeat(0, 0);
+
+ /* STATIC OPERATORS */
+ public static MeasureBeat operator +(MeasureBeat a) => a;
+ public static MeasureBeat operator -(MeasureBeat a)
+ => new MeasureBeat(-a.measure, -a.beat);
+
+ public static MeasureBeat operator +(MeasureBeat a, MeasureBeat b)
+ {
+ var meas = a.measure + b.measure;
+ var beat = a.beat + b.beat;
+ return new MeasureBeat(meas, beat);
+ }
+
+ public static MeasureBeat operator -(MeasureBeat a, MeasureBeat b)
+ {
+ return a + -b;
+ }
+
+ public static bool operator ==(MeasureBeat a, MeasureBeat b)
+ {
+ return a.measure == b.measure && a.beat == b.beat;
+ }
+
+ public static bool operator !=(MeasureBeat a, MeasureBeat b)
+ {
+ return !(a == b);
+ }
+
+ public static bool operator <(MeasureBeat a, MeasureBeat b)
+ {
+ if (a.measure < b.measure) return true;
+
+ if (a.measure == b.measure)
+ {
+ return a.beat < b.beat;
+ }
+
+ return false;
+ }
+
+ public static bool operator >(MeasureBeat a, MeasureBeat b)
+ {
+ if (a != b)
+ {
+ return !(a < b);
+ }
+ return false;
+ }
+
+ public static bool operator <=(MeasureBeat a, MeasureBeat b)
+ {
+ if (a == b) return true;
+ return a < b;
+ }
+
+ public static bool operator >=(MeasureBeat a, MeasureBeat b)
+ {
+ if (a == b) return true;
+ return a > b;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Chart/MeasureBeat.cs.meta b/Scripts/Data/Chart/MeasureBeat.cs.meta
new file mode 100644
index 0000000..a2ea869
--- /dev/null
+++ b/Scripts/Data/Chart/MeasureBeat.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f19917e007c0523e9a5ccccea23e9524
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/Note.cs b/Scripts/Data/Chart/Note.cs
new file mode 100755
index 0000000..e4fd040
--- /dev/null
+++ b/Scripts/Data/Chart/Note.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+
+namespace WacK.Data.Chart
+{
+ ///
+ /// Base class for various in-play note types.
+ ///
+ public abstract class Note
+ {
+ ///
+ /// Time in milliseconds which the note occurs.
+ ///
+ public double time = 0;
+
+ ///
+ /// Time of the note in MeasureBeat.
+ ///
+ public MeasureBeat measureBeat;
+
+ ///
+ /// The note's radial position out of 60.
+ ///
+ public int? position;
+
+ ///
+ /// The radial size of the note.
+ /// 1 <= size <= 60
+ ///
+ public int? size;
+
+ public Note(double time, MeasureBeat measureBeat, int? position = null, int? size = null)
+ {
+ this.time = time;
+ this.measureBeat = measureBeat;
+ this.position = position;
+ this.size = size;
+ }
+
+ public Note()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Chart/Note.cs.meta b/Scripts/Data/Chart/Note.cs.meta
new file mode 100644
index 0000000..404365d
--- /dev/null
+++ b/Scripts/Data/Chart/Note.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1ed8bb3e1326d18f9927b087b6e8fe7c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/NoteEvent.cs b/Scripts/Data/Chart/NoteEvent.cs
new file mode 100755
index 0000000..4bcf519
--- /dev/null
+++ b/Scripts/Data/Chart/NoteEvent.cs
@@ -0,0 +1,36 @@
+using System;
+
+using System.Collections.Generic;
+
+namespace WacK.Data.Chart
+{
+ public enum NoteEventType
+ {
+ Tempo,
+ TimeSignature,
+ ScrollSpeedMultiplier,
+ BGAdd,
+ BGRem,
+ }
+
+ ///
+ /// Represents an unplayable event with some associated data value.
+ ///
+ /// The value's type.
+ public class NoteEvent : Note
+ {
+ public NoteEventType type;
+
+ ///
+ /// A value whose function will vary depending on the type of note.
+ ///
+ public T value;
+
+ public NoteEvent(double time, MeasureBeat measureBeat, NoteEventType type, int? position = null, int? size = null, T value = default(T)) :
+ base(time, measureBeat, position, size)
+ {
+ this.value = value;
+ this.type = type;
+ }
+ }
+}
diff --git a/Scripts/Data/Chart/NoteEvent.cs.meta b/Scripts/Data/Chart/NoteEvent.cs.meta
new file mode 100644
index 0000000..26934cf
--- /dev/null
+++ b/Scripts/Data/Chart/NoteEvent.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f133260cfe1efeb06bc84818f1f694c0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/NoteHold.cs b/Scripts/Data/Chart/NoteHold.cs
new file mode 100755
index 0000000..389b006
--- /dev/null
+++ b/Scripts/Data/Chart/NoteHold.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WacK.Data.Chart
+{
+ ///
+ /// A hold note. Notably contains data about hold points.
+ ///
+ public class NoteHold : NotePlay
+ {
+ public SortedList points;
+
+ public NoteHold(double time, MeasureBeat measureBeat, int position, int size, int holdIndex, int holdNext, bool bonus = false)
+ : base(time, measureBeat, position, size,holdIndex, holdNext, type: NotePlayType.HoldStart, bonus: false)
+ {
+ // points = (SortedList)holdPoints.Skip(1);
+ }
+ }
+}
diff --git a/Scripts/Data/Chart/NoteHold.cs.meta b/Scripts/Data/Chart/NoteHold.cs.meta
new file mode 100644
index 0000000..57d1c40
--- /dev/null
+++ b/Scripts/Data/Chart/NoteHold.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 74c839480bb719c9b96e11709817d2e6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Chart/NotePlay.cs b/Scripts/Data/Chart/NotePlay.cs
new file mode 100755
index 0000000..85197d3
--- /dev/null
+++ b/Scripts/Data/Chart/NotePlay.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace WacK.Data.Chart
+{
+ public enum NotePlayType
+ {
+ Touch,
+ HoldStart,
+ HoldMid,
+ HoldEnd,
+ Untimed,
+ SwipeIn,
+ SwipeOut,
+ SwipeCW,
+ SwipeCCW,
+ }
+
+ ///
+ /// Represents playable notes.
+ ///
+ public class NotePlay : Note
+ {
+ public NotePlayType type { get; private set; }
+ public bool isBonus { get; private set; }
+ public int holdIdx { get; private set; }
+ public int holdNextIdx { get; private set; }
+
+ public NotePlay(double time, MeasureBeat measureBeat, int position, int size, int holdIndex = -1, int holdNext = -1,
+ NotePlayType type = NotePlayType.Touch, bool bonus = false)
+ : base(time, measureBeat, position, size)
+ {
+ this.type = type;
+ this.isBonus = bonus;
+ this.holdIdx = holdIndex;
+ this.holdNextIdx = holdNext;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Chart/NotePlay.cs.meta b/Scripts/Data/Chart/NotePlay.cs.meta
new file mode 100644
index 0000000..a7458c3
--- /dev/null
+++ b/Scripts/Data/Chart/NotePlay.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ccfc6a251580ee67a83e2269870a227c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Mer.meta b/Scripts/Data/Mer.meta
new file mode 100644
index 0000000..a6ae7fc
--- /dev/null
+++ b/Scripts/Data/Mer.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0ed6e3a31145daf8597c9bd80394d64d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Mer/Mer.cs b/Scripts/Data/Mer/Mer.cs
new file mode 100644
index 0000000..7cb711a
--- /dev/null
+++ b/Scripts/Data/Mer/Mer.cs
@@ -0,0 +1,146 @@
+/**
+ * Chart.cs
+ * Representation of a chart, constructed from a .mer file.
+ *
+ * by muskit
+ * July 1, 2022
+ **/
+
+using System;
+using System.Collections.Generic;
+
+namespace WacK.Data.Mer
+{
+ ///
+ /// Structurized representation of a .mer file.
+ ///
+ public class Mer
+ {
+ ///
+ /// HIERARCHY:
+ /// Key is measure.
+ /// Value is List of (beat/1920, Notes) tuples.
+ ///
+ public SortedList> notes = new SortedList>();
+
+ public int playableNoteCount { get; private set; }
+
+ ///
+ /// Construct Chart from contents of .mer file.
+ ///
+ /// Contents of a .mer file.
+ public Mer(string str)
+ {
+ if (str == String.Empty) return;
+
+ playableNoteCount = 0;
+
+ List lines = new List(str.Split('\n'));
+ foreach (var line in lines)
+ {
+ List tokens = new List(line.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries));
+ if (tokens.Count == 0) continue;
+ if (tokens[0][0] == '#') continue;
+
+ int currentMeasure = int.Parse(tokens[0]);
+ int currentBeat = int.Parse(tokens[1]);
+
+ if (!notes.ContainsKey(currentMeasure))
+ {
+ notes[currentMeasure] = new List<(int, MerNote)>();
+ }
+
+ switch (tokens[2])
+ {
+ case "1": // common note types
+ switch(tokens[3])
+ {
+ case "1": // touch
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.Touch)));
+ ++playableNoteCount;
+ break;
+ case "2": // touch w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.Touch, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "20": // touch w/ bonus (+ "big effect")
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.Touch, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "16": // untimed
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.Untimed)));
+ ++playableNoteCount;
+ break;
+ case "26": // untimed w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.Untimed, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "3": // swipe in (red)
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeIn)));
+ ++playableNoteCount;
+ break;
+ case "21": // swipe in w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeIn, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "4": // swipe out (blue)
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeOut)));
+ ++playableNoteCount;
+ break;
+ case "22": // swipe out w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeOut, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "7": // swipe CCW
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeCCW)));
+ ++playableNoteCount;
+ break;
+ case "8": // swipe CCW w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeCCW, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "5": // swipe CW
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeCW)));
+ ++playableNoteCount;
+ break;
+ case "6": // swipe CW w/ bonus
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), type: MerType.SwipeCW, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "9": // hold start
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), holdIndex: int.Parse(tokens[4]), holdNext: int.Parse(tokens[8]), type: MerType.HoldStart)));
+ break;
+ case "25": // hold start (w/ bonus)
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), holdIndex: int.Parse(tokens[4]), holdNext: int.Parse(tokens[8]), type: MerType.HoldStart, bonus: true)));
+ ++playableNoteCount;
+ break;
+ case "10": // hold middle
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), holdIndex: int.Parse(tokens[4]), holdNext: int.Parse(tokens[8]), type: MerType.HoldMid)));
+ break;
+ case "11": // hold end
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), holdIndex: int.Parse(tokens[4]), type: MerType.HoldEnd)));
+ break;
+ case "12": // BG add
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), value: tokens[8], type: MerType.BGAdd)));
+ break;
+ case "13": // BG rem
+ notes[currentMeasure].Add((currentBeat, new MerNote(int.Parse(tokens[5]), int.Parse(tokens[6]), value: tokens[8], type: MerType.BGRem)));
+ break;
+
+ }
+ break;
+ case "2": // tempo
+ notes[currentMeasure].Add((currentBeat, new MerNote(value: tokens[3], type: MerType.Tempo)));
+ break;
+ case "3": // beats per measure
+ notes[currentMeasure].Add((currentBeat, new MerNote(value: $"{tokens[3]} {tokens[4]}", type: MerType.TimeSignature)));
+ break;
+ }
+ }
+ foreach (var measure in notes)
+ {
+ measure.Value.Sort((x, y) => x.Item1.CompareTo(y.Item1));
+ }
+ }
+ }
+}
diff --git a/Scripts/Data/Mer/Mer.cs.meta b/Scripts/Data/Mer/Mer.cs.meta
new file mode 100644
index 0000000..4ed5e08
--- /dev/null
+++ b/Scripts/Data/Mer/Mer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 03f65fd134dbd18faba49dbb870b3450
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Data/Mer/MerNote.cs b/Scripts/Data/Mer/MerNote.cs
new file mode 100644
index 0000000..a867d5b
--- /dev/null
+++ b/Scripts/Data/Mer/MerNote.cs
@@ -0,0 +1,38 @@
+/**
+ * Note.cs
+ * A struct representing a note.
+ *
+ * by muskit
+ * July 1, 2022
+ **/
+
+namespace WacK.Data.Mer
+{
+ public enum MerType
+ {
+ Touch, HoldStart, HoldMid, HoldEnd, Untimed, SwipeIn, SwipeOut, SwipeCW, SwipeCCW, Tempo, TimeSignature, BGAdd, BGRem
+ }
+ public struct MerNote
+ {
+ public MerType noteType { get; private set; }
+ public bool isBonus { get; private set; }
+
+ // Radial values in minutes
+ public int position { get; private set; }
+ public int size { get; private set; } // 1 <= size <= 60
+ public string value { get; private set; }
+ public int holdIdx { get; private set; }
+ public int holdNextIdx { get; private set; }
+
+ public MerNote(int position = 0, int size = 1, string value = "", int holdIndex = -1, int holdNext = -1, MerType type = MerType.Touch, bool bonus = false)
+ {
+ this.position = position;
+ this.size = size;
+ this.value = value;
+ this.holdIdx = holdIndex;
+ this.holdNextIdx = holdNext;
+ this.noteType = type;
+ this.isBonus = bonus;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Data/Mer/MerNote.cs.meta b/Scripts/Data/Mer/MerNote.cs.meta
new file mode 100644
index 0000000..84590cb
--- /dev/null
+++ b/Scripts/Data/Mer/MerNote.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f03d973b6879b96a5b79467d1dd3cbfc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Scenes/DebugChartLoader.cs b/Scripts/Scenes/DebugChartLoader.cs
new file mode 100644
index 0000000..2dee1df
--- /dev/null
+++ b/Scripts/Scenes/DebugChartLoader.cs
@@ -0,0 +1,68 @@
+using Godot;
+using System;
+
+namespace WacK.Scenes
+{
+ public partial class DebugChartLoader : Node
+ {
+ [Export]
+ private LineEdit inputField;
+
+ [Export]
+ private LineEdit soundField;
+
+ [Export]
+ private OptionButton difficultyButton;
+
+ [Export]
+ private Button playButton;
+
+
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ playButton.Pressed += PlayClicked;
+ }
+
+ private void PlayClicked()
+ {
+ // TODO: globally accessible verify song folder and chart/audio function
+ var songPath = $"user://songs/{inputField.Text}";
+ var chartPath = $"{songPath}/{difficultyButton.Selected}.mer";
+ var soundPath = $"{songPath}/{soundField.Text}";
+ GD.Print($"Song: {songPath}\nChart: {chartPath}\nSound: {soundPath}");
+
+ // folder check
+ using var dir = DirAccess.Open(songPath);
+ if (dir == null)
+ {
+ GD.PrintErr($"Error occurred opening song folder!\n{DirAccess.GetOpenError()}");
+ return;
+ }
+
+ // chart check
+ using var chartFile = FileAccess.Open(chartPath, FileAccess.ModeFlags.Read);
+ if (chartFile == null)
+ {
+ GD.PrintErr($"Error occurred opening chart!\n{FileAccess.GetOpenError()}");
+ return;
+ }
+
+ // sound check
+ using var soundFile = FileAccess.Open(soundPath, FileAccess.ModeFlags.Read);
+ if (soundFile == null)
+ {
+ GD.PrintErr($"Error occurred opening sound!\n{FileAccess.GetOpenError()}");
+ return;
+ }
+
+ Play.playParams = new PlayParameters(chartPath, soundPath);
+ GetTree().ChangeSceneToFile("res://Scenes/Play.tscn");
+ }
+
+ public override void _ExitTree()
+ {
+ playButton.Pressed -= PlayClicked;
+ }
+ }
+}
diff --git a/Scripts/Scenes/Play.cs b/Scripts/Scenes/Play.cs
new file mode 100644
index 0000000..3d8886f
--- /dev/null
+++ b/Scripts/Scenes/Play.cs
@@ -0,0 +1,35 @@
+using Godot;
+
+namespace WacK.Scenes
+{
+ public class PlayParameters
+ {
+ /* TODO: store song ID from internal database
+ public string songID;
+ public Difficulty diff;
+ */
+ public string chartPath;
+ public string soundPath;
+
+ public PlayParameters(string chPath, string snPath)
+ {
+ chartPath = chPath;
+ soundPath = snPath;
+ }
+ }
+ public partial class Play : Node
+ {
+ // initialized by another scene, BEFORE loading this one!
+ public static PlayParameters playParams;
+
+ private void Start()
+ {
+
+ }
+
+ private void OnDestroy()
+ {
+ playParams = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Scenes/Startup.cs b/Scripts/Scenes/Startup.cs
new file mode 100644
index 0000000..b5281ba
--- /dev/null
+++ b/Scripts/Scenes/Startup.cs
@@ -0,0 +1,46 @@
+using Godot;
+using System;
+
+namespace WacK.Scenes
+{
+ public partial class Startup : Node
+ {
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ GD.Print($"User directory: {OS.GetUserDataDir()}");
+ using var songDir = DirAccess.Open("user://songs");
+ if (songDir != null)
+ {
+ GD.Print("Successfully opened songs directory!");
+ // load songs
+ }
+ else
+ {
+ GD.Print("Could not find songs directory! Creating it...");
+
+ DirAccess.MakeDirAbsolute("user://songs");
+ using var newSongDir = DirAccess.Open("user://songs");
+
+ if (newSongDir != null)
+ {
+ GD.Print("Songs folder created successfully!");
+ // create note
+ var note = "Place song folders here. Nested folders supported for organization.\n";
+ using var f = FileAccess.Open($"{newSongDir.GetCurrentDir()}/note.txt", FileAccess.ModeFlags.Write);
+ f.StoreString(note);
+
+ // TODO: add in-game notice
+ }
+ else
+ {
+ GD.PrintErr($"Could not create the songs directory!\n{DirAccess.GetOpenError()}");
+ }
+
+ }
+
+ // Change scenes
+ GetTree().ChangeSceneToFile("res://Scenes/DebugChartLoader.tscn");
+ }
+ }
+}
diff --git a/Scripts/Util.cs b/Scripts/Util.cs
new file mode 100644
index 0000000..82162ed
--- /dev/null
+++ b/Scripts/Util.cs
@@ -0,0 +1,147 @@
+/**
+ * Util.cs
+ * Various conversion functions.
+ *
+ * by muskit
+ * July 26, 2022
+ **/
+
+using System;
+
+using Godot;
+
+using WacK.Configuration;
+
+namespace WacK
+{
+ public static class Util
+ {
+ public static float Seg2Rad(float seg)
+ {
+ return Mathf.DegToRad(6f * seg);
+ }
+ public static float Rad2Seg(float angle)
+ {
+ return Mathf.RadToDeg(angle / 6f);
+ }
+
+ public static int InterpInt(int a, int b, float ratio)
+ {
+ if (a == 0 && b == 0)
+ return 0;
+ return (int)Math.Round(a + (b - a) * ratio);
+ }
+
+ public static float InterpFloat(float a, float b, float ratio)
+ {
+ if (a == 0 && b == 0)
+ return 0;
+ return a + (b - a) * ratio;
+ }
+
+ // Returns an equivalent destination angle that's closest to the origin.
+ public static float NearestAngle(float origin, float destination)
+ {
+ float result = destination;
+
+ float plus = destination + 2f * Mathf.Pi;
+ float minus = destination - 2f * Mathf.Pi;
+ float minusDelta = Mathf.Abs(minus - origin);
+ float normDelta = Mathf.Abs(destination - origin);
+ float plusDelta = Mathf.Abs(plus - origin);
+ if (plusDelta < normDelta)
+ result = plus;
+ if (minusDelta < normDelta)
+ result = minus;
+
+ return result;
+ }
+
+ // Return an equivalent minute that's closest to the origin.
+ public static float NearestMinute(int origin, int destination)
+ {
+ int result = destination % 60;
+
+ int plus = destination + 60;
+ int minus = destination - 60;
+ int minusDelta = Math.Abs(minus - origin);
+ int normDelta = Math.Abs(destination - origin);
+ int plusDelta = Math.Abs(plus - origin);
+ if (plusDelta < normDelta)
+ result = plus;
+ if (minusDelta < normDelta)
+ result = minus;
+
+ return result;
+ }
+
+ public static float ScreenPixelToRad(Vector2 pos)
+ {
+ var resolution = DisplayServer.WindowGetSize();
+ var origin = new Vector2(resolution.X / 2 - 1, resolution.Y / 2 - 1);
+
+ return Mathf.Atan2(pos.Y - origin.Y, pos.X - origin.X);
+ }
+
+ public static int TouchPosToSegmentInt(Vector2 pos, Vector2 touchResolution)
+ {
+ var origin = new Vector2(touchResolution.X / 2 - 1, touchResolution.Y / 2 - 1);
+ var angle = Mathf.Atan2(pos.Y - origin.Y, pos.X - origin.X);
+
+ if (angle > 0)
+ angle = 2f * Mathf.Pi - angle;
+
+ return Mathf.FloorToInt(Mathf.Abs(angle) / 2f * Mathf.Pi * 60) % 60;
+ }
+
+ public static int ScreenPixelToSegmentInt(Vector2 pos)
+ {
+ var angle = ScreenPixelToRad(pos);
+ if (angle > 0)
+ angle = 2f * Mathf.Pi - angle;
+
+ return Mathf.FloorToInt(Mathf.Abs(angle) / 2f * Mathf.Pi * 60) % 60;
+ }
+
+ public static float NoteTime(int measure, int beat, float tempo, int beatsPerMeasure)
+ {
+ if (tempo == 0) return 0; // avoid divide by 0
+
+ return 60f / tempo * beatsPerMeasure * ((float)measure + (float)beat / 1920f);
+ }
+
+ public static float NotePosition(int measure, int beat, float tempo, int beatsPerMeasure)
+ {
+ if (tempo == 0) return 0; // avoid divide by 0
+ return TimeToPosition(60f / tempo * beatsPerMeasure * ((float)measure + (float)beat / 1920f));
+ }
+
+ public static float TimeToPosition(float time)
+ {
+ return time * PlaySettings.playSpeedMultiplier * Constants.SCROLL_MULT;
+ }
+
+ public static float PositionToTime(float pos)
+ {
+ return pos / PlaySettings.playSpeedMultiplier / Constants.SCROLL_MULT;
+ }
+
+ // TODO: notes scale to scroll position instead of strikeline
+ // (where calibration offsets can be applied)
+ public static Vector3 NoteScale(float zPos, float zOrigin = 0)
+ {
+ var val = zPos - zOrigin;
+ if (val <= Constants.NOTE_DRAW_DISTANCE)
+ {
+ var ratio = Mathf.Clamp((Constants.NOTE_DRAW_DISTANCE - val) / Constants.NOTE_DRAW_DISTANCE, 0, 1);
+ return new Vector3(ratio, ratio, 1);
+ }
+ return Vector3.Zero;
+ }
+
+ public static string DifficultyValueToString(float diffPoint)
+ {
+ return Mathf.FloorToInt(diffPoint).ToString() + (diffPoint > Mathf.Floor(diffPoint) ? "+" : "");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Things/TunnelObjects/2D/Background/Background.tscn b/Things/TunnelObjects/2D/Background/Background.tscn
new file mode 100644
index 0000000..68d6a51
--- /dev/null
+++ b/Things/TunnelObjects/2D/Background/Background.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3 uid="uid://b7dhdsryff4rn"]
+
+[node name="TunnelObjects" type="Node2D"]
diff --git a/Things/TunnelObjects/2D/Background/BackgroundSegment.tscn b/Things/TunnelObjects/2D/Background/BackgroundSegment.tscn
new file mode 100644
index 0000000..69792e2
--- /dev/null
+++ b/Things/TunnelObjects/2D/Background/BackgroundSegment.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3 uid="uid://cvxp0w0urvyhj"]
+
+[node name="BackgroundSegment" type="Node2D"]
diff --git a/WacK.csproj b/WacK.csproj
new file mode 100644
index 0000000..8ee8c44
--- /dev/null
+++ b/WacK.csproj
@@ -0,0 +1,6 @@
+
+
+ net6.0
+ true
+
+
\ No newline at end of file
diff --git a/WacK.sln b/WacK.sln
new file mode 100644
index 0000000..a8d2bb6
--- /dev/null
+++ b/WacK.sln
@@ -0,0 +1,19 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WacK", "WacK.csproj", "{2797AE7C-0780-47EF-B89B-1F339403C89F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ ExportDebug|Any CPU = ExportDebug|Any CPU
+ ExportRelease|Any CPU = ExportRelease|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
+ {2797AE7C-0780-47EF-B89B-1F339403C89F}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..b370ceb
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 0000000..752497a
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cprfryf1c7kr2"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/project.godot b/project.godot
new file mode 100644
index 0000000..09d6ec6
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,32 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="WacK"
+run/main_scene="res://Scenes/_STARTUP.tscn"
+config/features=PackedStringArray("4.1", "C#", "Mobile")
+config/icon="res://icon.svg"
+
+[display]
+
+window/size/viewport_width=1920
+window/size/viewport_height=1080
+window/stretch/mode="canvas_items"
+window/stretch/aspect="expand"
+window/vsync/vsync_mode=3
+
+[dotnet]
+
+project/assembly_name="WacK"
+
+[rendering]
+
+renderer/rendering_method="mobile"