diff --git a/Assets/Audio.meta b/Assets/Audio.meta
new file mode 100644
index 0000000..5efda84
--- /dev/null
+++ b/Assets/Audio.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3d988beb579a940d7bfbc1ad7e2e7e17
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX.meta b/Assets/Audio/SFX.meta
new file mode 100644
index 0000000..ef07f18
--- /dev/null
+++ b/Assets/Audio/SFX.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ddb76c89063d14d3eb74a6c0bbf5f8f2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/ButtonHit.wav b/Assets/Audio/SFX/ButtonHit.wav
new file mode 100644
index 0000000..dd2c4de
Binary files /dev/null and b/Assets/Audio/SFX/ButtonHit.wav differ
diff --git a/Assets/Audio/SFX/ButtonHit.wav.meta b/Assets/Audio/SFX/ButtonHit.wav.meta
new file mode 100644
index 0000000..86a9d81
--- /dev/null
+++ b/Assets/Audio/SFX/ButtonHit.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 7bb487aa2617a4056a1b39a0273320d2
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/CorrectHit.wav b/Assets/Audio/SFX/CorrectHit.wav
new file mode 100644
index 0000000..1da576b
Binary files /dev/null and b/Assets/Audio/SFX/CorrectHit.wav differ
diff --git a/Assets/Audio/SFX/CorrectHit.wav.meta b/Assets/Audio/SFX/CorrectHit.wav.meta
new file mode 100644
index 0000000..6fe47db
--- /dev/null
+++ b/Assets/Audio/SFX/CorrectHit.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 3933bfed4d8be4f9fabe55d5ade4c07e
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/IncorrectHit.wav b/Assets/Audio/SFX/IncorrectHit.wav
new file mode 100644
index 0000000..5db3f40
Binary files /dev/null and b/Assets/Audio/SFX/IncorrectHit.wav differ
diff --git a/Assets/Audio/SFX/IncorrectHit.wav.meta b/Assets/Audio/SFX/IncorrectHit.wav.meta
new file mode 100644
index 0000000..112fbcf
--- /dev/null
+++ b/Assets/Audio/SFX/IncorrectHit.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 530d8f9f532ea4f2e9098500121f2c57
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/MissHit.wav b/Assets/Audio/SFX/MissHit.wav
new file mode 100644
index 0000000..5645b18
Binary files /dev/null and b/Assets/Audio/SFX/MissHit.wav differ
diff --git a/Assets/Audio/SFX/MissHit.wav.meta b/Assets/Audio/SFX/MissHit.wav.meta
new file mode 100644
index 0000000..b3954ef
--- /dev/null
+++ b/Assets/Audio/SFX/MissHit.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 0ea1f6e4ffd644b628855576f686e3b4
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Audio/SFX/WrongDirection.wav b/Assets/Audio/SFX/WrongDirection.wav
new file mode 100644
index 0000000..4f600f1
Binary files /dev/null and b/Assets/Audio/SFX/WrongDirection.wav differ
diff --git a/Assets/Audio/SFX/WrongDirection.wav.meta b/Assets/Audio/SFX/WrongDirection.wav.meta
new file mode 100644
index 0000000..2ec8079
--- /dev/null
+++ b/Assets/Audio/SFX/WrongDirection.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 920c5009ea0384f22a22a9b366964a4a
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Integration.meta b/Assets/Integration.meta
new file mode 100644
index 0000000..d56ae88
--- /dev/null
+++ b/Assets/Integration.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3d326dd4d0d254842be556b977461446
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Integration/DebugPrototypeController.cs b/Assets/Integration/DebugPrototypeController.cs
new file mode 100644
index 0000000..5cd79bd
--- /dev/null
+++ b/Assets/Integration/DebugPrototypeController.cs
@@ -0,0 +1,220 @@
+using TMPro;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.UI;
+
+/**
+ * Prototype debug panel. Forces 16:9 aspect ratio via camera viewport.
+ *
+ */
+public class DebugPrototypeController : MonoBehaviour
+{
+ #region Inspector Fields — Scriptable Objects
+
+ [Header("Scriptable Objects")]
+ [Tooltip("Controls note appearance timing (note appearance beats slider).")]
+ [SerializeField] private RhythmVisualConfig _rhythmVisualConfig;
+
+ #endregion
+
+ #region Inspector Fields — Note Appearance Beats
+
+ [Header("Note Appearance Beats")]
+ [Tooltip("Slider that sets RhythmVisualConfig.NoteAppearanceBeats. " +
+ "Higher value = notes appear further ahead = more reaction time.")]
+ [SerializeField] private Slider _noteBeatsSlider;
+ [Tooltip("Optional label displaying the current beats value.")]
+ [SerializeField] private TMP_Text _noteBeatsLabel;
+ [Tooltip("Minimum note appearance beats.")]
+ [SerializeField] private float _noteBeatsMin = 0.5f;
+ [Tooltip("Maximum note appearance beats.")]
+ [SerializeField] private float _noteBeatsMax = 6f;
+
+ #endregion
+
+ #region Inspector Fields — Tree Preview
+
+ [Header("Tree Preview")]
+ [Tooltip("The CameraRig driving gameplay camera movement.")]
+ [SerializeField] private CameraRig _cameraRig;
+ [Tooltip("Slider that zooms Camera.main back along Z for a tree overview.")]
+ [SerializeField] private Slider _cameraZSlider;
+ [Tooltip("Optional label displaying the current camera Z value.")]
+ [SerializeField] private TMP_Text _cameraZLabel;
+ [Tooltip("Background plane whose XY scale is adjusted proportionally as the camera moves back.")]
+ [SerializeField] private Transform _backgroundPlane;
+
+ #endregion
+
+ #region Inspector Fields — Controls
+
+ [Header("Controls")]
+ [Tooltip("Reloads the scene at the specified build index.")]
+ [SerializeField] private Button _restartButton;
+ [Tooltip("Build index of the scene to load on restart.")]
+ [SerializeField] private int _gameSceneIndex;
+
+ #endregion
+
+ #region Private State
+
+ private Camera _mainCamera;
+ private float _initialCameraZ;
+ private Vector3 _basePlaneScale;
+
+ #endregion
+
+ #region Lifecycle
+
+ private void Awake()
+ {
+ ApplyAspectRatio();
+ SetupNoteBeatsSlider();
+ SetupCameraZSlider();
+ SetupRestartButton();
+ }
+
+ private void Start()
+ {
+ InitializeCameraPreview();
+ }
+
+ private void OnDestroy()
+ {
+ if (_cameraRig != null)
+ _cameraRig.OnGameplayMoved -= OnGameplayMoved;
+ }
+
+ #endregion
+
+ #region Setup
+
+ private void ApplyAspectRatio()
+ {
+ Camera cam = Camera.main;
+ if (cam == null) return;
+
+ float targetAspect = 16f / 9f;
+ float windowAspect = (float)Screen.width / Screen.height;
+
+ if (windowAspect > targetAspect)
+ {
+ float scaleWidth = targetAspect / windowAspect;
+ cam.rect = new Rect((1f - scaleWidth) * 0.5f, 0f, scaleWidth, 1f);
+ }
+ else
+ {
+ float scaleHeight = windowAspect / targetAspect;
+ cam.rect = new Rect(0f, (1f - scaleHeight) * 0.5f, 1f, scaleHeight);
+ }
+ }
+
+ private void SetupNoteBeatsSlider()
+ {
+ if (_noteBeatsSlider == null) return;
+
+ _noteBeatsSlider.minValue = _noteBeatsMin;
+ _noteBeatsSlider.maxValue = _noteBeatsMax;
+ _noteBeatsSlider.value = _rhythmVisualConfig != null
+ ? _rhythmVisualConfig.NoteAppearanceBeats
+ : 2f;
+
+ _noteBeatsSlider.onValueChanged.AddListener(OnNoteBeatsChanged);
+ RefreshNoteBeatsLabel(_noteBeatsSlider.value);
+ }
+
+ private void SetupCameraZSlider()
+ {
+ if (_cameraZSlider == null) return;
+
+ _cameraZSlider.minValue = 0f;
+ _cameraZSlider.maxValue = 1f;
+ _cameraZSlider.value = 0f;
+ _cameraZSlider.onValueChanged.AddListener(OnCameraZChanged);
+ }
+
+ /**
+ * Deferred to Start so CameraRig.Initialize (called in game controller Awake) has
+ * already positioned the camera before we snapshot the initial Z.
+ *
+ */
+ private void InitializeCameraPreview()
+ {
+ _mainCamera = Camera.main;
+ if (_mainCamera == null) return;
+
+ _initialCameraZ = _mainCamera.transform.position.z;
+
+ if (_backgroundPlane != null)
+ _basePlaneScale = _backgroundPlane.localScale;
+
+ RefreshCameraZLabel();
+
+ if (_cameraRig != null)
+ _cameraRig.OnGameplayMoved += OnGameplayMoved;
+ }
+
+ private void SetupRestartButton()
+ {
+ if (_restartButton == null) return;
+ _restartButton.onClick.AddListener(OnRestartClicked);
+ }
+
+ #endregion
+
+ #region Callbacks
+
+ private void OnNoteBeatsChanged(float value)
+ {
+ if (_rhythmVisualConfig != null)
+ _rhythmVisualConfig.NoteAppearanceBeats = value;
+
+ RefreshNoteBeatsLabel(value);
+ }
+
+ private void OnCameraZChanged(float value)
+ {
+ if (_mainCamera == null || Mathf.Approximately(_initialCameraZ, 0f)) return;
+
+ float newZ = _initialCameraZ * (1f + value);
+
+ Vector3 pos = _mainCamera.transform.position;
+ pos.z = newZ;
+ _mainCamera.transform.position = pos;
+
+ if (_backgroundPlane != null)
+ _backgroundPlane.localScale = _basePlaneScale * (newZ / _initialCameraZ);
+
+ RefreshCameraZLabel();
+ }
+
+ private void OnGameplayMoved()
+ {
+ if (_cameraZSlider != null)
+ _cameraZSlider.SetValueWithoutNotify(0f);
+
+ if (_backgroundPlane != null)
+ _backgroundPlane.localScale = _basePlaneScale;
+
+ RefreshCameraZLabel();
+ }
+
+ private void OnRestartClicked()
+ {
+ SceneManager.LoadScene(_gameSceneIndex);
+ }
+
+ private void RefreshNoteBeatsLabel(float value)
+ {
+ if (_noteBeatsLabel != null)
+ _noteBeatsLabel.text = $"Note Speed: {value:F2} beats";
+ }
+
+ private void RefreshCameraZLabel()
+ {
+ if (_cameraZLabel != null)
+ _cameraZLabel.text = $"Preview Zoom: {_cameraZSlider.value * 100f:F0}%";
+ }
+
+ #endregion
+}
diff --git a/Assets/Integration/DebugPrototypeController.cs.meta b/Assets/Integration/DebugPrototypeController.cs.meta
new file mode 100644
index 0000000..89500ec
--- /dev/null
+++ b/Assets/Integration/DebugPrototypeController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a12a8f60640784ff6928d5c61ac956c9
\ No newline at end of file
diff --git a/Assets/Integration/TreeInputRhythmAdapter.cs b/Assets/Integration/TreeInputRhythmAdapter.cs
new file mode 100644
index 0000000..20947f0
--- /dev/null
+++ b/Assets/Integration/TreeInputRhythmAdapter.cs
@@ -0,0 +1,77 @@
+using System;
+
+/**
+ * Adapts to
+ * so the existing tree game inputs drive the rhythm system without any new
+ * InputActions or action maps. Each directional action is mapped to a configurable
+ * rhythm lane index that converts back to a
+ * after the beat window check passes.
+ *
+ */
+public class TreeInputRhythmAdapter : IRhythmInputProvider
+{
+ #region Private Events
+
+ private event Action _onLanePressed;
+ private event Action _onLaneReleased;
+
+ #endregion
+
+ #region IRhythmInputProvider
+
+ /** */
+ event Action IRhythmInputProvider.OnLanePressed
+ {
+ add => _onLanePressed += value;
+ remove => _onLanePressed -= value;
+ }
+
+ /** */
+ event Action IRhythmInputProvider.OnLaneReleased
+ {
+ add => _onLaneReleased += value;
+ remove => _onLaneReleased -= value;
+ }
+
+ /** */
+ public void Enable() => _playerInput.Enable();
+
+ /** */
+ public void Disable() => _playerInput.Disable();
+
+ #endregion
+
+ #region Fields
+
+ private readonly IPlayerInputProvider _playerInput;
+ private readonly int _leftLane;
+ private readonly int _submitLane;
+ private readonly int _rightLane;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Wires events to lane-indexed rhythm events.
+ *
+ * The upstream tree game input source.
+ * Lane index emitted when NavLeft is pressed (default 0).
+ * Lane index emitted when Submit is pressed (default 1).
+ * Lane index emitted when NavRight is pressed (default 2).
+ */
+ public TreeInputRhythmAdapter(IPlayerInputProvider playerInput,
+ int leftLane = 0, int submitLane = 1, int rightLane = 2)
+ {
+ _playerInput = playerInput;
+ _leftLane = leftLane;
+ _submitLane = submitLane;
+ _rightLane = rightLane;
+
+ _playerInput.OnNavLeft += () => _onLanePressed?.Invoke(_leftLane);
+ _playerInput.OnNavRight += () => _onLanePressed?.Invoke(_rightLane);
+ _playerInput.OnSubmit += () => _onLanePressed?.Invoke(_submitLane);
+ }
+
+ #endregion
+}
diff --git a/Assets/Integration/TreeInputRhythmAdapter.cs.meta b/Assets/Integration/TreeInputRhythmAdapter.cs.meta
new file mode 100644
index 0000000..7d2d390
--- /dev/null
+++ b/Assets/Integration/TreeInputRhythmAdapter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a85633517b3bab345a98fe7e26743e9c
\ No newline at end of file
diff --git a/Assets/Integration/TreeRhythmController.cs b/Assets/Integration/TreeRhythmController.cs
new file mode 100644
index 0000000..26ec6b2
--- /dev/null
+++ b/Assets/Integration/TreeRhythmController.cs
@@ -0,0 +1,393 @@
+using Cysharp.Threading.Tasks;
+using TMPro;
+using UnityEngine;
+
+/**
+ * Composition root for the combined tree and rhythm game mode.
+ *
+ */
+public class TreeRhythmController : MonoBehaviour, IRhythmGameAdapter
+{
+ #region Inspector Fields — Tree
+
+ [Header("Tree Configuration")]
+ [Tooltip("Visual and layout parameters shared with the tree visualizer.")]
+ [SerializeField] private TreeVisualizerConfig _config;
+
+ [Header("Tree Scene References")]
+ [Tooltip("Prefab instantiated for each BST node.")]
+ [SerializeField] private GameObject _nodePrefab;
+ [Tooltip("Drives camera panning to follow the carrier node.")]
+ [SerializeField] private CameraRig _cameraRig;
+ [Tooltip("Parent transform that holds edge line renderers.")]
+ [SerializeField] private Transform _edgeContainer;
+
+ [Header("Tree UI")]
+ [Tooltip("Label displaying the running error count.")]
+ [SerializeField] private TMP_Text _errorCountLabel;
+
+ [Header("Sample Tree")]
+ [Tooltip("Values pre-inserted into the tree before gameplay starts. Leave empty for a blank tree.")]
+ [SerializeField] private IntSampleTreeProvider _sampleTreeProvider;
+
+ [Header("Animation")]
+ [Tooltip("Sound effects and animation parameters for node reactions.")]
+ [SerializeField] private TreeAnimationConfig _animationConfig;
+
+ #endregion
+
+ #region Inspector Fields — Rhythm
+
+ [Header("Rhythm Configuration")]
+ [Tooltip("Beatmap asset containing song audio, BPM, notes, and key points.")]
+ [SerializeField] private BeatmapData _beatmap;
+ [Tooltip("Hit window and scoring parameters.")]
+ [SerializeField] private HitWindowConfig _hitWindows;
+ [Tooltip("Calibration offsets for audio and visual latency compensation.")]
+ [SerializeField] private RhythmCalibrationConfig _calibration;
+
+ [Header("Rhythm Scene References")]
+ [Tooltip("AudioSource used to play the song. Must be on a persistent GameObject.")]
+ [SerializeField] private AudioSource _audioSource;
+ [Tooltip("Optional: drives note visuals. Null if running rhythm in headless/audio-only mode.")]
+ [SerializeField] private RhythmVisualizerController _visualizer;
+
+ [Header("Lane → Action Mapping")]
+ [Tooltip("Maps each IRhythmInputProvider lane index to a NavigationAction. " +
+ "Default order: [0]=Left, [1]=Place, [2]=Right.")]
+ [SerializeField] private NavigationAction[] _laneToAction =
+ {
+ NavigationAction.Left,
+ NavigationAction.Place,
+ NavigationAction.Right
+ };
+
+ #endregion
+
+ #region Private Systems
+
+ private TreeNavigator _navigator;
+ private UnityInputProvider _rawInput;
+ private TreeInputRhythmAdapter _rhythmInput;
+ private TreeRhythmReactionPolicy _reactionPolicy;
+ private RhythmManager _rhythmManager;
+ private IntValueGenerator _generator;
+ private TreeLayoutManager _layout;
+ private int _lastPulseSubdivision = -1;
+
+ #endregion
+
+ #region Carrier State
+
+ private TreeNodeView _carrierView;
+ private TreeNode _activeComparisonNode;
+ private bool _lastActionSucceeded;
+
+ #endregion
+
+ #region Lifecycle
+
+ private void Awake()
+ {
+ _reactionPolicy = new TreeRhythmReactionPolicy();
+ _rawInput = new UnityInputProvider();
+ _rhythmInput = new TreeInputRhythmAdapter(_rawInput);
+
+ float noteAppearanceBeats = _visualizer != null ? _visualizer.NoteAppearanceBeats : 2f;
+ _rhythmManager = new RhythmManager(_audioSource, _hitWindows, _calibration, _reactionPolicy, noteAppearanceBeats);
+
+ var rules = new BSTInsertionRules();
+ var hooks = new TreeAnimationHooks(_animationConfig, _audioSource, () => _carrierView);
+ _navigator = new TreeNavigator(rules, hooks);
+ _generator = new IntValueGenerator(_config.MinValue, _config.MaxValue);
+ _layout = new TreeLayoutManager(_nodePrefab, _config, _edgeContainer);
+
+ SubscribeNavigatorEvents();
+ SubscribeRhythmInput();
+
+ if (_beatmap != null)
+ _rhythmManager.LoadBeatmap(_beatmap);
+
+ SubscribeRhythmEvents();
+
+ if (_visualizer != null)
+ _visualizer.Initialize(_rhythmManager);
+
+ _cameraRig.Initialize(_config);
+ _rawInput.Enable();
+ }
+
+ private void Start()
+ {
+ PopulateSampleTree();
+ if (_beatmap != null)
+ _rhythmManager.Play();
+ }
+
+ private void Update()
+ {
+ if (_visualizer == null)
+ _rhythmManager.Tick();
+
+ if (_visualizer != null)
+ _rhythmManager.NoteAppearanceBeats = _visualizer.NoteAppearanceBeats;
+
+ TickBeatPulse();
+ }
+
+ private void OnDestroy()
+ {
+ _rawInput.Disable();
+ _rhythmInput.Disable();
+ }
+
+ #endregion
+
+ #region Wiring
+
+ private void SubscribeNavigatorEvents()
+ {
+ _navigator.OnNavigationBegan += OnNavigationBegan;
+ _navigator.OnMovedLeft += OnMoved;
+ _navigator.OnMovedRight += OnMoved;
+ _navigator.OnMovedLeft += _ => _lastActionSucceeded = true;
+ _navigator.OnMovedRight += _ => _lastActionSucceeded = true;
+ _navigator.OnNodeInserted += _ => _lastActionSucceeded = true;
+ _navigator.OnInvalidActionAttempted += OnInvalidAction;
+ _navigator.OnReachedEmptySlot += context => OnReachedEmptySlotAsync(context).Forget();
+ _navigator.OnNodeInserted += node => OnNodeInsertedAsync(node).Forget();
+ _navigator.OnNavigationComplete += () => OnNavigationCompleteAsync().Forget();
+ }
+
+ private void SubscribeRhythmInput()
+ {
+ IRhythmInputProvider input = _rhythmInput;
+ input.OnLanePressed += lane => HandleLanePressed(lane).Forget();
+ }
+
+ private void SubscribeRhythmEvents()
+ {
+ _rhythmManager.OnNoteScheduled += OnNoteScheduled;
+ _rhythmManager.OnNoteHit += OnHit;
+ _rhythmManager.OnNoteMissed += OnMiss;
+ _rhythmManager.OnKeyPointReached += OnKeyPoint;
+ _rhythmManager.OnBeatPulse += OnBeatPulse;
+ _rhythmManager.OnSongComplete += OnSongComplete;
+ }
+
+ #endregion
+
+ #region Rhythm Input Gate
+
+ private async UniTaskVoid HandleLanePressed(int lane)
+ {
+ NavigationAction action = LaneToAction(lane);
+ bool actionMatchesRequired = action == _navigator.GetRequiredAction();
+
+ HitResult result = _rhythmManager.TryConsumeHit(0,
+ actionMatchesRequired ? (HitRating?)null : HitRating.Miss);
+
+ if (!result.IsHit)
+ {
+ if (_animationConfig != null && _animationConfig.OffBeatClip != null)
+ _audioSource.PlayOneShot(_animationConfig.OffBeatClip, _animationConfig.OffBeatVolume);
+
+ _reactionPolicy.OnOffBeatInput(lane,
+ _rhythmManager.ScoreTracker, _rhythmManager.HealthSystem);
+ return;
+ }
+
+ _lastActionSucceeded = false;
+ await _navigator.TryAction(action);
+
+ _reactionPolicy.OnHit(result, _lastActionSucceeded,
+ _rhythmManager.ScoreTracker, _rhythmManager.HealthSystem);
+ }
+
+ private NavigationAction LaneToAction(int lane)
+ {
+ if (_laneToAction == null || lane < 0 || lane >= _laneToAction.Length)
+ return NavigationAction.Left;
+ return _laneToAction[lane];
+ }
+
+ #endregion
+
+ #region IRhythmGameAdapter
+
+ /** */
+ public void OnNoteScheduled(ScheduledNoteInfo info) { }
+
+ /** */
+ public void OnHit(HitResult result) { }
+
+ /** */
+ public void OnMiss(NoteData note)
+ {
+ if (_animationConfig != null && _animationConfig.MissClip != null)
+ _audioSource.PlayOneShot(_animationConfig.MissClip, _animationConfig.MissVolume);
+ }
+
+ /** */
+ public void OnKeyPoint(KeyPointData keyPoint) { }
+
+ /** */
+ public void OnBeatPulse(double beat) { }
+
+ /** */
+ public void OnSongComplete() { }
+
+ #endregion
+
+ #region Beat Pulse
+
+ private void TickBeatPulse()
+ {
+ if (_animationConfig == null || _navigator.Root == null) return;
+
+ int subdivision = Mathf.FloorToInt((float)_rhythmManager.CurrentBeat * _animationConfig.BeatPulseMultiplier);
+ if (subdivision == _lastPulseSubdivision) return;
+
+ _lastPulseSubdivision = subdivision;
+ PulseTreeNodes(_navigator.Root);
+ }
+
+ private void PulseTreeNodes(TreeNode node)
+ {
+ if (node == null) return;
+
+ if (node.View != null && node.View != _carrierView)
+ node.View.PulseAsync(
+ _animationConfig.PulseScaleMultiplier,
+ _animationConfig.PulseDuration,
+ _animationConfig.PulseEase
+ ).Forget();
+
+ PulseTreeNodes(node.Left);
+ PulseTreeNodes(node.Right);
+ }
+
+ #endregion
+
+ #region Game Loop
+
+ private void PopulateSampleTree()
+ {
+ if (_sampleTreeProvider != null && _sampleTreeProvider.Values.Count > 0)
+ {
+ foreach (int value in _sampleTreeProvider.Values)
+ {
+ TreeNode node = _navigator.InsertDirect(value);
+ node.View = _layout.CreateNodeView(value.ToString());
+ node.View.SetState(NodeVisualState.Normal);
+ }
+
+ _layout.SnapToLayout(_navigator.Root);
+ Vector3 rootPos = _navigator.Root.View.transform.position;
+ _cameraRig.SnapTo(new Vector2(rootPos.x, rootPos.y));
+ }
+
+ BeginNextInsertion();
+ }
+
+ private void BeginNextInsertion()
+ {
+ _generator.Min = _config.MinValue;
+ _generator.Max = _config.MaxValue;
+ int value = _generator.GenerateNext();
+ _navigator.BeginInsertion(value).Forget();
+ }
+
+ #endregion
+
+ #region Navigator Event Handlers
+
+ private void OnNavigationBegan(NavigationContext context)
+ {
+ _carrierView = _layout.CreateNodeView(context.InsertValue.ToString());
+ _carrierView.SetState(NodeVisualState.Carrier);
+
+ Vector2 carrierPos = _layout.GetCarrierPosition(context);
+ _carrierView.SnapTo(carrierPos);
+ _cameraRig.SnapTo(carrierPos);
+
+ if (!context.IsAtEmptySlot)
+ {
+ _activeComparisonNode = context.CurrentNode;
+ _layout.SetComparisonFocus(context.CurrentNode);
+ }
+ }
+
+ private void OnMoved(NavigationContext context)
+ {
+ ClearActiveComparisonFocus();
+
+ if (context.IsAtEmptySlot) return;
+
+ _activeComparisonNode = context.CurrentNode;
+ _layout.SetComparisonFocus(context.CurrentNode);
+
+ MoveCarrierAndCamera(context);
+ }
+
+ private async UniTask OnReachedEmptySlotAsync(NavigationContext context)
+ {
+ _carrierView.SetState(NodeVisualState.CarrierReady);
+
+ Vector2 target = _layout.GetAnticipatedCarrierPosition(context);
+
+ await UniTask.WhenAll(
+ _carrierView.MoveToAsync(target, _config.CarrierMoveDuration, _config.CarrierMoveEase),
+ _cameraRig.MoveToAsync(target),
+ _layout.PreviewLayout(_navigator.Root, context.Depth)
+ );
+ }
+
+ private void OnInvalidAction(NavigationContext context, NavigationAction attempted)
+ {
+ if (_errorCountLabel != null)
+ _errorCountLabel.text = $"Errors: {_navigator.ErrorCount}";
+ }
+
+ private async UniTask OnNodeInsertedAsync(TreeNode node)
+ {
+ ClearActiveComparisonFocus();
+
+ _carrierView.SetState(NodeVisualState.Normal);
+ node.View = _carrierView;
+ _carrierView = null;
+
+ await UniTask.WhenAll(
+ _layout.RecalculateLayoutAsync(_navigator.Root),
+ _cameraRig.MoveToAsync(new Vector2(0f, _config.CarrierYOffset))
+ );
+ }
+
+ private async UniTask OnNavigationCompleteAsync()
+ {
+ await UniTask.Delay(_config.PostPlacementDelayMs);
+ BeginNextInsertion();
+ }
+
+ #endregion
+
+ #region Helpers
+
+ private void MoveCarrierAndCamera(NavigationContext context)
+ {
+ Vector2 pos = _layout.GetCarrierPosition(context);
+ _carrierView.MoveToAsync(pos, _config.CarrierMoveDuration, _config.CarrierMoveEase).Forget();
+ _cameraRig.MoveToAsync(pos).Forget();
+ }
+
+ private void ClearActiveComparisonFocus()
+ {
+ if (_activeComparisonNode != null)
+ {
+ _layout.ClearComparisonFocus(_activeComparisonNode);
+ _activeComparisonNode = null;
+ }
+ }
+
+ #endregion
+}
diff --git a/Assets/Integration/TreeRhythmController.cs.meta b/Assets/Integration/TreeRhythmController.cs.meta
new file mode 100644
index 0000000..d68e01c
--- /dev/null
+++ b/Assets/Integration/TreeRhythmController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0e36b33fed654fa4495553f5f9610203
\ No newline at end of file
diff --git a/Assets/Integration/TreeRhythmReactionPolicy.cs b/Assets/Integration/TreeRhythmReactionPolicy.cs
new file mode 100644
index 0000000..ff0935a
--- /dev/null
+++ b/Assets/Integration/TreeRhythmReactionPolicy.cs
@@ -0,0 +1,35 @@
+/**
+ * for the tree-rhythm game. Combo and score
+ * only increase when the player is both on-beat and chose the correct BST
+ * direction. An on-beat wrong-direction press incurs no penalty. An expired note (miss)
+ * breaks combo and drains HP.
+ *
+ */
+public class TreeRhythmReactionPolicy : IRhythmReactionPolicy
+{
+ #region IRhythmReactionPolicy
+
+ /** */
+ public void OnHit(HitResult result, bool gameActionSucceeded,
+ RhythmScoreTracker tracker, RhythmHealthSystem health)
+ {
+ if (!gameActionSucceeded) return;
+
+ tracker.RecordHit(result);
+ health.ApplyHit(result.Rating);
+ }
+
+ /** */
+ public void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health)
+ {
+ tracker.RecordMiss();
+ health.ApplyMiss();
+ }
+
+ /** */
+ public void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health)
+ {
+ }
+
+ #endregion
+}
diff --git a/Assets/Integration/TreeRhythmReactionPolicy.cs.meta b/Assets/Integration/TreeRhythmReactionPolicy.cs.meta
new file mode 100644
index 0000000..ca22e3e
--- /dev/null
+++ b/Assets/Integration/TreeRhythmReactionPolicy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e1ed1cdda88c0d9409d2d58889917165
\ No newline at end of file
diff --git a/Assets/Materials/Background.mat b/Assets/Materials/Background.mat
new file mode 100644
index 0000000..7f7e8d0
--- /dev/null
+++ b/Assets/Materials/Background.mat
@@ -0,0 +1,136 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-2578788880094833325
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: Background
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap:
+ RenderType: Opaque
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 0
+ - _DstBlendAlpha: 0
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0.555
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.104
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 0
+ - _WorkflowMode: 1
+ - _ZWrite: 1
+ m_Colors:
+ - _BaseColor: {r: 0, g: 0.0030281835, b: 0.03773582, a: 1}
+ - _Color: {r: 0, g: 0.0030281835, b: 0.03773582, a: 1}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/Materials/Background.mat.meta b/Assets/Materials/Background.mat.meta
new file mode 100644
index 0000000..bdd9e76
--- /dev/null
+++ b/Assets/Materials/Background.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9b6261c8e0b1e4ab89920f89d0bf8dc9
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Materials/NoteHitSpot.mat b/Assets/Materials/NoteHitSpot.mat
new file mode 100644
index 0000000..56781d3
--- /dev/null
+++ b/Assets/Materials/NoteHitSpot.mat
@@ -0,0 +1,136 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-2578788880094833325
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: NoteHitSpot
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap:
+ RenderType: Opaque
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 0
+ - _DstBlendAlpha: 0
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 0
+ - _WorkflowMode: 1
+ - _ZWrite: 1
+ m_Colors:
+ - _BaseColor: {r: 1, g: 1, b: 1, a: 1}
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/Materials/NoteHitSpot.mat.meta b/Assets/Materials/NoteHitSpot.mat.meta
new file mode 100644
index 0000000..4197ff1
--- /dev/null
+++ b/Assets/Materials/NoteHitSpot.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 792cca3ba846a4217aef4c35be2fd6a8
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Materials/RhythmNote.mat b/Assets/Materials/RhythmNote.mat
new file mode 100644
index 0000000..63c66ae
--- /dev/null
+++ b/Assets/Materials/RhythmNote.mat
@@ -0,0 +1,136 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &-2578788880094833325
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ version: 10
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: RhythmNote
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap:
+ RenderType: Opaque
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 0
+ - _DstBlendAlpha: 0
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 0
+ - _WorkflowMode: 1
+ - _ZWrite: 1
+ m_Colors:
+ - _BaseColor: {r: 1, g: 1, b: 1, a: 1}
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
diff --git a/Assets/Materials/RhythmNote.mat.meta b/Assets/Materials/RhythmNote.mat.meta
new file mode 100644
index 0000000..88b6ac5
--- /dev/null
+++ b/Assets/Materials/RhythmNote.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4229ef19f9bfb91498e0385e8c4e9f1a
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/Note.prefab b/Assets/Prefabs/Note.prefab
new file mode 100644
index 0000000..83e247b
--- /dev/null
+++ b/Assets/Prefabs/Note.prefab
@@ -0,0 +1,163 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &2132212214528617362
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6036712751960075183}
+ - component: {fileID: 3126917581800003242}
+ m_Layer: 0
+ m_Name: Note
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &6036712751960075183
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2132212214528617362}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 61309383839202681}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3126917581800003242
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2132212214528617362}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 913c914734a0019418f6d86bc7752916, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ _noteRenderer: {fileID: 7499671472802176758}
+ _holdTrail: {fileID: 0}
+ _hitParticles: {fileID: 0}
+ _hitPunchDuration: 0.12
+ _hitPunchScale: {x: 1.5, y: 1.5, z: 1}
+ _missFadeDuration: 0.2
+--- !u!1 &6088083392788960613
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 61309383839202681}
+ - component: {fileID: 8097035401195675295}
+ - component: {fileID: 7499671472802176758}
+ - component: {fileID: 3984210916691481064}
+ m_Layer: 0
+ m_Name: NodeBody
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &61309383839202681
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6088083392788960613}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068}
+ m_LocalPosition: {x: 0, y: 0, z: -0.5}
+ m_LocalScale: {x: 0.5, y: 0.05, z: 0.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 6036712751960075183}
+ m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
+--- !u!33 &8097035401195675295
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6088083392788960613}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &7499671472802176758
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6088083392788960613}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 4229ef19f9bfb91498e0385e8c4e9f1a, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &3984210916691481064
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6088083392788960613}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 0
+ serializedVersion: 2
+ m_Radius: 0.5000001
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0.000000059604645, y: 0, z: -0.00000008940697}
diff --git a/Assets/Prefabs/Note.prefab.meta b/Assets/Prefabs/Note.prefab.meta
new file mode 100644
index 0000000..fc3e107
--- /dev/null
+++ b/Assets/Prefabs/Note.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: a4a524929e253024fb536d59b8c205e0
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm.meta b/Assets/Rhythm.meta
new file mode 100644
index 0000000..3fb116e
--- /dev/null
+++ b/Assets/Rhythm.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2dcfa4265a8c1bd478b8e0339ca249a3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Core.meta b/Assets/Rhythm/Core.meta
new file mode 100644
index 0000000..aa0b701
--- /dev/null
+++ b/Assets/Rhythm/Core.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2b23d3bcb53bc2f45bacdf84e5a5697e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Core/BeatScheduler.cs b/Assets/Rhythm/Core/BeatScheduler.cs
new file mode 100644
index 0000000..b1f6c2a
--- /dev/null
+++ b/Assets/Rhythm/Core/BeatScheduler.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+
+/**
+ * Scans an ordered list of and fires
+ * when a note's spawn threshold is crossed.
+ *
+ */
+public class BeatScheduler
+{
+ #region Dependencies
+
+ /**
+ * How many beats before its target time a note is spawned.
+ *
+ */
+ public float NoteAppearanceBeats { get; set; }
+
+ #endregion
+
+ #region State
+
+ private IReadOnlyList _sortedNotes;
+ private int _nextScheduleIndex;
+ private RhythmManager _rhythmManager;
+
+ #endregion
+
+ #region Events
+
+ /**
+ * Fired when a note has reached its spawn threshold.
+ *
+ */
+ public event Action OnNoteScheduled;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a scheduler with the given look-ahead window.
+ *
+ *
+ * How many beats before its target time a note should be spawned.
+ * Sourced from .
+ *
+ */
+ public BeatScheduler(float noteAppearanceBeats)
+ {
+ NoteAppearanceBeats = noteAppearanceBeats;
+ }
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Loads a sorted note list and resets the schedule cursor to the beginning.
+ *
+ * Notes sorted ascending by beat.
+ * Used to convert beats to DSP arrival times.
+ */
+ public void LoadNotes(IReadOnlyList notes, RhythmManager rhythmManager)
+ {
+ _sortedNotes = notes;
+ _rhythmManager = rhythmManager;
+ _nextScheduleIndex = 0;
+ }
+
+ /**
+ * Moves the schedule cursor to the first note at or after the given beat.
+ *
+ * Beat position to seek to.
+ */
+ public void SeekTo(double beat)
+ {
+ _nextScheduleIndex = 0;
+ if (_sortedNotes == null) return;
+
+ double spawnThreshold = beat - NoteAppearanceBeats;
+ for (int i = 0; i < _sortedNotes.Count; i++)
+ {
+ if (_sortedNotes[i].Beat >= spawnThreshold)
+ {
+ _nextScheduleIndex = i;
+ return;
+ }
+ }
+ _nextScheduleIndex = _sortedNotes.Count;
+ }
+
+ /**
+ * Advances the scheduler. Fires for every
+ * note whose spawn threshold is greater than .
+ *
+ * Current fractional beat position.
+ * DSP time when the song began playing.
+ */
+ public void Tick(double currentBeat, double songStartDsp)
+ {
+ if (_sortedNotes == null || _rhythmManager == null) return;
+
+ while (_nextScheduleIndex < _sortedNotes.Count)
+ {
+ NoteData note = _sortedNotes[_nextScheduleIndex];
+ double spawnBeat = note.Beat - NoteAppearanceBeats;
+
+ if (currentBeat < spawnBeat) break;
+
+ float rate = _rhythmManager.PlaybackRate;
+ double spawnDsp = songStartDsp + _rhythmManager.BeatToSeconds(spawnBeat) / rate;
+ double hitDsp = songStartDsp + _rhythmManager.BeatToSeconds(note.Beat) / rate;
+ double holdEndDsp = note.NoteType == NoteType.Hold
+ ? songStartDsp + _rhythmManager.BeatToSeconds(note.Beat + note.HoldDuration) / rate
+ : 0.0;
+
+ OnNoteScheduled?.Invoke(new ScheduledNoteInfo(note, spawnDsp, hitDsp, holdEndDsp));
+ _nextScheduleIndex++;
+ }
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Core/BeatScheduler.cs.meta b/Assets/Rhythm/Core/BeatScheduler.cs.meta
new file mode 100644
index 0000000..bff48a9
--- /dev/null
+++ b/Assets/Rhythm/Core/BeatScheduler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 08c31d875ad558e42968b8f4cf6b00a5
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs
new file mode 100644
index 0000000..69053e1
--- /dev/null
+++ b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs
@@ -0,0 +1,30 @@
+/**
+ * Default for standalone rhythm games.
+ * Increments combo on any on-beat hit regardless of game action outcome,
+ * breaks combo on miss, and ignores off-beat inputs.
+ *
+ */
+public class DefaultRhythmReactionPolicy : IRhythmReactionPolicy
+{
+ #region IRhythmReactionPolicy
+
+ /** */
+ public void OnHit(HitResult result, bool gameActionSucceeded,
+ RhythmScoreTracker tracker, RhythmHealthSystem health)
+ {
+ tracker.RecordHit(result);
+ health.ApplyHit(result.Rating);
+ }
+
+ /** */
+ public void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health)
+ {
+ tracker.RecordMiss();
+ health.ApplyMiss();
+ }
+
+ /** */
+ public void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health) { }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta
new file mode 100644
index 0000000..56f434b
--- /dev/null
+++ b/Assets/Rhythm/Core/DefaultRhythmReactionPolicy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e2a9072b8c860ea499ee9d63d461bb3c
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/IRhythmReactionPolicy.cs b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs
new file mode 100644
index 0000000..f00e5b2
--- /dev/null
+++ b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs
@@ -0,0 +1,45 @@
+/**
+ * Defines the consequences of rhythm events on score and health. Implement
+ * this interface to customise what happens when a note is hit, missed, or
+ * when the player inputs off-beat. Swap implementations at runtime via
+ * .
+ *
+ */
+public interface IRhythmReactionPolicy
+{
+ /**
+ * Called after a note window was consumed (on-beat input) and after the
+ * downstream game system has reported whether the game action succeeded.
+ *
+ * Timing evaluation of the hit.
+ *
+ * True when the game action that consumed this beat window produced the
+ * intended outcome (e.g. correct BST direction). False for valid timing
+ * but incorrect game action.
+ *
+ * Score tracker to update.
+ * Health system to update.
+ */
+ void OnHit(HitResult result, bool gameActionSucceeded,
+ RhythmScoreTracker tracker, RhythmHealthSystem health);
+
+ /**
+ * Called when a note passes the Ok window boundary without any input
+ * consuming it..
+ *
+ * The note that was missed.
+ * Score tracker to update.
+ * Health system to update.
+ */
+ void OnMiss(NoteData note, RhythmScoreTracker tracker, RhythmHealthSystem health);
+
+ /**
+ * Called when the player inputs on a lane that has no open note window.
+ * Implementations may choose to penalise or ignore off-beat inputs.
+ *
+ * Lane the input was registered on.
+ * Score tracker to update.
+ * Health system to update.
+ */
+ void OnOffBeatInput(int lane, RhythmScoreTracker tracker, RhythmHealthSystem health);
+}
diff --git a/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta
new file mode 100644
index 0000000..21de8bc
--- /dev/null
+++ b/Assets/Rhythm/Core/IRhythmReactionPolicy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 95fc8ae4da82610498a5b99fa3e312aa
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/RhythmHealthSystem.cs b/Assets/Rhythm/Core/RhythmHealthSystem.cs
new file mode 100644
index 0000000..12b24ad
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmHealthSystem.cs
@@ -0,0 +1,114 @@
+using System;
+using UnityEngine;
+
+/**
+ * Optional health-point system for a rhythm session. All methods are no-ops
+ * when is false, so disabling
+ * health requires zero changes at the call site.
+ * One instance per player for multiplayer support.
+ *
+ */
+public class RhythmHealthSystem
+{
+ #region Dependencies
+
+ private readonly HitWindowConfig _config;
+
+ #endregion
+
+ #region Properties
+
+ /** Current HP value clamped to [0, MaxHp]. */
+ public float CurrentHp { get; private set; }
+
+ /** Maximum HP value from configuration. */
+ public float MaxHp => _config.MaxHp;
+
+ /** True when CurrentHp has reached 0 and health is enabled. */
+ public bool IsDead => _config.HealthEnabled && CurrentHp <= 0f;
+
+ /** HP as a normalised fraction in [0, 1]. */
+ public float HpPercent => _config.MaxHp > 0f ? CurrentHp / _config.MaxHp : 1f;
+
+ #endregion
+
+ #region Events
+
+ /**
+ * Fired when HP reaches 0 for the first time in a session.
+ * Not re-fired on subsequent misses once dead.
+ *
+ */
+ public event Action OnDeath;
+
+ /** Fired whenever HP changes. Argument is the new HP value. */
+ public event Action OnHpChanged;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a health system. HP starts at MaxHp.
+ *
+ * Configuration providing health parameters.
+ */
+ public RhythmHealthSystem(HitWindowConfig config)
+ {
+ _config = config;
+ CurrentHp = config.HealthEnabled ? config.MaxHp : 0f;
+ }
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Applies the HP delta for the given . No-op when
+ * health is disabled.
+ *
+ * Hit rating that determines the HP change.
+ */
+ public void ApplyHit(HitRating rating)
+ {
+ if (!_config.HealthEnabled) return;
+ ChangeHp(_config.GetHpDelta(rating));
+ }
+
+ /**
+ * Applies the miss HP drain. No-op when health is disabled.
+ *
+ */
+ public void ApplyMiss()
+ {
+ if (!_config.HealthEnabled) return;
+ ChangeHp(-_config.HpDrainMiss);
+ }
+
+ /**
+ * Resets HP to MaxHp. No-op when health is disabled.
+ *
+ */
+ public void Reset()
+ {
+ if (!_config.HealthEnabled) return;
+ CurrentHp = _config.MaxHp;
+ OnHpChanged?.Invoke(CurrentHp);
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private void ChangeHp(float delta)
+ {
+ bool wasDead = IsDead;
+ CurrentHp = Mathf.Clamp(CurrentHp + delta, 0f, _config.MaxHp);
+ OnHpChanged?.Invoke(CurrentHp);
+
+ if (!wasDead && IsDead)
+ OnDeath?.Invoke();
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta b/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta
new file mode 100644
index 0000000..7654f20
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmHealthSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 70b482f0b69c9ba4a8bc36cc3a658503
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/RhythmManager.cs b/Assets/Rhythm/Core/RhythmManager.cs
new file mode 100644
index 0000000..d957147
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmManager.cs
@@ -0,0 +1,595 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+#region Enums
+
+/**
+ * Current playback state of the rhythm engine.
+ *
+ */
+public enum PlaybackState { Stopped, Playing, Paused }
+
+#endregion
+
+#region Active Hold
+
+/**
+ * Internal tracking record for a hold note currently being held by the player.
+ *
+ */
+internal struct ActiveHold
+{
+ public ScheduledNoteInfo Info;
+ public bool IsReleased;
+}
+
+#endregion
+
+#region Rhythm Manager
+
+/**
+ * Central rhythm engine. Tracks DSP-based song time, drives
+ * on each frame, evaluates hit timing windows,
+ * and dispatches all rhythm events.
+ *
+ */
+public class RhythmManager
+{
+ #region Dependencies
+
+ private readonly AudioSource _audioSource;
+ private readonly HitWindowConfig _hitWindows;
+ private readonly RhythmCalibrationConfig _calibration;
+ private readonly BeatScheduler _scheduler;
+
+ #endregion
+
+ #region State
+
+ private BeatmapData _beatmap;
+ private IReadOnlyList _effectiveTimingPoints;
+ private double[] _segmentBoundarySecs;
+
+ private double _songStartDsp;
+ private double _accumulatedPauseSec;
+ private double _pauseStartDsp;
+
+ private float _playbackRate = 1f;
+ private PlaybackState _state;
+ private bool _autoplay;
+
+ private double _lastTickBeat;
+ private int _nextBeatPulseIndex;
+ private int _nextKeyPointIndex;
+
+ private readonly List _activeNotes = new List();
+ private readonly Dictionary _activeHolds = new Dictionary();
+
+ private int _autoplayNoteIndex;
+
+ #endregion
+
+ #region Public Properties
+
+ /** Current playback state. */
+ public PlaybackState State => _state;
+
+ /** Seconds elapsed since song start, accounting for pauses. */
+ public double SongElapsedSeconds
+ {
+ get
+ {
+ if (_state != PlaybackState.Playing) return 0.0;
+ return (AudioSettings.dspTime - _songStartDsp - _accumulatedPauseSec) * _playbackRate;
+ }
+ }
+
+ /** Current fractional beat position. */
+ public double CurrentBeat =>
+ _beatmap != null ? SecondsToBeat(SongElapsedSeconds) : 0.0;
+
+ /** Per-player score tracker. */
+ public RhythmScoreTracker ScoreTracker { get; private set; }
+
+ /** Per-player health system. */
+ public RhythmHealthSystem HealthSystem { get; private set; }
+
+ /**
+ * How many beats ahead notes are spawned. Delegates to the internal
+ * so changes take effect on the next tick
+ * without restarting playback. Update this each frame from a live config
+ * reference to make runtime inspector or slider changes propagate instantly.
+ *
+ */
+ public float NoteAppearanceBeats
+ {
+ get => _scheduler.NoteAppearanceBeats;
+ set => _scheduler.NoteAppearanceBeats = value;
+ }
+
+ /**
+ * Playback speed multiplier. 1 = normal, 0.5 = half speed, 2 = double speed.
+ * Sets so the audio and beat timing remain in
+ * sync. When changed while playing, automatically seeks to the current beat
+ * so all in-flight notes are rescheduled with the correct DSP times for the
+ * new rate.
+ *
+ */
+ public float PlaybackRate
+ {
+ get => _playbackRate;
+ set
+ {
+ value = Mathf.Max(0.1f, value);
+ if (Mathf.Approximately(_playbackRate, value)) return;
+
+ double beatSnapshot = CurrentBeat;
+ _playbackRate = value;
+ _audioSource.pitch = _playbackRate;
+
+ if (_state == PlaybackState.Playing)
+ Seek(beatSnapshot);
+ }
+ }
+
+ #endregion
+
+ #region Events
+
+ /**
+ * Fired each time a whole-beat boundary is crossed. Argument is the beat
+ * index (0, 1, 2, …) that was just entered.
+ *
+ */
+ public event Action OnBeatPulse;
+
+ /**
+ * Fired when a note crosses its spawn threshold.
+ *
+ */
+ public event Action OnNoteScheduled;
+
+ /**
+ * Fired when a note expires past the Ok window without being hit.
+ *
+ */
+ public event Action OnNoteMissed;
+
+ /**
+ * Fired immediately after successfully consumes
+ * a note. The reaction policy has not yet been called at this point; the
+ * adapter is expected to call it after determining game action outcome.
+ *
+ */
+ public event Action OnNoteHit;
+
+ /**
+ * Fired when a key point beat is crossed.
+ *
+ */
+ public event Action OnKeyPointReached;
+
+ /**
+ * Fired when the song audio reaches its end (last note beat + buffer).
+ *
+ */
+ public event Action OnSongComplete;
+
+ /**
+ * Fired whenever transitions.
+ *
+ */
+ public event Action OnPlaybackStateChanged;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a fully wired rhythm manager ready for beatmap loading.
+ *
+ * AudioSource used for DSP-scheduled playback.
+ * Timing window and scoring configuration.
+ * Per-device latency calibration offsets.
+ * Policy defining score/health consequences.
+ * How far ahead notes are spawned (beats).
+ */
+ public RhythmManager(AudioSource audioSource, HitWindowConfig hitWindows,
+ RhythmCalibrationConfig calibration, IRhythmReactionPolicy reactionPolicy,
+ float noteAppearanceBeats = 2f)
+ {
+ _audioSource = audioSource;
+ _hitWindows = hitWindows;
+ _calibration = calibration;
+ _reactionPolicy = reactionPolicy;
+ _scheduler = new BeatScheduler(noteAppearanceBeats);
+ _scheduler.OnNoteScheduled += OnSchedulerNoteScheduled;
+ }
+
+ #endregion
+
+ #region Playback Control
+
+ /**
+ * Loads a beatmap and prepares timing tables. Does not start playback.
+ *
+ * The beatmap to load.
+ */
+ public void LoadBeatmap(BeatmapData beatmap)
+ {
+ _beatmap = beatmap;
+ _effectiveTimingPoints = BuildEffectiveTimingPoints(beatmap);
+ _segmentBoundarySecs = RhythmTimingMath.PrecomputeSegmentBoundaries(
+ _effectiveTimingPoints, beatmap.OffsetSeconds);
+
+ int totalNotes = beatmap.Notes?.Count ?? 0;
+ ScoreTracker = new RhythmScoreTracker(_hitWindows, totalNotes);
+ HealthSystem = new RhythmHealthSystem(_hitWindows);
+
+ _activeNotes.Clear();
+ _activeHolds.Clear();
+ _nextBeatPulseIndex = 0;
+ _nextKeyPointIndex = 0;
+ _autoplayNoteIndex = 0;
+ _lastTickBeat = 0.0;
+
+ List sorted = new List(beatmap.Notes ?? new List());
+ sorted.Sort((a, b) => a.Beat.CompareTo(b.Beat));
+ _scheduler.LoadNotes(sorted, this);
+ }
+
+ /**
+ * Starts playback from the beginning. Schedules audio via DSP for
+ * sample-accurate synchronisation.
+ *
+ */
+ public void Play()
+ {
+ float delay = _beatmap != null ? _beatmap.StartDelaySeconds : 0f;
+ float buffer = _calibration != null ? _calibration.DspScheduleBufferSeconds : 0.1f;
+ Play(AudioSettings.dspTime + buffer + delay);
+ }
+
+ /**
+ * Starts playback at an explicit DSP time (for synchronizing players).
+ *
+ * DSP time at which playback begins.
+ */
+ public void Play(double scheduledDspTime)
+ {
+ if (_beatmap == null) return;
+ _audioSource.clip = _beatmap.SongClip;
+ _audioSource.PlayScheduled(scheduledDspTime);
+ _songStartDsp = scheduledDspTime;
+ _accumulatedPauseSec = 0.0;
+ SetState(PlaybackState.Playing);
+ }
+
+ /**
+ * Pauses playback and preserves elapsed DSP time.
+ *
+ */
+ public void Pause()
+ {
+ if (_state != PlaybackState.Playing) return;
+ _audioSource.Pause();
+ _pauseStartDsp = AudioSettings.dspTime;
+ SetState(PlaybackState.Paused);
+ }
+
+ /**
+ * Resumes from a paused state.
+ *
+ */
+ public void Resume()
+ {
+ if (_state != PlaybackState.Paused) return;
+ _accumulatedPauseSec += AudioSettings.dspTime - _pauseStartDsp;
+ _audioSource.UnPause();
+ SetState(PlaybackState.Playing);
+ }
+
+ /**
+ * Stops playback and resets all runtime state. LoadBeatmap must be called
+ * again before the next Play.
+ *
+ */
+ public void Stop()
+ {
+ _audioSource.Stop();
+ _activeNotes.Clear();
+ _activeHolds.Clear();
+ SetState(PlaybackState.Stopped);
+ }
+
+ /**
+ * Seeks to a beat position and restarts audio from the corresponding time.
+ *
+ * Beat position to seek to.
+ */
+ public void Seek(double targetBeat)
+ {
+ if (_beatmap == null) return;
+
+ double targetSec = BeatToSeconds(targetBeat);
+ _audioSource.time = (float)targetSec;
+
+ double dspNow = AudioSettings.dspTime;
+ _songStartDsp = dspNow - targetSec / _playbackRate;
+ _accumulatedPauseSec = 0.0;
+ _lastTickBeat = targetBeat;
+
+ _nextBeatPulseIndex = Mathf.FloorToInt((float)targetBeat);
+ _nextKeyPointIndex = FindKeyPointIndexAtBeat(targetBeat);
+ _autoplayNoteIndex = 0;
+
+ _activeNotes.Clear();
+ _scheduler.SeekTo(targetBeat);
+ }
+
+ /**
+ * Enables or disables autoplay mode. In autoplay, notes are consumed
+ * at the Perfect window center automatically without player input.
+ *
+ * True to enable autoplay.
+ */
+ public void SetAutoplay(bool enabled)
+ {
+ _autoplay = enabled;
+ }
+
+ /**
+ * Replaces the active reaction policy at runtime. Useful for switching
+ * between game modes (e.g. practice vs. ranked).
+ *
+ * New policy to use.
+ */
+ public void SetReactionPolicy(IRhythmReactionPolicy policy)
+ {
+ _reactionPolicy = policy;
+ }
+
+ #endregion
+
+ #region Tick
+
+ /**
+ * Advances all rhythm state by one frame. Must be called from a
+ * MonoBehaviour.Update each frame while playing.
+ *
+ */
+ public void Tick()
+ {
+ if (_state != PlaybackState.Playing) return;
+
+ double currentBeat = CurrentBeat;
+ double dspNow = AudioSettings.dspTime;
+
+ _scheduler.Tick(currentBeat, _songStartDsp);
+
+ CheckAndFireBeatPulses(currentBeat);
+ CheckAndFireKeyPoints(currentBeat);
+ CheckForMissedNotes(dspNow);
+
+ if (_autoplay)
+ RunAutoplay(dspNow);
+
+ if (_beatmap != null && _beatmap.SongClip != null)
+ {
+ if (SongElapsedSeconds >= _beatmap.SongClip.length && _state == PlaybackState.Playing)
+ {
+ SetState(PlaybackState.Stopped);
+ OnSongComplete?.Invoke();
+ }
+ }
+
+ _lastTickBeat = currentBeat;
+ }
+
+ #endregion
+
+ #region Hit Evaluation
+
+ /**
+ * Attempts to consume the nearest hittable note in the given lane.
+ * Returns a with IsHit = false when no
+ * note is within the Ok window (off-beat input). The reaction policy is
+ * NOT called here — adapters call it after determining game action outcome.
+ *
+ * Zero-based lane index from the input provider.
+ * Hit evaluation result.
+ */
+ public HitResult TryConsumeHit(int lane, HitRating? ratingOverride = null)
+ {
+ double dspNow = AudioSettings.dspTime;
+ float calibOffset = _calibration != null ? _calibration.CalibrationOffsetSeconds : 0f;
+
+ int bestIndex = -1;
+ float bestError = float.MaxValue;
+
+ for (int i = 0; i < _activeNotes.Count; i++)
+ {
+ if (_activeNotes[i].Note.Lane != lane) continue;
+
+ float error = (float)(dspNow - _activeNotes[i].HitDspTime + calibOffset);
+ float absError = Mathf.Abs(error);
+
+ if (absError > _hitWindows.OkWindow) continue;
+ if (absError < bestError)
+ {
+ bestError = absError;
+ bestIndex = i;
+ }
+ }
+
+ if (bestIndex < 0)
+ {
+ _reactionPolicy?.OnOffBeatInput(lane, ScoreTracker, HealthSystem);
+ return HitResult.Miss(lane);
+ }
+
+ ScheduledNoteInfo consumed = _activeNotes[bestIndex];
+ _activeNotes.RemoveAt(bestIndex);
+
+ float rawError = (float)(dspNow - consumed.HitDspTime + calibOffset);
+ HitRating rating = ratingOverride ?? _hitWindows.EvaluateWindow(Mathf.Abs(rawError));
+ bool isEarly = rawError < -_hitWindows.PerfectWindow;
+ bool isLate = rawError > _hitWindows.PerfectWindow;
+
+ var result = new HitResult(true, rating, rawError, isEarly, isLate,
+ lane, consumed.Note);
+ OnNoteHit?.Invoke(result);
+
+ if (consumed.Note.NoteType == NoteType.Hold)
+ _activeHolds[lane] = new ActiveHold { Info = consumed, IsReleased = false };
+
+ return result;
+ }
+
+ /**
+ * Signals that the player released a hold note input on the given lane.
+ * Evaluates whether the hold was completed or broken early.
+ *
+ * Lane on which the key was released.
+ */
+ public void NotifyHoldRelease(int lane)
+ {
+ if (!_activeHolds.TryGetValue(lane, out ActiveHold hold)) return;
+
+ double dspNow = AudioSettings.dspTime;
+ bool completed = dspNow >= hold.Info.HoldEndDspTime - _hitWindows.OkWindow;
+
+ if (!completed)
+ {
+ _reactionPolicy?.OnMiss(hold.Info.Note, ScoreTracker, HealthSystem);
+ OnNoteMissed?.Invoke(hold.Info.Note);
+ }
+
+ _activeHolds.Remove(lane);
+ }
+
+ #endregion
+
+ #region Beat / Time Conversion
+
+ /**
+ * Converts a fractional beat position to seconds from song start.
+ *
+ * Beat position to convert.
+ * Seconds from song start.
+ */
+ public double BeatToSeconds(double beat) =>
+ RhythmTimingMath.BeatToSeconds(beat, _effectiveTimingPoints, _beatmap.OffsetSeconds);
+
+ /**
+ * Converts seconds from song start back to a fractional beat position.
+ *
+ * Seconds to convert.
+ * Fractional beat position.
+ */
+ public double SecondsToBeat(double seconds) =>
+ RhythmTimingMath.SecondsToBeat(seconds, _effectiveTimingPoints,
+ _beatmap.OffsetSeconds, _segmentBoundarySecs);
+
+ #endregion
+
+ #region Private Helpers
+
+ private IRhythmReactionPolicy _reactionPolicy;
+
+ private void SetState(PlaybackState newState)
+ {
+ _state = newState;
+ OnPlaybackStateChanged?.Invoke(newState);
+ }
+
+ private void OnSchedulerNoteScheduled(ScheduledNoteInfo info)
+ {
+ _activeNotes.Add(info);
+ OnNoteScheduled?.Invoke(info);
+ }
+
+ private void CheckAndFireBeatPulses(double currentBeat)
+ {
+ while (_nextBeatPulseIndex <= (int)currentBeat)
+ {
+ OnBeatPulse?.Invoke(_nextBeatPulseIndex);
+ _nextBeatPulseIndex++;
+ }
+ }
+
+ private void CheckAndFireKeyPoints(double currentBeat)
+ {
+ if (_beatmap?.KeyPoints == null) return;
+ while (_nextKeyPointIndex < _beatmap.KeyPoints.Count &&
+ _beatmap.KeyPoints[_nextKeyPointIndex].Beat <= currentBeat)
+ {
+ OnKeyPointReached?.Invoke(_beatmap.KeyPoints[_nextKeyPointIndex]);
+ _nextKeyPointIndex++;
+ }
+ }
+
+ private void CheckForMissedNotes(double dspNow)
+ {
+ if (_hitWindows == null) return;
+
+ float calibOffset = _calibration != null ? _calibration.CalibrationOffsetSeconds : 0f;
+
+ for (int i = _activeNotes.Count - 1; i >= 0; i--)
+ {
+ float error = (float)(dspNow - _activeNotes[i].HitDspTime + calibOffset);
+ if (error > _hitWindows.OkWindow)
+ {
+ NoteData missed = _activeNotes[i].Note;
+ _activeNotes.RemoveAt(i);
+ _reactionPolicy?.OnMiss(missed, ScoreTracker, HealthSystem);
+ OnNoteMissed?.Invoke(missed);
+ }
+ }
+ }
+
+ private void RunAutoplay(double dspNow)
+ {
+ if (_beatmap?.Notes == null) return;
+ for (int i = 0; i < _activeNotes.Count; i++)
+ {
+ double hitDsp = _activeNotes[i].HitDspTime;
+ if (dspNow >= hitDsp)
+ {
+ int lane = _activeNotes[i].Note.Lane;
+ HitResult result = TryConsumeHit(lane);
+ if (result.IsHit)
+ _reactionPolicy?.OnHit(result, true, ScoreTracker, HealthSystem);
+ break;
+ }
+ }
+ }
+
+ private int FindKeyPointIndexAtBeat(double beat)
+ {
+ if (_beatmap?.KeyPoints == null) return 0;
+ for (int i = 0; i < _beatmap.KeyPoints.Count; i++)
+ {
+ if (_beatmap.KeyPoints[i].Beat >= beat) return i;
+ }
+ return _beatmap.KeyPoints.Count;
+ }
+
+ private static IReadOnlyList BuildEffectiveTimingPoints(BeatmapData beatmap)
+ {
+ var list = new List();
+ if (beatmap.TimingPoints == null || beatmap.TimingPoints.Count == 0 ||
+ beatmap.TimingPoints[0].Beat > 0.0)
+ {
+ list.Add(new TimingPoint { Beat = 0.0, Bpm = beatmap.BaseBpm });
+ }
+ if (beatmap.TimingPoints != null)
+ list.AddRange(beatmap.TimingPoints);
+ return list;
+ }
+
+ #endregion
+}
+
+#endregion
diff --git a/Assets/Rhythm/Core/RhythmManager.cs.meta b/Assets/Rhythm/Core/RhythmManager.cs.meta
new file mode 100644
index 0000000..0b54950
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 884b33d131820a74e89cc4c5980c9e2a
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/RhythmScoreTracker.cs b/Assets/Rhythm/Core/RhythmScoreTracker.cs
new file mode 100644
index 0000000..5025881
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmScoreTracker.cs
@@ -0,0 +1,192 @@
+using System;
+using UnityEngine;
+
+#region Enums
+
+/**
+ * Letter grade awarded at the end of a song based on accuracy.
+ * Thresholds are configured in .
+ *
+ */
+public enum Grade { SS, S, A, B, C, D, F }
+
+#endregion
+
+#region Score Tracker
+
+/**
+ * Tracks score, combo, accuracy, and grade for a single player during a
+ * rhythm session.
+ *
+ */
+public class RhythmScoreTracker
+{
+ #region Dependencies
+
+ private readonly HitWindowConfig _config;
+
+ #endregion
+
+ #region Score State
+
+ /** Number of Perfect hits recorded. */
+ public int PerfectCount { get; private set; }
+
+ /** Number of Good hits recorded. */
+ public int GoodCount { get; private set; }
+
+ /** Number of Ok hits recorded. */
+ public int OkCount { get; private set; }
+
+ /** Number of Misses recorded (expired notes or off-beat inputs that break combo). */
+ public int MissCount { get; private set; }
+
+ /** Current consecutive hit streak. */
+ public int Combo { get; private set; }
+
+ /** Highest combo reached in this session. */
+ public int MaxCombo { get; private set; }
+
+ /** Total accumulated score including multiplier. */
+ public long TotalScore { get; private set; }
+
+ /** Accuracy as a value from 0 to 1 based on weighted hit counts. */
+ public float Accuracy { get; private set; }
+
+ /** Current letter grade derived from . */
+ public Grade CurrentGrade { get; private set; }
+
+ /** Total notes in the beatmap. Used for accuracy denominator. */
+ public int TotalNotes { get; private set; }
+
+ #endregion
+
+ #region Events
+
+ /** Fired after every or call. */
+ public event Action OnScoreUpdated;
+
+ /** Fired when a miss breaks the current combo. */
+ public event Action OnComboBreak;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a score tracker configured for the given beatmap and windows.
+ *
+ * Scoring configuration.
+ * Total note count in the beatmap (for accuracy denominator).
+ */
+ public RhythmScoreTracker(HitWindowConfig config, int totalNotes)
+ {
+ _config = config;
+ TotalNotes = totalNotes;
+ CurrentGrade = Grade.F;
+ }
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Records a hit result and updates all derived statistics.
+ *
+ * The evaluated hit result.
+ */
+ public void RecordHit(HitResult result)
+ {
+ switch (result.Rating)
+ {
+ case HitRating.Perfect: PerfectCount++; break;
+ case HitRating.Good: GoodCount++; break;
+ case HitRating.Ok: OkCount++; break;
+ }
+
+ Combo++;
+ if (Combo > MaxCombo) MaxCombo = Combo;
+
+ float multiplier = CalculateMultiplier();
+ TotalScore += (long)(_config.GetBaseScore(result.Rating) * multiplier);
+
+ RecalculateAccuracy();
+ RecalculateGrade();
+ OnScoreUpdated?.Invoke(result);
+ }
+
+ /**
+ * Records a miss (note expired or combo-breaking input) and breaks the combo.
+ *
+ */
+ public void RecordMiss()
+ {
+ MissCount++;
+ bool hadCombo = Combo > 0;
+ Combo = 0;
+
+ RecalculateAccuracy();
+ RecalculateGrade();
+
+ if (hadCombo)
+ OnComboBreak?.Invoke();
+
+ OnScoreUpdated?.Invoke(HitResult.Miss(0));
+ }
+
+ /**
+ * Resets all statistics to their initial state.
+ *
+ */
+ public void Reset()
+ {
+ PerfectCount = GoodCount = OkCount = MissCount = 0;
+ Combo = MaxCombo = 0;
+ TotalScore = 0;
+ Accuracy = 0f;
+ CurrentGrade = Grade.F;
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private float CalculateMultiplier()
+ {
+ int steps = Combo / Mathf.Max(1, _config.ComboStepSize);
+ float multiplier = _config.BaseMultiplier + steps * 0.5f;
+ return Mathf.Min(multiplier, _config.MaxMultiplier);
+ }
+
+ private void RecalculateAccuracy()
+ {
+ int judged = PerfectCount + GoodCount + OkCount + MissCount;
+ if (judged == 0 || TotalNotes == 0)
+ {
+ Accuracy = 0f;
+ return;
+ }
+
+ float weightedSum = PerfectCount * _config.AccuracyWeightPerfect
+ + GoodCount * _config.AccuracyWeightGood
+ + OkCount * _config.AccuracyWeightOk
+ + MissCount * _config.AccuracyWeightMiss;
+
+ Accuracy = weightedSum / TotalNotes;
+ }
+
+ private void RecalculateGrade()
+ {
+ if (Accuracy >= _config.GradeSSThreshold) CurrentGrade = Grade.SS;
+ else if (Accuracy >= _config.GradeSThreshold) CurrentGrade = Grade.S;
+ else if (Accuracy >= _config.GradeAThreshold) CurrentGrade = Grade.A;
+ else if (Accuracy >= _config.GradeBThreshold) CurrentGrade = Grade.B;
+ else if (Accuracy >= _config.GradeCThreshold) CurrentGrade = Grade.C;
+ else if (Accuracy >= _config.GradeDThreshold) CurrentGrade = Grade.D;
+ else CurrentGrade = Grade.F;
+ }
+
+ #endregion
+}
+
+#endregion
diff --git a/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta b/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta
new file mode 100644
index 0000000..5ee2314
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmScoreTracker.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3f81dd90ca949be4e854dcd4bc3e9014
\ No newline at end of file
diff --git a/Assets/Rhythm/Core/RhythmTimingMath.cs b/Assets/Rhythm/Core/RhythmTimingMath.cs
new file mode 100644
index 0000000..de4d75c
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmTimingMath.cs
@@ -0,0 +1,124 @@
+using System.Collections.Generic;
+
+/**
+ * Static utility class providing beat-to-seconds and seconds-to-beat
+ * conversions that account for multiple BPM sections.
+ *
+ */
+public static class RhythmTimingMath
+{
+ #region Public API
+
+ /**
+ * Converts a fractional beat position to seconds elapsed from song start.
+ * Accumulates time across BPM segments defined by .
+ * OffsetSeconds is added once to the final result.
+ *
+ *
+ * Segment i spans [T[i].Beat, T[i+1].Beat).
+ * seconds += (min(queryBeat, T[i+1].Beat) − T[i].Beat) × (60.0 / T[i].Bpm)
+ * The last segment has no upper bound so T[n+1].Beat = queryBeat.
+ * OffsetSeconds shifts the entire timeline.
+ *
+ * Fractional beat position to convert.
+ * BPM sections sorted ascending by Beat.
+ * Must have at least one entry at Beat = 0.
+ * Silence/padding before beat 0.
+ * Seconds from song start corresponding to the beat.
+ */
+ public static double BeatToSeconds(double beat,
+ IReadOnlyList timingPoints, float offsetSeconds)
+ {
+ double seconds = offsetSeconds;
+
+ /* Beats before the first timing point extrapolate backward using that
+ point's BPM — this gives notes with spawn-beats < 0 a non-zero travel
+ duration instead of clamping them to offsetSeconds. */
+ if (beat < timingPoints[0].Beat)
+ {
+ seconds += (beat - timingPoints[0].Beat) * (60.0 / timingPoints[0].Bpm);
+ return seconds;
+ }
+
+ for (int i = 0; i < timingPoints.Count; i++)
+ {
+ double segStart = timingPoints[i].Beat;
+ if (beat <= segStart) break;
+
+ double segEnd = (i + 1 < timingPoints.Count)
+ ? timingPoints[i + 1].Beat
+ : beat;
+
+ double clampedEnd = (beat < segEnd) ? beat : segEnd;
+ seconds += (clampedEnd - segStart) * (60.0 / timingPoints[i].Bpm);
+ }
+ return seconds;
+ }
+
+ /**
+ * Converts seconds elapsed from song start back to a fractional beat position.
+ * When is provided each call is O(1);
+ * otherwise segments are traversed linearly.
+ *
+ * Seconds from song start to convert.
+ * BPM sections sorted ascending by Beat.
+ * Must have at least one entry at Beat = 0.
+ * Same value passed to BeatToSeconds.
+ * Precomputed boundary seconds from
+ * . Pass null to compute on the fly.
+ * Fractional beat position corresponding to the seconds.
+ */
+ public static double SecondsToBeat(double seconds,
+ IReadOnlyList timingPoints, float offsetSeconds,
+ IReadOnlyList segmentBoundarySecs = null)
+ {
+ double adjusted = seconds - offsetSeconds;
+
+ for (int i = 0; i < timingPoints.Count; i++)
+ {
+ double segStartSec = segmentBoundarySecs != null
+ ? segmentBoundarySecs[i] - offsetSeconds
+ : BeatToSeconds(timingPoints[i].Beat, timingPoints, 0f);
+
+ double segEndSec = (i + 1 < timingPoints.Count)
+ ? (segmentBoundarySecs != null
+ ? segmentBoundarySecs[i + 1] - offsetSeconds
+ : BeatToSeconds(timingPoints[i + 1].Beat, timingPoints, 0f))
+ : double.PositiveInfinity;
+
+ if (adjusted <= segEndSec)
+ {
+ double excessSec = adjusted - segStartSec;
+ return timingPoints[i].Beat + excessSec / (60.0 / timingPoints[i].Bpm);
+ }
+ }
+
+ double lastSegStartSec = segmentBoundarySecs != null
+ ? segmentBoundarySecs[timingPoints.Count - 1] - offsetSeconds
+ : BeatToSeconds(timingPoints[timingPoints.Count - 1].Beat, timingPoints, 0f);
+
+ return timingPoints[timingPoints.Count - 1].Beat
+ + (adjusted - lastSegStartSec) / (60.0 / timingPoints[timingPoints.Count - 1].Bpm);
+ }
+
+ /**
+ * Precomputes the seconds value at each boundary.
+ * Store the returned array and pass it to to
+ * make that conversion O(1) at runtime.
+ *
+ * BPM sections sorted ascending by Beat.
+ * Same offset passed to BeatToSeconds.
+ * Array of length timingPoints.Count where index i is the seconds
+ * value at timingPoints[i].Beat.
+ */
+ public static double[] PrecomputeSegmentBoundaries(
+ IReadOnlyList timingPoints, float offsetSeconds)
+ {
+ var boundaries = new double[timingPoints.Count];
+ for (int i = 0; i < timingPoints.Count; i++)
+ boundaries[i] = BeatToSeconds(timingPoints[i].Beat, timingPoints, offsetSeconds);
+ return boundaries;
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Core/RhythmTimingMath.cs.meta b/Assets/Rhythm/Core/RhythmTimingMath.cs.meta
new file mode 100644
index 0000000..aca150f
--- /dev/null
+++ b/Assets/Rhythm/Core/RhythmTimingMath.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 82e58281b4c97ca4b82fba821a87aa6f
\ No newline at end of file
diff --git a/Assets/Rhythm/Data.meta b/Assets/Rhythm/Data.meta
new file mode 100644
index 0000000..0bcb25c
--- /dev/null
+++ b/Assets/Rhythm/Data.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: eaf07a0316e5b49469e4aa2d72d7d1a3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Data/BeatmapData.cs b/Assets/Rhythm/Data/BeatmapData.cs
new file mode 100644
index 0000000..00711e6
--- /dev/null
+++ b/Assets/Rhythm/Data/BeatmapData.cs
@@ -0,0 +1,166 @@
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+/**
+ * ScriptableObject containing all data for a single song and its beatmap.
+ * Beat positions are stored as fractional beats so the data is BPM-agnostic.
+ * All beat-to-time conversions delegate to so
+ * the editor and runtime always agree.
+ *
+ */
+[CreateAssetMenu(menuName = "Rhythm/BeatmapData", fileName = "NewBeatmap")]
+public class BeatmapData : ScriptableObject
+{
+ #region Metadata
+
+ [Header("Metadata")]
+
+ [Tooltip("Display name of the song shown in menus.")]
+ public string SongName;
+
+ [Tooltip("Artist or composer name.")]
+ public string Author;
+
+ [Tooltip("Album or source name (optional).")]
+ public string Album;
+
+ [Tooltip("The AudioClip to play for this song.")]
+ public AudioClip SongClip;
+
+ [Tooltip("Thumbnail image shown in the song selection screen.")]
+ public Sprite Thumbnail;
+
+ [Tooltip("Start time in seconds for the song preview in selection screens.")]
+ public float PreviewStartTime;
+
+ #endregion
+
+ #region Timing
+
+ [Header("Timing")]
+
+ [Tooltip("Primary BPM of the song. Used for all beats when TimingPoints is empty.")]
+ public float BaseBpm = 120f;
+
+ [Tooltip("Seconds of silence before beat 1. Positive values delay the first beat.")]
+ public float OffsetSeconds;
+
+ [Tooltip("Extra seconds to wait after Play() is called before the audio and notes begin. " +
+ "Gives the player time to get ready. Does not affect note timing math.")]
+ public float StartDelaySeconds;
+
+ [Tooltip("Additional BPM sections for tempo changes. Must be sorted ascending by Beat. " +
+ "Leave empty to use BaseBpm throughout.")]
+ public List TimingPoints = new List();
+
+ #endregion
+
+ #region Difficulty
+
+ [Header("Difficulty")]
+
+ [Tooltip("Human-readable difficulty label (e.g. Easy, Normal, Hard, Expert).")]
+ public string DifficultyName;
+
+ [Tooltip("Enum difficulty level. Used by SongManifest to identify this beatmap.")]
+ public Difficulty DifficultyLevel;
+
+ [Tooltip("Numeric difficulty rating shown to the player (e.g. 1.0 – 10.0).")]
+ public float DifficultyRating;
+
+ #endregion
+
+ #region Notes
+
+ [Header("Notes")]
+
+ [Tooltip("All notes in this beatmap. The beatmap editor keeps these sorted by Beat.")]
+ public List Notes = new List();
+
+ #endregion
+
+ #region Key Points
+
+ [Header("Key Points")]
+
+ [Tooltip("Tagged non-note events used for visual effects, tutorial triggers, etc.")]
+ public List KeyPoints = new List();
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Converts a fractional beat position to seconds elapsed from song start,
+ * accounting for all and .
+ *
+ * Fractional beat position.
+ * Seconds from song start.
+ */
+ public double BeatToSeconds(double beat) =>
+ RhythmTimingMath.BeatToSeconds(beat, GetEffectiveTimingPoints(), OffsetSeconds);
+
+ /**
+ * Converts seconds elapsed from song start back to a fractional beat position.
+ *
+ * Seconds elapsed from song start.
+ * Fractional beat position.
+ */
+ public double SecondsToBeat(double seconds) =>
+ RhythmTimingMath.SecondsToBeat(seconds, GetEffectiveTimingPoints(), OffsetSeconds);
+
+ /**
+ * Returns all notes whose matches the given index,
+ * in ascending beat order.
+ *
+ * Zero-based lane index to filter by.
+ */
+ public IEnumerable GetNotesInLane(int lane) =>
+ Notes.Where(n => n.Lane == lane).OrderBy(n => n.Beat);
+
+ /**
+ * Returns all key points whose array contains
+ * the specified tag (case-sensitive), in ascending beat order.
+ *
+ * Tag to search for.
+ */
+ public IEnumerable GetKeyPointsByTag(string tag) =>
+ KeyPoints.Where(kp => kp.Tags != null && System.Array.IndexOf(kp.Tags, tag) >= 0)
+ .OrderBy(kp => kp.Beat);
+
+ /**
+ * Validates that are sorted ascending by Beat.
+ *
+ * True if valid or the list is empty.
+ */
+ public bool ValidateTimingPoints()
+ {
+ for (int i = 1; i < TimingPoints.Count; i++)
+ {
+ if (TimingPoints[i].Beat <= TimingPoints[i - 1].Beat)
+ return false;
+ }
+ return true;
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private IReadOnlyList GetEffectiveTimingPoints()
+ {
+ if (TimingPoints == null || TimingPoints.Count == 0 ||
+ TimingPoints[0].Beat > 0.0)
+ {
+ var list = new List();
+ list.Add(new TimingPoint { Beat = 0.0, Bpm = BaseBpm });
+ if (TimingPoints != null)
+ list.AddRange(TimingPoints);
+ return list;
+ }
+ return TimingPoints;
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Data/BeatmapData.cs.meta b/Assets/Rhythm/Data/BeatmapData.cs.meta
new file mode 100644
index 0000000..37f58a9
--- /dev/null
+++ b/Assets/Rhythm/Data/BeatmapData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4ce5ecd1e95d4474f8092d79730c87bd
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/Difficulty.cs b/Assets/Rhythm/Data/Difficulty.cs
new file mode 100644
index 0000000..e0dfac7
--- /dev/null
+++ b/Assets/Rhythm/Data/Difficulty.cs
@@ -0,0 +1,13 @@
+/**
+ * Difficulty levels for a beatmap. Used by to
+ * group multiple assets under one song.
+ *
+ */
+public enum Difficulty
+{
+ Easy,
+ Normal,
+ Hard,
+ Expert,
+ ExpertPlus
+}
diff --git a/Assets/Rhythm/Data/Difficulty.cs.meta b/Assets/Rhythm/Data/Difficulty.cs.meta
new file mode 100644
index 0000000..1780d1c
--- /dev/null
+++ b/Assets/Rhythm/Data/Difficulty.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 69ca55fb103411444a9f8b6c5d883279
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/HitResult.cs b/Assets/Rhythm/Data/HitResult.cs
new file mode 100644
index 0000000..d44c94c
--- /dev/null
+++ b/Assets/Rhythm/Data/HitResult.cs
@@ -0,0 +1,87 @@
+#region Enums
+
+/**
+ * The quality rating of a single note hit.
+ *
+ */
+public enum HitRating
+{
+ Perfect,
+ Good,
+ Ok,
+ Miss
+}
+
+#endregion
+
+#region Hit Result
+
+/**
+ * The result of evaluating a player input against an active note
+ * window.
+ *
+ */
+public readonly struct HitResult
+{
+ #region Fields
+
+ /** Whether the input landed within any hit window (Ok or better). */
+ public readonly bool IsHit;
+
+ /** Quality rating of the hit. */
+ public readonly HitRating Rating;
+
+ /**
+ * Signed timing error in seconds. Negative = hit early, positive = hit late.
+ * Zero for misses.
+ *
+ */
+ public readonly float TimingError;
+
+ /** True when TimingError is meaningfully negative (early hit). */
+ public readonly bool IsEarly;
+
+ /** True when TimingError is meaningfully positive (late hit). */
+ public readonly bool IsLate;
+
+ /** Zero-based lane index from which the hit was recorded. */
+ public readonly int Lane;
+
+ /** The note that was consumed by this hit. */
+ public readonly NoteData Note;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a fully populated hit result.
+ *
+ */
+ public HitResult(bool isHit, HitRating rating, float timingError, bool isEarly, bool isLate, int lane, NoteData note)
+ {
+ IsHit = isHit;
+ Rating = rating;
+ TimingError = timingError;
+ IsEarly = isEarly;
+ IsLate = isLate;
+ Lane = lane;
+ Note = note;
+ }
+
+ #endregion
+
+ #region Factories
+
+ /**
+ * Returns a Miss result for an off-beat input with no note consumed.
+ *
+ * Lane the input was registered on.
+ */
+ public static HitResult Miss(int lane) =>
+ new HitResult(false, HitRating.Miss, 0f, false, false, lane, default);
+
+ #endregion
+}
+
+#endregion
diff --git a/Assets/Rhythm/Data/HitResult.cs.meta b/Assets/Rhythm/Data/HitResult.cs.meta
new file mode 100644
index 0000000..f84dcf2
--- /dev/null
+++ b/Assets/Rhythm/Data/HitResult.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2a6076bf4015bba48843f49b90d2db80
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/HitWindowConfig.cs b/Assets/Rhythm/Data/HitWindowConfig.cs
new file mode 100644
index 0000000..a52fa88
--- /dev/null
+++ b/Assets/Rhythm/Data/HitWindowConfig.cs
@@ -0,0 +1,190 @@
+using UnityEngine;
+
+/**
+ * ScriptableObject that defines all timing windows, scoring values, grade
+ * thresholds, health parameters, and accuracy weights. Every judgment-related
+ * constant lives here so they can be tuned without recompiling.
+ *
+ */
+[CreateAssetMenu(menuName = "Rhythm/HitWindowConfig", fileName = "HitWindowConfig")]
+public class HitWindowConfig : ScriptableObject
+{
+ #region Timing Windows
+
+ [Header("Timing Windows (± seconds)")]
+
+ [Tooltip("Half-width of the Perfect window in seconds. Input within ±PerfectWindow of the note's target time earns Perfect.")]
+ public float PerfectWindow = 0.022f;
+
+ [Tooltip("Half-width of the Good window in seconds. Input within ±GoodWindow (outside Perfect) earns Good.")]
+ public float GoodWindow = 0.060f;
+
+ [Tooltip("Half-width of the Ok window in seconds. Input within ±OkWindow (outside Good) earns Ok. Beyond this is a Miss.")]
+ public float OkWindow = 0.120f;
+
+ #endregion
+
+ #region Score Values
+
+ [Header("Score Values")]
+
+ [Tooltip("Base score awarded for a Perfect hit before combo multiplier.")]
+ public int PerfectScore = 300;
+
+ [Tooltip("Base score awarded for a Good hit before combo multiplier.")]
+ public int GoodScore = 200;
+
+ [Tooltip("Base score awarded for an Ok hit before combo multiplier.")]
+ public int OkScore = 100;
+
+ [Tooltip("Base score awarded for a Miss. Typically 0.")]
+ public int MissScore = 0;
+
+ #endregion
+
+ #region Combo Multiplier
+
+ [Header("Combo Multiplier")]
+
+ [Tooltip("Score multiplier applied when combo is 0. Typically 1.")]
+ public float BaseMultiplier = 1.0f;
+
+ [Tooltip("Maximum score multiplier attainable through combo.")]
+ public float MaxMultiplier = 8.0f;
+
+ [Tooltip("Number of consecutive hits required to increment the multiplier by one step.")]
+ public int ComboStepSize = 10;
+
+ #endregion
+
+ #region Grade Thresholds
+
+ [Header("Grade Thresholds (accuracy 0–1)")]
+
+ [Tooltip("Minimum accuracy for SS grade (typically 1.0 = 100% perfect).")]
+ public float GradeSSThreshold = 1.00f;
+
+ [Tooltip("Minimum accuracy for S grade.")]
+ public float GradeSThreshold = 0.97f;
+
+ [Tooltip("Minimum accuracy for A grade.")]
+ public float GradeAThreshold = 0.93f;
+
+ [Tooltip("Minimum accuracy for B grade.")]
+ public float GradeBThreshold = 0.85f;
+
+ [Tooltip("Minimum accuracy for C grade.")]
+ public float GradeCThreshold = 0.75f;
+
+ [Tooltip("Minimum accuracy for D grade. Below this threshold earns F.")]
+ public float GradeDThreshold = 0.60f;
+
+ #endregion
+
+ #region Health System
+
+ [Header("Health System")]
+
+ [Tooltip("When false all health methods are no-ops and the player cannot fail.")]
+ public bool HealthEnabled = false;
+
+ [Tooltip("Maximum HP value. Current HP starts at this value.")]
+ public float MaxHp = 100f;
+
+ [Tooltip("HP restored on a Perfect hit.")]
+ public float HpGainPerfect = 2f;
+
+ [Tooltip("HP restored on a Good hit.")]
+ public float HpGainGood = 1f;
+
+ [Tooltip("HP restored on an Ok hit.")]
+ public float HpGainOk = 0f;
+
+ [Tooltip("HP lost on a Miss.")]
+ public float HpDrainMiss = 10f;
+
+ #endregion
+
+ #region Accuracy Weights
+
+ [Header("Accuracy Weights")]
+
+ [Tooltip("Weight of a Perfect hit in the accuracy formula (0–1). Default 1.0.")]
+ public float AccuracyWeightPerfect = 1.00f;
+
+ [Tooltip("Weight of a Good hit in the accuracy formula. Default 0.67.")]
+ public float AccuracyWeightGood = 0.67f;
+
+ [Tooltip("Weight of an Ok hit in the accuracy formula. Default 0.33.")]
+ public float AccuracyWeightOk = 0.33f;
+
+ [Tooltip("Weight of a Miss in the accuracy formula. Default 0.")]
+ public float AccuracyWeightMiss = 0.00f;
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Returns the for a given absolute timing error.
+ *
+ * Absolute value of timing error in seconds.
+ */
+ public HitRating EvaluateWindow(float absErrorSeconds)
+ {
+ if (absErrorSeconds <= PerfectWindow) return HitRating.Perfect;
+ if (absErrorSeconds <= GoodWindow) return HitRating.Good;
+ if (absErrorSeconds <= OkWindow) return HitRating.Ok;
+ return HitRating.Miss;
+ }
+
+ /**
+ * Returns the base score value for a given .
+ *
+ */
+ public int GetBaseScore(HitRating rating)
+ {
+ return rating switch
+ {
+ HitRating.Perfect => PerfectScore,
+ HitRating.Good => GoodScore,
+ HitRating.Ok => OkScore,
+ _ => MissScore
+ };
+ }
+
+ /**
+ * Returns the accuracy weight for a given .
+ *
+ */
+ public float GetAccuracyWeight(HitRating rating)
+ {
+ return rating switch
+ {
+ HitRating.Perfect => AccuracyWeightPerfect,
+ HitRating.Good => AccuracyWeightGood,
+ HitRating.Ok => AccuracyWeightOk,
+ _ => AccuracyWeightMiss
+ };
+ }
+
+ /**
+ * Returns the HP delta (positive = gain, negative = drain) for a given
+ * . Always returns 0 when
+ * is false.
+ *
+ */
+ public float GetHpDelta(HitRating rating)
+ {
+ if (!HealthEnabled) return 0f;
+ return rating switch
+ {
+ HitRating.Perfect => HpGainPerfect,
+ HitRating.Good => HpGainGood,
+ HitRating.Ok => HpGainOk,
+ _ => -HpDrainMiss
+ };
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Data/HitWindowConfig.cs.meta b/Assets/Rhythm/Data/HitWindowConfig.cs.meta
new file mode 100644
index 0000000..f713f24
--- /dev/null
+++ b/Assets/Rhythm/Data/HitWindowConfig.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7e494470476ae5847b3b0d9bbc5fff7e
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/KeyPointData.cs b/Assets/Rhythm/Data/KeyPointData.cs
new file mode 100644
index 0000000..d0f1fac
--- /dev/null
+++ b/Assets/Rhythm/Data/KeyPointData.cs
@@ -0,0 +1,33 @@
+using System;
+using UnityEngine;
+
+/**
+ * A tagged event at a specific beat position used for visual effects, tutorial
+ * triggers, or any other non-note beat-aligned event. Key points are fired
+ * by via OnKeyPointReached.
+ *
+ */
+[Serializable]
+public struct KeyPointData
+{
+ /** Fractional beat position at which this event fires. */
+ [Tooltip("Beat position at which this key point fires. Fractional values supported.")]
+ public double Beat;
+
+ /**
+ * Semantic tags for filtering (e.g. "drop", "chorus", "tutorial_arrow").
+ * Query via .
+ *
+ */
+ [Tooltip("Semantic tags for filtering in adapters (e.g. \"drop\", \"chorus\", \"camera_shake\").")]
+ public string[] Tags;
+
+ /**
+ * Optional freeform payload. Accepts plain text or serialized JSON for
+ * structured data consumed by adapters.
+ *
+ */
+ [Tooltip("Optional freeform or JSON payload consumed by IRhythmGameAdapter implementations.")]
+ [TextArea]
+ public string Data;
+}
diff --git a/Assets/Rhythm/Data/KeyPointData.cs.meta b/Assets/Rhythm/Data/KeyPointData.cs.meta
new file mode 100644
index 0000000..e64940c
--- /dev/null
+++ b/Assets/Rhythm/Data/KeyPointData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 24980be3ff989554d8cb0a8b33a2016f
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/NoteData.cs b/Assets/Rhythm/Data/NoteData.cs
new file mode 100644
index 0000000..e2b0c42
--- /dev/null
+++ b/Assets/Rhythm/Data/NoteData.cs
@@ -0,0 +1,60 @@
+using System;
+using UnityEngine;
+
+#region Enums
+
+/**
+ * The type of note in a beatmap.
+ *
+ */
+public enum NoteType
+{
+ Normal,
+ Hold
+}
+
+#endregion
+
+#region Note Data
+
+/**
+ * A single note entry in a beatmap. Beat positions are stored as fractional
+ * beats (e.g. 4.5 = beat four plus a half beat) so the data is BPM-agnostic
+ * and survives BPM changes correctly.
+ *
+ */
+[Serializable]
+public struct NoteData
+{
+ /**
+ * Beat position at which this note must be hit. Fractional values are valid.
+ *
+ */
+ [Tooltip("Beat position of this note. Fractional values supported (e.g. 4.5 = beat 4 + half beat).")]
+ public double Beat;
+
+ /** Zero-based lane index this note belongs to. */
+ [Tooltip("Zero-based lane index. Must match a valid index in RhythmVisualConfig.LanePaths.")]
+ public int Lane;
+
+ /** Type of note: Normal or Hold. */
+ [Tooltip("Normal = tap; Hold = sustain for HoldDuration beats.")]
+ public NoteType NoteType;
+
+ /**
+ * Duration of a hold note in beats. Ignored for Normal notes.
+ *
+ */
+ [Tooltip("Duration in beats for Hold notes. Ignored for Normal notes.")]
+ public double HoldDuration;
+
+ /**
+ * Stable editor-assigned GUID used for undo/redo operations in the
+ * beatmap editor. Not used at runtime.
+ *
+ */
+ [HideInInspector]
+ public string EditorId;
+}
+
+#endregion
diff --git a/Assets/Rhythm/Data/NoteData.cs.meta b/Assets/Rhythm/Data/NoteData.cs.meta
new file mode 100644
index 0000000..fe19708
--- /dev/null
+++ b/Assets/Rhythm/Data/NoteData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e83d686ba366dc844ade2c9617370fb8
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/RhythmCalibrationConfig.cs b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs
new file mode 100644
index 0000000..02c4e9c
--- /dev/null
+++ b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs
@@ -0,0 +1,33 @@
+using UnityEngine;
+
+/**
+ * ScriptableObject storing per-device audio and visual latency calibration
+ * offsets.
+ *
+ */
+[CreateAssetMenu(menuName = "Rhythm/RhythmCalibrationConfig", fileName = "RhythmCalibrationConfig")]
+public class RhythmCalibrationConfig : ScriptableObject
+{
+ #region Offsets
+
+ [Header("Scheduling")]
+
+ [Tooltip("How many seconds in advance PlayScheduled is called relative to AudioSettings.dspTime. " +
+ "Increase if notes and audio fall out of sync on slow devices.")]
+ public float DspScheduleBufferSeconds = 0.1f;
+
+ [Header("Audio Offset")]
+
+ [Tooltip("Shift applied to all note hit window evaluations in seconds. " +
+ "Positive = notes arrive earlier from the player's perspective (compensates for late audio). " +
+ "Negative = notes arrive later.")]
+ public float CalibrationOffsetSeconds = 0.0f;
+
+ [Header("Visual Offset")]
+
+ [Tooltip("Shifts note visual position along the path independently of hit window timing. " +
+ "Use to compensate for display latency without affecting judgment accuracy.")]
+ public float VisualOffsetSeconds = 0.0f;
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta
new file mode 100644
index 0000000..1b6e115
--- /dev/null
+++ b/Assets/Rhythm/Data/RhythmCalibrationConfig.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3b666d4b58929be4ba0e72378442a4cc
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/RhythmVisualConfig.cs b/Assets/Rhythm/Data/RhythmVisualConfig.cs
new file mode 100644
index 0000000..c58c0c3
--- /dev/null
+++ b/Assets/Rhythm/Data/RhythmVisualConfig.cs
@@ -0,0 +1,134 @@
+using UnityEngine;
+
+/**
+ * ScriptableObject containing all non-scene visual settings for the rhythm
+ * system.
+ *
+ */
+[CreateAssetMenu(menuName = "Rhythm/RhythmVisualConfig", fileName = "RhythmVisualConfig")]
+public class RhythmVisualConfig : ScriptableObject
+{
+ #region Spawning
+
+ [Header("Note Spawning")]
+
+ [Tooltip("How many beats before its target time a note should be spawned and become visible. Controls apparent note speed.")]
+ public float NoteAppearanceBeats = 2.0f;
+
+ [Tooltip("Seconds after a note's hit time before the visualizer force-despawns it as a safety net. " +
+ "Covers edge cases where OnNoteMissed fires late or not at all.")]
+ public float MissCleanupGraceSeconds = 0.5f;
+
+ [Header("Object Pooling")]
+
+ [Tooltip("Number of Normal note views pre-allocated in the pool at startup.")]
+ public int NormalPoolSize = 16;
+
+ [Tooltip("Number of Hold note views pre-allocated in the pool at startup.")]
+ public int HoldPoolSize = 8;
+
+ #endregion
+
+ #region Colors
+
+ [Header("Note Colors")]
+
+ [Tooltip("Default tint for Normal notes.")]
+ public Color NormalNoteColor = Color.white;
+
+ [Tooltip("Default tint for Hold notes.")]
+ public Color HoldNoteColor = new Color(0.4f, 0.8f, 1.0f, 1f);
+
+ [Header("Hit Flash Colors")]
+
+ [Tooltip("Color flash shown on a Perfect hit.")]
+ public Color HitFlashPerfect = Color.yellow;
+
+ [Tooltip("Color flash shown on a Good hit.")]
+ public Color HitFlashGood = Color.green;
+
+ [Tooltip("Color flash shown on an Ok hit.")]
+ public Color HitFlashOk = Color.cyan;
+
+ [Tooltip("Color flash shown on a Miss.")]
+ public Color HitFlashMiss = Color.red;
+
+ #endregion
+
+ #region Hit Zone
+
+ [Header("Hit Zone")]
+
+ [Tooltip("World-space radius of each lane's hit zone indicator sphere.")]
+ public float HitZoneRadius = 0.5f;
+
+ [Tooltip("World-space center positions for each lane's hit zone. Index matches lane index.")]
+ public Vector3[] LaneHitZonePositions = new Vector3[0];
+
+ #endregion
+
+ #region Prefabs
+
+ [Header("Prefabs")]
+
+ [Tooltip("Prefab instantiated for Normal notes. Must have a RhythmNoteView component.")]
+ public GameObject NormalNotePrefab;
+
+ [Tooltip("Prefab instantiated for Hold notes. Must have a RhythmNoteView component.")]
+ public GameObject HoldNotePrefab;
+
+ #endregion
+
+ #region Feedback Labels
+
+ [Header("Feedback UI Prefabs")]
+
+ [Tooltip("Prefab for the 'Perfect!' feedback label.")]
+ public GameObject FeedbackPerfectPrefab;
+
+ [Tooltip("Prefab for the 'Good' feedback label.")]
+ public GameObject FeedbackGoodPrefab;
+
+ [Tooltip("Prefab for the 'Ok' feedback label.")]
+ public GameObject FeedbackOkPrefab;
+
+ [Tooltip("Prefab for the 'Miss' feedback label.")]
+ public GameObject FeedbackMissPrefab;
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Returns the hit flash color for the given .
+ *
+ */
+ public Color GetHitFlashColor(HitRating rating)
+ {
+ return rating switch
+ {
+ HitRating.Perfect => HitFlashPerfect,
+ HitRating.Good => HitFlashGood,
+ HitRating.Ok => HitFlashOk,
+ _ => HitFlashMiss
+ };
+ }
+
+ /**
+ * Returns the feedback prefab for the given .
+ * May return null if the prefab is not assigned.
+ *
+ */
+ public GameObject GetFeedbackPrefab(HitRating rating)
+ {
+ return rating switch
+ {
+ HitRating.Perfect => FeedbackPerfectPrefab,
+ HitRating.Good => FeedbackGoodPrefab,
+ HitRating.Ok => FeedbackOkPrefab,
+ _ => FeedbackMissPrefab
+ };
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta b/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta
new file mode 100644
index 0000000..20b47bf
--- /dev/null
+++ b/Assets/Rhythm/Data/RhythmVisualConfig.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fb02592c77f9e1f42a56dc9c4989ab30
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/ScheduledNoteInfo.cs b/Assets/Rhythm/Data/ScheduledNoteInfo.cs
new file mode 100644
index 0000000..1143fa8
--- /dev/null
+++ b/Assets/Rhythm/Data/ScheduledNoteInfo.cs
@@ -0,0 +1,51 @@
+/**
+ * All data the visualizer needs to spawn and drive a note view. Produced by
+ * and forwarded via .
+ *
+ */
+public readonly struct ScheduledNoteInfo
+{
+ #region Fields
+
+ /** The beatmap note this info was generated for. */
+ public readonly NoteData Note;
+
+ /**
+ * DSP time at which the note view should first become visible (spawn position).
+ * Computed as songStartDsp + BeatToSeconds(note.Beat - noteAppearanceBeats).
+ *
+ */
+ public readonly double SpawnDspTime;
+
+ /**
+ * DSP time at which the note reaches the hit zone. This is the target for
+ * hit window evaluation and visual arrival.
+ *
+ */
+ public readonly double HitDspTime;
+
+ /**
+ * DSP time at which a hold note's tail ends. Zero for Normal notes.
+ * Computed as songStartDsp + BeatToSeconds(note.Beat + note.HoldDuration).
+ *
+ */
+ public readonly double HoldEndDspTime;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates a scheduled note info with all timing pre-computed in DSP time.
+ *
+ */
+ public ScheduledNoteInfo(NoteData note, double spawnDspTime, double hitDspTime, double holdEndDspTime)
+ {
+ Note = note;
+ SpawnDspTime = spawnDspTime;
+ HitDspTime = hitDspTime;
+ HoldEndDspTime = holdEndDspTime;
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta b/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta
new file mode 100644
index 0000000..c9e57dc
--- /dev/null
+++ b/Assets/Rhythm/Data/ScheduledNoteInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f2efcb900017ef04aa05c179af9c17b7
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/SongManifest.cs b/Assets/Rhythm/Data/SongManifest.cs
new file mode 100644
index 0000000..bc7f354
--- /dev/null
+++ b/Assets/Rhythm/Data/SongManifest.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+/**
+ * Groups multiple assets under one song, one per
+ * level.
+ *
+ */
+[CreateAssetMenu(menuName = "Rhythm/SongManifest", fileName = "NewSongManifest")]
+public class SongManifest : ScriptableObject
+{
+ [Tooltip("Display name of this song shown in menus and the Beatmap Editor title.")]
+ public string SongName;
+
+ [Tooltip("One entry per difficulty level. Order determines tab order in the Beatmap Editor.")]
+ public List Difficulties = new List();
+
+ /**
+ * Returns the for the given difficulty,
+ * or null if no entry with that level exists.
+ *
+ */
+ public BeatmapData GetBeatmap(Difficulty level)
+ {
+ foreach (DifficultyEntry entry in Difficulties)
+ {
+ if (entry.Level == level)
+ return entry.Beatmap;
+ }
+ return null;
+ }
+}
+
+/**
+ * One row in a : a difficulty level paired with
+ * the asset that implements it.
+ *
+ */
+[System.Serializable]
+public struct DifficultyEntry
+{
+ [Tooltip("Difficulty level for this entry.")]
+ public Difficulty Level;
+
+ [Tooltip("BeatmapData asset for this difficulty. Assign an existing asset or use " +
+ "Duplicate in the Beatmap Editor to generate one from another difficulty.")]
+ public BeatmapData Beatmap;
+}
diff --git a/Assets/Rhythm/Data/SongManifest.cs.meta b/Assets/Rhythm/Data/SongManifest.cs.meta
new file mode 100644
index 0000000..edae86a
--- /dev/null
+++ b/Assets/Rhythm/Data/SongManifest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 318b60a35bddb77409b7ec4125f99b64
\ No newline at end of file
diff --git a/Assets/Rhythm/Data/TimingPoint.cs b/Assets/Rhythm/Data/TimingPoint.cs
new file mode 100644
index 0000000..4eeaa93
--- /dev/null
+++ b/Assets/Rhythm/Data/TimingPoint.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEngine;
+
+/**
+ * Defines a BPM section within a song. Timing points must be sorted ascending
+ * by Beat. The section from TimingPoint[i].Beat to TimingPoint[i+1].Beat uses
+ * TimingPoint[i].Bpm for all beat-to-seconds conversions.
+ *
+ */
+[Serializable]
+public struct TimingPoint
+{
+ /** Beat at which this BPM section begins. */
+ [Tooltip("Beat at which this BPM section begins. Must be sorted ascending across the list.")]
+ public double Beat;
+
+ /**
+ * Beats per minute from this point forward until the next TimingPoint.
+ *
+ */
+ [Tooltip("Beats per minute for this section.")]
+ public float Bpm;
+}
diff --git a/Assets/Rhythm/Data/TimingPoint.cs.meta b/Assets/Rhythm/Data/TimingPoint.cs.meta
new file mode 100644
index 0000000..299fba7
--- /dev/null
+++ b/Assets/Rhythm/Data/TimingPoint.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a0b5b521639ce334eb9b04a8ce58a7c0
\ No newline at end of file
diff --git a/Assets/Rhythm/Editor.meta b/Assets/Rhythm/Editor.meta
new file mode 100644
index 0000000..53befec
--- /dev/null
+++ b/Assets/Rhythm/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6355b9f25866f6c4f81dc3404bb1f635
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Editor/BeatmapDataEditor.cs b/Assets/Rhythm/Editor/BeatmapDataEditor.cs
new file mode 100644
index 0000000..ded9e2e
--- /dev/null
+++ b/Assets/Rhythm/Editor/BeatmapDataEditor.cs
@@ -0,0 +1,22 @@
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEngine;
+
+/**
+ * Custom inspector for . Adds a prominent
+ * "Open in Beatmap Editor" button above the default fields.
+ *
+ */
+[CustomEditor(typeof(BeatmapData))]
+public class BeatmapDataEditor : Editor
+{
+ public override void OnInspectorGUI()
+ {
+ if (GUILayout.Button("Open in Beatmap Editor", GUILayout.Height(28f)))
+ BeatmapEditorWindow.OpenWithBeatmap((BeatmapData)target);
+
+ EditorGUILayout.Space(4f);
+ DrawDefaultInspector();
+ }
+}
+#endif
diff --git a/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta b/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta
new file mode 100644
index 0000000..1f884eb
--- /dev/null
+++ b/Assets/Rhythm/Editor/BeatmapDataEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fad61c040e35d7d43b1320cbc4c7ffc1
\ No newline at end of file
diff --git a/Assets/Rhythm/Editor/BeatmapEditorWindow.cs b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs
new file mode 100644
index 0000000..4cb625c
--- /dev/null
+++ b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs
@@ -0,0 +1,892 @@
+#if UNITY_EDITOR
+using System;
+using System.Collections.Generic;
+using UnityEditor;
+using UnityEngine;
+
+/**
+ * Unity EditorWindow for authoring assets.
+ * Provides a beat-based timeline, audio preview, note placement, key point
+ * editing, difficulty management via , and a BPM
+ * tap calculator. All edits go through so they
+ * participate in Undo/Redo.
+ * Open via menu: Rhythm / Beatmap Editor, or via the "Open in Beatmap Editor"
+ * button on any BeatmapData or SongManifest inspector.
+ *
+ */
+public class BeatmapEditorWindow : EditorWindow
+{
+ #region Menu Item / Static Entry Points
+
+ [MenuItem("Rhythm/Beatmap Editor")]
+ public static void OpenWindow()
+ {
+ var window = GetWindow("Beatmap Editor");
+ window.minSize = new Vector2(700f, 400f);
+ window.Show();
+ }
+
+ /**
+ * Opens the editor and loads the given beatmap directly, without requiring
+ * a . Called by the BeatmapData custom inspector.
+ *
+ */
+ public static void OpenWithBeatmap(BeatmapData beatmap)
+ {
+ var window = GetWindow("Beatmap Editor");
+ window.minSize = new Vector2(700f, 400f);
+ window._beatmap = beatmap;
+ window._serializedBeatmap = beatmap != null ? new SerializedObject(beatmap) : null;
+ window._editingKeyPointIndex = -1;
+ window.StopPlayback();
+ window.Show();
+ window.Focus();
+ }
+
+ /**
+ * Opens the editor with a loaded. Automatically
+ * activates the first difficulty tab. Called by the SongManifest custom inspector.
+ *
+ */
+ public static void OpenWithManifest(SongManifest manifest)
+ {
+ var window = GetWindow("Beatmap Editor");
+ window.minSize = new Vector2(700f, 400f);
+ window._manifest = manifest;
+ window._activeDifficultyIndex = -1;
+ if (manifest != null && manifest.Difficulties.Count > 0)
+ {
+ window._activeDifficultyIndex = 0;
+ BeatmapData bm = manifest.Difficulties[0].Beatmap;
+ window._beatmap = bm;
+ window._serializedBeatmap = bm != null ? new SerializedObject(bm) : null;
+ }
+ window._editingKeyPointIndex = -1;
+ window.StopPlayback();
+ window.Show();
+ window.Focus();
+ }
+
+ #endregion
+
+ #region Constants
+
+ private const float HeaderHeight = 80f;
+ private const float LaneHeight = 48f;
+ private const float LaneHeaderWidth = 60f;
+ private const float KeyPointRowHeight = 24f;
+ private const float PlayheadWidth = 2f;
+ private const float NoteWidth = 12f;
+ private const float MinZoom = 0.25f;
+ private const float MaxZoom = 8f;
+
+ #endregion
+
+ #region State
+
+ private BeatmapData _beatmap;
+ private SerializedObject _serializedBeatmap;
+ private SongManifest _manifest;
+ private int _activeDifficultyIndex = -1;
+ private int _duplicateDifficultyIndex = 0;
+
+ private bool _isPlaying;
+ private double _playbackStartEditorTime;
+ private double _scrubBeat;
+
+ private float _zoom = 1f;
+ private float _scrollOffset = 0f;
+ private Vector2 _scrollPos;
+
+ private bool _snapToGrid = true;
+ private float _snapResolution = 0.25f;
+
+ private int _selectedLane = 0;
+ private bool _showKeyPoints = true;
+
+ private readonly List _tapTimes = new List();
+ private double _tappedBpm;
+
+ private int _editingKeyPointIndex = -1;
+
+ private static readonly float[] SnapOptions = { 1f, 0.5f, 0.25f, 0.125f, 0.0625f };
+ private static readonly string[] SnapLabels = { "1/1", "1/2", "1/4", "1/8", "1/16" };
+
+ #endregion
+
+ #region EditorWindow Lifecycle
+
+ private void OnEnable()
+ {
+ EditorApplication.update += OnEditorUpdate;
+ titleContent = new GUIContent("Beatmap Editor");
+ }
+
+ private void OnDisable()
+ {
+ EditorApplication.update -= OnEditorUpdate;
+ StopPlayback();
+ }
+
+ private void OnGUI()
+ {
+ DrawToolbar();
+ DrawDifficultyTabs();
+ DrawMetadataAndTap();
+ EditorGUILayout.Space(4f);
+ DrawTimeline();
+ DrawKeyPointInspector();
+ }
+
+ private void OnEditorUpdate()
+ {
+ if (!_isPlaying) return;
+ double elapsed = EditorApplication.timeSinceStartup - _playbackStartEditorTime;
+ if (_beatmap != null)
+ _scrubBeat = _beatmap.SecondsToBeat(elapsed);
+ Repaint();
+ }
+
+ #endregion
+
+ #region Toolbar
+
+ private void DrawToolbar()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+
+ EditorGUILayout.LabelField("Beatmap:", GUILayout.Width(60f));
+ var newBeatmap = (BeatmapData)EditorGUILayout.ObjectField(
+ _beatmap, typeof(BeatmapData), false, GUILayout.Width(180f));
+ if (newBeatmap != _beatmap)
+ {
+ _beatmap = newBeatmap;
+ _serializedBeatmap = _beatmap != null ? new SerializedObject(_beatmap) : null;
+ _activeDifficultyIndex = -1;
+ if (_manifest != null && _beatmap != null)
+ {
+ for (int i = 0; i < _manifest.Difficulties.Count; i++)
+ {
+ if (_manifest.Difficulties[i].Beatmap == _beatmap)
+ {
+ _activeDifficultyIndex = i;
+ break;
+ }
+ }
+ }
+ _editingKeyPointIndex = -1;
+ StopPlayback();
+ }
+
+ GUILayout.Space(6f);
+
+ EditorGUILayout.LabelField("Manifest:", GUILayout.Width(58f));
+ var newManifest = (SongManifest)EditorGUILayout.ObjectField(
+ _manifest, typeof(SongManifest), false, GUILayout.Width(150f));
+ if (newManifest != _manifest)
+ {
+ _manifest = newManifest;
+ _activeDifficultyIndex = -1;
+ if (_manifest != null)
+ {
+ bool found = false;
+ for (int i = 0; i < _manifest.Difficulties.Count; i++)
+ {
+ if (_manifest.Difficulties[i].Beatmap == _beatmap)
+ {
+ _activeDifficultyIndex = i;
+ found = true;
+ break;
+ }
+ }
+ if (!found && _beatmap == null && _manifest.Difficulties.Count > 0)
+ {
+ _activeDifficultyIndex = 0;
+ BeatmapData bm = _manifest.Difficulties[0].Beatmap;
+ _beatmap = bm;
+ _serializedBeatmap = bm != null ? new SerializedObject(bm) : null;
+ }
+ }
+ }
+
+ GUILayout.Space(8f);
+
+ GUI.enabled = _beatmap != null;
+ if (GUILayout.Button(_isPlaying ? "■ Stop" : "▶ Play", EditorStyles.toolbarButton, GUILayout.Width(60f)))
+ {
+ if (_isPlaying) StopPlayback();
+ else StartPlayback();
+ }
+ GUI.enabled = true;
+
+ GUILayout.Space(8f);
+
+ _snapToGrid = GUILayout.Toggle(_snapToGrid, "Snap", EditorStyles.toolbarButton);
+ GUILayout.Space(4f);
+
+ int currentSnapIndex = Array.IndexOf(SnapOptions, _snapResolution);
+ if (currentSnapIndex < 0) currentSnapIndex = 2;
+ int newSnapIndex = EditorGUILayout.Popup(currentSnapIndex, SnapLabels,
+ EditorStyles.toolbarPopup, GUILayout.Width(40f));
+ _snapResolution = SnapOptions[newSnapIndex];
+
+ GUILayout.Space(8f);
+ EditorGUILayout.LabelField("Zoom:", GUILayout.Width(40f));
+ _zoom = EditorGUILayout.Slider(_zoom, MinZoom, MaxZoom, GUILayout.Width(100f));
+
+ GUILayout.FlexibleSpace();
+
+ _showKeyPoints = GUILayout.Toggle(_showKeyPoints, "Key Points", EditorStyles.toolbarButton);
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ #endregion
+
+ #region Difficulty Tabs
+
+ private void DrawDifficultyTabs()
+ {
+ if (_manifest == null) return;
+
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+
+ for (int i = 0; i < _manifest.Difficulties.Count; i++)
+ {
+ bool isActive = i == _activeDifficultyIndex;
+ if (isActive)
+ GUI.backgroundColor = new Color(0.6f, 0.85f, 1f);
+
+ if (GUILayout.Button(_manifest.Difficulties[i].Level.ToString(),
+ EditorStyles.toolbarButton, GUILayout.Width(72f)))
+ {
+ if (!isActive)
+ SwitchToDifficulty(i);
+ }
+
+ GUI.backgroundColor = Color.white;
+ }
+
+ GUILayout.Space(8f);
+
+ var usedLevels = BuildUsedLevels();
+ var available = BuildAvailableLevels(usedLevels);
+
+ if (available.Count > 0)
+ {
+ _duplicateDifficultyIndex = Mathf.Clamp(_duplicateDifficultyIndex, 0, available.Count - 1);
+ string[] availNames = available.ConvertAll(d => d.ToString()).ToArray();
+
+ if (_beatmap != null && _activeDifficultyIndex >= 0)
+ {
+ EditorGUILayout.LabelField("Duplicate as:", GUILayout.Width(84f));
+ _duplicateDifficultyIndex = EditorGUILayout.Popup(
+ _duplicateDifficultyIndex, availNames,
+ EditorStyles.toolbarPopup, GUILayout.Width(84f));
+ if (GUILayout.Button("Create", EditorStyles.toolbarButton, GUILayout.Width(52f)))
+ DuplicateBeatmapAs(available[_duplicateDifficultyIndex]);
+ }
+ else if (_beatmap != null && _activeDifficultyIndex < 0)
+ {
+ EditorGUILayout.LabelField("Add as:", GUILayout.Width(48f));
+ _duplicateDifficultyIndex = EditorGUILayout.Popup(
+ _duplicateDifficultyIndex, availNames,
+ EditorStyles.toolbarPopup, GUILayout.Width(84f));
+ if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(40f)))
+ AddCurrentBeatmapToManifest(available[_duplicateDifficultyIndex]);
+ }
+ }
+
+ GUILayout.FlexibleSpace();
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private HashSet BuildUsedLevels()
+ {
+ var set = new HashSet();
+ if (_manifest == null) return set;
+ foreach (DifficultyEntry e in _manifest.Difficulties)
+ set.Add(e.Level);
+ return set;
+ }
+
+ private List BuildAvailableLevels(HashSet used)
+ {
+ var list = new List();
+ foreach (Difficulty d in Enum.GetValues(typeof(Difficulty)))
+ if (!used.Contains(d)) list.Add(d);
+ return list;
+ }
+
+ private void SwitchToDifficulty(int index)
+ {
+ if (_manifest == null || index < 0 || index >= _manifest.Difficulties.Count) return;
+ _activeDifficultyIndex = index;
+ BeatmapData bm = _manifest.Difficulties[index].Beatmap;
+ _beatmap = bm;
+ _serializedBeatmap = bm != null ? new SerializedObject(bm) : null;
+ _editingKeyPointIndex = -1;
+ StopPlayback();
+ }
+
+ private void DuplicateBeatmapAs(Difficulty targetLevel)
+ {
+ if (_beatmap == null || _manifest == null) return;
+
+ string sourcePath = AssetDatabase.GetAssetPath(_beatmap);
+ if (string.IsNullOrEmpty(sourcePath)) return;
+
+ int lastSlash = sourcePath.LastIndexOf('/');
+ string dir = lastSlash >= 0 ? sourcePath.Substring(0, lastSlash) : "Assets";
+ string songName = !string.IsNullOrEmpty(_manifest.SongName) ? _manifest.SongName : "Song";
+ string newPath = AssetDatabase.GenerateUniqueAssetPath(
+ $"{dir}/{songName}_{targetLevel}.asset");
+
+ AssetDatabase.CopyAsset(sourcePath, newPath);
+ BeatmapData copy = AssetDatabase.LoadAssetAtPath(newPath);
+ if (copy == null) return;
+
+ copy.DifficultyLevel = targetLevel;
+ copy.DifficultyName = targetLevel.ToString();
+ EditorUtility.SetDirty(copy);
+
+ Undo.RecordObject(_manifest, "Duplicate Beatmap");
+ _manifest.Difficulties.Add(new DifficultyEntry { Level = targetLevel, Beatmap = copy });
+ EditorUtility.SetDirty(_manifest);
+ AssetDatabase.SaveAssets();
+
+ _activeDifficultyIndex = _manifest.Difficulties.Count - 1;
+ _beatmap = copy;
+ _serializedBeatmap = new SerializedObject(copy);
+ _editingKeyPointIndex = -1;
+ }
+
+ private void AddCurrentBeatmapToManifest(Difficulty level)
+ {
+ if (_beatmap == null || _manifest == null) return;
+
+ Undo.RecordObject(_manifest, "Add Beatmap to Manifest");
+ _manifest.Difficulties.Add(new DifficultyEntry { Level = level, Beatmap = _beatmap });
+ EditorUtility.SetDirty(_manifest);
+ AssetDatabase.SaveAssets();
+
+ _activeDifficultyIndex = _manifest.Difficulties.Count - 1;
+
+ _beatmap.DifficultyLevel = level;
+ _beatmap.DifficultyName = level.ToString();
+ EditorUtility.SetDirty(_beatmap);
+ }
+
+ #endregion
+
+ #region Metadata and BPM Tap
+
+ private void DrawMetadataAndTap()
+ {
+ if (_beatmap == null || _serializedBeatmap == null) return;
+
+ _serializedBeatmap.Update();
+
+ EditorGUILayout.BeginHorizontal();
+
+ EditorGUILayout.BeginVertical(GUILayout.Width(300f));
+ EditorGUILayout.LabelField("Song Metadata", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("SongName"));
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("Author"));
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("BaseBpm"));
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("OffsetSeconds"));
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("DifficultyName"));
+ EditorGUILayout.PropertyField(_serializedBeatmap.FindProperty("DifficultyLevel"));
+ EditorGUILayout.EndVertical();
+
+ GUILayout.Space(16f);
+
+ EditorGUILayout.BeginVertical(GUILayout.Width(220f));
+ EditorGUILayout.LabelField("BPM Tap Calculator", EditorStyles.boldLabel);
+ if (GUILayout.Button("Tap", GUILayout.Height(32f)))
+ RecordTap();
+
+ if (_tappedBpm > 0)
+ {
+ EditorGUILayout.LabelField($"Detected BPM: {_tappedBpm:F2}");
+ if (GUILayout.Button("Apply to BaseBpm"))
+ ApplyTappedBpm();
+ }
+
+ if (_tapTimes.Count > 0 && GUILayout.Button("Clear Taps"))
+ _tapTimes.Clear();
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.BeginVertical(GUILayout.Width(180f));
+ EditorGUILayout.LabelField("Lanes", EditorStyles.boldLabel);
+ int laneCount = GetLaneCount();
+ EditorGUILayout.LabelField($"Detected lanes: {laneCount}");
+ EditorGUILayout.LabelField("Selected lane:");
+ _selectedLane = EditorGUILayout.IntSlider(_selectedLane, 0, Mathf.Max(0, laneCount - 1));
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.EndHorizontal();
+
+ _serializedBeatmap.ApplyModifiedProperties();
+ }
+
+ #endregion
+
+ #region Timeline
+
+ private void DrawTimeline()
+ {
+ if (_beatmap == null) return;
+
+ int laneCount = Mathf.Max(1, GetLaneCount());
+ float timelineHeight = KeyPointRowHeight + laneCount * LaneHeight + 24f;
+
+ Rect timelineRect = GUILayoutUtility.GetRect(
+ GUIContent.none, GUIStyle.none,
+ GUILayout.ExpandWidth(true), GUILayout.Height(timelineHeight));
+
+ if (timelineRect.width < 10f) return;
+
+ HandleTimelineEvents(timelineRect);
+
+ DrawTimelineBackground(timelineRect, laneCount);
+ DrawBeatGrid(timelineRect);
+
+ if (_showKeyPoints)
+ DrawKeyPoints(timelineRect);
+
+ DrawNotes(timelineRect, laneCount);
+ DrawLaneHeaders(timelineRect, laneCount);
+ DrawPlayhead(timelineRect);
+ }
+
+ private void DrawTimelineBackground(Rect rect, int laneCount)
+ {
+ EditorGUI.DrawRect(rect, new Color(0.15f, 0.15f, 0.15f));
+
+ float keyPointRowY = rect.y;
+ EditorGUI.DrawRect(new Rect(rect.x, keyPointRowY, rect.width, KeyPointRowHeight),
+ new Color(0.12f, 0.12f, 0.18f));
+
+ for (int i = 0; i < laneCount; i++)
+ {
+ float y = rect.y + KeyPointRowHeight + i * LaneHeight;
+ Color laneColor = i % 2 == 0
+ ? new Color(0.18f, 0.18f, 0.18f)
+ : new Color(0.20f, 0.20f, 0.20f);
+ EditorGUI.DrawRect(new Rect(rect.x + LaneHeaderWidth, y, rect.width - LaneHeaderWidth, LaneHeight),
+ laneColor);
+ }
+ }
+
+ private void DrawBeatGrid(Rect rect)
+ {
+ if (_beatmap == null) return;
+
+ float beatsVisible = (rect.width - LaneHeaderWidth) / PixelsPerBeat();
+ double startBeat = _scrollOffset;
+ double endBeat = startBeat + beatsVisible + 1.0;
+
+ double subDiv = _snapResolution;
+
+ for (double b = Math.Floor(startBeat / subDiv) * subDiv; b <= endBeat; b += subDiv)
+ {
+ float x = BeatToPixel(b, rect);
+ if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue;
+
+ bool isWholeBeat = Math.Abs(b - Math.Round(b)) < 0.001;
+ Color lineColor = isWholeBeat
+ ? new Color(0.5f, 0.5f, 0.5f, 0.9f)
+ : new Color(0.3f, 0.3f, 0.3f, 0.5f);
+
+ Handles.color = lineColor;
+ Handles.DrawLine(
+ new Vector3(x, rect.y + KeyPointRowHeight),
+ new Vector3(x, rect.yMax));
+
+ if (isWholeBeat)
+ {
+ GUI.Label(new Rect(x + 2f, rect.y + KeyPointRowHeight, 40f, 14f),
+ $"{(int)Math.Round(b)}", EditorStyles.miniLabel);
+ }
+ }
+ }
+
+ private void DrawNotes(Rect rect, int laneCount)
+ {
+ if (_beatmap?.Notes == null) return;
+
+ for (int i = 0; i < _beatmap.Notes.Count; i++)
+ {
+ NoteData note = _beatmap.Notes[i];
+ if (note.Lane >= laneCount) continue;
+
+ float x = BeatToPixel(note.Beat, rect);
+ float y = rect.y + KeyPointRowHeight + note.Lane * LaneHeight + LaneHeight * 0.25f;
+ float h = LaneHeight * 0.5f;
+
+ if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue;
+
+ Color noteColor = note.NoteType == NoteType.Hold
+ ? new Color(0.4f, 0.8f, 1.0f)
+ : new Color(1f, 0.85f, 0.2f);
+
+ Rect noteRect = new Rect(x - NoteWidth * 0.5f, y, NoteWidth, h);
+
+ if (note.NoteType == NoteType.Hold && note.HoldDuration > 0)
+ {
+ float endX = BeatToPixel(note.Beat + note.HoldDuration, rect);
+ EditorGUI.DrawRect(new Rect(x, y + h * 0.35f, endX - x, h * 0.3f),
+ new Color(noteColor.r, noteColor.g, noteColor.b, 0.5f));
+ }
+
+ EditorGUI.DrawRect(noteRect, noteColor);
+
+ Rect hitRect = new Rect(x - 8f, y - 4f, 16f, h + 8f);
+ if (Event.current.type == EventType.MouseDown &&
+ Event.current.button == 1 &&
+ hitRect.Contains(Event.current.mousePosition))
+ {
+ int capturedIndex = i;
+ var menu = new GenericMenu();
+ menu.AddItem(new GUIContent("Delete Note"), false, () => DeleteNote(capturedIndex));
+ menu.ShowAsContext();
+ Event.current.Use();
+ }
+ }
+ }
+
+ private void DrawKeyPoints(Rect rect)
+ {
+ if (_beatmap?.KeyPoints == null) return;
+
+ float rowY = rect.y + 4f;
+ float rowH = KeyPointRowHeight - 8f;
+
+ for (int i = 0; i < _beatmap.KeyPoints.Count; i++)
+ {
+ KeyPointData kp = _beatmap.KeyPoints[i];
+ float x = BeatToPixel(kp.Beat, rect);
+ if (x < rect.x + LaneHeaderWidth || x > rect.xMax) continue;
+
+ Color kpColor = _editingKeyPointIndex == i
+ ? new Color(1f, 0.5f, 0.1f)
+ : new Color(0.7f, 0.4f, 1.0f);
+
+ Vector3[] triangle =
+ {
+ new Vector3(x, rowY + rowH),
+ new Vector3(x - 6f, rowY),
+ new Vector3(x + 6f, rowY)
+ };
+ Handles.color = kpColor;
+ Handles.DrawAAConvexPolygon(triangle);
+
+ string label = kp.Tags != null && kp.Tags.Length > 0 ? kp.Tags[0] : "kp";
+ GUI.Label(new Rect(x + 7f, rowY, 80f, rowH), label, EditorStyles.miniLabel);
+
+ Rect hitRect = new Rect(x - 8f, rowY, 16f, rowH);
+ if (Event.current.type == EventType.MouseDown &&
+ Event.current.button == 0 &&
+ hitRect.Contains(Event.current.mousePosition))
+ {
+ _editingKeyPointIndex = i;
+ Event.current.Use();
+ }
+
+ if (Event.current.type == EventType.MouseDown &&
+ Event.current.button == 1 &&
+ hitRect.Contains(Event.current.mousePosition))
+ {
+ int capturedIndex = i;
+ var menu = new GenericMenu();
+ menu.AddItem(new GUIContent("Delete Key Point"), false, () => DeleteKeyPoint(capturedIndex));
+ menu.ShowAsContext();
+ Event.current.Use();
+ }
+ }
+ }
+
+ private void DrawLaneHeaders(Rect rect, int laneCount)
+ {
+ EditorGUI.DrawRect(new Rect(rect.x, rect.y, LaneHeaderWidth, rect.height),
+ new Color(0.1f, 0.1f, 0.1f));
+
+ for (int i = 0; i < laneCount; i++)
+ {
+ float y = rect.y + KeyPointRowHeight + i * LaneHeight;
+ GUI.Label(new Rect(rect.x + 4f, y + LaneHeight * 0.35f, LaneHeaderWidth - 4f, 18f),
+ $"Lane {i}", EditorStyles.miniLabel);
+ }
+ }
+
+ private void DrawPlayhead(Rect rect)
+ {
+ float x = BeatToPixel(_scrubBeat, rect);
+ if (x >= rect.x + LaneHeaderWidth && x <= rect.xMax)
+ {
+ Handles.color = new Color(1f, 0.3f, 0.3f, 0.9f);
+ Handles.DrawLine(
+ new Vector3(x, rect.y),
+ new Vector3(x, rect.yMax));
+ }
+ }
+
+ #endregion
+
+ #region Key Point Inspector
+
+ private void DrawKeyPointInspector()
+ {
+ if (_editingKeyPointIndex < 0 || _beatmap == null) return;
+ if (_editingKeyPointIndex >= _beatmap.KeyPoints.Count)
+ {
+ _editingKeyPointIndex = -1;
+ return;
+ }
+
+ EditorGUILayout.Space(4f);
+ EditorGUILayout.LabelField(
+ $"Key Point at beat {_beatmap.KeyPoints[_editingKeyPointIndex].Beat:F3}",
+ EditorStyles.boldLabel);
+
+ _serializedBeatmap.Update();
+ SerializedProperty kpList = _serializedBeatmap.FindProperty("KeyPoints");
+ SerializedProperty kpProp = kpList.GetArrayElementAtIndex(_editingKeyPointIndex);
+ SerializedProperty tagsArr = kpProp.FindPropertyRelative("Tags");
+
+ if (tagsArr.arraySize == 0)
+ tagsArr.InsertArrayElementAtIndex(0);
+
+ EditorGUILayout.LabelField("Event Name", EditorStyles.boldLabel);
+ SerializedProperty firstTag = tagsArr.GetArrayElementAtIndex(0);
+ firstTag.stringValue = EditorGUILayout.TextField("Name", firstTag.stringValue);
+
+ if (tagsArr.arraySize > 1)
+ {
+ EditorGUILayout.LabelField("Additional Tags");
+ for (int t = 1; t < tagsArr.arraySize; t++)
+ {
+ EditorGUILayout.BeginHorizontal();
+ SerializedProperty tagProp = tagsArr.GetArrayElementAtIndex(t);
+ tagProp.stringValue = EditorGUILayout.TextField(tagProp.stringValue);
+ if (GUILayout.Button("✕", GUILayout.Width(24f)))
+ {
+ tagsArr.DeleteArrayElementAtIndex(t);
+ break;
+ }
+ EditorGUILayout.EndHorizontal();
+ }
+ }
+
+ if (GUILayout.Button("+ Tag", GUILayout.Width(60f)))
+ tagsArr.InsertArrayElementAtIndex(tagsArr.arraySize);
+
+ EditorGUILayout.Space(4f);
+ EditorGUILayout.PropertyField(kpProp.FindPropertyRelative("Data"));
+
+ _serializedBeatmap.ApplyModifiedProperties();
+
+ if (GUILayout.Button("Close Inspector", GUILayout.Width(130f)))
+ _editingKeyPointIndex = -1;
+ }
+
+ #endregion
+
+ #region Timeline Interaction
+
+ private void HandleTimelineEvents(Rect timelineRect)
+ {
+ Event evt = Event.current;
+
+ if (evt.type == EventType.ScrollWheel && timelineRect.Contains(evt.mousePosition))
+ {
+ /* Ctrl+scroll = zoom; plain scroll = horizontal pan (0.5 beats per notch) */
+ if (evt.control)
+ _zoom = Mathf.Clamp(_zoom - evt.delta.y * 0.1f, MinZoom, MaxZoom);
+ else
+ _scrollOffset = Mathf.Max(0f, _scrollOffset + evt.delta.y * 0.5f);
+
+ evt.Use();
+ Repaint();
+ }
+
+ if (evt.type == EventType.MouseDown && evt.button == 0 &&
+ timelineRect.Contains(evt.mousePosition))
+ {
+ float relX = evt.mousePosition.x - (timelineRect.x + LaneHeaderWidth);
+ if (relX < 0f) return;
+
+ float clickedY = evt.mousePosition.y - (timelineRect.y + KeyPointRowHeight);
+
+ if (evt.mousePosition.y < timelineRect.y + KeyPointRowHeight)
+ {
+ double beat = SnapBeat(PixelToBeat(evt.mousePosition.x, timelineRect));
+ AddKeyPoint(beat);
+ evt.Use();
+ return;
+ }
+
+ if (clickedY < 0f) return;
+
+ int lane = (int)(clickedY / LaneHeight);
+ double noteBeat = SnapBeat(PixelToBeat(evt.mousePosition.x, timelineRect));
+ AddNote(noteBeat, lane);
+ evt.Use();
+ }
+
+ if (evt.type == EventType.MouseDrag && evt.button == 2 &&
+ timelineRect.Contains(evt.mousePosition))
+ {
+ _scrollOffset -= evt.delta.x / PixelsPerBeat();
+ _scrollOffset = Mathf.Max(0f, _scrollOffset);
+ evt.Use();
+ Repaint();
+ }
+ }
+
+ private double SnapBeat(double beat)
+ {
+ if (!_snapToGrid) return beat;
+ return Math.Round(beat / _snapResolution) * _snapResolution;
+ }
+
+ private double PixelToBeat(float pixelX, Rect timelineRect)
+ {
+ float relX = pixelX - (timelineRect.x + LaneHeaderWidth);
+ return _scrollOffset + relX / PixelsPerBeat();
+ }
+
+ private float BeatToPixel(double beat, Rect timelineRect)
+ {
+ return timelineRect.x + LaneHeaderWidth + (float)((beat - _scrollOffset) * PixelsPerBeat());
+ }
+
+ private float PixelsPerBeat() => 80f * _zoom;
+
+ private int GetLaneCount()
+ {
+ if (_beatmap?.Notes == null || _beatmap.Notes.Count == 0) return 1;
+ int max = 0;
+ foreach (var n in _beatmap.Notes)
+ if (n.Lane > max) max = n.Lane;
+ return max + 1;
+ }
+
+ #endregion
+
+ #region Note Operations
+
+ private void AddNote(double beat, int lane)
+ {
+ if (_beatmap == null || beat < 0.0) return;
+
+ Undo.RecordObject(_beatmap, "Add Note");
+ var note = new NoteData
+ {
+ Beat = beat,
+ Lane = lane,
+ NoteType = NoteType.Normal,
+ HoldDuration = 0.0,
+ EditorId = Guid.NewGuid().ToString()
+ };
+ _beatmap.Notes.Add(note);
+ _beatmap.Notes.Sort((a, b) => a.Beat.CompareTo(b.Beat));
+ EditorUtility.SetDirty(_beatmap);
+ Repaint();
+ }
+
+ private void DeleteNote(int index)
+ {
+ if (_beatmap == null || index < 0 || index >= _beatmap.Notes.Count) return;
+ Undo.RecordObject(_beatmap, "Delete Note");
+ _beatmap.Notes.RemoveAt(index);
+ EditorUtility.SetDirty(_beatmap);
+ Repaint();
+ }
+
+ private void AddKeyPoint(double beat)
+ {
+ if (_beatmap == null || beat < 0.0) return;
+
+ Undo.RecordObject(_beatmap, "Add Key Point");
+ var kp = new KeyPointData
+ {
+ Beat = beat,
+ Tags = new string[] { "event" },
+ Data = ""
+ };
+ _beatmap.KeyPoints.Add(kp);
+ _beatmap.KeyPoints.Sort((a, b) => a.Beat.CompareTo(b.Beat));
+ EditorUtility.SetDirty(_beatmap);
+ Repaint();
+ }
+
+ private void DeleteKeyPoint(int index)
+ {
+ if (_beatmap == null || index < 0 || index >= _beatmap.KeyPoints.Count) return;
+ Undo.RecordObject(_beatmap, "Delete Key Point");
+ _beatmap.KeyPoints.RemoveAt(index);
+ if (_editingKeyPointIndex == index) _editingKeyPointIndex = -1;
+ EditorUtility.SetDirty(_beatmap);
+ Repaint();
+ }
+
+ #endregion
+
+ #region Audio Playback
+
+ private void StartPlayback()
+ {
+ if (_beatmap?.SongClip == null) return;
+ _isPlaying = true;
+ _playbackStartEditorTime = EditorApplication.timeSinceStartup
+ - (_beatmap.BeatToSeconds(_scrubBeat));
+ }
+
+ private void StopPlayback()
+ {
+ _isPlaying = false;
+ }
+
+ #endregion
+
+ #region BPM Tap
+
+ private void RecordTap()
+ {
+ double now = EditorApplication.timeSinceStartup;
+ if (_tapTimes.Count > 0 && now - _tapTimes[_tapTimes.Count - 1] > 3.0)
+ _tapTimes.Clear();
+
+ _tapTimes.Add(now);
+ CalculateTappedBpm();
+ }
+
+ private void CalculateTappedBpm()
+ {
+ if (_tapTimes.Count < 2)
+ {
+ _tappedBpm = 0;
+ return;
+ }
+
+ double totalInterval = _tapTimes[_tapTimes.Count - 1] - _tapTimes[0];
+ double avgInterval = totalInterval / (_tapTimes.Count - 1);
+ _tappedBpm = 60.0 / avgInterval;
+ }
+
+ private void ApplyTappedBpm()
+ {
+ if (_beatmap == null || _tappedBpm <= 0) return;
+ Undo.RecordObject(_beatmap, "Apply Tapped BPM");
+ _beatmap.BaseBpm = (float)Math.Round(_tappedBpm, 2);
+ EditorUtility.SetDirty(_beatmap);
+ _tapTimes.Clear();
+ _tappedBpm = 0;
+ }
+
+ #endregion
+}
+#endif
diff --git a/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta
new file mode 100644
index 0000000..9d28400
--- /dev/null
+++ b/Assets/Rhythm/Editor/BeatmapEditorWindow.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6d5f65be5a4a44f498782102b44b1638
\ No newline at end of file
diff --git a/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs
new file mode 100644
index 0000000..c116205
--- /dev/null
+++ b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs
@@ -0,0 +1,146 @@
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEngine;
+
+/**
+ * Custom inspector and SceneView editor for .
+ * Draws the Bezier path curve, shows draggable handles on each waypoint, and
+ * labels the spawn and hit zone endpoints.
+ *
+ */
+[CustomEditor(typeof(NotePathBehaviour))]
+public class NotePathBehaviourEditor : Editor
+{
+ #region Constants
+
+ private const int CurveSampleCount = 32;
+ private static readonly Color PathColor = new Color(0.2f, 0.9f, 1.0f, 1f);
+ private static readonly Color EasedPathColor = new Color(1.0f, 0.6f, 0.1f, 0.7f);
+ private static readonly Color SpawnColor = new Color(0.2f, 1.0f, 0.3f, 1f);
+ private static readonly Color HitZoneColor = new Color(1.0f, 0.2f, 0.2f, 1f);
+ private static readonly Color WaypointColor = new Color(1.0f, 1.0f, 0.3f, 1f);
+ private const float WaypointHandleSize = 0.15f;
+ private const float EndpointSphereSize = 0.12f;
+
+ #endregion
+
+ #region Unity Editor Lifecycle
+
+ /**
+ * Draws waypoint handles and the Bezier path in the SceneView.
+ *
+ */
+ public void OnSceneGUI()
+ {
+ var behaviour = (NotePathBehaviour)target;
+ Transform[] waypoints = behaviour.Waypoints;
+
+ if (waypoints == null || waypoints.Length < 2) return;
+
+ DrawWaypointHandles(behaviour, waypoints);
+ DrawBezierCurve(behaviour, waypoints);
+ DrawEasedPathSamples(behaviour);
+ DrawEndpointLabels(behaviour);
+ }
+
+ /**
+ * Draws a helper button in the Inspector to collect children.
+ *
+ */
+ public override void OnInspectorGUI()
+ {
+ DrawDefaultInspector();
+
+ EditorGUILayout.Space();
+ if (GUILayout.Button("Collect Children As Waypoints"))
+ {
+ var behaviour = (NotePathBehaviour)target;
+ Undo.RecordObject(behaviour, "Collect Children As Waypoints");
+ behaviour.CollectChildrenAsWaypoints();
+ EditorUtility.SetDirty(behaviour);
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private void DrawWaypointHandles(NotePathBehaviour behaviour, Transform[] waypoints)
+ {
+ for (int i = 0; i < waypoints.Length; i++)
+ {
+ if (waypoints[i] == null) continue;
+
+ Handles.color = WaypointColor;
+ float size = HandleUtility.GetHandleSize(waypoints[i].position) * WaypointHandleSize;
+
+ EditorGUI.BeginChangeCheck();
+ Vector3 newPos = Handles.FreeMoveHandle(
+ waypoints[i].position,
+ size,
+ Vector3.zero,
+ Handles.SphereHandleCap);
+
+ if (EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObject(waypoints[i], "Move Waypoint");
+ waypoints[i].position = newPos;
+ }
+ }
+ }
+
+ private void DrawBezierCurve(NotePathBehaviour behaviour, Transform[] waypoints)
+ {
+ Handles.color = PathColor;
+ Vector3 prev = waypoints[0].position;
+ for (int i = 1; i <= CurveSampleCount; i++)
+ {
+ float t = i / (float)CurveSampleCount;
+ Vector3 pos = behaviour.EvaluatePosition(t);
+ Handles.DrawLine(prev, pos);
+ prev = pos;
+ }
+ }
+
+ private void DrawEasedPathSamples(NotePathBehaviour behaviour)
+ {
+ Handles.color = EasedPathColor;
+ const int dotCount = 16;
+ for (int i = 0; i <= dotCount; i++)
+ {
+ float t = i / (float)dotCount;
+ Vector3 pos = behaviour.EvaluatePosition(t);
+ float size = HandleUtility.GetHandleSize(pos) * 0.06f;
+ Handles.DrawSolidDisc(pos, Camera.current != null ? -Camera.current.transform.forward : Vector3.forward, size);
+ }
+ }
+
+ private void DrawEndpointLabels(NotePathBehaviour behaviour)
+ {
+ GUIStyle spawnStyle = new GUIStyle();
+ spawnStyle.normal.textColor = SpawnColor;
+ spawnStyle.fontStyle = FontStyle.Bold;
+
+ GUIStyle hitStyle = new GUIStyle();
+ hitStyle.normal.textColor = HitZoneColor;
+ hitStyle.fontStyle = FontStyle.Bold;
+
+ Handles.Label(behaviour.SpawnPosition + Vector3.up * 0.3f, "Spawn", spawnStyle);
+ Handles.Label(behaviour.HitZonePosition + Vector3.up * 0.3f, "Hit Zone", hitStyle);
+
+ Handles.color = SpawnColor;
+ float spawnSize = HandleUtility.GetHandleSize(behaviour.SpawnPosition) * EndpointSphereSize;
+ Handles.DrawSolidDisc(behaviour.SpawnPosition,
+ Camera.current != null ? -Camera.current.transform.forward : Vector3.forward,
+ spawnSize);
+
+ Handles.color = HitZoneColor;
+ float hitSize = HandleUtility.GetHandleSize(behaviour.HitZonePosition) * EndpointSphereSize;
+ Handles.DrawSolidDisc(behaviour.HitZonePosition,
+ Camera.current != null ? -Camera.current.transform.forward : Vector3.forward,
+ hitSize);
+ }
+
+ #endregion
+}
+#endif
diff --git a/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta
new file mode 100644
index 0000000..65d566f
--- /dev/null
+++ b/Assets/Rhythm/Editor/NotePathBehaviourEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dcf7c727336299f43b249b15434cb289
\ No newline at end of file
diff --git a/Assets/Rhythm/Editor/SongManifestEditor.cs b/Assets/Rhythm/Editor/SongManifestEditor.cs
new file mode 100644
index 0000000..6ed9582
--- /dev/null
+++ b/Assets/Rhythm/Editor/SongManifestEditor.cs
@@ -0,0 +1,23 @@
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEngine;
+
+/**
+ * Custom inspector for . Adds a prominent
+ * "Open in Beatmap Editor" button that loads the manifest and auto-activates
+ * its first difficulty tab.
+ *
+ */
+[CustomEditor(typeof(SongManifest))]
+public class SongManifestEditor : Editor
+{
+ public override void OnInspectorGUI()
+ {
+ if (GUILayout.Button("Open in Beatmap Editor", GUILayout.Height(28f)))
+ BeatmapEditorWindow.OpenWithManifest((SongManifest)target);
+
+ EditorGUILayout.Space(4f);
+ DrawDefaultInspector();
+ }
+}
+#endif
diff --git a/Assets/Rhythm/Editor/SongManifestEditor.cs.meta b/Assets/Rhythm/Editor/SongManifestEditor.cs.meta
new file mode 100644
index 0000000..6a56522
--- /dev/null
+++ b/Assets/Rhythm/Editor/SongManifestEditor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 66c34fa5a92532e42ab380c35d77b9c7
\ No newline at end of file
diff --git a/Assets/Rhythm/Input.meta b/Assets/Rhythm/Input.meta
new file mode 100644
index 0000000..7d83f7f
--- /dev/null
+++ b/Assets/Rhythm/Input.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e1a0f952eed56ec4097e7d8a257fa359
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Input/IRhythmInputProvider.cs b/Assets/Rhythm/Input/IRhythmInputProvider.cs
new file mode 100644
index 0000000..497db71
--- /dev/null
+++ b/Assets/Rhythm/Input/IRhythmInputProvider.cs
@@ -0,0 +1,29 @@
+using System;
+
+/**
+ * Input abstraction for lane-based rhythm input. Lane indices correspond to
+ * indices.
+ *
+ */
+public interface IRhythmInputProvider
+{
+ /**
+ * Fired when a player presses a lane key.
+ * Argument is the zero-based lane index.
+ *
+ */
+ event Action OnLanePressed;
+
+ /**
+ * Fired when a player releases a lane key. Used for hold note tracking.
+ * Argument is the zero-based lane index.
+ *
+ */
+ event Action OnLaneReleased;
+
+ /** Enables input event forwarding. */
+ void Enable();
+
+ /** Disables input event forwarding without destroying the provider. */
+ void Disable();
+}
diff --git a/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta b/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta
new file mode 100644
index 0000000..2bb24c9
--- /dev/null
+++ b/Assets/Rhythm/Input/IRhythmInputProvider.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: be102ba49b6449e4bb655bec938526f4
\ No newline at end of file
diff --git a/Assets/Rhythm/Input/UnityRhythmInputProvider.cs b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs
new file mode 100644
index 0000000..41a8805
--- /dev/null
+++ b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs
@@ -0,0 +1,96 @@
+using System;
+using UnityEngine.InputSystem;
+
+/**
+ * implementation for standalone lane-based
+ * rhythm games. Creates one per lane using the Unity
+ * Input System. Default keyboard bindings are D / F / J / K for lanes 0–3.
+ * Pass custom binding paths to the constructor to override.
+ *
+ */
+public class UnityRhythmInputProvider : IRhythmInputProvider
+{
+ #region Private Events
+
+ private event Action _onLanePressed;
+ private event Action _onLaneReleased;
+
+ #endregion
+
+ #region IRhythmInputProvider
+
+ /** */
+ event Action IRhythmInputProvider.OnLanePressed
+ {
+ add => _onLanePressed += value;
+ remove => _onLanePressed -= value;
+ }
+
+ /** */
+ event Action IRhythmInputProvider.OnLaneReleased
+ {
+ add => _onLaneReleased += value;
+ remove => _onLaneReleased -= value;
+ }
+
+ /** */
+ public void Enable()
+ {
+ foreach (var action in _laneActions)
+ action.Enable();
+ }
+
+ /** */
+ public void Disable()
+ {
+ foreach (var action in _laneActions)
+ action.Disable();
+ }
+
+ #endregion
+
+ #region Fields
+
+ private readonly InputAction[] _laneActions;
+
+ private static readonly string[] DefaultBindings =
+ {
+ "/d",
+ "/f",
+ "/j",
+ "/k"
+ };
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates input actions for each lane with default or custom key bindings.
+ *
+ * Number of lanes to create actions for.
+ *
+ * Optional per-lane Unity InputSystem binding path strings
+ * (e.g. "<Keyboard>/space"). Indices beyond this array fall back to
+ * the defaults (D / F / J / K).
+ *
+ */
+ public UnityRhythmInputProvider(int laneCount = 4, string[] bindingPaths = null)
+ {
+ _laneActions = new InputAction[laneCount];
+ for (int i = 0; i < laneCount; i++)
+ {
+ string binding = bindingPaths != null && i < bindingPaths.Length
+ ? bindingPaths[i]
+ : (i < DefaultBindings.Length ? DefaultBindings[i] : null);
+
+ _laneActions[i] = new InputAction($"Lane{i}", InputActionType.Button, binding);
+
+ int laneIndex = i;
+ _laneActions[i].performed += _ => _onLanePressed?.Invoke(laneIndex);
+ _laneActions[i].canceled += _ => _onLaneReleased?.Invoke(laneIndex);
+ }
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta
new file mode 100644
index 0000000..0bbaffa
--- /dev/null
+++ b/Assets/Rhythm/Input/UnityRhythmInputProvider.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1815caa93d35a0744878495403912889
\ No newline at end of file
diff --git a/Assets/Rhythm/Integration.meta b/Assets/Rhythm/Integration.meta
new file mode 100644
index 0000000..2275b38
--- /dev/null
+++ b/Assets/Rhythm/Integration.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 39972456b2068d445b71dccc940591d1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Integration/IRhythmGameAdapter.cs b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs
new file mode 100644
index 0000000..9fe9995
--- /dev/null
+++ b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs
@@ -0,0 +1,54 @@
+/**
+ * Portability contract for any game system that wants to receive rhythm
+ * events without depending directly on .
+ *
+ */
+public interface IRhythmGameAdapter
+{
+ /**
+ * A note has crossed its spawn threshold and is heading toward the hit zone.
+ * Use this to prepare game-side state before the player must act.
+ *
+ * DSP timing and note data for the spawned note.
+ */
+ void OnNoteScheduled(ScheduledNoteInfo info);
+
+ /**
+ * A note was hit by the player. Rating and timing error are in
+ * .
+ *
+ * Full hit evaluation result.
+ */
+ void OnHit(HitResult result);
+
+ /**
+ * A note expired past the Ok window without being hit.
+ *
+ * The note that was missed.
+ */
+ void OnMiss(NoteData note);
+
+ /**
+ * A tagged key point beat was crossed. Inspect
+ * to determine the event type and
+ * for structured payload.
+ *
+ * The key point that was reached.
+ */
+ void OnKeyPoint(KeyPointData keyPoint);
+
+ /**
+ * A whole beat boundary was crossed. Useful for visual pulses,
+ * environment beat-sync effects, and metronome feedback.
+ *
+ * Beat index (0, 1, 2, …) that was just entered.
+ */
+ void OnBeatPulse(double beat);
+
+ /**
+ * Song playback reached its end. Perform result screen transitions or
+ * cleanup here.
+ *
+ */
+ void OnSongComplete();
+}
diff --git a/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta
new file mode 100644
index 0000000..30eb7a7
--- /dev/null
+++ b/Assets/Rhythm/Integration/IRhythmGameAdapter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4d35d623502a1b74aaca48b8ade77d2a
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual.meta b/Assets/Rhythm/Visual.meta
new file mode 100644
index 0000000..8adc506
--- /dev/null
+++ b/Assets/Rhythm/Visual.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 80805a8056a52d040b759c09349844bb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs
new file mode 100644
index 0000000..a4106a6
--- /dev/null
+++ b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs
@@ -0,0 +1,27 @@
+using Cysharp.Threading.Tasks;
+
+/**
+ * No-op base implementation of . Every method
+ * returns immediately.
+ *
+ */
+public class DefaultVisualRhythmHooks : IVisualRhythmHooks
+{
+ /** */
+ public virtual UniTask OnNoteSpawned(IRhythmNoteView view) => UniTask.CompletedTask;
+
+ /** */
+ public virtual UniTask OnNoteUpdated(IRhythmNoteView view, float headT) => UniTask.CompletedTask;
+
+ /** */
+ public virtual UniTask OnNoteHit(IRhythmNoteView view, HitResult result) => UniTask.CompletedTask;
+
+ /** */
+ public virtual UniTask OnNoteMissed(IRhythmNoteView view, NoteData note) => UniTask.CompletedTask;
+
+ /** */
+ public virtual UniTask OnBeatPulse(double beat) => UniTask.CompletedTask;
+
+ /** */
+ public virtual UniTask OnKeyPointReached(KeyPointData keyPoint) => UniTask.CompletedTask;
+}
diff --git a/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta
new file mode 100644
index 0000000..210c1c5
--- /dev/null
+++ b/Assets/Rhythm/Visual/DefaultVisualRhythmHooks.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3588d4b877963e54da8874cada6f68a6
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual/IRhythmNoteView.cs b/Assets/Rhythm/Visual/IRhythmNoteView.cs
new file mode 100644
index 0000000..41771fe
--- /dev/null
+++ b/Assets/Rhythm/Visual/IRhythmNoteView.cs
@@ -0,0 +1,57 @@
+using Cysharp.Threading.Tasks;
+
+/**
+ * Contract for any MonoBehaviour that represents a note on screen. Swap
+ * implementations to change note visual style without touching
+ * .
+ *
+ */
+public interface IRhythmNoteView
+{
+ /** Zero-based lane index this note belongs to. */
+ int Lane { get; }
+
+ /** The scheduled info this view was initialised with. */
+ ScheduledNoteInfo Info { get; }
+
+ /**
+ * Called once immediately after the view is taken from the pool. Sets up
+ * the path, colours, and initial state.
+ *
+ * DSP timing and note data.
+ * Path behaviour for this lane.
+ * Visual configuration asset.
+ */
+ void Initialize(ScheduledNoteInfo info, NotePathBehaviour path, RhythmVisualConfig config);
+
+ /**
+ * Called every frame while the note is active. Move the note head to
+ * path.EvaluatePosition(headT) and stretch hold tail if applicable.
+ *
+ * Normalised progress of the note head [0, 1]. 0 = spawn, 1 = hit zone.
+ * Normalised progress of the hold tail end. Equal to headT for Normal notes.
+ */
+ void UpdateProgress(float headT, float tailT);
+
+ /**
+ * Plays the hit feedback animation for the given rating. The note view is
+ * typically returned to the pool after this UniTask completes.
+ *
+ * Rating that determines the flash colour.
+ */
+ UniTask PlayHitEffect(HitRating rating);
+
+ /**
+ * Plays the miss animation (fade out, fall off screen, etc.) then calls
+ * automatically when complete.
+ *
+ */
+ UniTask PlayMissEffect();
+
+ /**
+ * Returns this view to the pool or destroys it. Called by the visualizer
+ * controller when a note is finished.
+ *
+ */
+ void Despawn();
+}
diff --git a/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta b/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta
new file mode 100644
index 0000000..7caf0d2
--- /dev/null
+++ b/Assets/Rhythm/Visual/IRhythmNoteView.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 66d4729ad3f454a45b18e3c1bef1c7a3
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual/IVisualRhythmHooks.cs b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs
new file mode 100644
index 0000000..5c08aa4
--- /dev/null
+++ b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs
@@ -0,0 +1,59 @@
+using Cysharp.Threading.Tasks;
+
+/**
+ * Async hook interface for injecting visual and audio effects into the rhythm
+ * visualizer flow.
+ *
+ */
+public interface IVisualRhythmHooks
+{
+ /**
+ * Called when a note view is spawned and positioned at its spawn point.
+ * Await to delay further processing (e.g. play a spawn animation).
+ *
+ * The note view that was spawned.
+ */
+ UniTask OnNoteSpawned(IRhythmNoteView view);
+
+ /**
+ * Called every frame while a note is active, before
+ * is called.
+ * Keep implementations lightweight — this fires every frame per active note.
+ *
+ * The active note view.
+ * Current normalised progress [0, 1] of the note head.
+ */
+ UniTask OnNoteUpdated(IRhythmNoteView view, float headT);
+
+ /**
+ * Called when a note is hit by the player, before the note view's own hit
+ * effect runs. Await to sequence effects.
+ *
+ * The note view that was hit.
+ * The full hit result including rating and timing error.
+ */
+ UniTask OnNoteHit(IRhythmNoteView view, HitResult result);
+
+ /**
+ * Called when a note expires without being hit.
+ *
+ * The note view that was missed.
+ * The note data of the missed note.
+ */
+ UniTask OnNoteMissed(IRhythmNoteView view, NoteData note);
+
+ /**
+ * Called each time a whole-beat boundary is crossed.
+ *
+ * The beat index that was just entered.
+ */
+ UniTask OnBeatPulse(double beat);
+
+ /**
+ * Called when a tagged key point beat is crossed.
+ * Inspect to determine the event type.
+ *
+ * The key point that was reached.
+ */
+ UniTask OnKeyPointReached(KeyPointData keyPoint);
+}
diff --git a/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta
new file mode 100644
index 0000000..c72ee35
--- /dev/null
+++ b/Assets/Rhythm/Visual/IVisualRhythmHooks.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6348a894b5398e549b444dfc2519a634
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual/NotePathBehaviour.cs b/Assets/Rhythm/Visual/NotePathBehaviour.cs
new file mode 100644
index 0000000..dfdfdb5
--- /dev/null
+++ b/Assets/Rhythm/Visual/NotePathBehaviour.cs
@@ -0,0 +1,145 @@
+using UnityEngine;
+
+/**
+ * Defines the 3D path a note travels from its spawn position to the hit zone.
+ * Control points are child objects so they can be
+ * dragged directly in the Scene view. The path is evaluated via De Casteljau
+ * cubic Bezier with an remapping progress before
+ * sampling, decoupling path shape from movement velocity profile.
+ *
+ */
+public class NotePathBehaviour : MonoBehaviour
+{
+ #region Inspector Fields
+
+ [Tooltip("Control point transforms. First = spawn position, last = hit zone. " +
+ "Use 4 points for a cubic Bezier. Drag these in the Scene view to adjust the path.")]
+ [SerializeField] private Transform[] _waypoints;
+
+ [Tooltip("Remaps linear progress t (0–1) before Bezier sampling. " +
+ "X axis = linear t, Y axis = remapped t. Default is linear (no remap).")]
+ [SerializeField] private AnimationCurve _easeCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
+
+ [Tooltip("Draw the path curve in play mode for debugging.")]
+ [SerializeField] private bool _drawGizmosInPlayMode;
+
+ #endregion
+
+ #region Public Properties
+
+ /** World position of the spawn point (t = 0). */
+ public Vector3 SpawnPosition => EvaluatePosition(0f);
+
+ /** World position of the hit zone (t = 1). */
+ public Vector3 HitZonePosition => EvaluatePosition(1f);
+
+ /** The configured waypoint transforms. */
+ public Transform[] Waypoints => _waypoints;
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * Evaluates the world-space position at normalised progress .
+ * The ease curve is applied to before sampling the
+ * Bezier, so path shape and velocity profile are independently controllable.
+ *
+ * Linear progress in [0, 1]. 0 = spawn, 1 = hit zone.
+ * World-space Vector3 position at the given progress.
+ */
+ public Vector3 EvaluatePosition(float t)
+ {
+ if (_waypoints == null || _waypoints.Length == 0) return transform.position;
+
+ float easedT = _easeCurve.Evaluate(Mathf.Clamp01(t));
+ Vector3[] positions = GetWaypointPositions();
+ return EvaluateBezier(positions, easedT);
+ }
+
+ #endregion
+
+ #region Context Menu Helpers
+
+ /**
+ * Populates from all direct child transforms in
+ * hierarchy order. Invoke from the component context menu in the Inspector.
+ *
+ */
+ [ContextMenu("Collect Children As Waypoints")]
+ public void CollectChildrenAsWaypoints()
+ {
+ _waypoints = new Transform[transform.childCount];
+ for (int i = 0; i < transform.childCount; i++)
+ _waypoints[i] = transform.GetChild(i);
+ }
+
+ #endregion
+
+ #region Gizmos
+
+ private void OnDrawGizmosSelected()
+ {
+ DrawPathGizmo();
+ }
+
+ private void OnDrawGizmos()
+ {
+ if (_drawGizmosInPlayMode && Application.isPlaying)
+ DrawPathGizmo();
+ }
+
+ private void DrawPathGizmo()
+ {
+ if (_waypoints == null || _waypoints.Length < 2) return;
+
+ Vector3[] positions = GetWaypointPositions();
+
+ Gizmos.color = Color.cyan;
+ const int segments = 24;
+ Vector3 prev = EvaluateBezier(positions, 0f);
+ for (int i = 1; i <= segments; i++)
+ {
+ float t = i / (float)segments;
+ Vector3 next = EvaluateBezier(positions, t);
+ Gizmos.DrawLine(prev, next);
+ prev = next;
+ }
+
+ Gizmos.color = Color.green;
+ Gizmos.DrawSphere(positions[0], 0.1f);
+
+ Gizmos.color = Color.red;
+ Gizmos.DrawSphere(positions[positions.Length - 1], 0.1f);
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private Vector3[] GetWaypointPositions()
+ {
+ var positions = new Vector3[_waypoints.Length];
+ for (int i = 0; i < _waypoints.Length; i++)
+ positions[i] = _waypoints[i] != null ? _waypoints[i].position : transform.position;
+ return positions;
+ }
+
+ /**
+ * De Casteljau algorithm: repeatedly lerps between adjacent points until
+ * one point remains. Works for any number of control points (linear,
+ * quadratic, cubic, higher-order).
+ *
+ */
+ private static Vector3 EvaluateBezier(Vector3[] pts, float t)
+ {
+ Vector3[] work = (Vector3[])pts.Clone();
+ int n = work.Length - 1;
+ for (int r = 1; r <= n; r++)
+ for (int i = 0; i <= n - r; i++)
+ work[i] = Vector3.LerpUnclamped(work[i], work[i + 1], t);
+ return work[0];
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta b/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta
new file mode 100644
index 0000000..ca46d9a
--- /dev/null
+++ b/Assets/Rhythm/Visual/NotePathBehaviour.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 84f4dc4f60fba7e4db3098b36887c32b
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual/RhythmNoteView.cs b/Assets/Rhythm/Visual/RhythmNoteView.cs
new file mode 100644
index 0000000..23c3cdc
--- /dev/null
+++ b/Assets/Rhythm/Visual/RhythmNoteView.cs
@@ -0,0 +1,158 @@
+using Cysharp.Threading.Tasks;
+using PrimeTween;
+using UnityEngine;
+
+/**
+ * Concrete MonoBehaviour implementation of .
+ *
+ */
+public class RhythmNoteView : MonoBehaviour, IRhythmNoteView
+{
+ #region Inspector Fields
+
+ [Tooltip("Renderer for the note head (MeshRenderer, SkinnedMeshRenderer, etc.). " +
+ "Leave null if color is handled by child objects or materials directly.")]
+ [SerializeField] private Renderer _noteRenderer;
+
+ [Tooltip("LineRenderer that draws the hold trail between head and tail positions. " +
+ "Leave null for Normal note prefabs or if no trail is desired.")]
+ [SerializeField] private LineRenderer _holdTrail;
+
+ [Tooltip("Particle system burst played on hit. Leave null to skip.")]
+ [SerializeField] private ParticleSystem _hitParticles;
+
+ [Tooltip("Duration of the hit scale punch animation in seconds.")]
+ [SerializeField] private float _hitPunchDuration = 0.12f;
+
+ [Tooltip("Peak scale during the hit punch animation.")]
+ [SerializeField] private Vector3 _hitPunchScale = new Vector3(1.5f, 1.5f, 1.5f);
+
+ [Tooltip("Duration of the miss shrink-to-zero animation in seconds.")]
+ [SerializeField] private float _missFadeDuration = 0.2f;
+
+ #endregion
+
+ #region Private State
+
+ private ScheduledNoteInfo _info;
+ private NotePathBehaviour _path;
+ private RhythmVisualConfig _config;
+ private System.Action _despawnCallback;
+
+ private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
+ private MaterialPropertyBlock _propBlock;
+
+ #endregion
+
+ #region IRhythmNoteView
+
+ /** */
+ public int Lane => _info.Note.Lane;
+
+ /** */
+ public ScheduledNoteInfo Info => _info;
+
+ /** */
+ public void Initialize(ScheduledNoteInfo info, NotePathBehaviour path, RhythmVisualConfig config)
+ {
+ _info = info;
+ _path = path;
+ _config = config;
+
+ _propBlock ??= new MaterialPropertyBlock();
+
+ Color baseColor = info.Note.NoteType == NoteType.Hold
+ ? (config != null ? config.HoldNoteColor : Color.white)
+ : (config != null ? config.NormalNoteColor : Color.white);
+
+ SetRendererColor(baseColor);
+
+ if (_holdTrail != null)
+ {
+ bool isHold = info.Note.NoteType == NoteType.Hold;
+ _holdTrail.enabled = isHold;
+ if (isHold)
+ {
+ _holdTrail.positionCount = 2;
+ Vector3 spawnPos = path != null ? path.EvaluatePosition(0f) : transform.position;
+ _holdTrail.SetPosition(0, spawnPos);
+ _holdTrail.SetPosition(1, spawnPos);
+ }
+ }
+
+ transform.position = path != null ? path.EvaluatePosition(0f) : transform.position;
+ transform.localScale = Vector3.one;
+ }
+
+ /** */
+ public void UpdateProgress(float headT, float tailT)
+ {
+ if (_path == null) return;
+
+ transform.position = _path.EvaluatePosition(headT);
+
+ if (_holdTrail != null && _holdTrail.enabled)
+ {
+ _holdTrail.SetPosition(0, transform.position);
+ _holdTrail.SetPosition(1, _path.EvaluatePosition(tailT));
+ }
+ }
+
+ /** */
+ public async UniTask PlayHitEffect(HitRating rating)
+ {
+ Color flashColor = _config != null ? _config.GetHitFlashColor(rating) : Color.white;
+ SetRendererColor(flashColor);
+
+ if (_hitParticles != null) _hitParticles.Play();
+
+ await Tween.Scale(transform, _hitPunchScale, _hitPunchDuration * 0.5f, Ease.OutBack);
+ await Tween.Scale(transform, Vector3.zero, _hitPunchDuration * 0.5f, Ease.InCubic);
+
+ Despawn();
+ }
+
+ /** */
+ public async UniTask PlayMissEffect()
+ {
+ Color missColor = _config != null ? _config.GetHitFlashColor(HitRating.Miss) : Color.red;
+ SetRendererColor(missColor);
+ await Tween.Scale(transform, Vector3.zero, _missFadeDuration, Ease.InCubic);
+ Despawn();
+ }
+
+ /** */
+ public void Despawn()
+ {
+ Tween.StopAll(gameObject);
+ gameObject.SetActive(false);
+ _despawnCallback?.Invoke();
+ }
+
+ #endregion
+
+ #region Public Helpers
+
+ /**
+ * Registers the callback invoked when is called.
+ * The visualizer controller uses this to return the view to its pool.
+ *
+ */
+ public void SetDespawnCallback(System.Action callback)
+ {
+ _despawnCallback = callback;
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private void SetRendererColor(Color color)
+ {
+ if (_noteRenderer == null) return;
+ _propBlock.SetColor(BaseColorId, color);
+ _noteRenderer.SetPropertyBlock(_propBlock);
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Visual/RhythmNoteView.cs.meta b/Assets/Rhythm/Visual/RhythmNoteView.cs.meta
new file mode 100644
index 0000000..582dc06
--- /dev/null
+++ b/Assets/Rhythm/Visual/RhythmNoteView.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 913c914734a0019418f6d86bc7752916
\ No newline at end of file
diff --git a/Assets/Rhythm/Visual/RhythmVisualizerController.cs b/Assets/Rhythm/Visual/RhythmVisualizerController.cs
new file mode 100644
index 0000000..55a079a
--- /dev/null
+++ b/Assets/Rhythm/Visual/RhythmVisualizerController.cs
@@ -0,0 +1,384 @@
+using System.Collections.Generic;
+using Cysharp.Threading.Tasks;
+using UnityEngine;
+
+/**
+ * MonoBehaviour bridge between events and
+ * objects. Manages two object pools (Normal and
+ * Hold), updates all active note positions every frame using live DSP time,
+ * and forwards events to an implementation
+ * for additional visual effects.
+ *
+ */
+public class RhythmVisualizerController : MonoBehaviour
+{
+ #region Inspector Fields
+
+ [Tooltip("Visual configuration asset (colours, prefabs, hit zone settings).")]
+ [SerializeField] private RhythmVisualConfig _visualConfig;
+
+ [Tooltip("One NotePathBehaviour per lane. Index must match the lane index in NoteData.")]
+ [SerializeField] private NotePathBehaviour[] _lanePaths;
+
+ [Tooltip("Parent transform under which note view GameObjects are instantiated.")]
+ [SerializeField] private Transform _noteContainer;
+
+ [Header("Mirror Paths")]
+ [Tooltip("Optional mirror path per lane (parallel array to Lane Paths). " +
+ "When assigned for a lane, every note on that lane spawns a second " +
+ "visual-only note on the mirror path — no rhythm logic involved.")]
+ [SerializeField] private NotePathBehaviour[] _mirrorPaths;
+
+ #endregion
+
+ #region Private State
+
+ private RhythmManager _rhythmManager;
+ private IVisualRhythmHooks _hooks;
+
+ private readonly List _activeNotes = new List();
+ private readonly Dictionary _mirrorViews = new Dictionary();
+
+ private readonly Queue _normalPool = new Queue();
+ private readonly Queue _holdPool = new Queue();
+ private readonly Queue _mirrorNormalPool = new Queue();
+ private readonly Queue _mirrorHoldPool = new Queue();
+
+ #endregion
+
+ #region Public API
+
+ /**
+ * The look-ahead window (in beats) read from the visual config. Consumed by
+ * the owner (e.g. ) when constructing
+ * so both systems use the same value.
+ *
+ */
+ public float NoteAppearanceBeats => _visualConfig != null ? _visualConfig.NoteAppearanceBeats : 2f;
+
+ #endregion
+
+ #region Initialisation
+
+ /**
+ * Wires the visualizer to a . Must be called
+ * once after the manager is constructed. Subscribes to all events and
+ * pre-warms the note pools.
+ *
+ * The rhythm manager to visualize.
+ * Optional animation hooks. Defaults to no-op if null.
+ */
+ public void Initialize(RhythmManager manager, IVisualRhythmHooks hooks = null)
+ {
+ _rhythmManager = manager;
+ _hooks = hooks ?? new DefaultVisualRhythmHooks();
+
+ int normalSize = _visualConfig != null ? _visualConfig.NormalPoolSize : 16;
+ int holdSize = _visualConfig != null ? _visualConfig.HoldPoolSize : 8;
+
+ PrewarmPool(_visualConfig.NormalNotePrefab, _normalPool, normalSize);
+ PrewarmPool(_visualConfig.HoldNotePrefab, _holdPool, holdSize);
+
+ if (HasAnyMirrorPath())
+ {
+ PrewarmPool(_visualConfig.NormalNotePrefab, _mirrorNormalPool, normalSize);
+ PrewarmPool(_visualConfig.HoldNotePrefab, _mirrorHoldPool, holdSize);
+ }
+
+ manager.OnNoteScheduled += OnNoteScheduled;
+ manager.OnNoteHit += OnNoteHit;
+ manager.OnNoteMissed += OnNoteMissed;
+ manager.OnBeatPulse += OnBeatPulse;
+ manager.OnKeyPointReached += OnKeyPointReached;
+ manager.OnSongComplete += OnSongComplete;
+ }
+
+ #endregion
+
+ #region Unity Lifecycle
+
+ private void Update()
+ {
+ _rhythmManager?.Tick();
+ UpdateAllNotes();
+ }
+
+ private void OnDestroy()
+ {
+ if (_rhythmManager == null) return;
+ _rhythmManager.OnNoteScheduled -= OnNoteScheduled;
+ _rhythmManager.OnNoteHit -= OnNoteHit;
+ _rhythmManager.OnNoteMissed -= OnNoteMissed;
+ _rhythmManager.OnBeatPulse -= OnBeatPulse;
+ _rhythmManager.OnKeyPointReached -= OnKeyPointReached;
+ _rhythmManager.OnSongComplete -= OnSongComplete;
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void OnNoteScheduled(ScheduledNoteInfo info)
+ {
+ IRhythmNoteView view = SpawnNote(info);
+ if (view == null) return;
+
+ _activeNotes.Add(view);
+
+ RhythmNoteView mirror = SpawnMirrorNote(info);
+ if (mirror != null)
+ _mirrorViews[view] = mirror;
+
+ _hooks.OnNoteSpawned(view).Forget();
+ }
+
+ private void OnNoteHit(HitResult result)
+ {
+ IRhythmNoteView view = FindActiveView(result.Note);
+ if (view == null) return;
+
+ _activeNotes.Remove(view);
+ RhythmNoteView mirror = ConsumeMirror(view);
+ PlayHitEffectChain(view, result, mirror).Forget();
+ }
+
+ private void OnNoteMissed(NoteData note)
+ {
+ IRhythmNoteView view = FindActiveView(note);
+ if (view == null) return;
+
+ _activeNotes.Remove(view);
+ RhythmNoteView mirror = ConsumeMirror(view);
+ PlayMissEffectChain(view, note, mirror).Forget();
+ }
+
+ private async UniTaskVoid PlayHitEffectChain(IRhythmNoteView view, HitResult result, RhythmNoteView mirror)
+ {
+ await _hooks.OnNoteHit(view, result);
+ view.PlayHitEffect(result.Rating).Forget();
+ mirror?.PlayHitEffect(result.Rating).Forget();
+ SpawnFeedback(result.Note.Lane, _visualConfig.GetFeedbackPrefab(result.Rating));
+ }
+
+ private async UniTaskVoid PlayMissEffectChain(IRhythmNoteView view, NoteData note, RhythmNoteView mirror)
+ {
+ await _hooks.OnNoteMissed(view, note);
+ view.PlayMissEffect().Forget();
+ mirror?.PlayMissEffect().Forget();
+ SpawnFeedback(note.Lane, _visualConfig != null ? _visualConfig.FeedbackMissPrefab : null);
+ }
+
+ private void OnBeatPulse(double beat)
+ {
+ _hooks.OnBeatPulse(beat).Forget();
+ }
+
+ private void OnKeyPointReached(KeyPointData keyPoint)
+ {
+ _hooks.OnKeyPointReached(keyPoint).Forget();
+ }
+
+ private void OnSongComplete() { }
+
+ #endregion
+
+ #region Note Management
+
+ private void UpdateAllNotes()
+ {
+ double dspNow = AudioSettings.dspTime;
+
+ for (int i = _activeNotes.Count - 1; i >= 0; i--)
+ {
+ IRhythmNoteView view = _activeNotes[i];
+ ScheduledNoteInfo info = view.Info;
+
+ double travelDuration = info.HitDspTime - info.SpawnDspTime;
+ if (travelDuration <= 0.0) continue;
+
+ /* Safety net: OnNoteMissed may not have fired or FindActiveView may have
+ failed to match — force-despawn anything past the grace window. */
+ double cleanupGrace = _visualConfig != null ? _visualConfig.MissCleanupGraceSeconds : 0.5f;
+ if (dspNow > info.HitDspTime + cleanupGrace)
+ {
+ _activeNotes.RemoveAt(i);
+ RhythmNoteView staleMirror = ConsumeMirror(view);
+ view.PlayMissEffect().Forget();
+ staleMirror?.PlayMissEffect().Forget();
+ continue;
+ }
+
+ /* headT: 0 at spawn, 1 when note reaches hit zone */
+ float headT = Mathf.Clamp01((float)((dspNow - info.SpawnDspTime) / travelDuration));
+
+ float tailT = headT;
+ if (info.Note.NoteType == NoteType.Hold && info.HoldEndDspTime > info.HitDspTime)
+ {
+ double holdDuration = info.HoldEndDspTime - info.SpawnDspTime;
+ tailT = Mathf.Clamp01((float)((dspNow - info.SpawnDspTime) / holdDuration));
+ }
+
+ _hooks.OnNoteUpdated(view, headT).Forget();
+ view.UpdateProgress(headT, tailT);
+
+ if (_mirrorViews.TryGetValue(view, out RhythmNoteView mirror))
+ mirror.UpdateProgress(headT, tailT);
+ }
+ }
+
+ private IRhythmNoteView SpawnNote(ScheduledNoteInfo info)
+ {
+ bool isHold = info.Note.NoteType == NoteType.Hold;
+ RhythmNoteView view = BorrowFromPool(isHold ? _holdPool : _normalPool,
+ isHold ? _visualConfig.HoldNotePrefab
+ : _visualConfig.NormalNotePrefab);
+ if (view == null) return null;
+
+ NotePathBehaviour path = GetLanePath(info.Note.Lane);
+ view.Initialize(info, path, _visualConfig);
+ view.SetDespawnCallback(() => ReturnToPool(view));
+ return view;
+ }
+
+ private IRhythmNoteView FindActiveView(NoteData note)
+ {
+ for (int i = 0; i < _activeNotes.Count; i++)
+ {
+ if (_activeNotes[i].Info.Note.EditorId == note.EditorId &&
+ _activeNotes[i].Info.Note.Beat == note.Beat &&
+ _activeNotes[i].Info.Note.Lane == note.Lane)
+ return _activeNotes[i];
+ }
+ return null;
+ }
+
+ private RhythmNoteView SpawnMirrorNote(ScheduledNoteInfo info)
+ {
+ NotePathBehaviour mirrorPath = GetMirrorPath(info.Note.Lane);
+ if (mirrorPath == null) return null;
+
+ bool isHold = info.Note.NoteType == NoteType.Hold;
+ RhythmNoteView mirror = BorrowFromPool(isHold ? _mirrorHoldPool : _mirrorNormalPool,
+ isHold ? _visualConfig.HoldNotePrefab
+ : _visualConfig.NormalNotePrefab);
+ if (mirror == null) return null;
+
+ mirror.Initialize(info, mirrorPath, _visualConfig);
+ mirror.SetDespawnCallback(() => ReturnToMirrorPool(mirror));
+ return mirror;
+ }
+
+ private RhythmNoteView ConsumeMirror(IRhythmNoteView primaryView)
+ {
+ if (!_mirrorViews.TryGetValue(primaryView, out RhythmNoteView mirror)) return null;
+ _mirrorViews.Remove(primaryView);
+ return mirror;
+ }
+
+ private NotePathBehaviour GetLanePath(int lane)
+ {
+ if (_lanePaths != null && lane >= 0 && lane < _lanePaths.Length)
+ return _lanePaths[lane];
+ return null;
+ }
+
+ private NotePathBehaviour GetMirrorPath(int lane)
+ {
+ if (_mirrorPaths != null && lane >= 0 && lane < _mirrorPaths.Length)
+ return _mirrorPaths[lane];
+ return null;
+ }
+
+ private bool HasAnyMirrorPath()
+ {
+ if (_mirrorPaths == null) return false;
+ foreach (var p in _mirrorPaths)
+ if (p != null) return true;
+ return false;
+ }
+
+ #endregion
+
+ #region Pooling
+
+ private void PrewarmPool(GameObject prefab, Queue pool, int count)
+ {
+ if (prefab == null) return;
+ for (int i = 0; i < count; i++)
+ {
+ RhythmNoteView view = CreateView(prefab);
+ view.gameObject.SetActive(false);
+ pool.Enqueue(view);
+ }
+ }
+
+ private RhythmNoteView BorrowFromPool(Queue pool, GameObject prefab)
+ {
+ RhythmNoteView view = null;
+
+ while (pool.Count > 0)
+ {
+ view = pool.Dequeue();
+ if (view != null) break;
+ }
+
+ if (view == null)
+ {
+ if (prefab == null) return null;
+ view = CreateView(prefab);
+ }
+
+ view.gameObject.SetActive(true);
+ return view;
+ }
+
+ private void ReturnToPool(RhythmNoteView view)
+ {
+ bool isHold = view.Info.Note.NoteType == NoteType.Hold;
+ Queue pool = isHold ? _holdPool : _normalPool;
+ pool.Enqueue(view);
+ }
+
+ private void ReturnToMirrorPool(RhythmNoteView view)
+ {
+ bool isHold = view.Info.Note.NoteType == NoteType.Hold;
+ Queue pool = isHold ? _mirrorHoldPool : _mirrorNormalPool;
+ pool.Enqueue(view);
+ }
+
+ private RhythmNoteView CreateView(GameObject prefab)
+ {
+ Transform parent = _noteContainer != null ? _noteContainer : transform;
+ GameObject go = Instantiate(prefab, parent);
+ return go.GetComponent();
+ }
+
+ #endregion
+
+ #region Feedback Labels
+
+ private void SpawnFeedback(int lane, GameObject prefab)
+ {
+ if (prefab == null || _visualConfig == null) return;
+ if (_visualConfig.LaneHitZonePositions == null || lane < 0 || lane >= _visualConfig.LaneHitZonePositions.Length) return;
+
+ Transform parent = _noteContainer != null ? _noteContainer : transform;
+ GameObject go = Instantiate(prefab, _visualConfig.LaneHitZonePositions[lane], Quaternion.identity, parent);
+ Destroy(go, 1f);
+ }
+
+ #endregion
+
+ #region Editor Gizmos
+
+ private void OnDrawGizmos()
+ {
+ if (_visualConfig == null || _visualConfig.LaneHitZonePositions == null) return;
+
+ Gizmos.color = new Color(1f, 0.5f, 0f, 0.5f);
+ foreach (Vector3 pos in _visualConfig.LaneHitZonePositions)
+ Gizmos.DrawWireSphere(pos, _visualConfig.HitZoneRadius);
+ }
+
+ #endregion
+}
diff --git a/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta b/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta
new file mode 100644
index 0000000..90fb289
--- /dev/null
+++ b/Assets/Rhythm/Visual/RhythmVisualizerController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7f15a2ff39e3f3c4db49fb4f8409f9e6
\ No newline at end of file
diff --git a/Assets/Scenes/GameScene.unity b/Assets/Scenes/GameScene.unity
index 4ab57a5..434eb97 100644
--- a/Assets/Scenes/GameScene.unity
+++ b/Assets/Scenes/GameScene.unity
@@ -210,7 +210,10 @@ Transform:
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children: []
+ m_Children:
+ - {fileID: 1192756032}
+ - {fileID: 552294209}
+ - {fileID: 1148278824}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &68930445
@@ -301,7 +304,7 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!1 &689797283
+--- !u!1 &195719195
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -309,78 +312,120 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 689797286}
- - component: {fileID: 689797285}
- - component: {fileID: 689797284}
- m_Layer: 0
- m_Name: EventSystem
+ - component: {fileID: 195719196}
+ - component: {fileID: 195719199}
+ - component: {fileID: 195719198}
+ - component: {fileID: 195719197}
+ m_Layer: 5
+ m_Name: Restart
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!114 &689797284
+--- !u!224 &195719196
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 195719195}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1.9, y: 1.9, z: 1.9}
+ m_ConstrainProportionsScale: 1
+ m_Children:
+ - {fileID: 473532651}
+ m_Father: {fileID: 2104414999}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 1, y: 1}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 737.7, y: 236.20001}
+ m_SizeDelta: {x: 160, y: 30}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &195719197
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 689797283}
+ m_GameObject: {fileID: 195719195}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3}
+ m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_SendPointerHoverToParent: 1
- m_MoveRepeatDelay: 0.5
- m_MoveRepeatRate: 0.1
- m_XRTrackingOrigin: {fileID: 0}
- m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
- m_DeselectOnBackgroundClick: 1
- m_PointerBehavior: 0
- m_CursorLockBehavior: 0
- m_ScrollDeltaPerTick: 6
---- !u!114 &689797285
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 195719198}
+ m_OnClick:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!114 &195719198
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 689797283}
+ m_GameObject: {fileID: 195719195}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
- m_FirstSelected: {fileID: 0}
- m_sendNavigationEvents: 1
- m_DragThreshold: 10
---- !u!4 &689797286
-Transform:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &195719199
+CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 689797283}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!1 &934141671
+ m_GameObject: {fileID: 195719195}
+ m_CullTransparentMesh: 1
+--- !u!1 &222589560
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -388,49 +433,74 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 934141673}
- - component: {fileID: 934141672}
- m_Layer: 0
- m_Name: GameController
+ - component: {fileID: 222589561}
+ - component: {fileID: 222589563}
+ - component: {fileID: 222589562}
+ m_Layer: 5
+ m_Name: Handle
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!114 &934141672
+--- !u!224 &222589561
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 222589560}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 546759842}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &222589562
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 934141671}
+ m_GameObject: {fileID: 222589560}
m_Enabled: 1
m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 51e93b1393d71450f89ef7d43b0b2a26, type: 3}
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
- _config: {fileID: 11400000, guid: 5f86e1357cf9047feb3a73b01384300a, type: 2}
- _nodePrefab: {fileID: 2132212214528617362, guid: 47434b2d937064155aca3329574f5c2c, type: 3}
- _cameraRig: {fileID: 68930445}
- _edgeContainer: {fileID: 141288317}
- _errorCountLabel: {fileID: 1625478451}
- _sampleTreeProvider: {fileID: 11400000, guid: 5e742b1b425de4606ad22578dcb5452d, type: 2}
---- !u!4 &934141673
-Transform:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &222589563
+CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 934141671}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!1 &1482817658
+ m_GameObject: {fileID: 222589560}
+ m_CullTransparentMesh: 1
+--- !u!1 &451490097
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -438,120 +508,107 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 1482817661}
- - component: {fileID: 1482817660}
- - component: {fileID: 1482817659}
+ - component: {fileID: 451490098}
+ - component: {fileID: 451490101}
+ - component: {fileID: 451490100}
+ - component: {fileID: 451490099}
m_Layer: 0
- m_Name: Directional Light
+ m_Name: NoteHitSpot
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!114 &1482817659
-MonoBehaviour:
+--- !u!4 &451490098
+Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1482817658}
+ m_GameObject: {fileID: 451490097}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 0.1, z: 0.2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1595041900}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!65 &451490099
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 451490097}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
m_Enabled: 1
- m_EditorHideFlags: 0
- m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
- m_Name:
- m_EditorClassIdentifier:
- m_Version: 3
- m_UsePipelineSettings: 1
- m_AdditionalLightsShadowResolutionTier: 2
- m_LightLayerMask: 1
- m_RenderingLayers: 1
- m_CustomShadowLayers: 0
- m_ShadowLayerMask: 1
- m_ShadowRenderingLayers: 1
- m_LightCookieSize: {x: 1, y: 1}
- m_LightCookieOffset: {x: 0, y: 0}
- m_SoftShadowQuality: 0
---- !u!108 &1482817660
-Light:
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &451490100
+MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1482817658}
+ m_GameObject: {fileID: 451490097}
m_Enabled: 1
- serializedVersion: 11
- m_Type: 1
- m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
- m_Intensity: 1
- m_Range: 10
- m_SpotAngle: 30
- m_InnerSpotAngle: 21.80208
- m_CookieSize: 10
- m_Shadows:
- m_Type: 2
- m_Resolution: -1
- m_CustomResolution: -1
- m_Strength: 1
- m_Bias: 0.05
- m_NormalBias: 0.4
- m_NearPlane: 0.2
- m_CullingMatrixOverride:
- e00: 1
- e01: 0
- e02: 0
- e03: 0
- e10: 0
- e11: 1
- e12: 0
- e13: 0
- e20: 0
- e21: 0
- e22: 1
- e23: 0
- e30: 0
- e31: 0
- e32: 0
- e33: 1
- m_UseCullingMatrixOverride: 0
- m_Cookie: {fileID: 0}
- m_DrawHalo: 0
- m_Flare: {fileID: 0}
- m_RenderMode: 0
- m_CullingMask:
- serializedVersion: 2
- m_Bits: 4294967295
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
m_RenderingLayerMask: 1
- m_Lightmapping: 4
- m_LightShadowCasterMode: 0
- m_AreaSize: {x: 1, y: 1}
- m_BounceIntensity: 1
- m_ColorTemperature: 6570
- m_UseColorTemperature: 0
- m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
- m_UseBoundingSphereOverride: 0
- m_UseViewFrustumForShadowCasterCull: 1
- m_ForceVisible: 0
- m_ShadowRadius: 0
- m_ShadowAngle: 0
- m_LightUnit: 1
- m_LuxAtDistance: 1
- m_EnableSpotReflector: 1
---- !u!4 &1482817661
-Transform:
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 792cca3ba846a4217aef4c35be2fd6a8, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &451490101
+MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1482817658}
- serializedVersion: 2
- m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
- m_LocalPosition: {x: 0, y: 3, z: 0}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
---- !u!1 &1625478449
+ m_GameObject: {fileID: 451490097}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!1 &473532650
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -559,42 +616,42 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 1625478450}
- - component: {fileID: 1625478452}
- - component: {fileID: 1625478451}
+ - component: {fileID: 473532651}
+ - component: {fileID: 473532653}
+ - component: {fileID: 473532652}
m_Layer: 5
- m_Name: ErrorCount
+ m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
---- !u!224 &1625478450
+--- !u!224 &473532651
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1625478449}
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_GameObject: {fileID: 473532650}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
- m_Father: {fileID: 2100820702}
+ m_Father: {fileID: 195719196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
- m_AnchorMin: {x: 0, y: 1}
- m_AnchorMax: {x: 0, y: 1}
- m_AnchoredPosition: {x: 109, y: -36}
- m_SizeDelta: {x: 200, y: 50}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
---- !u!114 &1625478451
+--- !u!114 &473532652
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1625478449}
+ m_GameObject: {fileID: 473532650}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
@@ -608,7 +665,9 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_text: 'Errors: '
+ m_text: 'Restart
+
+'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
@@ -617,8 +676,8 @@ MonoBehaviour:
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
- rgba: 4294967295
- m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ rgba: 4281479730
+ m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
@@ -635,15 +694,15 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
- m_fontSize: 36
- m_fontSizeBase: 36
+ m_fontSize: 24
+ m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
- m_HorizontalAlignment: 1
- m_VerticalAlignment: 256
+ m_HorizontalAlignment: 2
+ m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
@@ -680,15 +739,15 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
---- !u!222 &1625478452
+--- !u!222 &473532653
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 1625478449}
+ m_GameObject: {fileID: 473532650}
m_CullTransparentMesh: 1
---- !u!1 &2100820698
+--- !u!1 &515217940
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -696,7 +755,2123 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- - component: {fileID: 2100820702}
+ - component: {fileID: 515217941}
+ m_Layer: 5
+ m_Name: Fill Area
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &515217941
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 515217940}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 2014446948}
+ m_Father: {fileID: 661169485}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.25}
+ m_AnchorMax: {x: 1, y: 0.75}
+ m_AnchoredPosition: {x: 5, y: 0}
+ m_SizeDelta: {x: -20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &546759841
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 546759842}
+ m_Layer: 5
+ m_Name: Handle Slide Area
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &546759842
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 546759841}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 222589561}
+ m_Father: {fileID: 661169485}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: -20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &552294208
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 552294209}
+ m_Layer: 0
+ m_Name: NoteContainer
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &552294209
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 552294208}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: -1, z: 10}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 68930444}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &566812980
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 566812981}
+ - component: {fileID: 566812983}
+ - component: {fileID: 566812982}
+ m_Layer: 5
+ m_Name: Label
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &566812981
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 566812980}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1329169888}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 195.7, y: 61.8}
+ m_SizeDelta: {x: 276, y: 50}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &566812982
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 566812980}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: 'Beat Speed: 2'
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 36
+ m_fontSizeBase: 36
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 18
+ m_fontSizeMax: 72
+ m_fontStyle: 0
+ m_HorizontalAlignment: 4
+ m_VerticalAlignment: 256
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 0
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 0
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: -223.27539, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!222 &566812983
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 566812980}
+ m_CullTransparentMesh: 1
+--- !u!1 &571306883
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 571306884}
+ - component: {fileID: 571306886}
+ - component: {fileID: 571306885}
+ m_Layer: 5
+ m_Name: Handle
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &571306884
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 571306883}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1974904754}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &571306885
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 571306883}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 0
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &571306886
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 571306883}
+ m_CullTransparentMesh: 1
+--- !u!1 &590320708
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 590320709}
+ - component: {fileID: 590320711}
+ - component: {fileID: 590320710}
+ m_Layer: 5
+ m_Name: Background
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &590320709
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 590320708}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1667507392}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.25}
+ m_AnchorMax: {x: 1, y: 0.75}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &590320710
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 590320708}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 0.22745098}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &590320711
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 590320708}
+ m_CullTransparentMesh: 1
+--- !u!1 &661169484
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 661169485}
+ - component: {fileID: 661169486}
+ m_Layer: 5
+ m_Name: Slider
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &661169485
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 661169484}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 2.2, y: 2.2, z: 2.2}
+ m_ConstrainProportionsScale: 1
+ m_Children:
+ - {fileID: 810347529}
+ - {fileID: 515217941}
+ - {fileID: 546759842}
+ m_Father: {fileID: 1329169888}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 176, y: 22}
+ m_SizeDelta: {x: 160, y: 20}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &661169486
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 661169484}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 67db9e8f0e2ae9c40bc1e2b64352a6b4, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 1, g: 1, b: 1, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 222589562}
+ m_FillRect: {fileID: 2014446948}
+ m_HandleRect: {fileID: 222589561}
+ m_Direction: 1
+ m_MinValue: 0
+ m_MaxValue: 1
+ m_WholeNumbers: 0
+ m_Value: 0.669
+ m_OnValueChanged:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!1 &689797283
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 689797286}
+ - component: {fileID: 689797285}
+ - component: {fileID: 689797284}
+ m_Layer: 0
+ m_Name: EventSystem
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &689797284
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 689797283}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_SendPointerHoverToParent: 1
+ m_MoveRepeatDelay: 0.5
+ m_MoveRepeatRate: 0.1
+ m_XRTrackingOrigin: {fileID: 0}
+ m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3}
+ m_DeselectOnBackgroundClick: 1
+ m_PointerBehavior: 0
+ m_CursorLockBehavior: 0
+ m_ScrollDeltaPerTick: 6
+--- !u!114 &689797285
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 689797283}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_FirstSelected: {fileID: 0}
+ m_sendNavigationEvents: 1
+ m_DragThreshold: 10
+--- !u!4 &689797286
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 689797283}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &743136009
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 743136010}
+ m_Layer: 5
+ m_Name: Camera
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &743136010
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 743136009}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1667507392}
+ - {fileID: 1305696686}
+ m_Father: {fileID: 2104414999}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 1, y: 1}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 544, y: 286}
+ m_SizeDelta: {x: 352, y: 44}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &805334466
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 805334468}
+ - component: {fileID: 805334467}
+ m_Layer: 0
+ m_Name: Audio
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!82 &805334467
+AudioSource:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 805334466}
+ m_Enabled: 1
+ serializedVersion: 4
+ OutputAudioMixerGroup: {fileID: 0}
+ m_audioClip: {fileID: 0}
+ m_Resource: {fileID: 0}
+ m_PlayOnAwake: 1
+ m_Volume: 1
+ m_Pitch: 1
+ Loop: 0
+ Mute: 0
+ Spatialize: 0
+ SpatializePostEffects: 0
+ Priority: 128
+ DopplerLevel: 1
+ MinDistance: 1
+ MaxDistance: 500
+ Pan2D: 0
+ rolloffMode: 0
+ BypassEffects: 0
+ BypassListenerEffects: 0
+ BypassReverbZones: 0
+ rolloffCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ - serializedVersion: 3
+ time: 1
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ panLevelCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ spreadCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ reverbZoneMixCustomCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 1
+ inSlope: 0
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0.33333334
+ outWeight: 0.33333334
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+--- !u!4 &805334468
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 805334466}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &810347528
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 810347529}
+ - component: {fileID: 810347531}
+ - component: {fileID: 810347530}
+ m_Layer: 5
+ m_Name: Background
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &810347529
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 810347528}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 661169485}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.25}
+ m_AnchorMax: {x: 1, y: 0.75}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 0, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &810347530
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 810347528}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 0.22745098}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &810347531
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 810347528}
+ m_CullTransparentMesh: 1
+--- !u!1 &837510802
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 837510803}
+ m_Layer: 0
+ m_Name: Point (2)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &837510803
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 837510802}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 4, z: 11}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1978219628}
+ m_Father: {fileID: 1192756032}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &934141671
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 934141673}
+ - component: {fileID: 934141675}
+ - component: {fileID: 934141674}
+ m_Layer: 0
+ m_Name: GameController
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &934141673
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934141671}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &934141674
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934141671}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 7f15a2ff39e3f3c4db49fb4f8409f9e6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ _visualConfig: {fileID: 11400000, guid: cecb790105d80e648867f3a87090d970, type: 2}
+ _lanePaths:
+ - {fileID: 1192756033}
+ _noteContainer: {fileID: 552294209}
+ _mirrorPaths: []
+--- !u!114 &934141675
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 934141671}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 0e36b33fed654fa4495553f5f9610203, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ _config: {fileID: 11400000, guid: 5f86e1357cf9047feb3a73b01384300a, type: 2}
+ _nodePrefab: {fileID: 2132212214528617362, guid: 47434b2d937064155aca3329574f5c2c, type: 3}
+ _cameraRig: {fileID: 68930445}
+ _edgeContainer: {fileID: 141288317}
+ _errorCountLabel: {fileID: 1625478451}
+ _sampleTreeProvider: {fileID: 11400000, guid: 5e742b1b425de4606ad22578dcb5452d, type: 2}
+ _animationConfig: {fileID: 11400000, guid: cd1dd569b0aee4eb89ba696fe4f3164d, type: 2}
+ _beatmap: {fileID: 11400000, guid: 9d66e8de59dee1246b742c0a1d1fdcdd, type: 2}
+ _hitWindows: {fileID: 11400000, guid: 2d35008a557ccca439a1278a088cb8e8, type: 2}
+ _calibration: {fileID: 11400000, guid: 7835c71bd9045204dae701ad89457734, type: 2}
+ _audioSource: {fileID: 805334467}
+ _visualizer: {fileID: 934141674}
+ _laneToAction: 000000000200000001000000
+--- !u!1 &1035885513
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1035885514}
+ - component: {fileID: 1035885516}
+ - component: {fileID: 1035885515}
+ m_Layer: 5
+ m_Name: Fill
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1035885514
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1035885513}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1786209251}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 10, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1035885515
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1035885513}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.5518868, g: 1, b: 0.6274163, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &1035885516
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1035885513}
+ m_CullTransparentMesh: 1
+--- !u!1 &1148278823
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1148278824}
+ - component: {fileID: 1148278827}
+ - component: {fileID: 1148278826}
+ - component: {fileID: 1148278825}
+ m_Layer: 0
+ m_Name: Background
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1148278824
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1148278823}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0.7071068, y: 0, z: 0, w: 0.7071068}
+ m_LocalPosition: {x: 0, y: 0, z: 30}
+ m_LocalScale: {x: 20, y: 20, z: 20}
+ m_ConstrainProportionsScale: 1
+ m_Children: []
+ m_Father: {fileID: 68930444}
+ m_LocalEulerAnglesHint: {x: -90, y: 0, z: 0}
+--- !u!64 &1148278825
+MeshCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1148278823}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 5
+ m_Convex: 0
+ m_CookingOptions: 30
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &1148278826
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1148278823}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 9b6261c8e0b1e4ab89920f89d0bf8dc9, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1148278827
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1148278823}
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!1 &1192756031
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1192756032}
+ - component: {fileID: 1192756033}
+ m_Layer: 0
+ m_Name: LeftNotePath
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1192756032
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192756031}
+ serializedVersion: 2
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1450648614}
+ - {fileID: 837510803}
+ - {fileID: 1595041900}
+ m_Father: {fileID: 68930444}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &1192756033
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192756031}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84f4dc4f60fba7e4db3098b36887c32b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ _waypoints:
+ - {fileID: 1450648614}
+ - {fileID: 837510803}
+ - {fileID: 1595041900}
+ _easeCurve:
+ serializedVersion: 2
+ m_Curve:
+ - serializedVersion: 3
+ time: 0
+ value: 0
+ inSlope: 0
+ outSlope: 1
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ - serializedVersion: 3
+ time: 1
+ value: 1
+ inSlope: 1
+ outSlope: 0
+ tangentMode: 0
+ weightedMode: 0
+ inWeight: 0
+ outWeight: 0
+ m_PreInfinity: 2
+ m_PostInfinity: 2
+ m_RotationOrder: 4
+ _drawGizmosInPlayMode: 0
+--- !u!1 &1305696685
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1305696686}
+ - component: {fileID: 1305696688}
+ - component: {fileID: 1305696687}
+ m_Layer: 5
+ m_Name: Label
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1305696686
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1305696685}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 743136010}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 195.7, y: 61.8}
+ m_SizeDelta: {x: 276, y: 50}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1305696687
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1305696685}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: 'Beat Speed: 2'
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 36
+ m_fontSizeBase: 36
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 18
+ m_fontSizeMax: 72
+ m_fontStyle: 0
+ m_HorizontalAlignment: 4
+ m_VerticalAlignment: 256
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 0
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 0
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: -223.27539, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!222 &1305696688
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1305696685}
+ m_CullTransparentMesh: 1
+--- !u!1 &1329169887
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1329169888}
+ m_Layer: 5
+ m_Name: BeatSpeed
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1329169888
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1329169887}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 661169485}
+ - {fileID: 566812981}
+ m_Father: {fileID: 2104414999}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 1, y: 1}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 544, y: 380.5}
+ m_SizeDelta: {x: 352, y: 44}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &1450648613
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1450648614}
+ m_Layer: 0
+ m_Name: Point (1)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1450648614
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1450648613}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 8, z: 11}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1649977640}
+ m_Father: {fileID: 1192756032}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1482817658
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1482817661}
+ - component: {fileID: 1482817660}
+ - component: {fileID: 1482817659}
+ m_Layer: 0
+ m_Name: Directional Light
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1482817659
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1482817658}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Version: 3
+ m_UsePipelineSettings: 1
+ m_AdditionalLightsShadowResolutionTier: 2
+ m_LightLayerMask: 1
+ m_RenderingLayers: 1
+ m_CustomShadowLayers: 0
+ m_ShadowLayerMask: 1
+ m_ShadowRenderingLayers: 1
+ m_LightCookieSize: {x: 1, y: 1}
+ m_LightCookieOffset: {x: 0, y: 0}
+ m_SoftShadowQuality: 0
+--- !u!108 &1482817660
+Light:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1482817658}
+ m_Enabled: 1
+ serializedVersion: 11
+ m_Type: 1
+ m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
+ m_Intensity: 1
+ m_Range: 10
+ m_SpotAngle: 30
+ m_InnerSpotAngle: 21.80208
+ m_CookieSize: 10
+ m_Shadows:
+ m_Type: 2
+ m_Resolution: -1
+ m_CustomResolution: -1
+ m_Strength: 1
+ m_Bias: 0.05
+ m_NormalBias: 0.4
+ m_NearPlane: 0.2
+ m_CullingMatrixOverride:
+ e00: 1
+ e01: 0
+ e02: 0
+ e03: 0
+ e10: 0
+ e11: 1
+ e12: 0
+ e13: 0
+ e20: 0
+ e21: 0
+ e22: 1
+ e23: 0
+ e30: 0
+ e31: 0
+ e32: 0
+ e33: 1
+ m_UseCullingMatrixOverride: 0
+ m_Cookie: {fileID: 0}
+ m_DrawHalo: 0
+ m_Flare: {fileID: 0}
+ m_RenderMode: 0
+ m_CullingMask:
+ serializedVersion: 2
+ m_Bits: 4294967295
+ m_RenderingLayerMask: 1
+ m_Lightmapping: 4
+ m_LightShadowCasterMode: 0
+ m_AreaSize: {x: 1, y: 1}
+ m_BounceIntensity: 1
+ m_ColorTemperature: 6570
+ m_UseColorTemperature: 0
+ m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
+ m_UseBoundingSphereOverride: 0
+ m_UseViewFrustumForShadowCasterCull: 1
+ m_ForceVisible: 0
+ m_ShadowRadius: 0
+ m_ShadowAngle: 0
+ m_LightUnit: 1
+ m_LuxAtDistance: 1
+ m_EnableSpotReflector: 1
+--- !u!4 &1482817661
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1482817658}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
+ m_LocalPosition: {x: 0, y: 3, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
+--- !u!1 &1595041899
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1595041900}
+ m_Layer: 0
+ m_Name: Point (3)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &1595041900
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1595041899}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 1.25, z: 11}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 451490098}
+ m_Father: {fileID: 1192756032}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1625478449
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1625478450}
+ - component: {fileID: 1625478452}
+ - component: {fileID: 1625478451}
+ m_Layer: 5
+ m_Name: ErrorCount
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1625478450
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1625478449}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 2100820702}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 1}
+ m_AnchorMax: {x: 0, y: 1}
+ m_AnchoredPosition: {x: 109, y: -36}
+ m_SizeDelta: {x: 200, y: 50}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1625478451
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1625478449}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 1, g: 1, b: 1, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_text: 'Errors: 0'
+ m_isRightToLeft: 0
+ m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+ m_fontSharedMaterials: []
+ m_fontMaterial: {fileID: 0}
+ m_fontMaterials: []
+ m_fontColor32:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+ m_enableVertexGradient: 0
+ m_colorMode: 3
+ m_fontColorGradient:
+ topLeft: {r: 1, g: 1, b: 1, a: 1}
+ topRight: {r: 1, g: 1, b: 1, a: 1}
+ bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+ bottomRight: {r: 1, g: 1, b: 1, a: 1}
+ m_fontColorGradientPreset: {fileID: 0}
+ m_spriteAsset: {fileID: 0}
+ m_tintAllSprites: 0
+ m_StyleSheet: {fileID: 0}
+ m_TextStyleHashCode: -1183493901
+ m_overrideHtmlColors: 0
+ m_faceColor:
+ serializedVersion: 2
+ rgba: 4294967295
+ m_fontSize: 36
+ m_fontSizeBase: 36
+ m_fontWeight: 400
+ m_enableAutoSizing: 0
+ m_fontSizeMin: 18
+ m_fontSizeMax: 72
+ m_fontStyle: 0
+ m_HorizontalAlignment: 1
+ m_VerticalAlignment: 256
+ m_textAlignment: 65535
+ m_characterSpacing: 0
+ m_characterHorizontalScale: 1
+ m_wordSpacing: 0
+ m_lineSpacing: 0
+ m_lineSpacingMax: 0
+ m_paragraphSpacing: 0
+ m_charWidthMaxAdj: 0
+ m_TextWrappingMode: 1
+ m_wordWrappingRatios: 0.4
+ m_overflowMode: 0
+ m_linkedTextComponent: {fileID: 0}
+ parentLinkedComponent: {fileID: 0}
+ m_enableKerning: 0
+ m_ActiveFontFeatures: 6e72656b
+ m_enableExtraPadding: 0
+ checkPaddingRequired: 0
+ m_isRichText: 1
+ m_EmojiFallbackSupport: 1
+ m_parseCtrlCharacters: 1
+ m_isOrthographic: 1
+ m_isCullingEnabled: 0
+ m_horizontalMapping: 0
+ m_verticalMapping: 0
+ m_uvLineOffset: 0
+ m_geometrySortingOrder: 0
+ m_IsTextObjectScaleStatic: 0
+ m_VertexBufferAutoSizeReduction: 0
+ m_useMaxVisibleDescender: 1
+ m_pageToDisplay: 1
+ m_margin: {x: 0, y: 0, z: 0, w: 0}
+ m_isUsingLegacyAnimationComponent: 0
+ m_isVolumetricText: 0
+ m_hasFontAssetChanged: 0
+ m_baseMaterial: {fileID: 0}
+ m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
+--- !u!222 &1625478452
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1625478449}
+ m_CullTransparentMesh: 1
+--- !u!1 &1649977639
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1649977640}
+ - component: {fileID: 1649977643}
+ - component: {fileID: 1649977642}
+ - component: {fileID: 1649977641}
+ m_Layer: 0
+ m_Name: Cube
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 0
+--- !u!4 &1649977640
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1649977639}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 1450648614}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!65 &1649977641
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1649977639}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &1649977642
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1649977639}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1649977643
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1649977639}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!1 &1667507391
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1667507392}
+ - component: {fileID: 1667507393}
+ m_Layer: 5
+ m_Name: Slider
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1667507392
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1667507391}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 2.2, y: 2.2, z: 2.2}
+ m_ConstrainProportionsScale: 1
+ m_Children:
+ - {fileID: 590320709}
+ - {fileID: 1786209251}
+ - {fileID: 1974904754}
+ m_Father: {fileID: 743136010}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 176, y: 22}
+ m_SizeDelta: {x: 160, y: 20}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &1667507393
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1667507391}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 67db9e8f0e2ae9c40bc1e2b64352a6b4, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Navigation:
+ m_Mode: 3
+ m_WrapAround: 0
+ m_SelectOnUp: {fileID: 0}
+ m_SelectOnDown: {fileID: 0}
+ m_SelectOnLeft: {fileID: 0}
+ m_SelectOnRight: {fileID: 0}
+ m_Transition: 1
+ m_Colors:
+ m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
+ m_HighlightedColor: {r: 1, g: 1, b: 1, a: 1}
+ m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
+ m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+ m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
+ m_ColorMultiplier: 1
+ m_FadeDuration: 0.1
+ m_SpriteState:
+ m_HighlightedSprite: {fileID: 0}
+ m_PressedSprite: {fileID: 0}
+ m_SelectedSprite: {fileID: 0}
+ m_DisabledSprite: {fileID: 0}
+ m_AnimationTriggers:
+ m_NormalTrigger: Normal
+ m_HighlightedTrigger: Highlighted
+ m_PressedTrigger: Pressed
+ m_SelectedTrigger: Selected
+ m_DisabledTrigger: Disabled
+ m_Interactable: 1
+ m_TargetGraphic: {fileID: 571306885}
+ m_FillRect: {fileID: 1035885514}
+ m_HandleRect: {fileID: 571306884}
+ m_Direction: 1
+ m_MinValue: 0
+ m_MaxValue: 1
+ m_WholeNumbers: 0
+ m_Value: 0.669
+ m_OnValueChanged:
+ m_PersistentCalls:
+ m_Calls: []
+--- !u!1 &1786209250
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1786209251}
+ m_Layer: 5
+ m_Name: Fill Area
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1786209251
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1786209250}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1035885514}
+ m_Father: {fileID: 1667507392}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0.25}
+ m_AnchorMax: {x: 1, y: 0.75}
+ m_AnchoredPosition: {x: 5, y: 0}
+ m_SizeDelta: {x: -20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &1974904753
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1974904754}
+ m_Layer: 5
+ m_Name: Handle Slide Area
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &1974904754
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1974904753}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 571306884}
+ m_Father: {fileID: 1667507392}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 1, y: 1}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: -20, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!1 &1978219627
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1978219628}
+ - component: {fileID: 1978219631}
+ - component: {fileID: 1978219630}
+ - component: {fileID: 1978219629}
+ m_Layer: 0
+ m_Name: Cube
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 0
+--- !u!4 &1978219628
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1978219627}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 837510803}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!65 &1978219629
+BoxCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1978219627}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Size: {x: 1, y: 1, z: 1}
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!23 &1978219630
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1978219627}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1978219631
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1978219627}
+ m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!1 &2014446947
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2014446948}
+ - component: {fileID: 2014446950}
+ - component: {fileID: 2014446949}
+ m_Layer: 5
+ m_Name: Fill
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &2014446948
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2014446947}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 515217941}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0, y: 0}
+ m_AnchorMax: {x: 0, y: 0}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 10, y: 0}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &2014446949
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2014446947}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.5518868, g: 1, b: 0.6274163, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 1
+--- !u!222 &2014446950
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2014446947}
+ m_CullTransparentMesh: 1
+--- !u!1 &2100820698
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2100820702}
- component: {fileID: 2100820701}
- component: {fileID: 2100820700}
- component: {fileID: 2100820699}
@@ -783,6 +2958,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1625478450}
+ - {fileID: 2104414999}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -790,6 +2966,68 @@ RectTransform:
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 0}
+--- !u!1 &2104414998
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2104414999}
+ - component: {fileID: 2104415000}
+ m_Layer: 5
+ m_Name: DebugUI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &2104414999
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2104414998}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 1329169888}
+ - {fileID: 743136010}
+ - {fileID: 195719196}
+ m_Father: {fileID: 2100820702}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0.5}
+ m_AnchorMax: {x: 0.5, y: 0.5}
+ m_AnchoredPosition: {x: 0, y: 0}
+ m_SizeDelta: {x: 100, y: 100}
+ m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &2104415000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2104414998}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: a12a8f60640784ff6928d5c61ac956c9, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ _rhythmVisualConfig: {fileID: 11400000, guid: cecb790105d80e648867f3a87090d970, type: 2}
+ _noteBeatsSlider: {fileID: 661169486}
+ _noteBeatsLabel: {fileID: 566812982}
+ _noteBeatsMin: 0.5
+ _noteBeatsMax: 6
+ _cameraRig: {fileID: 68930445}
+ _cameraZSlider: {fileID: 1667507393}
+ _cameraZLabel: {fileID: 1305696687}
+ _backgroundPlane: {fileID: 1148278824}
+ _restartButton: {fileID: 195719197}
+ _gameSceneIndex: 0
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -800,3 +3038,4 @@ SceneRoots:
- {fileID: 934141673}
- {fileID: 141288317}
- {fileID: 2100820702}
+ - {fileID: 805334468}
diff --git a/Assets/ScriptableObjects/Configs.meta b/Assets/ScriptableObjects/Configs.meta
new file mode 100644
index 0000000..7dc9646
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5a134c11c1556f44580516f36b324fe5
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/Configs/HitWindowConfig.asset b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset
new file mode 100644
index 0000000..53b165e
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset
@@ -0,0 +1,40 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 7e494470476ae5847b3b0d9bbc5fff7e, type: 3}
+ m_Name: HitWindowConfig
+ m_EditorClassIdentifier:
+ PerfectWindow: 0.03
+ GoodWindow: 0.07
+ OkWindow: 0.12
+ PerfectScore: 300
+ GoodScore: 200
+ OkScore: 100
+ MissScore: 0
+ BaseMultiplier: 1
+ MaxMultiplier: 8
+ ComboStepSize: 10
+ GradeSSThreshold: 1
+ GradeSThreshold: 0.97
+ GradeAThreshold: 0.93
+ GradeBThreshold: 0.85
+ GradeCThreshold: 0.75
+ GradeDThreshold: 0.6
+ HealthEnabled: 0
+ MaxHp: 100
+ HpGainPerfect: 2
+ HpGainGood: 1
+ HpGainOk: 0
+ HpDrainMiss: 10
+ AccuracyWeightPerfect: 1
+ AccuracyWeightGood: 0.67
+ AccuracyWeightOk: 0.33
+ AccuracyWeightMiss: 0
diff --git a/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta
new file mode 100644
index 0000000..1e19110
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/HitWindowConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2d35008a557ccca439a1278a088cb8e8
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset
new file mode 100644
index 0000000..b560d73
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset
@@ -0,0 +1,16 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 3b666d4b58929be4ba0e72378442a4cc, type: 3}
+ m_Name: RhythmCalibrationConfig
+ m_EditorClassIdentifier:
+ CalibrationOffsetSeconds: 0
+ VisualOffsetSeconds: 0
diff --git a/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta
new file mode 100644
index 0000000..f7d1f40
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/RhythmCalibrationConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7835c71bd9045204dae701ad89457734
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset
new file mode 100644
index 0000000..d4cf152
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset
@@ -0,0 +1,32 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fb02592c77f9e1f42a56dc9c4989ab30, type: 3}
+ m_Name: RhythmVisualConfig
+ m_EditorClassIdentifier:
+ NoteAppearanceBeats: 1.503571
+ MissCleanupGraceSeconds: 0.5
+ NormalPoolSize: 16
+ HoldPoolSize: 8
+ NormalNoteColor: {r: 0.99986243, g: 0.8915094, b: 1, a: 1}
+ HoldNoteColor: {r: 0.4, g: 0.8, b: 1, a: 1}
+ HitFlashPerfect: {r: 1, g: 0.8243091, b: 0.3915094, a: 1}
+ HitFlashGood: {r: 0.5424528, g: 1, b: 0.5424528, a: 1}
+ HitFlashOk: {r: 1, g: 0.3915094, b: 0.8627911, a: 1}
+ HitFlashMiss: {r: 1, g: 0.21226418, b: 0.21226418, a: 1}
+ HitZoneRadius: 0.5
+ LaneHitZonePositions: []
+ NormalNotePrefab: {fileID: 2132212214528617362, guid: a4a524929e253024fb536d59b8c205e0, type: 3}
+ HoldNotePrefab: {fileID: 0}
+ FeedbackPerfectPrefab: {fileID: 0}
+ FeedbackGoodPrefab: {fileID: 0}
+ FeedbackOkPrefab: {fileID: 0}
+ FeedbackMissPrefab: {fileID: 0}
diff --git a/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta
new file mode 100644
index 0000000..86994b5
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/RhythmVisualConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: cecb790105d80e648867f3a87090d970
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset
new file mode 100644
index 0000000..8fb04e1
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset
@@ -0,0 +1,31 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 08d88fdfd5135499187cd39a1423b188, type: 3}
+ m_Name: TreeAnimationConfig
+ m_EditorClassIdentifier:
+ _hitClip: {fileID: 8300000, guid: 3933bfed4d8be4f9fabe55d5ade4c07e, type: 3}
+ _wrongInputClip: {fileID: 8300000, guid: 920c5009ea0384f22a22a9b366964a4a, type: 3}
+ _missClip: {fileID: 0}
+ _offBeatClip: {fileID: 8300000, guid: 7bb487aa2617a4056a1b39a0273320d2, type: 3}
+ _hitVolume: 0.8
+ _wrongInputVolume: 0.8
+ _missVolume: 0.8
+ _offBeatVolume: 0.6
+ _pulseScaleMultiplier: 1.15
+ _pulseDuration: 0.12
+ _pulseEase: 6
+ _beatPulseMultiplier: 2
+ _bounceOffset: 0.3
+ _bounceDuration: 0.25
+ _bounceOutEase: 6
+ _bounceReturnEase: 24
+ _wrongInputLockoutDuration: 0.15
diff --git a/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta
new file mode 100644
index 0000000..180b611
--- /dev/null
+++ b/Assets/ScriptableObjects/Configs/TreeAnimationConfig.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: cd1dd569b0aee4eb89ba696fe4f3164d
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/TreeVisualizerConfig.asset b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset
similarity index 67%
rename from Assets/ScriptableObjects/TreeVisualizerConfig.asset
rename to Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset
index e0fa0cf..52b7960 100644
--- a/Assets/ScriptableObjects/TreeVisualizerConfig.asset
+++ b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset
@@ -18,21 +18,21 @@ MonoBehaviour:
CameraMoveDuration: 0.2
CameraEase: 9
CameraRotation: {x: 0, y: 0, z: 0}
- CameraPositionOffset: {x: 0, y: 0, z: -8}
- CameraOrthographicSize: 10
+ CameraPositionOffset: {x: 0, y: 0, z: -6}
+ CameraOrthographicSize: 5
CameraFieldOfView: 60
NodeRepositionDuration: 0.2
RepositionEase: 27
CarrierMoveDuration: 0.2
CarrierMoveEase: 9
- PostPlacementDelayMs: 500
+ PostPlacementDelayMs: 100
CarrierReadyAlpha: 0.65
- NormalNodeColor: {r: 1, g: 1, b: 1, a: 1}
- FocusedNodeColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}
- CarrierColor: {r: 0, g: 1, b: 1, a: 1}
- CarrierReadyColor: {r: 0.2, g: 1, b: 0.4, a: 1}
+ NormalNodeColor: {r: 0.8911494, g: 0.7080812, b: 0.9811321, a: 1}
+ FocusedNodeColor: {r: 0.64480627, g: 0.2971698, b: 1, a: 1}
+ CarrierColor: {r: 0.43867922, g: 0.53349704, b: 1, a: 1}
+ CarrierReadyColor: {r: 0.41037738, g: 1, b: 0.56451243, a: 1}
EdgeMaterial: {fileID: 10306, guid: 0000000000000000f000000000000000, type: 0}
- EdgeColor: {r: 0.7, g: 0.7, b: 0.7, a: 1}
+ EdgeColor: {r: 1, g: 0.36320752, b: 0.99139464, a: 1}
EdgeWidth: 0.06
MinValue: 1
MaxValue: 99
diff --git a/Assets/ScriptableObjects/TreeVisualizerConfig.asset.meta b/Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset.meta
similarity index 100%
rename from Assets/ScriptableObjects/TreeVisualizerConfig.asset.meta
rename to Assets/ScriptableObjects/Configs/TreeVisualizerConfig.asset.meta
diff --git a/Assets/ScriptableObjects/SampleTrees.meta b/Assets/ScriptableObjects/SampleTrees.meta
new file mode 100644
index 0000000..25d50c6
--- /dev/null
+++ b/Assets/ScriptableObjects/SampleTrees.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 94145dd846f3f364c8a7f04e3aa54525
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/ScriptableObjects/IntSampleTree.asset b/Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset
similarity index 100%
rename from Assets/ScriptableObjects/IntSampleTree.asset
rename to Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset
diff --git a/Assets/ScriptableObjects/IntSampleTree.asset.meta b/Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset.meta
similarity index 100%
rename from Assets/ScriptableObjects/IntSampleTree.asset.meta
rename to Assets/ScriptableObjects/SampleTrees/IntSampleTree.asset.meta
diff --git a/Assets/Songs.meta b/Assets/Songs.meta
new file mode 100644
index 0000000..c2c3916
--- /dev/null
+++ b/Assets/Songs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6feeb535242ea4444bdbc48b38c4798c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/HailPurdue.meta b/Assets/Songs/HailPurdue.meta
new file mode 100644
index 0000000..7b9b9ad
--- /dev/null
+++ b/Assets/Songs/HailPurdue.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 85cb1e49b4435984e920da8dc93cc764
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/HailPurdue/Hail-Purdue.wav b/Assets/Songs/HailPurdue/Hail-Purdue.wav
new file mode 100644
index 0000000..e2f3c94
Binary files /dev/null and b/Assets/Songs/HailPurdue/Hail-Purdue.wav differ
diff --git a/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta b/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta
new file mode 100644
index 0000000..bf98a9b
--- /dev/null
+++ b/Assets/Songs/HailPurdue/Hail-Purdue.wav.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 8359952b42d6e514a92a4fcbdc0ad344
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/TestSong.meta b/Assets/Songs/TestSong.meta
new file mode 100644
index 0000000..bb1f103
--- /dev/null
+++ b/Assets/Songs/TestSong.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ee602f77a558b4ef4b7b27cba574c6f1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/TestSong/TestManifest.asset b/Assets/Songs/TestSong/TestManifest.asset
new file mode 100644
index 0000000..c0593f5
--- /dev/null
+++ b/Assets/Songs/TestSong/TestManifest.asset
@@ -0,0 +1,18 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 318b60a35bddb77409b7ec4125f99b64, type: 3}
+ m_Name: TestManifest
+ m_EditorClassIdentifier:
+ SongName:
+ Difficulties:
+ - Level: 1
+ Beatmap: {fileID: 11400000, guid: 9d66e8de59dee1246b742c0a1d1fdcdd, type: 2}
diff --git a/Assets/Songs/TestSong/TestManifest.asset.meta b/Assets/Songs/TestSong/TestManifest.asset.meta
new file mode 100644
index 0000000..6fa6d7c
--- /dev/null
+++ b/Assets/Songs/TestSong/TestManifest.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5ca5a381fbfa5de4f8f2680b32dfd1d8
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/TestSong/TestSong.asset b/Assets/Songs/TestSong/TestSong.asset
new file mode 100644
index 0000000..fd75085
--- /dev/null
+++ b/Assets/Songs/TestSong/TestSong.asset
@@ -0,0 +1,334 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 4ce5ecd1e95d4474f8092d79730c87bd, type: 3}
+ m_Name: TestSong
+ m_EditorClassIdentifier:
+ SongName:
+ Author:
+ Album:
+ SongClip: {fileID: 8300000, guid: e78d73311ea7e42639f730aca264fbe8, type: 3}
+ Thumbnail: {fileID: 0}
+ PreviewStartTime: 0
+ BaseBpm: 60
+ OffsetSeconds: 0
+ StartDelaySeconds: 2
+ TimingPoints: []
+ DifficultyName:
+ DifficultyLevel: 1
+ DifficultyRating: 0
+ Notes:
+ - Beat: 0
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 2ea6a103-f0bc-4273-98fc-ec27007b4897
+ - Beat: 1
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 71f3b0d6-bb58-4bbf-8396-45088b05943b
+ - Beat: 2
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 123523d2-ef7f-4542-ad07-23ed63b25255
+ - Beat: 3
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 42f0badf-bc50-4bd2-873c-79b97b300118
+ - Beat: 3.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: defb15ed-d5a8-48ae-838d-6d4072dc5ecc
+ - Beat: 4
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 981d33b3-c098-4193-a1e6-6047ef51ccff
+ - Beat: 5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 16ad27c9-b58b-4e1c-ab30-0709a3f1a99a
+ - Beat: 6
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 7b4cfc8c-0bec-4b1b-a4f3-53df8fbde46b
+ - Beat: 7
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 01f707d5-c224-417c-9bd7-13d67506bf38
+ - Beat: 7.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 84f83db2-850b-4d5a-836c-3f130e0e9a5b
+ - Beat: 8
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 63c67e61-09da-47d8-8210-71ec7ca1b29f
+ - Beat: 9
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 801af6d2-6550-41a3-9af7-0bf168d3ef2c
+ - Beat: 10
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: ee0d6700-c509-4e87-a8d0-04f2560cacba
+ - Beat: 11
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: fde61dd2-0d82-4e9b-b3c9-4ed768a49dc1
+ - Beat: 11.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: d8766c0c-6910-4ee0-9fe9-4ace3c935f94
+ - Beat: 12
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 0c8e4bcb-a877-4ca5-9a86-921a4d81a574
+ - Beat: 13
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 3627c561-082d-4e50-81b3-11abb6d18307
+ - Beat: 14
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 7277bb2d-239e-4be3-80e4-df17ebd7afd1
+ - Beat: 15
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 4c009917-395b-4017-9e69-a3185580bf9b
+ - Beat: 15.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 9fb9597a-4ade-49c6-a358-9cd730d1d21f
+ - Beat: 16
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 0dbb509f-d199-498f-92ce-da1bd95a8932
+ - Beat: 17
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: cc0c2a55-16cf-46ba-9e69-17eaed1bbbf1
+ - Beat: 18
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 14a1999b-44a0-4a51-9dfb-8e9d5c6b3cde
+ - Beat: 19
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: c7be1f26-bfd7-4deb-a281-b3c917e3a722
+ - Beat: 19.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 4fb9512a-5a4b-46f5-bc2b-2e1825671406
+ - Beat: 20
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 5a90cf0e-f9b0-42ee-9999-0dd228ee1e4e
+ - Beat: 21
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: f385187f-2b3d-499a-b3a1-404e43e9e460
+ - Beat: 22
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 8330c327-7e4b-4bab-82d0-584d87d923fe
+ - Beat: 23
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 73867a6d-7802-4551-8afa-77f8499d0184
+ - Beat: 23.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: d8485cf9-4042-43ba-b730-1cb4813203da
+ - Beat: 24
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 2a441a69-8bf1-4722-9561-c9c0dfecd9b5
+ - Beat: 25
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 851c4a5c-dd00-4efd-8a75-fcc3a3a13f44
+ - Beat: 26
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 824c2d12-ff42-43ed-9846-23ddf8bb57c4
+ - Beat: 27
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 612a1ac6-88bc-4df7-adff-1c5dbb6ea2df
+ - Beat: 27.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 577915b3-ff04-44b3-bb2a-51aada78cb1d
+ - Beat: 28
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 3f9a65c1-d557-402f-a0a1-d61376a90792
+ - Beat: 29
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 9f32905c-cf00-4fe5-8109-b30dd10e6673
+ - Beat: 30
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 40f3cb9c-f2e6-44f3-bb5d-245c7b9e0cf1
+ - Beat: 31
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: d5fa7319-5384-4a2f-bb96-150e69d58122
+ - Beat: 31.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: ce1129b2-1003-4f6a-9a0f-2de668e05989
+ - Beat: 32
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 3b355cd2-59d3-4aef-806b-f7440a097124
+ - Beat: 33
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 088efe9f-af73-4e5d-81ab-2df58779d730
+ - Beat: 34
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 9fdfdefd-3707-4c62-bb02-1b1b6f8f3f05
+ - Beat: 35
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 8a5429d0-7fb6-40c1-a7f2-76c1a79c3b29
+ - Beat: 35.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: fe5fa10c-ffc0-4080-a1c3-209ebb925d57
+ - Beat: 36
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 08c5e53e-0079-479b-abbf-597098b78caa
+ - Beat: 37
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: b7ebca3d-0d8e-4bc1-a780-bf879320a3b7
+ - Beat: 38
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 2b45a89c-7b3e-4491-a138-67b152b1a214
+ - Beat: 39
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: d458a1fa-aba7-4e18-b213-d0c58253b7f3
+ - Beat: 39.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 1d0a2877-f875-4478-9bf5-2608984a6109
+ - Beat: 40
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 47687dc7-ded6-41a0-9ac3-370d64f823cf
+ - Beat: 41
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 5236bcd9-215c-4ca1-8d83-cb4bc04508f6
+ - Beat: 42
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 5ba98406-3af6-4155-ac43-c8d284ec2aac
+ - Beat: 43
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 25620c3b-9407-41d6-ab8a-738c9e5a580d
+ - Beat: 43.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 7d3d14ed-7de9-4855-8e31-cd86c6f41e22
+ - Beat: 44
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 83a96792-68d9-4d4b-8d42-ad51933ce6ee
+ - Beat: 45
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 404d509b-60b7-47cf-9dd9-0956a9d2c5ce
+ - Beat: 46
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: e0f7bc41-bd2c-4fcc-94d8-1014e173f418
+ - Beat: 47
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 47132679-3435-46bd-af5d-c0f356acbc02
+ - Beat: 47.5
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 3ae025cf-5315-4908-9286-db0e4f605824
+ - Beat: 48
+ Lane: 0
+ NoteType: 0
+ HoldDuration: 0
+ EditorId: 35a862a7-bc51-4c82-87f7-f405a75dc950
+ KeyPoints: []
diff --git a/Assets/Songs/TestSong/TestSong.asset.meta b/Assets/Songs/TestSong/TestSong.asset.meta
new file mode 100644
index 0000000..bcd37d3
--- /dev/null
+++ b/Assets/Songs/TestSong/TestSong.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9d66e8de59dee1246b742c0a1d1fdcdd
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Songs/TestSong/TestSong.mp3 b/Assets/Songs/TestSong/TestSong.mp3
new file mode 100644
index 0000000..4ea4911
Binary files /dev/null and b/Assets/Songs/TestSong/TestSong.mp3 differ
diff --git a/Assets/Songs/TestSong/TestSong.mp3.meta b/Assets/Songs/TestSong/TestSong.mp3.meta
new file mode 100644
index 0000000..c21e411
--- /dev/null
+++ b/Assets/Songs/TestSong/TestSong.mp3.meta
@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: e78d73311ea7e42639f730aca264fbe8
+AudioImporter:
+ externalObjects: {}
+ serializedVersion: 8
+ defaultSettings:
+ serializedVersion: 2
+ loadType: 0
+ sampleRateSetting: 0
+ sampleRateOverride: 44100
+ compressionFormat: 1
+ quality: 1
+ conversionMode: 0
+ preloadAudioData: 0
+ platformSettingOverrides: {}
+ forceToMono: 0
+ normalize: 1
+ loadInBackground: 0
+ ambisonic: 0
+ 3D: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Tree/Controller/TreeGameController.cs b/Assets/Tree/Controller/TreeGameController.cs
index a1da826..6065442 100644
--- a/Assets/Tree/Controller/TreeGameController.cs
+++ b/Assets/Tree/Controller/TreeGameController.cs
@@ -14,6 +14,12 @@ public class TreeGameController : MonoBehaviour
[Header("Configuration")]
[SerializeField] private TreeVisualizerConfig _config;
+ [Header("Animation")]
+ [Tooltip("Sound effects and animation parameters for node reactions.")]
+ [SerializeField] private TreeAnimationConfig _animationConfig;
+ [Tooltip("AudioSource used to play one-shot sound effects.")]
+ [SerializeField] private AudioSource _audioSource;
+
[Header("Scene References")]
[SerializeField] private GameObject _nodePrefab;
[SerializeField] private CameraRig _cameraRig;
@@ -49,7 +55,7 @@ public class TreeGameController : MonoBehaviour
private void Awake()
{
var rules = new BSTInsertionRules();
- var hooks = new DefaultNavigationHooks();
+ var hooks = new TreeAnimationHooks(_animationConfig, _audioSource, () => _carrierView);
_navigator = new TreeNavigator(rules, hooks);
_generator = new IntValueGenerator(_config.MinValue, _config.MaxValue);
@@ -122,6 +128,8 @@ private void PopulateSampleTree()
private void BeginNextInsertion()
{
+ _generator.Min = _config.MinValue;
+ _generator.Max = _config.MaxValue;
int value = _generator.GenerateNext();
_navigator.BeginInsertion(value).Forget();
}
diff --git a/Assets/Tree/Generation/IntValueGenerator.cs b/Assets/Tree/Generation/IntValueGenerator.cs
index dbbec0f..3df2211 100644
--- a/Assets/Tree/Generation/IntValueGenerator.cs
+++ b/Assets/Tree/Generation/IntValueGenerator.cs
@@ -11,10 +11,14 @@ public class IntValueGenerator : IValueGenerator
{
#region Fields
- private readonly int _min;
- private readonly int _max;
private readonly Random _random;
+ /** Inclusive lower bound. Settable so live config changes propagate without recreating the generator. */
+ public int Min { get; set; }
+
+ /** Inclusive upper bound. Settable so live config changes propagate without recreating the generator. */
+ public int Max { get; set; }
+
#endregion
#region Constructor
@@ -27,8 +31,8 @@ public class IntValueGenerator : IValueGenerator
*/
public IntValueGenerator(int min = 1, int max = 99)
{
- _min = min;
- _max = max;
+ Min = min;
+ Max = max;
_random = new Random();
}
@@ -36,8 +40,8 @@ public IntValueGenerator(int min = 1, int max = 99)
#region IValueGenerator Implementation
- /** Returns a random integer in [min, max]. */
- public int GenerateNext() => _random.Next(_min, _max + 1);
+ /** Returns a random integer in [Min, Max]. */
+ public int GenerateNext() => _random.Next(Min, Max + 1);
#endregion
}
diff --git a/Assets/Tree/Hooks/TreeAnimationHooks.cs b/Assets/Tree/Hooks/TreeAnimationHooks.cs
new file mode 100644
index 0000000..d6636e2
--- /dev/null
+++ b/Assets/Tree/Hooks/TreeAnimationHooks.cs
@@ -0,0 +1,162 @@
+using System;
+using Cysharp.Threading.Tasks;
+using PrimeTween;
+using UnityEngine;
+
+/**
+ * Navigation hooks that drive sound effects, node pulse, and wrong-input carrier
+ * bounce animations.
+ *
+ * The comparable value type stored in the tree.
+ */
+public class TreeAnimationHooks : DefaultNavigationHooks
+{
+ #region Dependencies
+
+ private readonly TreeAnimationConfig _config;
+ private readonly AudioSource _audioSource;
+ private readonly Func _getCarrierView;
+
+ #endregion
+
+ #region Bounce State
+
+ private TreeNodeView _bounceCarrier;
+ private Vector3 _bounceOrigin;
+
+ #endregion
+
+ #region Constructor
+
+ /**
+ * Creates the hooks with the required collaborators.
+ *
+ * Animation parameters ScriptableObject.
+ * AudioSource used for all one-shot clips.
+ *
+ * Delegate that returns the current carrier view. Called at hook time rather
+ * than constructor time because the carrier is recreated each insertion round.
+ *
+ */
+ public TreeAnimationHooks(TreeAnimationConfig config, AudioSource audioSource, Func getCarrierView)
+ {
+ _config = config;
+ _audioSource = audioSource;
+ _getCarrierView = getCarrierView;
+ }
+
+ #endregion
+
+ #region Bounce Cancellation
+
+ /** */
+ public override UniTask OnBeforeNavigateLeft(NavigationContext ctx)
+ {
+ CancelBounce();
+ return UniTask.CompletedTask;
+ }
+
+ /** */
+ public override UniTask OnBeforeNavigateRight(NavigationContext ctx)
+ {
+ CancelBounce();
+ return UniTask.CompletedTask;
+ }
+
+ /** */
+ public override UniTask OnBeforePlace(NavigationContext ctx)
+ {
+ CancelBounce();
+ return UniTask.CompletedTask;
+ }
+
+ #endregion
+
+ #region Hit Sound
+
+ /** */
+ public override UniTask OnAfterNavigateLeft(NavigationContext ctx)
+ {
+ PlayHit();
+ return UniTask.CompletedTask;
+ }
+
+ /** */
+ public override UniTask OnAfterNavigateRight(NavigationContext ctx)
+ {
+ PlayHit();
+ return UniTask.CompletedTask;
+ }
+
+ /** */
+ public override UniTask OnAfterPlace(NavigationContext ctx)
+ {
+ PlayHit();
+ return UniTask.CompletedTask;
+ }
+
+ #endregion
+
+ #region Wrong Input
+
+ /** */
+ public override async UniTask OnInvalidAction(NavigationContext ctx, NavigationAction attempted)
+ {
+ if (_config.WrongInputClip != null)
+ _audioSource.PlayOneShot(_config.WrongInputClip, _config.WrongInputVolume);
+
+ TreeNodeView carrier = _getCarrierView();
+ if (carrier == null) return;
+
+ _bounceCarrier = carrier;
+ _bounceOrigin = carrier.transform.position;
+
+ RunBounceAsync(carrier, attempted).Forget();
+
+ if (_config.WrongInputLockoutDuration > 0f)
+ await UniTask.Delay(TimeSpan.FromSeconds(_config.WrongInputLockoutDuration));
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private async UniTaskVoid RunBounceAsync(TreeNodeView carrier, NavigationAction attempted)
+ {
+ Vector3 origin = _bounceOrigin;
+ Vector3 delta = attempted switch
+ {
+ NavigationAction.Left => Vector3.left * _config.BounceOffset,
+ NavigationAction.Right => Vector3.right * _config.BounceOffset,
+ NavigationAction.Place => Vector3.down * _config.BounceOffset,
+ _ => Vector3.zero
+ };
+
+ float outTime = _config.BounceDuration * 0.35f;
+ float returnTime = _config.BounceDuration * 0.65f;
+
+ await Tween.Position(carrier.transform, origin + delta, outTime, _config.BounceOutEase);
+
+ if (_bounceCarrier == carrier)
+ await Tween.Position(carrier.transform, origin, returnTime, _config.BounceReturnEase);
+
+ if (_bounceCarrier == carrier)
+ _bounceCarrier = null;
+ }
+
+ private void CancelBounce()
+ {
+ if (_bounceCarrier == null) return;
+ Tween.StopAll(_bounceCarrier.gameObject);
+ _bounceCarrier.SnapTo(new Vector2(_bounceOrigin.x, _bounceOrigin.y));
+ _bounceCarrier = null;
+ }
+
+ private void PlayHit()
+ {
+ if (_config.HitClip != null)
+ _audioSource.PlayOneShot(_config.HitClip, _config.HitVolume);
+ }
+
+ #endregion
+}
diff --git a/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta b/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta
new file mode 100644
index 0000000..c36e145
--- /dev/null
+++ b/Assets/Tree/Hooks/TreeAnimationHooks.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a469602f674d64a46b1f160acc7644f4
\ No newline at end of file
diff --git a/Assets/Tree/Navigation/TreeNavigator.cs b/Assets/Tree/Navigation/TreeNavigator.cs
index b8c6078..9305f46 100644
--- a/Assets/Tree/Navigation/TreeNavigator.cs
+++ b/Assets/Tree/Navigation/TreeNavigator.cs
@@ -28,6 +28,9 @@ public class TreeNavigator
/** Running count of invalid actions the player has taken across all sessions. */
public int ErrorCount { get; private set; }
+ /** Returns the action required at the current cursor position. */
+ public NavigationAction GetRequiredAction() => _rules.GetRequiredAction(CurrentContext);
+
#endregion
#region Events
diff --git a/Assets/Tree/Visual/CameraRig.cs b/Assets/Tree/Visual/CameraRig.cs
index af19a5c..c429019 100644
--- a/Assets/Tree/Visual/CameraRig.cs
+++ b/Assets/Tree/Visual/CameraRig.cs
@@ -23,6 +23,18 @@ public class CameraRig : MonoBehaviour
#endregion
+ #region Events
+
+ /**
+ * Fired at the start of every or
+ * call so external systems (e.g. a camera preview slider) can reset themselves
+ * before the rig takes over the position.
+ *
+ */
+ public event System.Action OnGameplayMoved;
+
+ #endregion
+
#region Lifecycle
private void Awake()
@@ -66,6 +78,7 @@ public void Initialize(TreeVisualizerConfig config)
*/
public async UniTask MoveToAsync(Vector2 worldPos)
{
+ OnGameplayMoved?.Invoke();
Vector3 target = new Vector3(
worldPos.x + _positionOffset.x,
worldPos.y + _positionOffset.y,
@@ -81,6 +94,7 @@ public async UniTask MoveToAsync(Vector2 worldPos)
*/
public void SnapTo(Vector2 worldPos)
{
+ OnGameplayMoved?.Invoke();
transform.position = new Vector3(
worldPos.x + _positionOffset.x,
worldPos.y + _positionOffset.y,
diff --git a/Assets/Tree/Visual/TreeAnimationConfig.cs b/Assets/Tree/Visual/TreeAnimationConfig.cs
new file mode 100644
index 0000000..1628b7b
--- /dev/null
+++ b/Assets/Tree/Visual/TreeAnimationConfig.cs
@@ -0,0 +1,135 @@
+using PrimeTween;
+using UnityEngine;
+
+/**
+ * ScriptableObject controlling all animation parameters for tree node reactions:
+ * sound effects, BPM-driven node pulsing, and wrong-input carrier bounce.
+ *
+ */
+[CreateAssetMenu(menuName = "Treeformance/TreeAnimationConfig", fileName = "TreeAnimationConfig")]
+public class TreeAnimationConfig : ScriptableObject
+{
+ #region Sound Effects
+
+ [Header("Sound Effects")]
+ [Tooltip("Played when the player navigates correctly (left, right, or place).")]
+ [SerializeField] private AudioClip _hitClip;
+
+ [Tooltip("Played when the player inputs an incorrect action.")]
+ [SerializeField] private AudioClip _wrongInputClip;
+
+ [Tooltip("Played when a rhythm note is missed entirely.")]
+ [SerializeField] private AudioClip _missClip;
+
+ [Tooltip("Played when a button is pressed outside the hit window (off-beat click with no note present).")]
+ [SerializeField] private AudioClip _offBeatClip;
+
+ [Tooltip("Volume for the hit sound.")]
+ [SerializeField] [Range(0f, 1f)] private float _hitVolume = 0.8f;
+
+ [Tooltip("Volume for the wrong-input sound.")]
+ [SerializeField] [Range(0f, 1f)] private float _wrongInputVolume = 0.8f;
+
+ [Tooltip("Volume for the miss sound.")]
+ [SerializeField] [Range(0f, 1f)] private float _missVolume = 0.8f;
+
+ [Tooltip("Volume for the off-beat click sound.")]
+ [SerializeField] [Range(0f, 1f)] private float _offBeatVolume = 0.6f;
+
+ #endregion
+
+ #region Node Pulse
+
+ [Header("Node Pulse")]
+ [Tooltip("Peak scale multiplier during a pulse. 1.15 = 15% larger at peak.")]
+ [SerializeField] private float _pulseScaleMultiplier = 1.15f;
+
+ [Tooltip("Total duration of one pulse cycle (expand + contract), in seconds.")]
+ [SerializeField] private float _pulseDuration = 0.12f;
+
+ [Tooltip("Easing applied to each half of the pulse.")]
+ [SerializeField] private Ease _pulseEase = Ease.OutQuad;
+
+ [Tooltip("Beat multiplier for node pulsing. " +
+ "1 = every beat (full-time), 2 = every half-beat (double-time), 0.5 = every 2 beats (half-time).")]
+ [SerializeField] private float _beatPulseMultiplier = 1f;
+
+ #endregion
+
+ #region Wrong Input Bounce
+
+ [Header("Wrong Input Bounce")]
+ [Tooltip("How far the carrier slides in world units before springing back.")]
+ [SerializeField] private float _bounceOffset = 0.3f;
+
+ [Tooltip("Total duration of the bounce animation (slide out + spring back), in seconds.")]
+ [SerializeField] private float _bounceDuration = 0.25f;
+
+ [Tooltip("Easing for the slide-away portion of the bounce.")]
+ [SerializeField] private Ease _bounceOutEase = Ease.OutQuad;
+
+ [Tooltip("Easing for the spring-return portion of the bounce.")]
+ [SerializeField] private Ease _bounceReturnEase = Ease.OutElastic;
+
+ [Tooltip("How long correct input is blocked after a wrong action, in seconds. " +
+ "Set to 0 to allow immediate re-input while the bounce plays visually. " +
+ "Values equal to or greater than BounceDuration lock the player out for the full animation.")]
+ [SerializeField] private float _wrongInputLockoutDuration = 0.15f;
+
+ #endregion
+
+ #region Properties
+
+ /** Clip played on correct navigation. */
+ public AudioClip HitClip => _hitClip;
+
+ /** Clip played on incorrect input. */
+ public AudioClip WrongInputClip => _wrongInputClip;
+
+ /** Clip played when a rhythm note is missed. */
+ public AudioClip MissClip => _missClip;
+
+ /** Clip played on an off-beat button press. */
+ public AudioClip OffBeatClip => _offBeatClip;
+
+ /** Volume for hit sounds. */
+ public float HitVolume => _hitVolume;
+
+ /** Volume for wrong-input sounds. */
+ public float WrongInputVolume => _wrongInputVolume;
+
+ /** Volume for miss sounds. */
+ public float MissVolume => _missVolume;
+
+ /** Volume for off-beat click sounds. */
+ public float OffBeatVolume => _offBeatVolume;
+
+ /** Peak scale factor during a node pulse. */
+ public float PulseScaleMultiplier => _pulseScaleMultiplier;
+
+ /** Total duration of a single pulse cycle. */
+ public float PulseDuration => _pulseDuration;
+
+ /** Easing for each pulse half. */
+ public Ease PulseEase => _pulseEase;
+
+ /** Multiplier applied to the beat counter when determining pulse frequency. */
+ public float BeatPulseMultiplier => _beatPulseMultiplier;
+
+ /** World-unit slide distance for the wrong-input bounce. */
+ public float BounceOffset => _bounceOffset;
+
+ /** Total bounce animation duration. */
+ public float BounceDuration => _bounceDuration;
+
+ /** Easing for the slide-out phase of the bounce. */
+ public Ease BounceOutEase => _bounceOutEase;
+
+ /** Easing for the spring-back phase of the bounce. */
+ public Ease BounceReturnEase => _bounceReturnEase;
+
+ /** Seconds correct input is blocked after a wrong action. Zero means no lockout. */
+ public float WrongInputLockoutDuration => _wrongInputLockoutDuration;
+
+ #endregion
+}
diff --git a/Assets/Tree/Visual/TreeAnimationConfig.cs.meta b/Assets/Tree/Visual/TreeAnimationConfig.cs.meta
new file mode 100644
index 0000000..1260b70
--- /dev/null
+++ b/Assets/Tree/Visual/TreeAnimationConfig.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 08d88fdfd5135499187cd39a1423b188
\ No newline at end of file
diff --git a/Assets/Tree/Visual/TreeNodeView.cs b/Assets/Tree/Visual/TreeNodeView.cs
index df08b6b..368c551 100644
--- a/Assets/Tree/Visual/TreeNodeView.cs
+++ b/Assets/Tree/Visual/TreeNodeView.cs
@@ -24,6 +24,8 @@ public class TreeNodeView : MonoBehaviour
private MaterialPropertyBlock _nodeBlock;
private MaterialPropertyBlock _ringBlock;
+ private Vector3 _baseScale;
+
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");
#endregion
@@ -42,6 +44,7 @@ public void Initialize(TreeVisualizerConfig config)
_config = config;
_nodeBlock = new MaterialPropertyBlock();
_ringBlock = new MaterialPropertyBlock();
+ _baseScale = transform.localScale;
SetState(NodeVisualState.Normal);
}
@@ -107,6 +110,23 @@ public async UniTask MoveToAsync(Vector2 position, float duration, Ease ease)
await Tween.Position(transform, target, duration, ease);
}
+ /**
+ * Scales the node to a peak then returns it to its base scale.
+ * Safe to fire-and-forget; a subsequent call while one is already running
+ * will be overridden by PrimeTween on the same transform property.
+ *
+ * Peak scale relative to base scale.
+ * Total duration of the expand + contract cycle.
+ * Easing applied to each half of the cycle.
+ */
+ public async UniTask PulseAsync(float scaleMultiplier, float duration, Ease ease)
+ {
+ Vector3 peak = _baseScale * scaleMultiplier;
+ float half = duration * 0.5f;
+ await Tween.Scale(transform, peak, half, ease);
+ await Tween.Scale(transform, _baseScale, half, ease);
+ }
+
/**
* Instantly repositions the node to the target world-space XY position.
* Z is preserved.