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 bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note private void Awake() { controller = GetComponent(); anim = GetComponent(); if (controller != null) { controller.OnJudgeZoneChanged += OnJudgeZoneChanged; } } private void OnDestroy() { if (controller != null) { controller.OnJudgeZoneChanged -= OnJudgeZoneChanged; } } private void OnJudgeZoneChanged(bool inZone) { hasEnteredLine = inZone; } private void OnEnable() { ResetState(); } private void PlayHitAnimation() { if (anim != null) { // 激活动画效果 GameObject anim.gameObject.SetActive(true); anim.PlayDestroyAnimation(noteColor); // 延迟禁用动画效果 GameObject,确保动画播放完毕 StartCoroutine(DelayedDisableAnimation(anim.gameObject, 0.5f)); // 0.5秒后禁用 } } private IEnumerator DelayedDisableAnimation(GameObject animationObject, float delay) { yield return new WaitForSeconds(delay); animationObject.SetActive(false); } 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) { this.id = id; this.trackIndex = trackIndex; // also set BaseNote.TrackIndex so other systems using TrackIndex property work consistently this.TrackIndex = trackIndex; 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}"); // 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; // 自动 Miss 判定:当音符经过判定线且未被判定时 if (hasEnteredLine && !isJudged) { if (segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange) { Debug.Log($"[HoldNote] START段超时自动Miss: {noteColor}"); HandleStart(); isJudged = true; } else if (segment == NoteSegment.End && Time.time > scheduledEndTime + (judgeConfig?.missRange ?? 0.3f)) { Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}"); HandleEnd(true); isJudged = true; } } // **通用长按状态更新:** if (isHoldActive && Input.GetKeyUp(keyToPress)) { isHoldActive = false; 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}"); } } if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress)) { isHoldActive = true; } // Start段判定 if (segment == NoteSegment.Start && hasEnteredLine) { 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() { float pressTime = Time.time; float offset = Mathf.Abs(pressTime - hitTime); string result; if (offset < 0.1f) // 判定成功 { result = "Perfect"; Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}"); JudgeManager.Instance.RegisterStartJudged(noteID, true); isHoldActive = true; // 成功判定Start,长按状态激活 PlayHitAnimation(); // Trigger skills configured to fire on note hits for this slot when the Start is successfully hit. if (!_hasTriggeredOnThisHold) { try { Debug.Log($"[HoldNote] Triggering SkillBuilder on START for slot {trackIndex} with result={result}"); // Prefer Hold type notification so skills expecting Hold can react. If it fails, fallback to Tap. bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Hold, this.noteID) ?? false; if (!triggered) { Debug.Log($"[HoldNote] START Hold notify did not trigger skill for slot {trackIndex}, trying Tap fallback"); triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false; } Debug.Log($"[HoldNote] START skill trigger result for slot {trackIndex}: {triggered}"); } catch (System.Exception ex) { Debug.LogError($"[HoldNote] NotifyNoteHit(Start) threw: {ex}"); } _hasTriggeredOnThisHold = true; // avoid triggering again at End } } else { result = "Miss"; Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}"); JudgeManager.Instance.RegisterStartJudged(noteID, false); // 明确标记未判定 isHoldActive = false; // Start Miss,长按状态不激活 // For Miss we should still notify shared logic once so it behaves like short notes try { // Treat as Tap-type miss for compatibility 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}"); } _hasTriggeredOnThisHold = true; // ensure we don't trigger again at End } // show judge result and play sound / update combo like short notes InputManager.Instance?.ShowJudgeResult(trackIndex, result); JudgeSoundManager.Instance?.PlayJudgeSound(result); teamUIController.Instance?.OnJudgeResult(result); StartCoroutine(DelayedReturn()); } private IEnumerator DelayedReturn() { yield return new WaitForSeconds(0.05f); if (gameObject.activeSelf) { ReturnToPool(); } } private void HandleEnd(bool forceMiss = false) { // 记录释放时间并在 JudgeManager 中登记已释放状态 if (!hasReleased) { releaseTime = Time.time; hasReleased = true; JudgeManager.Instance?.RegisterNoteReleased(noteID, true); } string result; // 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 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"; Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "强制Miss" : "头部未判定")}"); } else if (judgeConfig != null) { float diff = Mathf.Abs(releaseTime - scheduledEndTime); if (diff <= judgeConfig.perfectRange) { result = "Perfect"; } else if (diff <= judgeConfig.greatRange) { result = "Great"; } else if (diff <= judgeConfig.goodRange) { result = "Good"; } else if (diff <= judgeConfig.missRange) { result = "Miss"; } else { result = "Miss"; } } else { float diff = Mathf.Abs(releaseTime - scheduledEndTime); if (diff <= 0.1f) { result = "Perfect"; } else if (diff < 0.2f) { result = "Great"; } else if (diff < 0.3f) { result = "Good"; } else { result = "Bad"; } } InputManager.Instance?.ShowJudgeResult(trackIndex, result); teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数 // Notify SkillBuilder so Hold notes can trigger skills configured for Hold if (!_hasTriggeredOnThisHold) { 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(); 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})"); ReturnToPool(); } private void OnDisable() { 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}"); HandleEnd(); // 正常判定(基于 releaseTime) isJudged = true; } else { Debug.Log($"[HoldNote] OnDisable:End段未判定且尚有按键状态,跳过强制 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(); } }