870 lines
33 KiB
C#
870 lines
33 KiB
C#
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] 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;
|
||
}
|
||
} |