Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/HoldNote.cs
T

870 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum NoteSegment { None, Start, Middle, End }
[RequireComponent(typeof(HoldNoteController))]
public class HoldNote : BaseNote
{
private KeyCode keyToPress = KeyCode.None;
private string noteColor = string.Empty;
private string noteID = string.Empty;
private NoteSegment segment = NoteSegment.None;
private float hitTime = 0f;
private float releaseTime = 0f;
private bool hasReleased = true;
private bool hasEnteredLine = false;
private Coroutine autoReturnCoroutine;
private HoldNoteController controller;
private AnimationController anim;
public int id;
public int trackIndex;
public float speed;
public float startTime;
public float delay;
public bool isEnd;
public float scheduledEndTime;
public string color;
public KeyCode key;
public string type; // "start", "middle", or "end"
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
// 新增变量
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
[Header("判定区间配置")]
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
private NoteData noteData;
// store original transform scale so we can restore on reset
private Vector3 originalLocalScale = Vector3.one;
// visual transform (child) to scale instead of root to avoid affecting colliders
private Transform visualTransform = null;
private Vector3 originalVisualLocalScale = Vector3.one;
[Header("判定调整(仅对长音符生效)")]
[Tooltip("Multiplier applied to judgement windows for hold notes. >1 makes hold judgement more lenient (wider windows).")]
[Range(1f, 2f)]
public float holdWindowMultiplier = 1.3f;
public float visualSpeedMultiplier = 1f; // injected from NoteSpawner to adapt windows when visual speed changes
private void Awake()
{
controller = GetComponent<HoldNoteController>();
// Prefer the global shared AnimationController if available, otherwise fallback to local or scene instance
anim = AnimationController.Global;
if (anim == null)
{
anim = GetComponent<AnimationController>();
if (anim == null)
{
anim = FindObjectOfType<AnimationController>();
}
}
if (anim == null)
{
Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
}
if (controller != null)
{
controller.OnJudgeZoneChanged += OnJudgeZoneChanged;
}
// capture original local scale for this prefab instance
originalLocalScale = transform.localScale;
// find a dedicated visual child to scale (prefer child named "Visual")
visualTransform = transform.Find("Visual");
if (visualTransform == null)
{
// fallback: try to find first child that has a SpriteRenderer, MeshRenderer or CanvasRenderer
var sr = GetComponentInChildren<SpriteRenderer>(true);
if (sr != null) visualTransform = sr.transform;
else
{
var mr = GetComponentInChildren<MeshRenderer>(true);
if (mr != null) visualTransform = mr.transform;
else
{
var ir = GetComponentInChildren<UnityEngine.UI.Graphic>(true);
if (ir != null) visualTransform = ir.transform;
}
}
}
if (visualTransform != null)
originalVisualLocalScale = visualTransform.localScale;
else
originalVisualLocalScale = originalLocalScale;
}
private void OnDestroy()
{
if (controller != null)
{
controller.OnJudgeZoneChanged -= OnJudgeZoneChanged;
}
}
private void OnJudgeZoneChanged(bool inZone)
{
hasEnteredLine = inZone;
}
private void OnEnable()
{
ResetState();
}
private void PlayHitAnimation()
{
var controller = AnimationController.Global ?? anim;
if (controller != null)
{
controller.PlayDestroyAnimation(noteColor);
}
}
private IEnumerator DelayedDisableAnimation(GameObject animationObject, float delay)
{
// No-op: we used to disable the shared AnimationController here which stopped coroutines.
// Keep as no-op to avoid side effects.
yield return null;
}
public void Setup(int id, int trackIndex, float speed, float time, float delay,
bool isEnd, float scheduledEnd, string color, KeyCode key, string type, NoteJudgeConfig judgeConfig, NoteData noteData)
{
this.id = id;
this.trackIndex = trackIndex;
// also set BaseNote.TrackIndex so other systems using TrackIndex property work consistently
this.speed = speed;
this.startTime = time;
this.delay = delay;
this.isEnd = isEnd;
this.scheduledEndTime = scheduledEnd;
this.color = color;
this.key = key;
this.type = type;
this.judgeConfig = judgeConfig;
this.noteID = id.ToString();
this.keyToPress = key;
this.noteColor = color;
// hitTime now uses real time base passed in as 'time' plus delay
this.hitTime = time + delay;
this.segment = (delay == 0f) ? NoteSegment.Start :
(isEnd ? NoteSegment.End : NoteSegment.Middle);
this.hasEnteredLine = false;
this.hasReleased = false;
// 判定区间配置检查
if (judgeConfig == null)
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
else
Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
this.noteData = noteData;
// Debug info to help investigate end note visibility
Debug.Log($"[HoldNote.Setup] id={noteID} segment={segment} type={type} hitTime={hitTime:F2} scheduledEnd={scheduledEndTime:F2} color={color} track={trackIndex}");
// 在 Setup 时注册初始状态
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
if (segment == NoteSegment.Start)
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
if (segment == NoteSegment.End)
JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime);
controller?.SetSpeed(speed);
controller?.SetSegmentDelay(delay);
}
private void Update()
{
// 如果当前片段已经判定过,直接返回
if (isJudged) return;
// 重新启动长按粒子协程,如果长按状态恢复且协程未运行
if (!isHoldActive && JudgeManager.Instance.IsStartJudged(noteID) && !JudgeManager.Instance.HasNoteReleased(noteID) && Input.GetKey(keyToPress))
{
isHoldActive = true;
AnimationController.Global?.StartHoldParticles(noteColor);
}
// 自动 Miss 判定:当音符经过判定线且未被判定时
if (hasEnteredLine && !isJudged)
{
float missRangeScaled = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if(segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
{
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
// 只做 Miss 结果展示与登记
JudgeManager.Instance.RegisterStartJudged(noteID, false);
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
isJudged = true;
ReturnToPool();
}
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + missRangeScaled)
{
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
HandleEnd(true);
isJudged = true;
}
}
// **通用长按状态更新:**
if (isHoldActive && Input.GetKeyUp(keyToPress))
{
isHoldActive = false;
AnimationController.Global?.StopHoldParticles();
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
if (!hasReleased)
{
hasReleased = true;
releaseTime = Time.time;
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
}
}
// Start段判定
// allow early keypress within judgement window even before collider entered (helps slow visual speeds)
if (segment == NoteSegment.Start)
{
if (Input.GetKeyDown(keyToPress))
{
float pressTimeLocal = Time.time;
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if (Mathf.Abs(pressTimeLocal - hitTime) <= maxWindow)
{
HandleStart();
isJudged = true; // mark judged to avoid duplicates
}
else
{
// key pressed but outside hold window -> ignore (do not mark judged)
}
}
}
else if (segment == NoteSegment.Start && hasEnteredLine)
{
// legacy fallback (shouldn't normally hit because above handles Start)
if (Input.GetKeyDown(keyToPress))
{
HandleStart();
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
}
}
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
else if (segment == NoteSegment.Middle
&& JudgeManager.Instance.IsStartJudged(noteID)
&& isHoldActive
&& !JudgeManager.Instance.HasNoteReleased(noteID))
{
if (Time.time >= hitTime)
{
if (hasEnteredLine)
{
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
PlayHitAnimation();
// 把通过事件上报给 JudgeManager(记录中段通过)
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
ReturnToPool();
isJudged = true;
}
}
}
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
else if (segment == NoteSegment.End
&& JudgeManager.Instance.IsStartJudged(noteID)
&& !JudgeManager.Instance.HasNoteReleased(noteID))
{
if (Input.GetKeyUp(keyToPress))
{
HandleEnd();
isJudged = true;
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("JudgmentLine")) return;
hasEnteredLine = true;
if (segment == NoteSegment.Middle)
{
hasBeenHeldFromStart = Input.GetKey(keyToPress);
Debug.Log($"[HoldNote] Middle段进入判定线: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
}
else if (segment == NoteSegment.End)
{
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
{
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
HandleEnd();
isJudged = true;
// ReturnToPool handled inside HandleEnd
return;
}
autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck());
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (!collision.CompareTag("JudgmentLine")) return;
if (segment == NoteSegment.Start)
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
// 确保未判定的 Start 段在离开判定线时登记为 Miss
HandleStart();
isJudged = true;
ReturnToPool();
}
}
else if (segment == NoteSegment.Middle)
{
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
ReturnToPool();
}
else if (segment == NoteSegment.End)
{
if (autoReturnCoroutine != null)
{
StopCoroutine(autoReturnCoroutine);
autoReturnCoroutine = null;
}
// 在尾部离开判定线时,按如下规则处理:
// - 如果头部未判定:补偿 Miss 并回收
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
if (!isJudged)
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
if (!JudgeManager.Instance.HasNoteReleased(noteID))
HandleEnd(true);
ReturnToPool();
}
else
{
if (JudgeManager.Instance.HasNoteReleased(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
HandleEnd();
isJudged = true;
ReturnToPool();
}
else
{
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定,等待玩家松手或超时处理
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
}
}
}
else
{
ReturnToPool();
}
}
}
private IEnumerator DelayedAutoReturnCheck()
{
yield return null;
if (segment == NoteSegment.End)
{
float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time);
if (timeToWait > 0f)
{
yield return new WaitForSeconds(timeToWait);
}
if (!gameObject.activeSelf)
{
yield break;
}
ReturnToPool();
}
}
private void HandleStart()
{
if (!JudgeManager.Instance.TryResolveStart(noteID))
{
Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}");
return;
}
float pressTime = Time.time;
float rawOffsetMs = (hitTime - pressTime) * 1000f;
float offset = Mathf.Abs(pressTime - hitTime);
string result;
// compute scaled judgement windows for hold note start
// further scale windows according to visualSpeedMultiplier so slow visuals get more leniency
float visualScaleFactor = Mathf.Clamp(visualSpeedMultiplier, 0.5f, 2f);
float pRange = (judgeConfig?.perfectRange ?? 0.1f) * holdWindowMultiplier * visualScaleFactor;
float gRange = (judgeConfig?.greatRange ?? 0.2f) * holdWindowMultiplier * visualScaleFactor;
float gdRange = (judgeConfig?.goodRange ?? 0.3f) * holdWindowMultiplier * visualScaleFactor;
// evaluate Start judgement using scaled windows
if (offset <= pRange)
{
result = "Perfect";
ScoreManager.Instance.countPerfect += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true; // 成功判定Start,长按状态激活
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
// Trigger skills ... (unchanged)
}
else if (offset <= gRange)
{
result = "Great";
ScoreManager.Instance.countGreat += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Great(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
}
else if (offset <= gdRange)
{
result = "Good";
ScoreManager.Instance.countGood += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Good(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
}
else
{
result = "Miss";
if (JudgeManager.Instance.TryResolveStart(noteID))
{
ScoreManager.Instance.countMiss += 1;
}
Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, false);
isHoldActive = false;
try
{
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
Debug.Log($"[HoldNote] START Miss NotifyNoteHit fired for slot {trackIndex}: {triggered}");
}
catch (System.Exception ex)
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Start Miss) threw: {ex}");
}
}
// show judge result and play sound / update combo like short notes
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
JudgeSoundManager.Instance?.PlayJudgeSound(result);
teamUIController.Instance?.OnJudgeResult(result);
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result); // 是否启动头音符判定跳字
// --- Add scoring for Start like short notes ---
try
{
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
if (allyGo != null)
{
var ally = allyGo.GetComponent<AllyCombatant>();
if (ally != null)
{
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[HoldNote] Failed to add per-track score for START: {ex}");
}
if (segment != NoteSegment.Start)
{
StartCoroutine(DelayedReturn());
}
}
private IEnumerator DelayedReturn()
{
yield return new WaitForSeconds(0.05f);
if (gameObject.activeSelf)
{
ReturnToPool();
}
}
private void HandleEnd(bool forceMiss = false)
{
if (!JudgeManager.Instance.TryResolveEnd(noteID))
{
Debug.Log($"[HoldNote] END 已被判定,跳过: {noteColor}");
return;
}
// 记录释放时间并在 JudgeManager 中登记已释放状态
if (!hasReleased)
{
releaseTime = Time.time;
hasReleased = true;
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
}
string result;
float rawOffsetMsEnd = 0f;
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
if (!hasEnteredLine && !forceMiss)
{
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
return;
}
// 现在进行正常判定逻辑
releaseTime = Time.time;
hasReleased = true;
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
if (forceMiss || !JudgeManager.Instance.IsStartJudged(noteID))
{
result = "Miss";
// if (JudgeManager.Instance.TryResolveEnd(noteID))
{
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
else if (result == "Good") ScoreManager.Instance.countGood += 1;
else ScoreManager.Instance.countMiss += 1;
}
Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "强制Miss" : "头部未判定")}");
}
else
{
float diff = Mathf.Abs(releaseTime - scheduledEndTime);
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
if (judgeConfig != null)
{
// apply hold-specific multiplier to end judgement windows as well
float pEnd = judgeConfig.perfectRange * holdWindowMultiplier;
float gEnd = judgeConfig.greatRange * holdWindowMultiplier;
float gdEnd = judgeConfig.goodRange * holdWindowMultiplier;
float mEnd = judgeConfig.missRange * holdWindowMultiplier;
if (diff <= pEnd)
{
result = "Perfect";
}
else if (diff <= gEnd)
{
result = "Great";
}
else if (diff <= gdEnd)
{
result = "Good";
}
else if (diff <= mEnd)
{
result = "Miss";
}
else
{
result = "Miss";
}
}
else
{
if (diff <= 0.1f * holdWindowMultiplier)
{
result = "Perfect";
}
else if (diff < 0.2f * holdWindowMultiplier)
{
result = "Great";
}
else if (diff < 0.3f * holdWindowMultiplier)
{
result = "Good";
}
else
{
result = "Bad";
}
}
// --- 新增:根据最终确定的 result 统计 ---
if (result == "Perfect") ScoreManager.Instance.countPerfect += 1;
else if (result == "Great") ScoreManager.Instance.countGreat += 1;
else if (result == "Good") ScoreManager.Instance.countGood += 1;
else ScoreManager.Instance.countMiss += 1;
// --- 新增:记录偏差数据到 NoteData ---
if (noteData != null && result != "Miss")
{
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
}
}
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
// --- Add scoring for End like short notes ---
try
{
var allyGo = GameObject.Find($"ally_0{TrackIndex + 1}");
if (allyGo != null)
{
var ally = allyGo.GetComponent<AllyCombatant>();
if (ally != null)
{
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[HoldNote] Failed to add per-track score for END: {ex}");
}
// Notify SkillBuilder so Hold notes can trigger skills configured for Hold
if (JudgeManager.Instance.TryTriggerSkill(noteID))
{
var sb = SkillBuilder.Instance;
if (sb == null)
{
Debug.LogWarning($"[HoldNote] SkillBuilder.Instance is null when trying to notify for track {TrackIndex}");
}
else
{
var so = sb.GetAllyHeroSOBySlot(TrackIndex);
var def = so?.GetPrimarySkill();
Debug.Log($"[HoldNote] About to NotifyNoteHit: TrackIndex={TrackIndex} trackIndexField={trackIndex} so={(so != null ? so.name : "null")} skill={(def != null ? def.skillId : "null")} trigger={(def != null ? def.triggerCondition.ToString() : "-")}");
}
bool triggeredHold = false;
bool triggeredTap = false;
// Build candidate slot indices to try: prefer instance field, then BaseNote.TrackIndex, then try to match UI slots
var candidates = new List<int>();
candidates.Add(this.trackIndex);
if (!candidates.Contains(this.TrackIndex)) candidates.Add(this.TrackIndex);
// try to resolve by matching this object to UI slots
var ui = teamUIController.Instance;
if (ui != null)
{
for (int i = 0; i < ui.allySlotIds.Count; i++)
{
var slotObj = ui.GetAllyObjectBySlot(i);
if (slotObj == null) continue;
if (slotObj == this.gameObject || this.gameObject.transform.IsChildOf(slotObj.transform))
{
if (!candidates.Contains(i)) candidates.Add(i);
break;
}
}
}
Debug.Log($"[HoldNote] NotifyNoteHit candidate slots: {string.Join(",", candidates)}");
// Try each candidate: prefer Hold notification. Stop on first successful trigger.
foreach (var slot in candidates)
{
try
{
triggeredHold = SkillBuilder.Instance?.NotifyNoteHit(slot, result, SkillDefinition.NoteTypeTrigger.Hold, this.noteID) ?? false;
}
catch (System.Exception ex)
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Hold) threw for slot {slot}: {ex}");
}
if (triggeredHold)
{
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Hold) for slot {slot}");
break;
}
}
// If no candidate triggered via Hold, try Tap fallback on same candidate list
if (!triggeredHold)
{
foreach (var slot in candidates)
{
try
{
triggeredTap = SkillBuilder.Instance?.NotifyNoteHit(slot, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
}
catch (System.Exception ex)
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Tap) threw for slot {slot}: {ex}");
}
if (triggeredTap)
{
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Tap fallback) for slot {slot}");
break;
}
}
}
// _hasTriggeredOnThisHold = true;
Debug.Log($"[HoldNote] Hold end NotifyNoteHit called: TrackIndex={TrackIndex} trackIndexField={trackIndex} result={result} triggeredHold={triggeredHold} triggeredTap={triggeredTap}");
}
Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})");
AnimationController.Global?.StopHoldParticles();
ReturnToPool();
}
private void OnDisable()
{
if (segment == NoteSegment.End)
{
AnimationController.Global?.StopHoldParticles();
}
if (autoReturnCoroutine != null)
{
StopCoroutine(autoReturnCoroutine);
autoReturnCoroutine = null;
}
if (segment == NoteSegment.End &&
!isJudged && // 尚未判定
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
{
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
{
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
if (JudgeManager.Instance.TryResolveEnd(noteID))
{
HandleEnd();
}
isJudged = true;
}
else
{
Debug.Log($"[HoldNote] OnDisableEnd段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
}
}
controller?.StopMovement();
}
private void ReturnToPool()
{
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
if (segment == NoteSegment.Start && !hasEnteredLine)
{
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
return;
}
gameObject.SetActive(false);
if (segment == NoteSegment.Start)
NotePool.Instance.ReturnStartNote(gameObject, noteColor);
else if (segment == NoteSegment.End)
NotePool.Instance.ReturnHoldNoteEndSegment(gameObject, noteColor);
else
NotePool.Instance.ReturnHoldNoteSegment(gameObject, noteColor);
}
public void ResetState()
{
hasReleased = false;
segment = NoteSegment.None;
keyToPress = KeyCode.None;
noteColor = string.Empty;
noteID = string.Empty;
hitTime = 0f;
scheduledEndTime = 0f;
hasEnteredLine = false;
isJudged = false;
isHoldActive = false; // 重置长按标记
hasBeenHeldFromStart = false; // 重置
// _hasTriggeredOnThisHold = false; // 重置
if (autoReturnCoroutine != null)
{
StopCoroutine(autoReturnCoroutine);
autoReturnCoroutine = null;
}
controller?.StopMovement();
// restore visual scale
ResetVisualScale();
}
public void ApplyVisualScale(float scaleY)
{
// clamp to reasonable range to avoid extreme distortion
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
if (visualTransform != null)
{
var newScale = originalVisualLocalScale;
newScale.y = originalVisualLocalScale.y * s;
visualTransform.localScale = newScale;
}
else
{
var newScale = originalLocalScale;
newScale.y = originalLocalScale.y * s;
transform.localScale = newScale;
}
}
public void ResetVisualScale()
{
if (visualTransform != null)
visualTransform.localScale = originalVisualLocalScale;
else
transform.localScale = originalLocalScale;
}
}