Files

484 lines
19 KiB
C#
Raw Permalink Normal View History

2025-06-03 02:42:28 -04:00
using System;
using System.Collections.Generic;
using Dreamteck.Splines;
using Sirenix.OdinInspector;
2025-07-26 04:20:25 -04:00
using TMPro;
2025-06-03 02:42:28 -04:00
using UnityEngine;
namespace Ichni.RhythmGame
{
2025-07-21 05:42:20 -04:00
public abstract partial class NoteBase : GameElement, IHaveTimeDurationSubmodule, IComparable<NoteBase>
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
#region [] Basic & Info
2025-06-03 02:42:28 -04:00
[Title("Basic Info")]
public float exactJudgeTime;
public NoteJudgeIntervals judgeIntervals;
2026-03-14 03:13:10 -04:00
2025-06-03 02:42:28 -04:00
[Title("Track Info")]
public bool isOnTrack;
public Track track;
public SplinePositioner trackPositioner;
2025-07-08 14:28:40 -04:00
2025-06-03 02:42:28 -04:00
[Title("NoteVisual")]
public NoteVisualBase noteVisual;
2026-03-14 03:13:10 -04:00
2025-06-03 02:42:28 -04:00
[Title("Submodules")]
public TimeDurationSubmodule timeDurationSubmodule { get; set; }
2026-03-14 03:13:10 -04:00
public NoteJudgeSubmodule NoteJudgeSubmodule { get; set; }
public NoteAudioSubmodule NoteAudioSubmodule { get; set; }
2025-07-21 05:42:20 -04:00
[Title("In-Game Info")]
public bool isDuringJudging;
2025-07-08 14:28:40 -04:00
public Vector2 noteScreenPosition;
2025-08-27 21:45:18 -04:00
public Vector2 perfectNoteScreenPosition;
2025-07-08 14:28:40 -04:00
public bool isFirstJudged;
public bool isFinalJudged;
2025-06-03 02:42:28 -04:00
public override int HierarchyPriority => -10;
2026-03-14 03:13:10 -04:00
[Title("Debug")]
public TMP_Text judgeRankHint;
#endregion
#region [] GC-Free
protected List<EffectBase> generateEffects;
protected List<EffectBase> generalJudgeEffects;
protected List<EffectBase> perfectEffects;
protected List<EffectBase> goodEffects;
protected List<EffectBase> badEffects;
protected List<EffectBase> missEffects;
protected List<EffectBase> afterJudgeEffects;
protected float judgedTriggerTime;
protected NoteJudgeType judgedType;
protected bool isJudgedAndDestroying;
protected float destroyTimer;
// 用于记录是否离开屏幕的最后一帧有效坐标
protected Vector2 lastValidScreenPosition = -Vector2.one;
#endregion
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
#region [] Initialization
2025-06-03 02:42:28 -04:00
public override void SetDefaultSubmodules()
{
timeDurationSubmodule = new TimeDurationSubmodule(this);
2026-03-14 03:13:10 -04:00
NoteJudgeSubmodule = new NoteJudgeSubmodule(this);
2025-06-03 02:42:28 -04:00
}
public override void AfterInitialize()
{
2026-03-14 03:13:10 -04:00
// 安全读取字典防报错
generateEffects = GetEffectListSafe("Generate");
generalJudgeEffects = GetEffectListSafe("GeneralJudge");
perfectEffects = GetEffectListSafe("Perfect");
goodEffects = GetEffectListSafe("Good");
badEffects = GetEffectListSafe("Bad");
missEffects = GetEffectListSafe("Miss");
afterJudgeEffects = GetEffectListSafe("AfterJudge");
2025-08-27 21:45:18 -04:00
perfectNoteScreenPosition = -Vector2.one;
2026-03-14 03:13:10 -04:00
lastValidScreenPosition = -Vector2.one; // 初始化
2025-08-27 21:45:18 -04:00
2025-07-21 05:42:20 -04:00
float beyondTime = 0f;
2025-08-27 21:45:18 -04:00
2026-03-14 03:13:10 -04:00
// 【Review修改】使用无 GC 预存列表操作替代 String 调用
if (generateEffects != null)
2025-07-21 05:42:20 -04:00
{
2026-03-14 03:13:10 -04:00
for (int i = 0; i < generateEffects.Count; i++)
2025-08-11 14:04:06 -04:00
{
2026-03-14 03:13:10 -04:00
EffectBase effectBase = generateEffects[i];
if (effectBase is NoteGenerateEffect ge)
{
ge.Recover();
beyondTime = Mathf.Max(beyondTime, ge.generateTime);
}
else
{
effectBase.Recover();
}
2025-08-11 14:04:06 -04:00
}
2025-07-21 05:42:20 -04:00
}
2025-08-11 14:04:06 -04:00
2026-03-14 03:13:10 -04:00
if (exactJudgeTime - beyondTime - 0.5f > -GameManager.Instance.songInformation.delay)
2025-07-21 05:42:20 -04:00
{
gameObject.SetActive(false);
2026-03-14 03:13:10 -04:00
GameManager.Instance.noteManager.RegisterNote(this, exactJudgeTime - beyondTime - 0.5f);
2025-07-21 05:42:20 -04:00
}
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
#endregion
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
#region [] Main Update & Visual Calculate
public virtual bool ManualUpdate(float currentSongTime)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
// 若被 Judge 判定击中进入到特效摧毁倒计时
if (isJudgedAndDestroying)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
UpdatePostJudgeEffects();
destroyTimer -= Time.deltaTime;
if (destroyTimer <= 0)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
isJudgedAndDestroying = false;
// 【池化核心】调用 NoteManager 向 LeanPool 归还自己,向外抛出 false 要求将自己从更新名单剔除
GameManager.Instance.noteManager.DespawnNote(this);
return false;
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
return true;
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
if (isFinalJudged) return true;
// 1. 轨迹更新
UpdateNoteInTrack(currentSongTime);
// 2. 屏幕坐标系计算缓存
if (perfectNoteScreenPosition == -Vector2.one)
2025-06-03 02:42:28 -04:00
{
2025-08-27 21:45:18 -04:00
if (isDuringJudging)
{
noteScreenPosition = GetScreenPosition();
}
2026-03-14 03:13:10 -04:00
if (exactJudgeTime <= currentSongTime) // 代替 GameManager.Instance.songTime
2025-08-27 21:45:18 -04:00
{
perfectNoteScreenPosition = noteScreenPosition;
}
2025-06-03 02:42:28 -04:00
}
2025-07-26 04:20:25 -04:00
SetJudgeArea();
2026-03-14 03:13:10 -04:00
if (generateEffects != null)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
for (int i = 0; i < generateEffects.Count; i++)
{
generateEffects[i].UpdateEffect(exactJudgeTime);
}
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
// 自然 Miss 判定
if (!isFirstJudged && currentSongTime > exactJudgeTime + judgeIntervals.afterMiss)
2025-06-03 02:42:28 -04:00
{
2025-07-21 05:42:20 -04:00
Miss(exactJudgeTime + judgeIntervals.afterMiss);
2026-03-14 03:13:10 -04:00
GameManager.Instance.playingRecorder.resultData.Add(judgeIntervals.afterMiss);
2025-06-03 02:42:28 -04:00
isFirstJudged = true;
2025-07-08 14:28:40 -04:00
isFinalJudged = true;
2025-07-21 05:42:20 -04:00
RemoveFromCheckingList();
2025-06-03 02:42:28 -04:00
}
2026-02-13 09:19:55 -05:00
2026-03-14 03:13:10 -04:00
// AutoPlay
if (SettingsManager.instance.gameSettings.autoPlay && !isFirstJudged && currentSongTime >= exactJudgeTime)
2026-02-13 09:19:55 -05:00
{
2026-03-14 03:13:10 -04:00
ExecuteStartJudge(currentSongTime);
2026-02-13 09:19:55 -05:00
}
2026-03-14 03:13:10 -04:00
return true; // 仍然存活
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
public Vector2 GetScreenPosition()
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
Camera cam = GameManager.Instance.cameraManager.gameCamera.cam;
Vector3 screenPoint = cam.WorldToScreenPoint(noteVisual.noteVisualPosition);
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
// 判断是否在屏幕内并且在相机前方 (z>0)
bool isInsideScreen = screenPoint.z > 0 &&
screenPoint.x >= 0 && screenPoint.x <= Screen.width &&
screenPoint.y >= 0 && screenPoint.y <= Screen.height;
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
if (isInsideScreen)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
// 如果在视野中,则缓存坐标并返回
lastValidScreenPosition = new Vector2(screenPoint.x, screenPoint.y);
return lastValidScreenPosition;
}
else
{
// 如果已经离开屏幕,但是曾经存在缓存,就返回离开前最后一帧坐标
if (lastValidScreenPosition != -Vector2.one)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
return lastValidScreenPosition;
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
// 极端情况(生成时直接在屏幕外),直接返回换算的外界坐标兜底
return new Vector2(screenPoint.x, screenPoint.y);
2025-06-03 02:42:28 -04:00
}
}
2026-03-14 03:13:10 -04:00
protected virtual void SetJudgeArea()
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
if (!SettingsManager.instance.gameSettings.debugMode || NoteJudgeSubmodule?.judgeUnitList == null) return;
if (isDuringJudging && !isFirstJudged)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
2025-08-22 14:54:40 -04:00
{
2026-03-14 03:13:10 -04:00
var unit = NoteJudgeSubmodule.judgeUnitList[i];
if (!unit.isShowingJudge) unit.SetShowingJudge(true);
2025-08-22 14:54:40 -04:00
}
2026-03-14 03:13:10 -04:00
}
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
{
var unit = NoteJudgeSubmodule.judgeUnitList[i];
if (unit.isShowingJudge) unit.UpdateJudge();
}
if (!isDuringJudging && isFinalJudged)
{
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
2025-08-22 14:54:40 -04:00
{
2026-03-14 03:13:10 -04:00
var unit = NoteJudgeSubmodule.judgeUnitList[i];
if (unit.isShowingJudge) unit.SetShowingJudge(false);
2025-08-22 14:54:40 -04:00
}
2025-07-26 04:20:25 -04:00
}
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
#endregion
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
#region [] Track Logic
public void UpdateNoteInTrack(float songTime)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
if (!isOnTrack || track.trackTimeSubmodule == null) return;
if (track.trackTimeSubmodule is TrackTimeSubmoduleMovable)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
UpdateNoteInMovableTrack(songTime);
}
else if (track.trackTimeSubmodule is TrackTimeSubmoduleStatic)
2025-07-26 04:20:25 -04:00
{
2026-03-14 03:13:10 -04:00
UpdateNoteInStaticTrack(songTime);
2025-07-26 04:20:25 -04:00
}
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
public virtual void UpdateNoteInMovableTrack(float songTime)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
TrackTimeSubmoduleMovable trackTimeSubmoduleMovable = track.trackTimeSubmodule as TrackTimeSubmoduleMovable;
trackPositioner.SetPercent(trackTimeSubmoduleMovable.GetTrackPercent(exactJudgeTime));
}
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
public virtual void UpdateNoteInStaticTrack(float songTime)
2025-06-03 02:42:28 -04:00
{
2026-03-14 03:13:10 -04:00
TrackTimeSubmoduleStatic trackTimeSubmoduleStatic = track.trackTimeSubmodule as TrackTimeSubmoduleStatic;
float startMove = exactJudgeTime - trackTimeSubmoduleStatic.trackTotalTime;
float percent = AnimationCurveEvaluator.Evaluate(trackTimeSubmoduleStatic.animationCurveType, (songTime - startMove) / trackTimeSubmoduleStatic.trackTotalTime);
percent = Mathf.Clamp01(percent); // 替代 Max 和 Min 系统调用
trackPositioner.SetPercent(1 - percent);
}
public virtual void SetPerfectPosition()
{
if (isOnTrack && track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable)
2025-07-26 04:20:25 -04:00
{
2026-03-14 03:13:10 -04:00
float notePercent = movable.GetTrackPercent(CoreServices.TimeProvider.SongTime);
trackPositioner.SetPercent(notePercent);
2025-07-26 04:20:25 -04:00
}
2025-07-21 05:42:20 -04:00
}
2026-03-14 03:13:10 -04:00
protected virtual void SlowOffsetAfterExactJudgeTime()
2025-08-11 14:04:06 -04:00
{
2026-03-14 03:13:10 -04:00
if (isOnTrack && track.trackTimeSubmodule is TrackTimeSubmoduleMovable movable)
{
float slowedTime = (CoreServices.TimeProvider.SongTime - exactJudgeTime) * 0.8f;
float notePercent = movable.GetTrackPercent(exactJudgeTime + slowedTime);
trackPositioner.SetPercent(notePercent);
}
2025-08-11 14:04:06 -04:00
}
2026-03-14 03:13:10 -04:00
#endregion
#region [] Judge Dispatcher
public virtual void Perfect(float triggerTime) { ExecuteJudge(NoteJudgeType.Perfect, triggerTime); }
public virtual void Good(float triggerTime) { ExecuteJudge(NoteJudgeType.Good, triggerTime); }
public virtual void Bad(float triggerTime){ ExecuteJudge(NoteJudgeType.Bad, triggerTime); }
public virtual void Miss(float triggerTime) { ExecuteJudge(NoteJudgeType.Miss, triggerTime); }
public virtual void ExecuteStartJudge(float triggerTime) {}
protected virtual void RemoveFromCheckingList() => throw new NotImplementedException();
2025-08-11 14:04:06 -04:00
2026-03-14 03:13:10 -04:00
protected virtual NoteJudgeType GetStartJudgeType(float timeDifference)
2025-07-21 05:42:20 -04:00
{
2026-03-14 03:13:10 -04:00
return judgeIntervals.GetNoteJudgeType(timeDifference);
}
protected virtual void ExecuteJudge(NoteJudgeType judgeType, float triggerTime)
{
isDuringJudging = false;
judgedTriggerTime = triggerTime;
judgedType = judgeType;
switch (judgeType)
{
case NoteJudgeType.Perfect:
GameManager.Instance.playingRecorder.AddPerfect();
break;
case NoteJudgeType.Good:
GameManager.Instance.playingRecorder.AddGood();
break;
case NoteJudgeType.Bad:
GameManager.Instance.playingRecorder.AddBad();
break;
case NoteJudgeType.Miss:
GameManager.Instance.playingRecorder.AddMiss();
break;
2025-08-11 14:04:06 -04:00
}
2026-03-14 03:13:10 -04:00
NoteAudioSubmodule.PlayNoteJudgeAudios(judgeType);
if (isOnTrack) track.childElementList.Remove(this);
if (NoteJudgeSubmodule?.judgeUnitList != null)
2025-07-21 05:42:20 -04:00
{
2026-03-14 03:13:10 -04:00
for (int i = 0; i < NoteJudgeSubmodule.judgeUnitList.Count; i++)
2025-07-21 05:42:20 -04:00
{
2026-03-14 03:13:10 -04:00
var unit = NoteJudgeSubmodule.judgeUnitList[i];
if (unit.isShowingJudge)
2025-07-21 05:42:20 -04:00
{
unit.SetShowingJudge(false);
}
}
}
2026-03-14 03:13:10 -04:00
// 启动销毁计时器接管
isJudgedAndDestroying = true;
destroyTimer = 1.2f;
2025-07-21 05:42:20 -04:00
}
2026-03-14 03:13:10 -04:00
protected virtual void UpdatePostJudgeEffects()
2025-07-21 05:42:20 -04:00
{
2026-03-14 03:13:10 -04:00
UpdateEffectListInternal(generalJudgeEffects, judgedTriggerTime);
switch (judgedType)
{
case NoteJudgeType.Perfect:
UpdateEffectListInternal(perfectEffects, judgedTriggerTime);
break;
case NoteJudgeType.Good:
UpdateEffectListInternal(goodEffects, judgedTriggerTime);
break;
case NoteJudgeType.Bad:
UpdateEffectListInternal(badEffects, judgedTriggerTime);
break;
case NoteJudgeType.Miss:
UpdateEffectListInternal(missEffects, judgedTriggerTime);
break;
}
UpdateEffectListInternal(afterJudgeEffects, exactJudgeTime);
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
// 【Review修改】改用强制 for 循环,彻底禁用带局部参数传递的 Lambda 方法与扩展
private void UpdateEffectListInternal(List<EffectBase> effects, float time)
2025-07-26 04:20:25 -04:00
{
2026-03-14 03:13:10 -04:00
if (effects == null) return;
for (int i = 0; i < effects.Count; i++)
2025-07-26 04:20:25 -04:00
{
2026-03-14 03:13:10 -04:00
effects[i].UpdateEffect(time);
2025-07-26 04:20:25 -04:00
}
}
2025-08-11 14:04:06 -04:00
2026-03-14 03:13:10 -04:00
protected List<EffectBase> GetEffectListSafe(string key)
2025-08-11 14:04:06 -04:00
{
2026-03-14 03:13:10 -04:00
if (noteVisual?.effectSubmodule?.effectCollection != null &&
noteVisual.effectSubmodule.effectCollection.TryGetValue(key, out var list))
2025-08-11 14:04:06 -04:00
{
2026-03-14 03:13:10 -04:00
return list;
2025-08-11 14:04:06 -04:00
}
2026-03-14 03:13:10 -04:00
return null;
2025-08-11 14:04:06 -04:00
}
2025-08-22 14:54:40 -04:00
2026-03-14 03:13:10 -04:00
public int CompareTo(NoteBase other)
2025-08-22 14:54:40 -04:00
{
2026-03-14 03:13:10 -04:00
return exactJudgeTime.CompareTo(other.exactJudgeTime);
2025-08-22 14:54:40 -04:00
}
2026-03-14 03:13:10 -04:00
#endregion
2025-06-03 02:42:28 -04:00
}
2026-03-14 03:13:10 -04:00
#region [] Judge Type & Intervals
2025-06-03 02:42:28 -04:00
public abstract partial class NoteBase
2026-03-14 03:13:10 -04:00
{
2025-06-03 02:42:28 -04:00
public enum NoteJudgeType
{
2025-07-21 05:42:20 -04:00
Perfect = 0,
Good = 1,
Bad = 2,
Miss = 3,
NotJudged = -999
2025-06-03 02:42:28 -04:00
}
2025-07-21 05:42:20 -04:00
public static NoteJudgeType GetLowerType(NoteJudgeType typeA, NoteJudgeType typeB)
{
if (typeA == NoteJudgeType.NotJudged) return typeB;
if (typeB == NoteJudgeType.NotJudged) return typeA;
return typeA > typeB ? typeA : typeB;
}
2025-06-03 02:42:28 -04:00
public class NoteJudgeIntervals
{
2026-03-14 03:13:10 -04:00
public TimeInterval beforeMiss, beforeBad, beforeGood, perfect, afterGood, afterBad;
2025-06-03 02:42:28 -04:00
public float afterMiss;
2026-03-14 03:13:10 -04:00
public static readonly NoteJudgeIntervals TapDefault = new NoteJudgeIntervals(
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.125f),
new TimeInterval(-0.125f, -0.1f), new TimeInterval(-0.1f, 0.1f),
new TimeInterval(0.1f, 0.125f), new TimeInterval(0.125f, 0.15f), 0.15f);
public static readonly NoteJudgeIntervals StayDefault = new NoteJudgeIntervals(
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f),
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, 0.15f),
new TimeInterval(0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), 0.15f);
public static readonly NoteJudgeIntervals HoldDefault = new NoteJudgeIntervals(
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.125f),
new TimeInterval(-0.125f, -0.1f), new TimeInterval(-0.1f, 0.1f),
new TimeInterval(0.1f, 0.125f), new TimeInterval(0.125f, 0.15f), 0.15f);
public static readonly NoteJudgeIntervals FlickDefault = new NoteJudgeIntervals(
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, -0.15f),
new TimeInterval(-0.15f, -0.15f), new TimeInterval(-0.15f, 0.15f),
new TimeInterval(0.15f, 0.15f), new TimeInterval(0.15f, 0.15f), 0.15f);
2025-06-03 02:42:28 -04:00
public NoteJudgeIntervals(TimeInterval beforeMiss, TimeInterval beforeBad, TimeInterval beforeGood,
TimeInterval perfect, TimeInterval afterGood, TimeInterval afterBad, float afterMiss)
{
2026-03-14 03:13:10 -04:00
this.beforeMiss = beforeMiss; this.beforeBad = beforeBad;
this.beforeGood = beforeGood; this.perfect = perfect;
this.afterGood = afterGood; this.afterBad = afterBad;
2025-06-03 02:42:28 -04:00
this.afterMiss = afterMiss;
}
public NoteJudgeType GetNoteJudgeType(float timeDifference)
{
2026-03-14 03:13:10 -04:00
if (beforeMiss.IsInInterval(timeDifference)) return NoteJudgeType.Miss;
if (beforeBad.IsInInterval(timeDifference)) return NoteJudgeType.Bad;
if (beforeGood.IsInInterval(timeDifference)) return NoteJudgeType.Good;
if (perfect.IsInInterval(timeDifference)) return NoteJudgeType.Perfect;
if (afterGood.IsInInterval(timeDifference)) return NoteJudgeType.Good;
if (afterBad.IsInInterval(timeDifference)) return NoteJudgeType.Bad;
2025-06-03 02:42:28 -04:00
2026-03-14 03:13:10 -04:00
return NoteJudgeType.Miss;
2025-06-03 02:42:28 -04:00
}
}
public class TimeInterval
{
public float intervalStart;
public float intervalEnd;
public TimeInterval(float start, float end)
{
intervalStart = start;
intervalEnd = end;
}
public bool IsInInterval(float time)
{
if (Mathf.Approximately(intervalStart, intervalEnd)) return false;
return time >= intervalStart && time <= intervalEnd;
}
}
public static string GetNoteTypeName(NoteBase note)
{
return note switch
{
Tap => "Tap",
Stay => "Stay",
Hold => "Hold",
Flick => "Flick",
_ => throw new NotImplementedException("Note type not recognized")
};
}
}
2026-03-14 03:13:10 -04:00
#endregion
}