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(); // Prefer the global shared AnimationController if available, otherwise fallback to local or scene instance anim = AnimationController.Global; if (anim == null) { anim = GetComponent(); if (anim == null) { anim = FindObjectOfType(); } } 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(true); if (sr != null) visualTransform = sr.transform; else { var mr = GetComponentInChildren(true); if (mr != null) visualTransform = mr.transform; else { var ir = GetComponentInChildren(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}"); // register as missed 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(false); 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(false); 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(true); 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(bool forceMiss = false) { if (!JudgeManager.Instance.TryResolveStart(noteID)) { Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}"); return; } // If forced miss (e.g. leaving judge zone without press), register miss immediately if (forceMiss) { Debug.Log($"[HoldNote] START 强制 Miss (未按下): {noteColor}"); ScoreManager.Instance.countMiss += 1; JudgeManager.Instance.RegisterStartJudged(noteID, false); isHoldActive = false; InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss"); JudgeSoundManager.Instance?.PlayJudgeSound("Miss"); teamUIController.Instance?.OnJudgeResult("Miss"); return; } float pressTime = Time.time; float rawOffsetMs = (hitTime - pressTime) * 1000f; float offset = Mathf.Abs(pressTime - hitTime); string result; 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; if (offset <= pRange) { result = "Perfect"; ScoreManager.Instance.countPerfect += 1; if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs; JudgeManager.Instance.RegisterStartJudged(noteID, true); isHoldActive = true; PlayHitAnimation(); AnimationController.Global?.StartHoldParticles(noteColor); } else if (offset <= gRange) { result = "Great"; ScoreManager.Instance.countGreat += 1; if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs; 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; JudgeManager.Instance.RegisterStartJudged(noteID, true); isHoldActive = true; PlayHitAnimation(); AnimationController.Global?.StartHoldParticles(noteColor); } else { result = "Miss"; ScoreManager.Instance.countMiss += 1; 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(); 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(); 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(); 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] 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(); // 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; } }