长音符再修复 加入了关卡连接 优化了一些默认加载

This commit is contained in:
FloatGaming
2026-01-13 11:22:39 +08:00
parent 2738089af1
commit 2f9d4aae0c
426 changed files with 299413 additions and 1050 deletions
+282 -227
View File
@@ -18,6 +18,7 @@ public class HoldNote : BaseNote
private bool hasEnteredLine = false;
private Coroutine autoReturnCoroutine;
private Coroutine scheduledReturnCoroutine;
private HoldNoteController controller;
private AnimationController anim;
@@ -32,14 +33,15 @@ public class HoldNote : BaseNote
public KeyCode key;
public string type; // "start", "middle", or "end"
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
// Flags and runtime state
private bool isJudged = false; // whether this segment has been judged (true = evaluation done)
private bool isHoldActive = false; // whether the hold is currently active (player is holding)
// 新增变量
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
// whether this middle segment was held from the start (used for logic checks)
private bool hasBeenHeldFromStart = false;
[Header("判定区间配置")]
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
[Header("Judge configuration")]
public NoteJudgeConfig judgeConfig; // judge windows configuration
private NoteData noteData;
// store original transform scale so we can restore on reset
@@ -48,13 +50,33 @@ public class HoldNote : BaseNote
private Transform visualTransform = null;
private Vector3 originalVisualLocalScale = Vector3.one;
[Header("判定调整(仅对长音符生效)")]
[Header("Hold judgement adjustments")]
[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
// track when the hold actually started (real-time) so we can compute held fraction
private float holdStartTime = -1f;
// Cache allies per track to avoid GameObject.Find on first judgement.
private static AllyCombatant[] allyCache;
private static AllyCombatant GetAllyForTrackCached(int trackIndex)
{
if (trackIndex < 0) return null;
if (allyCache == null || allyCache.Length < 10) allyCache = new AllyCombatant[10];
if (allyCache[trackIndex] != null) return allyCache[trackIndex];
var allyGo = GameObject.Find($"ally_0{trackIndex + 1}");
if (allyGo != null)
{
allyCache[trackIndex] = allyGo.GetComponent<AllyCombatant>();
}
return allyCache[trackIndex];
}
private void Awake()
{
controller = GetComponent<HoldNoteController>();
@@ -71,7 +93,7 @@ public class HoldNote : BaseNote
if (anim == null)
{
Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
if (GameConfig.verboseLogs) Debug.LogWarning("HoldNote: No AnimationController found (Global/local/scene). Particle effects will be unavailable.");
}
if (controller != null)
@@ -166,48 +188,90 @@ public class HoldNote : BaseNote
this.hasEnteredLine = false;
this.hasReleased = false;
// 判定区间配置检查
if (judgeConfig == null)
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
{
if (GameConfig.verboseLogs) Debug.LogError($"HoldNoteJudgeConfig is null! judgeConfig not assigned. track={trackIndex}");
}
else
Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
{
if (GameConfig.verboseLogs) 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}");
if (GameConfig.verboseLogs)
{
Debug.Log($"[HoldNote.Setup] id={noteID} segment={segment} type={type} hitTime={hitTime:F2} scheduledEnd={scheduledEndTime:F2} color={color} track={trackIndex}");
Debug.Log($"[HoldNote.Setup] Controller present={(controller != null)}, AnimationController global={(AnimationController.Global != null)}, localAnim={(anim != null)}");
}
// 在 Setup 时注册初始状态
// Register initial judge state in JudgeManager
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
if (segment == NoteSegment.Start)
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // initially mark start as not judged
if (segment == NoteSegment.End)
JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime);
controller?.SetSpeed(speed);
controller?.SetSegmentDelay(delay);
// IMPORTANT: configure timing using the same timebase as hitTime.
// NoteSpawner/Setup passes 'time' as the base realtime hit point already (startTime + note.time + globalHitDelay).
// We need travelTime so the segment starts moving at (hitTime - travelTime) rather than (Time.time + delay).
float travelTime = 0f;
if (speed > 0.0001f)
{
// NoteSpawner.CalculateSpeed uses: speed = 10.75f / travelTime, so travelTime = 10.75f / speed
travelTime = 10.75f / speed;
}
if (controller != null)
{
controller.ConfigureTiming(time, delay, travelTime);
// If the activation time is already in the past at the moment of Setup,
// the segment should have already traveled some distance. In that case
// snap its position forward so it appears at the correct location.
// Do this only when activation happened before now to avoid snapping
// freshly spawned segments that haven't started moving yet.
if (controller.ActivationTime < Time.time - 0.0001f)
{
// Use current transform.position as the spawn origin (NoteSpawner sets this before Setup).
CalibratePosition(transform.position, 0f);
}
}
else
{
// fallback
controller?.SetSegmentDelay(delay);
}
// Removed: CalibratePosition for delay==0f was causing Start segments to snap to incorrect positions
// if they started moving immediately after Setup (due to activationTime <= Time.time).
// Start segments should begin at spawnPoint and move from there naturally.
}
private void Update()
{
// 如果当前片段已经判定过,直接返回
// If already judged, skip heavy logic
if (isJudged) return;
// 重新启动长按粒子协程,如果长按状态恢复且协程未运行
// If start was judged and player is holding key, start hold effects
if (!isHoldActive && JudgeManager.Instance.IsStartJudged(noteID) && !JudgeManager.Instance.HasNoteReleased(noteID) && Input.GetKey(keyToPress))
{
isHoldActive = true;
AnimationController.Global?.StartHoldParticles(noteColor);
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Start particles started for {noteID} color={noteColor} at time={Time.time:F3}");
}
// 自动 Miss 判定:当音符经过判定线且未被判定时
// Auto-miss checks when inside judge line
if (hasEnteredLine && !isJudged)
{
float missRangeScaled = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if(segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
if (segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
{
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START auto-Miss: {noteColor}");
// register as missed
JudgeManager.Instance.RegisterStartJudged(noteID, false);
@@ -216,64 +280,63 @@ public class HoldNote : BaseNote
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
teamUIController.Instance?.OnJudgeResult("Miss");
// Show judgement prefab for auto-miss as well (consistent with other notes)
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
isJudged = true;
ReturnToPool();
ScheduleReturnToPool(0.2f);
}
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + missRangeScaled)
{
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END auto-Miss: {noteColor}");
HandleEnd(true);
isJudged = true;
}
}
// **通用长按状态更新:**
// Handle key release while holding
if (isHoldActive && Input.GetKeyUp(keyToPress))
{
isHoldActive = false;
AnimationController.Global?.StopHoldParticles();
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] KeyUp {keyToPress} detected. 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 (GameConfig.verboseLogs) Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
EvaluateHoldEnd(releaseTime, false);
}
}
// Start段判定
// allow early keypress within judgement window even before collider entered (helps slow visual speeds)
// Start judgement input handling
if (segment == NoteSegment.Start)
{
if (Input.GetKeyDown(keyToPress))
{
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] KeyDown detected for {noteID} key={keyToPress} time={Time.time:F3} hitTime={hitTime:F3} hasEnteredLine={hasEnteredLine}");
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)
// do not mark isJudged here; Start remains active for hold
}
}
}
else if (segment == NoteSegment.Start && hasEnteredLine)
{
// legacy fallback (shouldn't normally hit because above handles Start)
// legacy fallback
if (Input.GetKeyDown(keyToPress))
{
HandleStart(false);
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
isJudged = true;
}
}
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
else if (segment == NoteSegment.Middle
&& JudgeManager.Instance.IsStartJudged(noteID)
&& isHoldActive
@@ -283,16 +346,14 @@ public class HoldNote : BaseNote
{
if (hasEnteredLine)
{
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle passed: {noteColor}");
PlayHitAnimation();
// 把通过事件上报给 JudgeManager(记录中段通过)
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
ReturnToPool();
ScheduleReturnToPool(0.2f);
isJudged = true;
}
}
}
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
else if (segment == NoteSegment.End
&& JudgeManager.Instance.IsStartJudged(noteID)
&& !JudgeManager.Instance.HasNoteReleased(noteID))
@@ -314,7 +375,7 @@ public class HoldNote : BaseNote
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 (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle enter: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}");
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
@@ -324,13 +385,12 @@ public class HoldNote : BaseNote
if (autoReturnCoroutine != null)
StopCoroutine(autoReturnCoroutine);
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
// If already released before reaching the line, handle end judgement immediately
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
{
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End entered but note already released: {noteColor}");
HandleEnd();
isJudged = true;
// ReturnToPool handled inside HandleEnd
return;
}
@@ -346,17 +406,16 @@ public class HoldNote : BaseNote
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
// 确保未判定的 Start 段在离开判定线时登记为 Miss
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Start left judge zone without being judged -> Miss: {noteColor}");
HandleStart(true);
isJudged = true;
ReturnToPool();
ScheduleReturnToPool(0.2f);
}
}
else if (segment == NoteSegment.Middle)
{
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
ReturnToPool();
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Middle left judge zone: {noteColor}");
ScheduleReturnToPool(0.2f);
}
else if (segment == NoteSegment.End)
{
@@ -366,44 +425,36 @@ public class HoldNote : BaseNote
autoReturnCoroutine = null;
}
// 在尾部离开判定线时,按如下规则处理:
// - 如果头部未判定:补偿 Miss 并回收
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
if (!isJudged)
{
if (!JudgeManager.Instance.IsStartJudged(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End left judge zone and start was not judged -> Miss: {noteColor}");
if (!JudgeManager.Instance.HasNoteReleased(noteID))
HandleEnd(true);
ReturnToPool();
ScheduleReturnToPool(0.2f);
}
else
{
if (JudgeManager.Instance.HasNoteReleased(noteID))
{
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] End left judge zone and was released -> HandleEnd: {noteColor}");
HandleEnd();
isJudged = true;
ReturnToPool();
}
else
{
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定,等待玩家松手或超时处理
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
ScheduleReturnToPool(0.2f);
}
}
}
else
{
ReturnToPool();
ScheduleReturnToPool(0.2f);
}
}
}
private IEnumerator DelayedAutoReturnCheck()
{
// yield one frame to ensure any state changes are settled
yield return null;
if (segment == NoteSegment.End)
@@ -411,35 +462,72 @@ public class HoldNote : BaseNote
float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time);
if (timeToWait > 0f)
{
yield return new WaitForSeconds(timeToWait);
// use scaled time here so pause affects this wait
float target = Time.time + timeToWait;
while (Time.time < target)
{
yield return null;
}
}
if (!gameObject.activeSelf)
// If the object was deactivated while waiting, bail out
if (!gameObject.activeInHierarchy)
{
yield break;
}
// wait a short buffer after arriving to judge line before returning to pool
ScheduleReturnToPool(0.2f);
}
}
// Schedules a return-to-pool after a short delay. Cancels any previously scheduled return.
private void ScheduleReturnToPool(float delay)
{
if (scheduledReturnCoroutine != null)
{
StopCoroutine(scheduledReturnCoroutine);
scheduledReturnCoroutine = null;
}
if (!gameObject.activeInHierarchy)
{
return;
}
scheduledReturnCoroutine = StartCoroutine(DelayedReturnToPool(delay));
}
private IEnumerator DelayedReturnToPool(float delay)
{
yield return new WaitForSeconds(delay);
if (gameObject.activeSelf)
{
ReturnToPool();
}
scheduledReturnCoroutine = null;
}
private void HandleStart(bool forceMiss = false)
{
if (!JudgeManager.Instance.TryResolveStart(noteID))
{
Debug.Log($"[HoldNote] START 已被判定,跳过: {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START try-resolve failed: {noteColor}");
return;
}
// If forced miss (e.g. leaving judge zone without press), register miss immediately
if (forceMiss)
{
Debug.Log($"[HoldNote] START 强制 Miss (未按下): {noteColor}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START forced 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");
// Also show judgement prefab for forced miss on start
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
return;
}
@@ -456,32 +544,43 @@ public class HoldNote : BaseNote
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);
// record when the hold started so we can compute held fraction later
holdStartTime = pressTime;
// store into pool for use on end
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
}
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);
holdStartTime = pressTime;
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
}
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);
holdStartTime = pressTime;
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
}
else
{
@@ -492,7 +591,7 @@ public class HoldNote : BaseNote
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}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] START Miss NotifyNoteHit fired for slot {trackIndex}: {triggered}");
}
catch (System.Exception ex)
{
@@ -500,30 +599,20 @@ public class HoldNote : BaseNote
}
}
// show judge result and play sound / update combo like short notes
// show judge result and update combo/UI 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
// Play judge sound only for START segment
if (segment == NoteSegment.Start)
{
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);
}
}
JudgeSoundManager.Instance?.PlayJudgeSound(result);
}
catch (System.Exception ex)
// Do not spawn the judgement animation prefab for Start (head) presses to avoid duplicated prefabs
if (segment != NoteSegment.Start)
{
Debug.LogWarning($"[HoldNote] Failed to add per-track score for START: {ex}");
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
}
if (segment != NoteSegment.Start)
@@ -537,136 +626,120 @@ public class HoldNote : BaseNote
yield return new WaitForSeconds(0.05f);
if (gameObject.activeSelf)
{
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] DelayedReturn: returning {noteID} to pool at time={Time.time:F3}");
ReturnToPool();
}
}
private void HandleEnd(bool forceMiss = false)
// central evaluation for hold end, given an actual releaseTime (real-time) or forceMiss
private void EvaluateHoldEnd(float actualReleaseTime, bool forceMiss)
{
if (isJudged) return; // already evaluated
// ensure we only resolve once per note
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}");
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] EvaluateHoldEnd: TryResolveEnd failed for {noteID}");
return;
}
// 现在进行正常判定逻辑
releaseTime = Time.time;
releaseTime = actualReleaseTime;
hasReleased = true;
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] EvaluateHoldEnd: noteID={noteID} releaseTime={releaseTime:F3} scheduledEnd={scheduledEndTime:F3} forceMiss={forceMiss}");
string result;
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" : "")}");
ScoreManager.Instance.countMiss += 1;
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] END judged as Miss: {noteColor} reason={(forceMiss ? "forced Miss" : "start not judged")}");
}
else
{
float diff = Mathf.Abs(releaseTime - scheduledEndTime);
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
if (judgeConfig != null)
// try to use pool record for accurate press duration
if (HoldNoteJudgePool.TryGet(noteID, out var info))
{
// 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 (GameConfig.verboseLogs) Debug.Log($"[HoldNote] Found pool info for {noteID}: pressTime={info.pressTime:F3} length={info.length:F3} scheduledEnd={info.scheduledEnd:F3}");
float playerHeld = Mathf.Clamp(releaseTime - info.pressTime, 0f, info.length);
float frac = info.length <= 0f ? 0f : (playerHeld / info.length);
if (diff <= pEnd)
if (frac > 0.8f)
{
result = "Perfect";
ScoreManager.Instance.countPerfect += 1;
}
else if (diff <= gEnd)
else if (frac > 0.5f)
{
result = "Great";
}
else if (diff <= gdEnd)
{
result = "Good";
}
else if (diff <= mEnd)
{
result = "Miss";
ScoreManager.Instance.countGreat += 1;
}
else
{
result = "Miss";
result = "Good";
ScoreManager.Instance.countGood += 1;
}
if (noteData != null && result != "Miss")
{
noteData.judgeOffsetMsEnd = (info.scheduledEnd - releaseTime) * 1000f;
}
// cleanup pool
HoldNoteJudgePool.Unregister(noteID);
}
else
{
if (diff <= 0.1f * holdWindowMultiplier)
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] No pool info for {noteID}, falling back to local held fraction calculation");
// fallback logic if no pool info
float holdRequired = Mathf.Max(0.0001f, scheduledEndTime - hitTime);
float held = Mathf.Clamp(actualReleaseTime - hitTime, 0f, holdRequired);
float frac = holdRequired <= 0f ? 0f : (held / holdRequired);
if (frac > 0.8f)
{
result = "Perfect";
ScoreManager.Instance.countPerfect += 1;
}
else if (diff < 0.2f * holdWindowMultiplier)
else if (frac > 0.5f)
{
result = "Great";
}
else if (diff < 0.3f * holdWindowMultiplier)
{
result = "Good";
ScoreManager.Instance.countGreat += 1;
}
else
{
result = "Bad";
result = "Good";
ScoreManager.Instance.countGood += 1;
}
}
// --- 新增:根据最终确定的 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;
float rawOffsetMsEnd = (scheduledEndTime - actualReleaseTime) * 1000f;
if (noteData != null && result != "Miss")
{
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
}
}
}
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END result for {noteID}: {result} (color={noteColor})");
// show UI/audio and combo
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
teamUIController.Instance?.OnJudgeResult(result); // 新增:更新combo计数
teamUIController.Instance?.OnJudgeResult(result);
// Ensure END always spawns judgement prefab (including Miss)
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 = GetAllyForTrackCached(TrackIndex);
if (ally != null)
{
var ally = allyGo.GetComponent<AllyCombatant>();
if (ally != null)
{
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
int added = ally.AddScoreForJudge(result);
float efficiency = ally.scoreEfficiency;
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
}
}
catch (System.Exception ex)
@@ -677,26 +750,12 @@ public class HoldNote : BaseNote
// 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)
{
@@ -712,9 +771,6 @@ public class HoldNote : BaseNote
}
}
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
@@ -725,14 +781,9 @@ public class HoldNote : BaseNote
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Hold) threw for slot {slot}: {ex}");
}
if (triggeredHold)
{
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Hold) for slot {slot}");
break;
}
if (triggeredHold) break;
}
// If no candidate triggered via Hold, try Tap fallback on same candidate list
if (!triggeredHold)
{
foreach (var slot in candidates)
@@ -745,68 +796,56 @@ public class HoldNote : BaseNote
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Tap) threw for slot {slot}: {ex}");
}
if (triggeredTap)
{
Debug.Log($"[HoldNote] NotifyNoteHit succeeded (Tap fallback) for slot {slot}");
break;
}
if (triggeredTap) break;
}
}
// _hasTriggeredOnThisHold = true;
Debug.Log($"[HoldNote] Hold end NotifyNoteHit called: TrackIndex={TrackIndex} trackIndexField={trackIndex} result={result} triggeredHold={triggeredHold} triggeredTap={triggeredTap}");
if (GameConfig.verboseLogs) 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();
isJudged = true;
ScheduleReturnToPool(0.2f);
}
private void OnDisable()
private void HandleEnd(bool forceMiss = false)
{
if (segment == NoteSegment.End)
if (!JudgeManager.Instance.TryResolveEnd(noteID))
{
AnimationController.Global?.StopHoldParticles();
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] END try-resolve failed: {noteColor}");
return;
}
if (autoReturnCoroutine != null)
// record release time and register release in JudgeManager
if (!hasReleased)
{
StopCoroutine(autoReturnCoroutine);
autoReturnCoroutine = null;
}
if (segment == NoteSegment.End &&
!isJudged && // 尚未判定
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
{
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
if (Input.GetKey(keyToPress))
{
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
if (JudgeManager.Instance.TryResolveEnd(noteID))
{
HandleEnd();
}
isJudged = true;
releaseTime = scheduledEndTime;
}
else
{
Debug.Log($"[HoldNote] OnDisableEnd段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
releaseTime = Time.time;
}
hasReleased = true;
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
}
controller?.StopMovement();
// central evaluation
EvaluateHoldEnd(releaseTime, forceMiss);
}
private void ReturnToPool()
{
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
if (!gameObject.activeSelf) return;
if (segment == NoteSegment.Start && !hasEnteredLine)
{
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
if (GameConfig.verboseLogs) Debug.LogWarning($"[HoldNote] Refusing to return a Start segment that never entered judge zone: {noteColor}");
return;
}
if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={Time.time:F3}");
gameObject.SetActive(false);
if (segment == NoteSegment.Start)
@@ -828,9 +867,10 @@ public class HoldNote : BaseNote
scheduledEndTime = 0f;
hasEnteredLine = false;
isJudged = false;
isHoldActive = false; // 重置长按标记
hasBeenHeldFromStart = false; // 重置
// _hasTriggeredOnThisHold = false; // 重置
isHoldActive = false;
hasBeenHeldFromStart = false;
holdStartTime = -1f;
if (autoReturnCoroutine != null)
{
@@ -846,7 +886,6 @@ public class HoldNote : BaseNote
public void ApplyVisualScale(float scaleY)
{
// clamp to reasonable range to avoid extreme distortion
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
if (visualTransform != null)
{
@@ -869,4 +908,20 @@ public class HoldNote : BaseNote
else
transform.localScale = originalLocalScale;
}
public void CalibratePosition(Vector3 spawnPointPosition, float tolerance = 0.02f)
{
if (controller == null) controller = GetComponent<HoldNoteController>();
if (controller == null) return;
float activation = controller.ActivationTime;
float s = controller.CurrentSpeed;
float elapsed = Mathf.Max(0f, Time.time - activation);
Vector3 expected = spawnPointPosition + Vector3.down * s * elapsed;
if (Vector3.Distance(transform.position, expected) > tolerance)
{
transform.position = expected;
}
}
}