修复一堆bug 加入粒子系统代替原击打特效 焕新长音符逻辑 修复多重计分 更新判定管理和音符生成器 搞了一个免费包
This commit is contained in:
@@ -20,26 +20,58 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
private int[] pmScoreSums = new int[5];
|
||||
|
||||
// per-track idol score sums (red, green, yellow, purple, blue)
|
||||
public int red_idolScore_sum = 0;
|
||||
public int green_idolScore_sum = 0;
|
||||
public int yellow_idolScore_sum = 0;
|
||||
public int purple_idolScore_sum = 0;
|
||||
public int blue_idolScore_sum = 0;
|
||||
|
||||
// aggregate of the five track idol sums
|
||||
public int allSum_idolScore = 0;
|
||||
|
||||
private int[] idolScoreSums = new int[5];
|
||||
|
||||
[Header("Judgement Statistics")]
|
||||
public int countPerfect = 0;
|
||||
public int countGreat = 0;
|
||||
public int countGood = 0;
|
||||
public int countMiss = 0;
|
||||
|
||||
public void ResetStatistics()
|
||||
{
|
||||
countPerfect = countGreat = countGood = countMiss = 0;
|
||||
}
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
|
||||
for (int i = 0; i < pmScoreSums.Length; i++) pmScoreSums[i] = 0;
|
||||
for (int i = 0; i < idolScoreSums.Length; i++) idolScoreSums[i] = 0;
|
||||
|
||||
ResetStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a per-note pm score delta to the specified track (0..4) and update named fields + aggregate.
|
||||
/// Also calculates idol score as pmDelta * scoreEfficiency.
|
||||
/// </summary>
|
||||
public void AddPmScoreForTrack(int trackIndex, int delta)
|
||||
public void AddPmScoreForTrack(int trackIndex, int pmDelta, float scoreEfficiency)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= pmScoreSums.Length) return;
|
||||
if (delta == 0) return;
|
||||
pmScoreSums[trackIndex] += delta;
|
||||
if (pmDelta == 0) return;
|
||||
pmScoreSums[trackIndex] += pmDelta;
|
||||
// clamp at int.MaxValue-1 to avoid overflow
|
||||
if (pmScoreSums[trackIndex] < 0) pmScoreSums[trackIndex] = 0;
|
||||
if (pmScoreSums[trackIndex] > int.MaxValue - 1) pmScoreSums[trackIndex] = int.MaxValue - 1;
|
||||
|
||||
// calculate idol score: pmDelta * scoreEfficiency
|
||||
int idolDelta = Mathf.FloorToInt(pmDelta * scoreEfficiency);
|
||||
idolScoreSums[trackIndex] += idolDelta;
|
||||
if (idolScoreSums[trackIndex] < 0) idolScoreSums[trackIndex] = 0;
|
||||
if (idolScoreSums[trackIndex] > int.MaxValue - 1) idolScoreSums[trackIndex] = int.MaxValue - 1;
|
||||
|
||||
// update named fields for easy access
|
||||
red_pmScore_sum = pmScoreSums[0];
|
||||
green_pmScore_sum = pmScoreSums[1];
|
||||
@@ -47,12 +79,21 @@ public class ScoreManager : MonoBehaviour
|
||||
purple_pmScore_sum = pmScoreSums[3];
|
||||
blue_pmScore_sum = pmScoreSums[4];
|
||||
|
||||
// recompute aggregate
|
||||
long agg = 0;
|
||||
for (int i = 0; i < pmScoreSums.Length; i++) agg += pmScoreSums[i];
|
||||
allSum_pmScore = (int)Mathf.Min((float)agg, (float)int.MaxValue - 1f);
|
||||
red_idolScore_sum = idolScoreSums[0];
|
||||
green_idolScore_sum = idolScoreSums[1];
|
||||
yellow_idolScore_sum = idolScoreSums[2];
|
||||
purple_idolScore_sum = idolScoreSums[3];
|
||||
blue_idolScore_sum = idolScoreSums[4];
|
||||
|
||||
Debug.Log($"[ScoreManager] Added {delta} to track {trackIndex} pm sum. New per-track sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSum={allSum_pmScore}");
|
||||
// recompute aggregates
|
||||
long aggPm = 0;
|
||||
long aggIdol = 0;
|
||||
for (int i = 0; i < pmScoreSums.Length; i++) aggPm += pmScoreSums[i];
|
||||
for (int i = 0; i < idolScoreSums.Length; i++) aggIdol += idolScoreSums[i];
|
||||
allSum_pmScore = (int)Mathf.Min((float)aggPm, (float)int.MaxValue - 1f);
|
||||
allSum_idolScore = (int)Mathf.Min((float)aggIdol, (float)int.MaxValue - 1f);
|
||||
|
||||
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
|
||||
|
||||
// Update UI in teamUIController if available (per-track pm sums + aggregate)
|
||||
var ui = teamUIController.Instance;
|
||||
@@ -67,10 +108,17 @@ public class ScoreManager : MonoBehaviour
|
||||
if (ui.purple_pmScore_sum != null) ui.purple_pmScore_sum.text = purple_pmScore_sum.ToString();
|
||||
if (ui.blue_pmScore_sum != null) ui.blue_pmScore_sum.text = blue_pmScore_sum.ToString();
|
||||
if (ui.allSum_pmScore != null) ui.allSum_pmScore.text = allSum_pmScore.ToString();
|
||||
|
||||
if (ui.red_idolScore_sum != null) ui.red_idolScore_sum.text = red_idolScore_sum.ToString();
|
||||
if (ui.green_idolScore_sum != null) ui.green_idolScore_sum.text = green_idolScore_sum.ToString();
|
||||
if (ui.yellow_idolScore_sum != null) ui.yellow_idolScore_sum.text = yellow_idolScore_sum.ToString();
|
||||
if (ui.purple_idolScore_sum != null) ui.purple_idolScore_sum.text = purple_idolScore_sum.ToString();
|
||||
if (ui.blue_idolScore_sum != null) ui.blue_idolScore_sum.text = blue_idolScore_sum.ToString();
|
||||
if (ui.allSum_idolScore != null) ui.allSum_idolScore.text = allSum_idolScore.ToString();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ScoreManager] Failed to write pm sums to teamUIController fields: {ex}");
|
||||
Debug.LogWarning($"[ScoreManager] Failed to write sums to teamUIController fields: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,4 +235,4 @@ public class ScoreManager : MonoBehaviour
|
||||
Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using System.Collections; // 保持对 Coroutine 的支持
|
||||
using System.Collections.Generic; // For List
|
||||
|
||||
public class AnimationController : MonoBehaviour
|
||||
{
|
||||
public GameObject redEffect; // 红色击打动画对象
|
||||
public GameObject greenEffect; // 绿色击打动画对象
|
||||
public GameObject yellowEffect; // 黄色击打动画对象
|
||||
public GameObject purpleEffect; // 紫色击打动画对象
|
||||
public GameObject blueEffect; // 蓝色击打动画对象
|
||||
[HideInInspector] public bool isGlobalController = false;
|
||||
|
||||
public static AnimationController Global;
|
||||
|
||||
public GameObject redEffect; // 红色击打动画对象
|
||||
public GameObject greenEffect; // 绿色击打动画对象
|
||||
public GameObject yellowEffect; // 黄色击打动画对象
|
||||
public GameObject purpleEffect; // 紫色击打动画对象
|
||||
public GameObject blueEffect; // 蓝色击打动画对象
|
||||
|
||||
private Animator redAnimator;
|
||||
private Animator greenAnimator;
|
||||
@@ -14,42 +21,434 @@ public class AnimationController : MonoBehaviour
|
||||
private Animator purpleAnimator;
|
||||
private Animator blueAnimator;
|
||||
|
||||
// Scene object used as particle source (must be assigned in scene or found automatically)
|
||||
public GameObject hit_particular_object;
|
||||
public GameObject hit_ring_object;
|
||||
|
||||
[Header("轨道颜色 (Hex Color Codes)")]
|
||||
// F86F64 (浅红色/珊瑚色)
|
||||
public string redColorHex = "#F86F64";
|
||||
// 40EC90 (亮绿色)
|
||||
public string greenColorHex = "#40EC90";
|
||||
// F7CB6C (金黄色)
|
||||
public string yellowColorHex = "#F7CB6C";
|
||||
// F888E7 (亮粉色/洋红色)
|
||||
public string purpleColorHex = "#F888E7";
|
||||
// 4FACFE (天蓝色)
|
||||
public string blueColorHex = "#4FACFE";
|
||||
[Header("Hold 粒子配置")]
|
||||
public float holdParticleInterval = 0.2f;
|
||||
|
||||
private Coroutine holdParticleCoroutine;
|
||||
private bool holdActive = false;
|
||||
private string currentHoldColor;
|
||||
private bool isHolding = false;
|
||||
|
||||
// Track active particle instances for immediate cleanup
|
||||
private List<GameObject> activeParticles = new List<GameObject>();
|
||||
|
||||
[Header("判定飘字prefabs")]
|
||||
public GameObject perfect_judge_prefab;
|
||||
public GameObject great_judge_prefab;
|
||||
public GameObject good_judge_prefab;
|
||||
public GameObject miss_judge_prefab;
|
||||
[Header("轨道关联的判定文字 (Legacy Text)")]
|
||||
// 请在 Inspector 中将对应的 UI Text 拖入
|
||||
public TextMeshProUGUI red_track_judgementText;
|
||||
public TextMeshProUGUI green_track_judgementText;
|
||||
public TextMeshProUGUI yellow_track_judgementText;
|
||||
public TextMeshProUGUI purple_track_judgementText;
|
||||
public TextMeshProUGUI blue_track_judgementText;
|
||||
|
||||
public void StartHoldParticles(string color)
|
||||
{
|
||||
// Prefer the Global controller if available so Start/Stop always affect the same instance
|
||||
if (AnimationController.Global != null && AnimationController.Global != this)
|
||||
{
|
||||
AnimationController.Global.InternalStartHoldParticles(color);
|
||||
return;
|
||||
}
|
||||
|
||||
InternalStartHoldParticles(color);
|
||||
}
|
||||
|
||||
public void StopHoldParticles()
|
||||
{
|
||||
if (AnimationController.Global != null && AnimationController.Global != this)
|
||||
{
|
||||
AnimationController.Global.InternalStopHoldParticles();
|
||||
return;
|
||||
}
|
||||
|
||||
InternalStopHoldParticles();
|
||||
}
|
||||
|
||||
// Internal implementations operate on this instance
|
||||
private void InternalStartHoldParticles(string color)
|
||||
{
|
||||
if (holdActive) return;
|
||||
|
||||
holdActive = true;
|
||||
currentHoldColor = color;
|
||||
|
||||
if (holdParticleCoroutine != null)
|
||||
{
|
||||
StopCoroutine(holdParticleCoroutine);
|
||||
}
|
||||
|
||||
holdParticleCoroutine = StartCoroutine(HoldParticleRoutine(color));
|
||||
}
|
||||
|
||||
private void InternalStopHoldParticles()
|
||||
{
|
||||
holdActive = false;
|
||||
|
||||
if (holdParticleCoroutine != null)
|
||||
{
|
||||
try { StopCoroutine(holdParticleCoroutine); } catch { }
|
||||
holdParticleCoroutine = null;
|
||||
}
|
||||
|
||||
// Immediately destroy all active particle instances
|
||||
foreach (var p in activeParticles)
|
||||
{
|
||||
if (p != null)
|
||||
{
|
||||
Destroy(p);
|
||||
}
|
||||
}
|
||||
activeParticles.Clear();
|
||||
}
|
||||
|
||||
private IEnumerator HoldParticleRoutine(string color)
|
||||
{
|
||||
while (holdActive)
|
||||
{
|
||||
yield return new WaitForSeconds(Mathf.Max(0.01f, holdParticleInterval));
|
||||
if (!holdActive) break;
|
||||
PlayDestroyAnimation(color);
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 获取每个动画对象的 Animator 组件
|
||||
redAnimator = redEffect.GetComponent<Animator>();
|
||||
greenAnimator = greenEffect.GetComponent<Animator>();
|
||||
yellowAnimator = yellowEffect.GetComponent<Animator>();
|
||||
purpleAnimator = purpleEffect.GetComponent<Animator>();
|
||||
blueAnimator = blueEffect.GetComponent<Animator>();
|
||||
if (isGlobalController)
|
||||
{
|
||||
if (Global != null && Global != this)
|
||||
{
|
||||
Debug.LogWarning("[AnimationController] Duplicate global detected, destroying self");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Global = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
Debug.Log("[AnimationController] Global AnimationController registered");
|
||||
}
|
||||
|
||||
// Animator cache
|
||||
redAnimator = redEffect != null ? redEffect.GetComponent<Animator>() : null;
|
||||
greenAnimator = greenEffect != null ? greenEffect.GetComponent<Animator>() : null;
|
||||
yellowAnimator = yellowEffect != null ? yellowEffect.GetComponent<Animator>() : null;
|
||||
purpleAnimator = purpleEffect != null ? purpleEffect.GetComponent<Animator>() : null;
|
||||
blueAnimator = blueEffect != null ? blueEffect.GetComponent<Animator>() : null;
|
||||
|
||||
// If the scene object reference was not assigned on this instance (common when this script is on a prefab),
|
||||
// try to locate a scene object automatically so runtime instances can still use a shared particle source.
|
||||
if (hit_particular_object == null)
|
||||
{
|
||||
// First try a tag-based lookup. Designer should assign the scene particle source a tag "HitParticleSource".
|
||||
try
|
||||
{
|
||||
var byTag = GameObject.FindWithTag("HitParticleSource");
|
||||
if (byTag != null)
|
||||
{
|
||||
hit_particular_object = byTag;
|
||||
Debug.Log("AnimationController: assigned hit_particular_object via tag 'HitParticleSource'.");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Next try common names if tag not used
|
||||
if (hit_particular_object == null)
|
||||
{
|
||||
var byName = GameObject.Find("HitParticleSource") ?? GameObject.Find("hit_particular_object") ?? GameObject.Find("Hit_Particle_Source");
|
||||
if (byName != null)
|
||||
{
|
||||
hit_particular_object = byName;
|
||||
Debug.Log("AnimationController: assigned hit_particular_object via GameObject.Find by name.");
|
||||
}
|
||||
}
|
||||
|
||||
if (hit_particular_object == null)
|
||||
{
|
||||
Debug.LogWarning("AnimationController: hit_particular_object not assigned in Inspector and automatic lookup failed.\n" +
|
||||
"You can either assign a scene object to 'hit_particular_object' on the prefab instance in the scene,\n" +
|
||||
"or tag the scene particle source with 'HitParticleSource', or place a GameObject named 'HitParticleSource' in the scene.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayDestroyAnimation(string color)
|
||||
{
|
||||
// Only use the scene object as the template. No prefab fallback.
|
||||
if (hit_particular_object == null)
|
||||
{
|
||||
Debug.LogError("hit_particular_object is null. Assign a scene particle source to AnimationController.hit_particular_object or tag a GameObject 'HitParticleSource'.");
|
||||
// As a fallback, trigger the Animator-based effects
|
||||
TriggerAnimatorEffect(color);
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject source = hit_particular_object;
|
||||
|
||||
// choose spawn position: prefer per-color effect transform if available
|
||||
Vector3 spawnPos = transform.position;
|
||||
Transform parentTransform = null;
|
||||
switch (color)
|
||||
{
|
||||
case "red":
|
||||
// 重置之前的触发器
|
||||
redAnimator.ResetTrigger("PlayRedDestroy");
|
||||
redAnimator.SetTrigger("PlayRedDestroy");
|
||||
break;
|
||||
case "green":
|
||||
greenAnimator.ResetTrigger("PlayGreenDestroy");
|
||||
greenAnimator.SetTrigger("PlayGreenDestroy");
|
||||
break;
|
||||
case "yellow":
|
||||
yellowAnimator.ResetTrigger("PlayYellowDestroy");
|
||||
yellowAnimator.SetTrigger("PlayYellowDestroy");
|
||||
break;
|
||||
case "purple":
|
||||
purpleAnimator.ResetTrigger("PlayPurpleDestroy");
|
||||
purpleAnimator.SetTrigger("PlayPurpleDestroy");
|
||||
break;
|
||||
case "blue":
|
||||
blueAnimator.ResetTrigger("PlayBlueDestroy");
|
||||
blueAnimator.SetTrigger("PlayBlueDestroy");
|
||||
break;
|
||||
case "red": if (redEffect != null) { spawnPos = redEffect.transform.position; parentTransform = redEffect.transform; } break;
|
||||
case "green": if (greenEffect != null) { spawnPos = greenEffect.transform.position; parentTransform = greenEffect.transform; } break;
|
||||
case "yellow": if (yellowEffect != null) { spawnPos = yellowEffect.transform.position; parentTransform = yellowEffect.transform; } break;
|
||||
case "purple": if (purpleEffect != null) { spawnPos = purpleEffect.transform.position; parentTransform = purpleEffect.transform; } break;
|
||||
case "blue": if (blueEffect != null) { spawnPos = blueEffect.transform.position; parentTransform = blueEffect.transform; } break;
|
||||
}
|
||||
|
||||
// 获取并解析颜色
|
||||
Color trackColor;
|
||||
string hexCode = GetHexCodeForColor(color);
|
||||
|
||||
// 尝试解析 16 进制颜色代码。如果失败,使用白色作为默认值。
|
||||
if (!ColorUtility.TryParseHtmlString(hexCode, out trackColor))
|
||||
{
|
||||
trackColor = Color.white;
|
||||
Debug.LogWarning($"Failed to parse hex color for '{color}' ({hexCode}). Using white.");
|
||||
}
|
||||
|
||||
SpawnJudgePrefabByTrackText(color);
|
||||
// Instantiate a copy of the scene object
|
||||
GameObject particleInstance = Instantiate(source, spawnPos, Quaternion.identity);
|
||||
if (particleInstance == null)
|
||||
{
|
||||
Debug.LogError("Failed to instantiate hit_particular_object");
|
||||
TriggerAnimatorEffect(color);
|
||||
return;
|
||||
}
|
||||
|
||||
particleInstance.SetActive(true);
|
||||
|
||||
// Find all ParticleSystems on the instance (root + children) and set their startColor
|
||||
var systems = particleInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (systems != null && systems.Length > 0)
|
||||
{
|
||||
float maxLifetime = 0f;
|
||||
foreach (var ps in systems)
|
||||
{
|
||||
var main = ps.main;
|
||||
// Set start color (works for most setups)
|
||||
main.startColor = trackColor;
|
||||
// Play the system
|
||||
ps.Play();
|
||||
// determine lifetime (use constantMax for safety)
|
||||
var lifetime = main.startLifetime;
|
||||
float life = lifetime.constantMax;
|
||||
if (life <= 0f) life = lifetime.constant; // fallback
|
||||
if (life > maxLifetime) maxLifetime = life;
|
||||
}
|
||||
|
||||
// Optionally parent the instance under the effect object so it moves with UI/slot
|
||||
if (parentTransform != null)
|
||||
{
|
||||
particleInstance.transform.SetParent(parentTransform, true);
|
||||
}
|
||||
|
||||
// Destroy after max lifetime + small buffer
|
||||
Destroy(particleInstance, Mathf.Max(0.5f, maxLifetime + 0.1f));
|
||||
|
||||
// Track for immediate cleanup
|
||||
activeParticles.Add(particleInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("No ParticleSystem components found on hit_particular_object instance");
|
||||
Destroy(particleInstance);
|
||||
TriggerAnimatorEffect(color);
|
||||
}
|
||||
|
||||
// NEW: instantiate and play ring effect (overlay) if assigned
|
||||
if (hit_ring_object != null)
|
||||
{
|
||||
GameObject ringInstance = Instantiate(hit_ring_object, spawnPos, Quaternion.identity);
|
||||
if (ringInstance != null)
|
||||
{
|
||||
Debug.Log($"AnimationController: instantiated hit_ring_object '{ringInstance.name}' at {spawnPos}. parentTransform={(parentTransform != null ? parentTransform.name : "null")} ");
|
||||
|
||||
// Parent before activation to avoid transform surprises when Simulation Space = Local
|
||||
if (parentTransform != null)
|
||||
{
|
||||
ringInstance.transform.SetParent(parentTransform, false);
|
||||
// keep world position at spawnPos
|
||||
ringInstance.transform.position = spawnPos;
|
||||
Debug.Log("AnimationController: ringInstance parent set to " + parentTransform.name);
|
||||
}
|
||||
|
||||
// Activate after parenting
|
||||
ringInstance.SetActive(true);
|
||||
|
||||
var ringSystems = ringInstance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
Debug.Log("AnimationController: ringSystems count=" + (ringSystems != null ? ringSystems.Length : 0));
|
||||
if (ringSystems != null && ringSystems.Length > 0)
|
||||
{
|
||||
float ringMaxLife = 0f;
|
||||
foreach (var rps in ringSystems)
|
||||
{
|
||||
var rmain = rps.main;
|
||||
// log important runtime properties for debugging
|
||||
Debug.Log($"ring PS: {rps.gameObject.name} simulationSpace={rmain.simulationSpace} startLifetime={rmain.startLifetime.constant} startColor={rmain.startColor.color}");
|
||||
|
||||
// reuse same track color
|
||||
rmain.startColor = trackColor;
|
||||
rps.Play();
|
||||
var rlifetime = rmain.startLifetime;
|
||||
float rlife = rlifetime.constantMax;
|
||||
if (rlife <= 0f) rlife = rlifetime.constant;
|
||||
if (rlife > ringMaxLife) ringMaxLife = rlife;
|
||||
|
||||
// Also log emission rate if available
|
||||
try
|
||||
{
|
||||
var emission = rps.emission;
|
||||
var rate = emission.rateOverTime;
|
||||
Debug.Log($"ring PS emission rateOverTime.constant (approx) = {rate.constant}");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
Destroy(ringInstance, Mathf.Max(0.5f, ringMaxLife + 0.1f));
|
||||
|
||||
// Track for immediate cleanup
|
||||
activeParticles.Add(ringInstance);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("hit_ring_object has no ParticleSystem components");
|
||||
Destroy(ringInstance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Failed to instantiate hit_ring_object");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 用于根据颜色名称获取对应的 16 进制代码
|
||||
private string GetHexCodeForColor(string color)
|
||||
{
|
||||
switch (color)
|
||||
{
|
||||
case "red": return redColorHex;
|
||||
case "green": return greenColorHex;
|
||||
case "yellow": return yellowColorHex;
|
||||
case "purple": return purpleColorHex;
|
||||
case "blue": return blueColorHex;
|
||||
default: return "#FFFFFF"; // 默认返回白色
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerAnimatorEffect(string color)
|
||||
{
|
||||
// Fallback to original Animator triggers if particle source missing
|
||||
switch (color)
|
||||
{
|
||||
case "red":
|
||||
if (redAnimator != null)
|
||||
{
|
||||
redAnimator.ResetTrigger("PlayRedDestroy");
|
||||
redAnimator.SetTrigger("PlayRedDestroy");
|
||||
}
|
||||
break;
|
||||
case "green":
|
||||
if (greenAnimator != null)
|
||||
{
|
||||
greenAnimator.ResetTrigger("PlayGreenDestroy");
|
||||
greenAnimator.SetTrigger("PlayGreenDestroy");
|
||||
}
|
||||
break;
|
||||
case "yellow":
|
||||
if (yellowAnimator != null)
|
||||
{
|
||||
yellowAnimator.ResetTrigger("PlayYellowDestroy");
|
||||
yellowAnimator.SetTrigger("PlayYellowDestroy");
|
||||
}
|
||||
break;
|
||||
case "purple":
|
||||
if (purpleAnimator != null)
|
||||
{
|
||||
purpleAnimator.ResetTrigger("PlayPurpleDestroy");
|
||||
purpleAnimator.SetTrigger("PlayPurpleDestroy");
|
||||
}
|
||||
break;
|
||||
case "blue":
|
||||
if (blueAnimator != null)
|
||||
{
|
||||
blueAnimator.ResetTrigger("PlayBlueDestroy");
|
||||
blueAnimator.SetTrigger("PlayBlueDestroy");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据传入的颜色频道,定位到对应的 UI Text 并读取其内容
|
||||
/// </summary>
|
||||
private void SpawnJudgePrefabByTrackText(string color)
|
||||
{
|
||||
TextMeshProUGUI targetText = null;
|
||||
Transform spawnPoint = null;
|
||||
|
||||
// 1. 匹配对应的文本组件和位置
|
||||
switch (color)
|
||||
{
|
||||
case "red": targetText = red_track_judgementText; spawnPoint = redEffect != null ? redEffect.transform : null; break;
|
||||
case "green": targetText = green_track_judgementText; spawnPoint = greenEffect != null ? greenEffect.transform : null; break;
|
||||
case "yellow": targetText = yellow_track_judgementText; spawnPoint = yellowEffect != null ? yellowEffect.transform : null; break;
|
||||
case "purple": targetText = purple_track_judgementText; spawnPoint = purpleEffect != null ? purpleEffect.transform : null; break;
|
||||
case "blue": targetText = blue_track_judgementText; spawnPoint = blueEffect != null ? blueEffect.transform : null; break;
|
||||
}
|
||||
if (targetText != null && spawnPoint != null)
|
||||
{
|
||||
// 打印调试信息,看程序到底读到了什么文字
|
||||
Debug.LogError($"轨道 {color} 当前读到的文字是: [{targetText.text}]");
|
||||
}
|
||||
|
||||
// 2. 如果找到了文本且文本不为空,则执行喷出逻辑
|
||||
if (targetText != null && spawnPoint != null && !string.IsNullOrEmpty(targetText.text))
|
||||
{
|
||||
DoExecutePrefabSpawn(targetText.text, spawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最终执行实例化的函数
|
||||
/// </summary>
|
||||
private void DoExecutePrefabSpawn(string judgeResult, Transform spawnTransform)
|
||||
{
|
||||
GameObject prefabToUse = null;
|
||||
|
||||
// 字符串匹配(需确保与 InputManager 中传入的字符串一致)
|
||||
if (judgeResult.Contains("Perfect")) prefabToUse = perfect_judge_prefab;
|
||||
else if (judgeResult.Contains("Great")) prefabToUse = great_judge_prefab;
|
||||
else if (judgeResult.Contains("Good")) prefabToUse = good_judge_prefab;
|
||||
else if (judgeResult.Contains("Miss")) prefabToUse = miss_judge_prefab;
|
||||
|
||||
if (prefabToUse != null)
|
||||
{
|
||||
// 在对应特效点生成判定 Prefab
|
||||
GameObject instance = Instantiate(prefabToUse, spawnTransform.position, Quaternion.identity);
|
||||
|
||||
// 自动销毁,防止堆积
|
||||
Destroy(instance, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour
|
||||
{
|
||||
public static Animation_GenerateJudgementSituationPrefab Instance;
|
||||
|
||||
[Header("生成位置 (五个轨道)")]
|
||||
public GameObject redEffect;
|
||||
public GameObject greenEffect;
|
||||
public GameObject yellowEffect;
|
||||
public GameObject purpleEffect;
|
||||
public GameObject blueEffect;
|
||||
|
||||
[Header("判定飘字 Prefabs")]
|
||||
public GameObject perfect_judge_prefab;
|
||||
public GameObject great_judge_prefab;
|
||||
public GameObject good_judge_prefab;
|
||||
public GameObject miss_judge_prefab;
|
||||
|
||||
[Header("缩放与随机")]
|
||||
public float baseScale = 1.0f;
|
||||
public bool useRandomScale = true;
|
||||
public float minScaleMultiplier = 0.8f;
|
||||
public float maxScaleMultiplier = 1.2f;
|
||||
|
||||
[Header("物理跳跃设置")]
|
||||
public float jumpForce = 5.0f; // 向上弹起的初始速度
|
||||
public float gravity = -12.0f; // 重力
|
||||
|
||||
[Header("时间与透明度控制 (秒)")]
|
||||
public float fadeInTime = 0.1f; // 生成后多少秒完成渐显 (0->1)
|
||||
public float fadeOutStartTime = 0.6f; // 生成后第几秒开始渐隐
|
||||
public float fadeOutDuration = 0.3f; // 渐隐动画持续时长
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
public void SpawnJudgePrefab(string color, string judgeResult)
|
||||
{
|
||||
Transform spawnPoint = GetSpawnPoint(color);
|
||||
if (spawnPoint == null) return;
|
||||
|
||||
GameObject prefabToUse = GetJudgePrefab(judgeResult);
|
||||
if (prefabToUse == null) return;
|
||||
|
||||
GameObject instance = Instantiate(prefabToUse, spawnPoint);
|
||||
instance.transform.localPosition = Vector3.zero;
|
||||
|
||||
// 处理缩放
|
||||
float finalScale = baseScale;
|
||||
if (useRandomScale) finalScale *= Random.Range(minScaleMultiplier, maxScaleMultiplier);
|
||||
instance.transform.localScale = new Vector3(finalScale, finalScale, 1f);
|
||||
|
||||
// 开启运动逻辑
|
||||
StartCoroutine(AnimateSprite(instance));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateSprite(GameObject obj)
|
||||
{
|
||||
if (obj == null) yield break;
|
||||
|
||||
SpriteRenderer sr = obj.GetComponent<SpriteRenderer>();
|
||||
float elapsed = 0f;
|
||||
float vVelocity = jumpForce;
|
||||
Vector3 currentLocalPos = Vector3.zero;
|
||||
|
||||
// 计算总生命周期:开始渐隐的时间 + 渐隐持续的时长
|
||||
float totalLifeTime = fadeOutStartTime + fadeOutDuration;
|
||||
|
||||
// 初始透明度
|
||||
if (sr != null)
|
||||
{
|
||||
Color c = sr.color;
|
||||
c.a = 0f;
|
||||
sr.color = c;
|
||||
}
|
||||
|
||||
// 只要没到总寿命,就持续执行
|
||||
while (elapsed < totalLifeTime)
|
||||
{
|
||||
if (obj == null) yield break;
|
||||
|
||||
float dt = Time.deltaTime;
|
||||
elapsed += dt;
|
||||
|
||||
// --- 1. 物理位移 ---
|
||||
vVelocity += gravity * dt;
|
||||
currentLocalPos.y += vVelocity * dt;
|
||||
obj.transform.localPosition = currentLocalPos;
|
||||
|
||||
// --- 2. 透明度逻辑 (基于秒数) ---
|
||||
if (sr != null)
|
||||
{
|
||||
float alpha = 1f;
|
||||
|
||||
// 渐显阶段:当前时间 < fadeInTime
|
||||
if (elapsed < fadeInTime)
|
||||
{
|
||||
alpha = Mathf.InverseLerp(0f, fadeInTime, elapsed);
|
||||
}
|
||||
// 渐隐阶段:当前时间 > fadeOutStartTime
|
||||
else if (elapsed > fadeOutStartTime)
|
||||
{
|
||||
// 在 fadeOutStartTime 到 totalLifeTime 之间从 1 变到 0
|
||||
alpha = Mathf.InverseLerp(totalLifeTime, fadeOutStartTime, elapsed);
|
||||
}
|
||||
// 中间完全显示阶段
|
||||
else
|
||||
{
|
||||
alpha = 1f;
|
||||
}
|
||||
|
||||
Color c = sr.color;
|
||||
c.a = Mathf.Clamp01(alpha);
|
||||
sr.color = c;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// --- 确保最后一帧是完全透明的 ---
|
||||
if (obj != null && sr != null)
|
||||
{
|
||||
Color c = sr.color;
|
||||
c.a = 0f;
|
||||
sr.color = c;
|
||||
}
|
||||
|
||||
// 等待渲染完成,防止闪除
|
||||
yield return new WaitForEndOfFrame();
|
||||
|
||||
if (obj != null) Destroy(obj);
|
||||
}
|
||||
|
||||
private Transform GetSpawnPoint(string color)
|
||||
{
|
||||
switch (color.ToLower())
|
||||
{
|
||||
case "red": return redEffect?.transform;
|
||||
case "green": return greenEffect?.transform;
|
||||
case "yellow": return yellowEffect?.transform;
|
||||
case "purple": return purpleEffect?.transform;
|
||||
case "blue": return blueEffect?.transform;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetJudgePrefab(string result)
|
||||
{
|
||||
switch (result.ToLower())
|
||||
{
|
||||
case "perfect": return perfect_judge_prefab;
|
||||
case "great": return great_judge_prefab;
|
||||
case "good": return good_judge_prefab;
|
||||
case "miss": return miss_judge_prefab;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb6998cd4a7dca042832c2544ca33c76
|
||||
@@ -9,6 +9,18 @@ public class NoteData
|
||||
public string color; // 音符颜色(如 "red", "blue")
|
||||
public string type; // 音符类型("tap" = 单击,"hold" = 长按)
|
||||
public float length; // 音符长度(仅用于长按音符,单位:秒)
|
||||
|
||||
// New fields for judgement recording
|
||||
// judgeOffsetMs: signed offset in milliseconds recorded at judgement time
|
||||
// positive = early (玩家按在目标时间之前), negative = late (玩家按在目标时间之后)
|
||||
// For tap notes this stores the single judgement offset. For hold notes, start judgement stored in judgeOffsetMs,
|
||||
// and end judgement stored in judgeOffsetMsEnd. Misses do not write offsets (left as NaN).
|
||||
public float judgeOffsetMs = float.NaN;
|
||||
public float judgeOffsetMsEnd = float.NaN;
|
||||
|
||||
// Optionally record result strings for debugging/analysis
|
||||
public string judgeResult = null;
|
||||
public string judgeResultEnd = null;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -4,14 +4,13 @@ using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class BeatmapManager : MonoBehaviour
|
||||
|
||||
{
|
||||
public Beatmap beatmap; // 当前谱面数据
|
||||
|
||||
// 音符生成器引用
|
||||
public NoteSpawner noteSpawner;
|
||||
|
||||
// 新增:teamUIController 引用,直接拖拽赋值
|
||||
// teamUIController 引用,直接拖拽赋值
|
||||
public teamUIController uiController;
|
||||
|
||||
// Extra fields from beatmap JSON (stored temporarily)
|
||||
@@ -40,15 +39,23 @@ public class BeatmapManager : MonoBehaviour
|
||||
[HideInInspector] public TrackStates parsedTrackStates;
|
||||
|
||||
// Multipliers for difficulty levels
|
||||
[Header("Difficulty Multipliers")]
|
||||
public float ezMultiplier = 1f;
|
||||
public float hdMultiplier = 1.5f;
|
||||
public float inMultiplier = 2f;
|
||||
public float imMultiplier = 3f;
|
||||
|
||||
// 新增:基础生命值缩放因子(Base HP Unit Scale)
|
||||
// 用于将 Note 数量(例如 709)缩放到 40000 左右的范围。
|
||||
// 计算公式:NoteAmount * DifficultyMult * BaseScale ≈ TotalHP
|
||||
[Header("HP Calculation Settings")]
|
||||
[Tooltip("Scales the note amount to enemy HP. Example: NoteAmount * MaxMultiplier * BaseScale ≈ TotalHP")]
|
||||
public float baseHpUnitScale = 1f;
|
||||
|
||||
// Total chart score and per-note score fields
|
||||
public int totalChartScore = 1000000; // Default total score for a chart
|
||||
public int perNoteScore;
|
||||
public int leftoverScore;
|
||||
public int totalChartScore = 1000000; // Default total score for a chart
|
||||
public int perNoteScore;
|
||||
public int leftoverScore;
|
||||
|
||||
// 从 JSON 文件加载谱面数据
|
||||
public void LoadBeatmap(string fileName)
|
||||
@@ -66,7 +73,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
|
||||
// 从原始 JSON 字符串加载谱面(允许任意磁盘路径先读取后传入)
|
||||
public void LoadBeatmapFromJsonString(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -77,7 +84,7 @@ public class BeatmapManager : MonoBehaviour
|
||||
ProcessJsonAndLoad(json);
|
||||
}
|
||||
|
||||
// 新增:解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
|
||||
// 解析 JSON 但不启动 NoteSpawner(用于 test mode 的延迟开始)
|
||||
public bool ParseJsonOnly(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
@@ -168,67 +175,8 @@ public class BeatmapManager : MonoBehaviour
|
||||
beatmap = parsed;
|
||||
Debug.Log("ParseJsonOnly: parsed beatmap " + beatmap.title);
|
||||
|
||||
// Process colorSegments if not empty (for both test and normal mode)
|
||||
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
||||
{
|
||||
// Determine multiplier based on difficulty
|
||||
float multiplier = ezMultiplier;
|
||||
if (parsedDifficulty == 1) multiplier = hdMultiplier;
|
||||
else if (parsedDifficulty == 2) multiplier = inMultiplier;
|
||||
else if (parsedDifficulty == 3) multiplier = imMultiplier;
|
||||
|
||||
// Calculate total HP
|
||||
float totalHP = parsedNoteAmount * multiplier;
|
||||
|
||||
// Set enemy slot IDs and calculate HP for each enemy
|
||||
List<int> enemyIds = new List<int>();
|
||||
foreach (var segment in parsedColorSegments)
|
||||
{
|
||||
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
|
||||
int enemyId;
|
||||
if (!int.TryParse(segment.enemyID, out enemyId))
|
||||
{
|
||||
Debug.Log($"Failed to parse enemyID '{segment.enemyID}' to int");
|
||||
continue;
|
||||
}
|
||||
Debug.Log($"Parsed enemyId: {enemyId}");
|
||||
enemyIds.Add(enemyId);
|
||||
|
||||
// Calculate individual enemy HP
|
||||
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
|
||||
|
||||
// Find and update the corresponding EnemyData_SO
|
||||
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
|
||||
foreach (var so in allEnemies)
|
||||
{
|
||||
if (so != null && so.enemyID == enemyId)
|
||||
{
|
||||
so.enemy_maxHP = enemyHP;
|
||||
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update teamUIController
|
||||
if (uiController != null)
|
||||
{
|
||||
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
|
||||
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
|
||||
while (enemyIds.Count < 5)
|
||||
{
|
||||
enemyIds.Add(0);
|
||||
}
|
||||
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
|
||||
uiController.enemySlotIds = enemyIds;
|
||||
Debug.LogWarning($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("teamUIController not assigned in the Inspector");
|
||||
}
|
||||
}
|
||||
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
|
||||
SetupEnemiesAndHP();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -270,73 +218,90 @@ public class BeatmapManager : MonoBehaviour
|
||||
// 加载默认谱面
|
||||
LoadBeatmap("Emilia_demo.json"); // 加载并实例化音符
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
|
||||
[System.Serializable]
|
||||
private class BeatmapExtra
|
||||
// 获取当前难度乘数
|
||||
private float GetCurrentDifficultyMultiplier()
|
||||
{
|
||||
public string title;
|
||||
public string composer;
|
||||
public string illustrator;
|
||||
public string charter;
|
||||
public string beatmapId;
|
||||
public int duration; // as int per request
|
||||
public int bpm; // as int per request
|
||||
public int difficulty;
|
||||
public string difficultyName;
|
||||
public string createdDate;
|
||||
public string lastSavedTime;
|
||||
public string musicFile;
|
||||
public string backgroundFile;
|
||||
public int noteAmount;
|
||||
public float globalDelaySeconds;
|
||||
|
||||
// New fields
|
||||
public bool enemyList_isEmpty;
|
||||
public ColorSegment[] colorSegments;
|
||||
public NoteStatistic[] noteStatistics;
|
||||
public TrackStates trackStates;
|
||||
// 假设 1=EZ, 2=HD, 3=IN, 4=IM
|
||||
if (parsedDifficulty == 1) return ezMultiplier;
|
||||
if (parsedDifficulty == 2) return hdMultiplier;
|
||||
if (parsedDifficulty == 3) return inMultiplier;
|
||||
if (parsedDifficulty == 4) return imMultiplier;
|
||||
return 1f; // 默认值
|
||||
}
|
||||
|
||||
// New serializable classes for the extra fields
|
||||
[System.Serializable]
|
||||
public class ColorSegment
|
||||
// 统一处理敌人 ID 提取、发送给 UI 以及 HP 计算的逻辑
|
||||
private void SetupEnemiesAndHP()
|
||||
{
|
||||
public string enemyID;
|
||||
public float percentage;
|
||||
}
|
||||
if (uiController == null)
|
||||
{
|
||||
Debug.LogError("teamUIController not assigned in BeatmapManager Inspector");
|
||||
return;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class NoteStatistic
|
||||
{
|
||||
public string colorType;
|
||||
public int count;
|
||||
}
|
||||
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
||||
{
|
||||
// 1. 提取敌人 IDs
|
||||
List<int> enemyIds = new List<int>();
|
||||
foreach (var segment in parsedColorSegments)
|
||||
{
|
||||
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
|
||||
int enemyId;
|
||||
if (!int.TryParse(segment.enemyID, out enemyId))
|
||||
{
|
||||
Debug.LogError($"Failed to parse enemyID '{segment.enemyID}' to int");
|
||||
continue;
|
||||
}
|
||||
enemyIds.Add(enemyId);
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TrackStates
|
||||
{
|
||||
public bool trackFading_active;
|
||||
public TrackState red;
|
||||
public TrackState green;
|
||||
public TrackState yellow;
|
||||
public TrackState purple;
|
||||
public TrackState blue;
|
||||
}
|
||||
// 2. 补齐 5 个槽位
|
||||
while (enemyIds.Count < 5)
|
||||
{
|
||||
enemyIds.Add(0);
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TrackState
|
||||
{
|
||||
public bool isOn;
|
||||
public float fadeTime;
|
||||
// A. 传递 ID 列表给 uiController
|
||||
Debug.Log($"Assigning enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
||||
uiController.enemySlotIds = enemyIds;
|
||||
|
||||
// B. 让 UI 控制器加载 SO (此时 SO 的 HP 仍是默认值)
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
|
||||
// C. 计算并应用 HP 逻辑
|
||||
if (parsedNoteAmount > 0)
|
||||
{
|
||||
float currentMultiplier = GetCurrentDifficultyMultiplier();
|
||||
|
||||
// 计算总 HP:音符数 * 难度乘数 * 基础缩放
|
||||
float totalCalculatedHP = parsedNoteAmount * currentMultiplier * baseHpUnitScale;
|
||||
|
||||
// 计算活跃敌人数量(非0 ID)
|
||||
int activeEnemyCount = enemyIds.FindAll(id => id != 0).Count;
|
||||
if (activeEnemyCount == 0) activeEnemyCount = 1; // 防止除零
|
||||
|
||||
// 计算单个敌人 HP (总血量 / 敌人数量,均匀分配)
|
||||
int individualMaxHP = Mathf.RoundToInt(totalCalculatedHP / activeEnemyCount);
|
||||
|
||||
Debug.Log($"[BeatmapManager] HP Calc -> Notes: {parsedNoteAmount}, Multiplier: {currentMultiplier}, Total: {totalCalculatedHP:F0}, ActiveEnemies: {activeEnemyCount}, Per Enemy: {individualMaxHP}");
|
||||
|
||||
// D. 【关键调用】将计算出的 HP 回填到 teamUIController 中的 SO 中
|
||||
uiController.ApplyCalculatedEnemyHP(individualMaxHP);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有敌人,也要通知 UI 控制器清空列表
|
||||
Debug.Log("Parsed enemy list is empty, clearing UI.");
|
||||
uiController.enemySlotIds = new List<int> { 0, 0, 0, 0, 0 };
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON string to Beatmap and extras, then pass to NoteSpawner
|
||||
private void ProcessJsonAndLoad(string json)
|
||||
{
|
||||
// Debug.LogError("ProcessJsonAndLoad called");
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
Debug.LogError("ProcessJsonAndLoad: json is empty");
|
||||
@@ -433,69 +398,18 @@ public class BeatmapManager : MonoBehaviour
|
||||
// assign and hand off to spawner
|
||||
beatmap = parsed;
|
||||
Debug.Log("谱面已加载:" + beatmap.title);
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
|
||||
// Process colorSegments if not empty
|
||||
if (!parsedEnemyListIsEmpty && parsedColorSegments != null && parsedColorSegments.Length > 0)
|
||||
if (noteSpawner != null)
|
||||
{
|
||||
// Determine multiplier based on difficulty
|
||||
float multiplier = ezMultiplier;
|
||||
if (parsedDifficulty == 1) multiplier = hdMultiplier;
|
||||
else if (parsedDifficulty == 2) multiplier = inMultiplier;
|
||||
else if (parsedDifficulty == 3) multiplier = imMultiplier;
|
||||
|
||||
// Calculate total HP
|
||||
float totalHP = parsedNoteAmount * multiplier;
|
||||
|
||||
// Set enemy slot IDs and calculate HP for each enemy
|
||||
List<int> enemyIds = new List<int>();
|
||||
foreach (var segment in parsedColorSegments)
|
||||
{
|
||||
Debug.Log($"Processing colorSegment: enemyID='{segment.enemyID}', percentage={segment.percentage}");
|
||||
int enemyId;
|
||||
if (!int.TryParse(segment.enemyID, out enemyId))
|
||||
{
|
||||
Debug.LogError($"Failed to parse enemyID '{segment.enemyID}' to int");
|
||||
continue;
|
||||
}
|
||||
Debug.LogError($"Parsed enemyId: {enemyId}");
|
||||
enemyIds.Add(enemyId);
|
||||
|
||||
// Calculate individual enemy HP
|
||||
int enemyHP = Mathf.RoundToInt(totalHP * segment.percentage);
|
||||
|
||||
// Find and update the corresponding EnemyData_SO
|
||||
EnemyData_SO[] allEnemies = Resources.LoadAll<EnemyData_SO>("");
|
||||
foreach (var so in allEnemies)
|
||||
{
|
||||
if (so != null && so.enemyID == enemyId)
|
||||
{
|
||||
so.enemy_maxHP = enemyHP;
|
||||
Debug.Log(string.Format("Set enemy {0} maxHP to {1}", enemyId, enemyHP));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update teamUIController
|
||||
if (uiController != null)
|
||||
{
|
||||
Debug.Log($"enemyIds before padding: {string.Join(",", enemyIds)}");
|
||||
// Ensure enemySlotIds has exactly 5 elements, padding with 0 if necessary
|
||||
while (enemyIds.Count < 5)
|
||||
{
|
||||
enemyIds.Add(0);
|
||||
}
|
||||
Debug.Log($"enemyIds after padding: {string.Join(",", enemyIds)}");
|
||||
uiController.enemySlotIds = enemyIds;
|
||||
Debug.LogError($"Assigned enemySlotIds to teamUIController: {string.Join(",", enemyIds)}");
|
||||
uiController.PopulateEnemySOsFromIds();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("teamUIController not assigned in the Inspector");
|
||||
}
|
||||
noteSpawner.LoadBeatmap(beatmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("NoteSpawner is null in BeatmapManager.");
|
||||
}
|
||||
|
||||
// 调用统一的敌人处理逻辑(包含 HP 计算和设置)
|
||||
SetupEnemiesAndHP();
|
||||
}
|
||||
|
||||
// Calculate per-note and leftover scores based on parsedNoteAmount
|
||||
@@ -514,4 +428,64 @@ public class BeatmapManager : MonoBehaviour
|
||||
Debug.LogWarning("parsedNoteAmount is zero or less; perNoteScore and leftoverScore set to 0.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class used to parse extra fields from JSON that Beatmap doesn't define
|
||||
[System.Serializable]
|
||||
private class BeatmapExtra
|
||||
{
|
||||
public string title;
|
||||
public string composer;
|
||||
public string illustrator;
|
||||
public string charter;
|
||||
public string beatmapId;
|
||||
public int duration; // as int per request
|
||||
public int bpm; // as int per request
|
||||
public int difficulty;
|
||||
public string difficultyName;
|
||||
public string createdDate;
|
||||
public string lastSavedTime;
|
||||
public string musicFile;
|
||||
public string backgroundFile;
|
||||
public int noteAmount;
|
||||
public float globalDelaySeconds;
|
||||
|
||||
// New fields
|
||||
public bool enemyList_isEmpty;
|
||||
public ColorSegment[] colorSegments;
|
||||
public NoteStatistic[] noteStatistics;
|
||||
public TrackStates trackStates;
|
||||
}
|
||||
|
||||
// New serializable classes for the extra fields
|
||||
[System.Serializable]
|
||||
public class ColorSegment
|
||||
{
|
||||
public string enemyID;
|
||||
public float percentage;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class NoteStatistic
|
||||
{
|
||||
public string colorType;
|
||||
public int count;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TrackStates
|
||||
{
|
||||
public bool trackFading_active;
|
||||
public TrackState red;
|
||||
public TrackState green;
|
||||
public TrackState yellow;
|
||||
public TrackState purple;
|
||||
public TrackState blue;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class TrackState
|
||||
{
|
||||
public bool isOn;
|
||||
public float fadeTime;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -32,21 +32,37 @@ public class HoldNote : BaseNote
|
||||
public KeyCode key;
|
||||
public string type; // "start", "middle", or "end"
|
||||
|
||||
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
|
||||
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
|
||||
private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定
|
||||
private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开
|
||||
|
||||
// 新增变量
|
||||
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
|
||||
// 新增变量
|
||||
private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定)
|
||||
|
||||
[Header("判定区间配置")]
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
|
||||
[Header("判定区间配置")]
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
|
||||
private NoteData noteData;
|
||||
|
||||
private bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note
|
||||
// private bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
controller = GetComponent<HoldNoteController>();
|
||||
anim = GetComponent<AnimationController>();
|
||||
// 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;
|
||||
@@ -73,30 +89,26 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void PlayHitAnimation()
|
||||
{
|
||||
if (anim != null)
|
||||
var controller = AnimationController.Global ?? anim;
|
||||
if (controller != null)
|
||||
{
|
||||
// 激活动画效果 GameObject
|
||||
anim.gameObject.SetActive(true);
|
||||
anim.PlayDestroyAnimation(noteColor);
|
||||
|
||||
// 延迟禁用动画效果 GameObject,确保动画播放完毕
|
||||
StartCoroutine(DelayedDisableAnimation(anim.gameObject, 0.5f)); // 0.5秒后禁用
|
||||
controller.PlayDestroyAnimation(noteColor);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator DelayedDisableAnimation(GameObject animationObject, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
animationObject.SetActive(false);
|
||||
// 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)
|
||||
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.TrackIndex = trackIndex;
|
||||
this.speed = speed;
|
||||
this.startTime = time;
|
||||
this.delay = delay;
|
||||
@@ -117,19 +129,21 @@ public class HoldNote : BaseNote
|
||||
this.hasEnteredLine = false;
|
||||
this.hasReleased = false;
|
||||
|
||||
// 判定区间配置检查
|
||||
// 判定区间配置检查
|
||||
if (judgeConfig == null)
|
||||
Debug.LogError($"HoldNoteJudgeConfig is null! 判定区间配置未传入!track={trackIndex}");
|
||||
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 时注册初始状态
|
||||
// 在 Setup 时注册初始状态
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, false);
|
||||
if (segment == NoteSegment.Start)
|
||||
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
|
||||
JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定
|
||||
|
||||
if (segment == NoteSegment.End)
|
||||
JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime);
|
||||
@@ -140,33 +154,50 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 如果当前片段已经判定过,直接返回
|
||||
// 如果当前片段已经判定过,直接返回
|
||||
if (isJudged) return;
|
||||
|
||||
// 自动 Miss 判定:当音符经过判定线且未被判定时
|
||||
// 重新启动长按粒子协程,如果长按状态恢复且协程未运行
|
||||
if (!isHoldActive && JudgeManager.Instance.IsStartJudged(noteID) && !JudgeManager.Instance.HasNoteReleased(noteID) && Input.GetKey(keyToPress))
|
||||
{
|
||||
isHoldActive = true;
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
}
|
||||
|
||||
// 自动 Miss 判定:当音符经过判定线且未被判定时
|
||||
if (hasEnteredLine && !isJudged)
|
||||
{
|
||||
if (segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
|
||||
if(segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
|
||||
{
|
||||
Debug.Log($"[HoldNote] START段超时自动Miss: {noteColor}");
|
||||
HandleStart();
|
||||
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 + (judgeConfig?.missRange ?? 0.3f))
|
||||
{
|
||||
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
|
||||
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}");
|
||||
AnimationController.Global?.StopHoldParticles();
|
||||
Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}");
|
||||
|
||||
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
|
||||
// 当玩家松开按键时,向 JudgeManager 注册该长音符已被释放,确保尾部能正确检查释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
hasReleased = true;
|
||||
@@ -176,21 +207,21 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
|
||||
/*if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
|
||||
{
|
||||
isHoldActive = true;
|
||||
}
|
||||
}*/
|
||||
|
||||
// Start段判定
|
||||
// Start段判定
|
||||
if (segment == NoteSegment.Start && hasEnteredLine)
|
||||
{
|
||||
if (Input.GetKeyDown(keyToPress))
|
||||
{
|
||||
HandleStart();
|
||||
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
|
||||
isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试
|
||||
}
|
||||
}
|
||||
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
|
||||
// Middle段判定:中段只负责视觉/回收,不做 End 判定统计
|
||||
else if (segment == NoteSegment.Middle
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& isHoldActive
|
||||
@@ -200,16 +231,16 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (hasEnteredLine)
|
||||
{
|
||||
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
|
||||
Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}");
|
||||
PlayHitAnimation();
|
||||
// 把通过事件上报给 JudgeManager(记录中段通过)
|
||||
// 把通过事件上报给 JudgeManager(记录中段通过)
|
||||
JudgeManager.Instance?.RegisterMiddlePassed(noteID);
|
||||
ReturnToPool();
|
||||
isJudged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
|
||||
// End段判定:现在 End 段判定由独立的尾部音符负责(即本对象仍可判定,但更重要的是尾部note会决定最终判定)
|
||||
else if (segment == NoteSegment.End
|
||||
&& JudgeManager.Instance.IsStartJudged(noteID)
|
||||
&& !JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
@@ -231,7 +262,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}");
|
||||
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);
|
||||
@@ -241,10 +272,10 @@ public class HoldNote : BaseNote
|
||||
if (autoReturnCoroutine != null)
|
||||
StopCoroutine(autoReturnCoroutine);
|
||||
|
||||
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
|
||||
// 如果在尾部进入判定区时玩家已经释放按键,则此时应立即做 End 判定
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isJudged)
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
|
||||
Debug.Log($"[HoldNote] End段进入判定线但已经释放,立即执行 End 判定: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
// ReturnToPool handled inside HandleEnd
|
||||
@@ -263,8 +294,8 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
|
||||
// 确保未判定的 Start 段在离开判定线时登记为 Miss
|
||||
Debug.Log($"[HoldNote] Start段离开判定线未判定,补偿 Miss 并回收: {noteColor}");
|
||||
// 确保未判定的 Start 段在离开判定线时登记为 Miss
|
||||
HandleStart();
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
@@ -272,7 +303,7 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
else if (segment == NoteSegment.Middle)
|
||||
{
|
||||
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
|
||||
Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}");
|
||||
ReturnToPool();
|
||||
}
|
||||
else if (segment == NoteSegment.End)
|
||||
@@ -283,15 +314,15 @@ public class HoldNote : BaseNote
|
||||
autoReturnCoroutine = null;
|
||||
}
|
||||
|
||||
// 在尾部离开判定线时,按如下规则处理:
|
||||
// - 如果头部未判定:补偿 Miss 并回收
|
||||
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
|
||||
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
|
||||
// 在尾部离开判定线时,按如下规则处理:
|
||||
// - 如果头部未判定:补偿 Miss 并回收
|
||||
// - 如果头部已判定且玩家已经释放:执行 End 判定并回收
|
||||
// - 如果头部已判定且玩家仍在按住:不要强制 Miss,也不要立即回收,等待玩家松手或超时处理
|
||||
if (!isJudged)
|
||||
{
|
||||
if (!JudgeManager.Instance.IsStartJudged(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
|
||||
Debug.Log($"[HoldNote] End段离开判定线且头部未判定,补偿 Miss: {noteColor}");
|
||||
if (!JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
HandleEnd(true);
|
||||
ReturnToPool();
|
||||
@@ -300,15 +331,15 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
|
||||
Debug.Log($"[HoldNote] End段离开判定线且已释放,执行 End 判定: {noteColor}");
|
||||
HandleEnd();
|
||||
isJudged = true;
|
||||
ReturnToPool();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
|
||||
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
|
||||
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
|
||||
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,19 +372,28 @@ public class HoldNote : BaseNote
|
||||
|
||||
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;
|
||||
if (offset < 0.1f) // 判定成功
|
||||
if (offset < (judgeConfig?.perfectRange ?? 0.1f)) // 判定成功
|
||||
{
|
||||
result = "Perfect";
|
||||
Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}");
|
||||
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,长按状态激活
|
||||
isHoldActive = true; // 成功判定Start,长按状态激活
|
||||
PlayHitAnimation();
|
||||
AnimationController.Global?.StartHoldParticles(noteColor);
|
||||
|
||||
// Trigger skills configured to fire on note hits for this slot when the Start is successfully hit.
|
||||
if (!_hasTriggeredOnThisHold)
|
||||
if (/*!_hasTriggeredOnThisHold*/JudgeManager.Instance.TryTriggerSkill(noteID))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -371,15 +411,21 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
Debug.LogError($"[HoldNote] NotifyNoteHit(Start) threw: {ex}");
|
||||
}
|
||||
_hasTriggeredOnThisHold = true; // avoid triggering again at End
|
||||
// _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,长按状态不激活
|
||||
if (JudgeManager.Instance.TryResolveStart(noteID)) // 或 TryResolveEnd
|
||||
{
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
}
|
||||
// if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
|
||||
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
|
||||
@@ -392,15 +438,39 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
Debug.LogError($"[HoldNote] NotifyNoteHit(Start Miss) threw: {ex}");
|
||||
}
|
||||
_hasTriggeredOnThisHold = true; // ensure we don't trigger again at End
|
||||
// _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);
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
|
||||
|
||||
StartCoroutine(DelayedReturn());
|
||||
// --- 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()
|
||||
@@ -414,7 +484,12 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void HandleEnd(bool forceMiss = false)
|
||||
{
|
||||
// 记录释放时间并在 JudgeManager 中登记已释放状态
|
||||
if (!JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
Debug.Log($"[HoldNote] END 已被判定,跳过: {noteColor}");
|
||||
return;
|
||||
}
|
||||
// 记录释放时间并在 JudgeManager 中登记已释放状态
|
||||
if (!hasReleased)
|
||||
{
|
||||
releaseTime = Time.time;
|
||||
@@ -423,15 +498,17 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
|
||||
string result;
|
||||
float rawOffsetMsEnd = 0f;
|
||||
|
||||
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
|
||||
|
||||
// 如果玩家在尾部进入判定区前就松手,则仅登记释放,不在此处进行最终判定和回收,除非是强制 Miss
|
||||
if (!hasEnteredLine && !forceMiss)
|
||||
{
|
||||
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
|
||||
Debug.Log($"[HoldNote] HandleEnd: 提前松手,记录释放但等待尾部进入判定区再判定. NoteID={noteID}, color={noteColor}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 现在进行正常判定逻辑
|
||||
// 现在进行正常判定逻辑
|
||||
releaseTime = Time.time;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance.RegisterNoteReleased(noteID, true);
|
||||
@@ -439,56 +516,101 @@ public class HoldNote : BaseNote
|
||||
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)
|
||||
// if (JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
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";
|
||||
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);
|
||||
if (diff <= 0.1f)
|
||||
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
|
||||
if (judgeConfig != null)
|
||||
{
|
||||
result = "Perfect";
|
||||
}
|
||||
else if (diff < 0.2f)
|
||||
{
|
||||
result = "Great";
|
||||
}
|
||||
else if (diff < 0.3f)
|
||||
{
|
||||
result = "Good";
|
||||
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
|
||||
{
|
||||
result = "Bad";
|
||||
if (diff <= 0.1f)
|
||||
{
|
||||
result = "Perfect";
|
||||
}
|
||||
else if (diff < 0.2f)
|
||||
{
|
||||
result = "Great";
|
||||
}
|
||||
else if (diff < 0.3f)
|
||||
{
|
||||
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计数
|
||||
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 (!_hasTriggeredOnThisHold)
|
||||
if (JudgeManager.Instance.TryTriggerSkill(noteID))
|
||||
{
|
||||
var sb = SkillBuilder.Instance;
|
||||
if (sb == null)
|
||||
@@ -499,7 +621,7 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
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():"-")}");
|
||||
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;
|
||||
@@ -566,15 +688,20 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
_hasTriggeredOnThisHold = true;
|
||||
// _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})");
|
||||
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);
|
||||
@@ -582,19 +709,23 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
|
||||
if (segment == NoteSegment.End &&
|
||||
!isJudged && // 尚未判定
|
||||
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
|
||||
!isJudged && // 尚未判定
|
||||
JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定
|
||||
{
|
||||
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
|
||||
// 只在头部已判定且玩家已松手时补偿 End 判定为 Miss;否则不要强制 Miss
|
||||
if (JudgeManager.Instance.HasNoteReleased(noteID) && !isHoldActive)
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
|
||||
HandleEnd(); // 正常判定(基于 releaseTime)
|
||||
Debug.Log($"[HoldNote] OnDisable补偿End段判定(已释放): {noteColor}");
|
||||
if (JudgeManager.Instance.TryResolveEnd(noteID))
|
||||
{
|
||||
HandleEnd();
|
||||
}
|
||||
|
||||
isJudged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[HoldNote] OnDisable:End段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
|
||||
Debug.Log($"[HoldNote] OnDisable:End段未判定且尚有按键状态,跳过强制 Miss: {noteColor}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,11 +734,11 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void ReturnToPool()
|
||||
{
|
||||
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
|
||||
if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用
|
||||
|
||||
if (segment == NoteSegment.Start && !hasEnteredLine)
|
||||
{
|
||||
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
|
||||
Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -632,9 +763,9 @@ public class HoldNote : BaseNote
|
||||
scheduledEndTime = 0f;
|
||||
hasEnteredLine = false;
|
||||
isJudged = false;
|
||||
isHoldActive = false; // 重置长按标记
|
||||
hasBeenHeldFromStart = false; // 重置
|
||||
_hasTriggeredOnThisHold = false; // 重置
|
||||
isHoldActive = false; // 重置长按标记
|
||||
hasBeenHeldFromStart = false; // 重置
|
||||
// _hasTriggeredOnThisHold = false; // 重置
|
||||
|
||||
if (autoReturnCoroutine != null)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86977693d5a2d364bb086303f60ca1b3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
guid: 86977693d5a2d364bb086303f60ca1b3
|
||||
@@ -5,40 +5,74 @@ public class JudgeManager : MonoBehaviour
|
||||
{
|
||||
public static JudgeManager Instance { get; private set; }
|
||||
|
||||
// ===== 原有数据 =====
|
||||
private Dictionary<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
|
||||
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
|
||||
private Dictionary<string, bool> releasedNotes = new Dictionary<string, bool>();
|
||||
// 记录 End 段的 scheduledEndTime(防止 End 被回收后仍然能判定)
|
||||
private Dictionary<string, float> noteEndTimes = new Dictionary<string, float>();
|
||||
|
||||
// 记录每条长音符中段通过的计数
|
||||
private Dictionary<string, int> middlePassedCounts = new Dictionary<string, int>();
|
||||
|
||||
// ===== 新增:长音符全局互斥状态 =====
|
||||
private class HoldJudgeState
|
||||
{
|
||||
public bool startResolved; // Start 是否已经判定过(成功 or 失败)
|
||||
public bool endResolved; // End 是否已经判定过
|
||||
public bool skillTriggered; // 技能是否已触发
|
||||
}
|
||||
|
||||
private Dictionary<string, HoldJudgeState> holdStates = new Dictionary<string, HoldJudgeState>();
|
||||
|
||||
private HoldJudgeState GetHoldState(string noteID)
|
||||
{
|
||||
if (!holdStates.ContainsKey(noteID))
|
||||
holdStates[noteID] = new HoldJudgeState();
|
||||
return holdStates[noteID];
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录某个颜色的长音符 End 段的 scheduledEndTime
|
||||
/// </summary>
|
||||
// ===== 新增:判定互斥闸门 =====
|
||||
public bool TryResolveStart(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.startResolved) return false;
|
||||
s.startResolved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryResolveEnd(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.endResolved) return false;
|
||||
s.endResolved = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryTriggerSkill(string noteID)
|
||||
{
|
||||
var s = GetHoldState(noteID);
|
||||
if (s.skillTriggered) return false;
|
||||
s.skillTriggered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearHoldState(string noteID)
|
||||
{
|
||||
if (holdStates.ContainsKey(noteID))
|
||||
holdStates.Remove(noteID);
|
||||
}
|
||||
|
||||
// ===== 原有接口(保持不变) =====
|
||||
|
||||
public void RegisterScheduledEndTime(string noteID, float endTime)
|
||||
{
|
||||
noteEndTimes[noteID] = endTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取某个颜色的 End 段的 scheduledEndTime
|
||||
/// </summary>
|
||||
public float GetScheduledEndTime(string noteID)
|
||||
{
|
||||
return noteEndTimes.ContainsKey(noteID) ? noteEndTimes[noteID] : 0f;
|
||||
@@ -65,23 +99,15 @@ public class JudgeManager : MonoBehaviour
|
||||
return releasedNotes.ContainsKey(noteID) && releasedNotes[noteID];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当音符进入判定区域时调用
|
||||
/// </summary>
|
||||
public void RegisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key))
|
||||
judgeQueues[key] = new Queue<Note>();
|
||||
|
||||
if (!judgeQueues[key].Contains(note))
|
||||
{
|
||||
judgeQueues[key].Enqueue(note);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当音符离开判定区域时调用
|
||||
/// </summary>
|
||||
public void UnregisterNote(KeyCode key, Note note)
|
||||
{
|
||||
if (!judgeQueues.ContainsKey(key)) return;
|
||||
@@ -91,39 +117,23 @@ public class JudgeManager : MonoBehaviour
|
||||
{
|
||||
Note n = judgeQueues[key].Dequeue();
|
||||
if (n != note)
|
||||
{
|
||||
newQueue.Enqueue(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
// **如果音符未被判定,则触发 Miss**
|
||||
if (!n.IsJudged())
|
||||
{
|
||||
n.JudgeMiss();
|
||||
}
|
||||
}
|
||||
else if (!n.IsJudged())
|
||||
n.JudgeMiss();
|
||||
}
|
||||
judgeQueues[key] = newQueue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按键按下时,判定队列中最早进入的音符
|
||||
/// </summary>
|
||||
public void JudgeEarliestNote(KeyCode key)
|
||||
{
|
||||
if (judgeQueues.ContainsKey(key) && judgeQueues[key].Count > 0)
|
||||
{
|
||||
Note note = judgeQueues[key].Dequeue(); // 只取最早的音符
|
||||
Note note = judgeQueues[key].Dequeue();
|
||||
if (note != null && !note.IsJudged())
|
||||
{
|
||||
note.Judge();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录中段通过,用于调试或进一步判定策略
|
||||
/// </summary>
|
||||
public void RegisterMiddlePassed(string noteID)
|
||||
{
|
||||
if (!middlePassedCounts.ContainsKey(noteID))
|
||||
@@ -139,9 +149,10 @@ public class JudgeManager : MonoBehaviour
|
||||
|
||||
public void ClearNoteRecord(string noteID)
|
||||
{
|
||||
if (startJudgedNotes.ContainsKey(noteID)) startJudgedNotes.Remove(noteID);
|
||||
if (releasedNotes.ContainsKey(noteID)) releasedNotes.Remove(noteID);
|
||||
if (noteEndTimes.ContainsKey(noteID)) noteEndTimes.Remove(noteID);
|
||||
if (middlePassedCounts.ContainsKey(noteID)) middlePassedCounts.Remove(noteID);
|
||||
startJudgedNotes.Remove(noteID);
|
||||
releasedNotes.Remove(noteID);
|
||||
noteEndTimes.Remove(noteID);
|
||||
middlePassedCounts.Remove(noteID);
|
||||
ClearHoldState(noteID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public class Note : BaseNote
|
||||
private NoteController controller;
|
||||
private bool isJudged = false;
|
||||
|
||||
private NoteData noteData;
|
||||
|
||||
[Header("判定区间配置")]
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
|
||||
|
||||
@@ -17,7 +19,7 @@ public class Note : BaseNote
|
||||
controller = GetComponent<NoteController>();
|
||||
}
|
||||
|
||||
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig)
|
||||
public void Setup(KeyCode key, int trackIndex, float speed, float hitTime, string color, NoteJudgeConfig judgeConfig, NoteData data)
|
||||
{
|
||||
keyToPress = key;
|
||||
noteColor = color;
|
||||
@@ -26,6 +28,7 @@ public class Note : BaseNote
|
||||
this.hitTime = hitTime;
|
||||
isJudged = false;
|
||||
this.judgeConfig = judgeConfig;
|
||||
this.noteData = data;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
@@ -53,6 +56,8 @@ public class Note : BaseNote
|
||||
return;
|
||||
|
||||
float timeDifference = Mathf.Abs(Time.time - hitTime);
|
||||
// 计算正负偏差:正值提前,负值落后
|
||||
float rawOffsetMs = (hitTime - Time.time) * 1000f;
|
||||
string judgeResult = null;
|
||||
|
||||
if (judgeConfig != null)
|
||||
@@ -61,22 +66,29 @@ public class Note : BaseNote
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Perfect");
|
||||
judgeResult = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.greatRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Great");
|
||||
judgeResult = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.goodRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Good");
|
||||
judgeResult = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
}
|
||||
else if (timeDifference <= judgeConfig.missRange)
|
||||
{
|
||||
Debug.Log($"{keyToPress}: Miss");
|
||||
judgeResult = "Miss";
|
||||
// Do not return here - let shared post-judge logic run so NotifyNoteHit is invoked for Miss as well
|
||||
/*ScoreManager.Instance.countMiss += 1;
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;*/
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -112,6 +124,7 @@ public class Note : BaseNote
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult);
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult);
|
||||
teamUIController.Instance?.OnJudgeResult(judgeResult); // 更新combo计数
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult);
|
||||
|
||||
// Calculate score addition via AllyCombatant and add to per-track pm sums
|
||||
try
|
||||
@@ -123,7 +136,8 @@ public class Note : BaseNote
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge(judgeResult);
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,32 +159,34 @@ public class Note : BaseNote
|
||||
|
||||
public void Judge()
|
||||
{
|
||||
if (isJudged)
|
||||
if (isJudged)
|
||||
return;
|
||||
isJudged = true;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
controller.StopMovement();
|
||||
controller.PlayHitEffect();
|
||||
controller.PlayHitEffect();
|
||||
}
|
||||
ReturnToPool();
|
||||
if (anim !=null)
|
||||
if (anim != null)
|
||||
{
|
||||
anim.PlayDestroyAnimation(noteColor);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void JudgeMiss()
|
||||
{
|
||||
if (isJudged) return;
|
||||
isJudged = true;
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
Debug.Log($"{keyToPress} Miss");
|
||||
// Ensure UI shows Miss and combo is updated when a note auto-misses
|
||||
InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss");
|
||||
JudgeSoundManager.Instance?.PlayJudgeSound("Miss");
|
||||
teamUIController.Instance?.OnJudgeResult("Miss");
|
||||
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss");
|
||||
|
||||
// Add score for Miss as well (some systems may give 0)
|
||||
try
|
||||
@@ -182,7 +198,8 @@ public class Note : BaseNote
|
||||
if (ally != null)
|
||||
{
|
||||
int added = ally.AddScoreForJudge("Miss");
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added);
|
||||
float efficiency = ally.scoreEfficiency;
|
||||
ScoreManager.Instance?.AddPmScoreForTrack(TrackIndex, added, efficiency);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,4 +250,4 @@ public class Note : BaseNote
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ public class NoteJudgeConfig : ScriptableObject
|
||||
{
|
||||
[Header("判定区间(单位:秒)")]
|
||||
[Tooltip("Perfect")]
|
||||
public float perfectRange = 0.05f;
|
||||
public float perfectRange = 0.1f;
|
||||
[Tooltip("Great")]
|
||||
public float greatRange = 0.1f;
|
||||
public float greatRange = 0.2f;
|
||||
[Tooltip("Good")]
|
||||
public float goodRange = 0.2f;
|
||||
public float goodRange = 0.3f;
|
||||
[Tooltip("Miss")]
|
||||
public float missRange = 0.3f;
|
||||
public float missRange = 0.5f;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
// pass realtime hit time to Note.Setup, include globalHitDelay
|
||||
float rawHit = startTime + noteData.time + globalHitDelay;
|
||||
float realtimeHit = Mathf.Max(0f, rawHit); // clamp to non-negative
|
||||
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig);
|
||||
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig, noteData);
|
||||
if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + (60f / bpm) * 4}");
|
||||
}
|
||||
else
|
||||
@@ -179,7 +179,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay
|
||||
float rawStartHit = startTime + noteData.time + globalHitDelay;
|
||||
float startHit = Mathf.Max(0f, rawStartHit);
|
||||
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig);
|
||||
holdNote.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -208,7 +208,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
// 中段都标记为 middle,pass realtime base time and segmentDelay
|
||||
float rawBase = startTime + noteData.time + globalHitDelay;
|
||||
float baseHit = Mathf.Max(0f, rawBase);
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig);
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,7 +238,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
float endDelay = segmentCount * actualSegmentInterval; // 通常等于 noteData.length
|
||||
float rawBase = startTime + noteData.time + globalHitDelay;
|
||||
float baseHit = Mathf.Max(0f, rawBase);
|
||||
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig);
|
||||
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class settlementController : MonoBehaviour
|
||||
{
|
||||
[Header("文本")]
|
||||
public Text songName_Text;
|
||||
[Tooltip("当前关卡的进度百分比")]
|
||||
public Text thisLevel_currentPercentage_Text;
|
||||
public Text finalScore_Text;
|
||||
public Text pmScoreSum_Text;
|
||||
public Text idolScoreSum_Text;
|
||||
|
||||
[Header("奖励内容信息")]
|
||||
public Text reward_playerEXP_Text;
|
||||
public Text reward_money_Text;
|
||||
public Text reward_idolEXP_bottle_Text;
|
||||
|
||||
[Header("给多少钱")]
|
||||
[SerializeField] private long moneyToGive_thisLevel;
|
||||
|
||||
|
||||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||||
void Start()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// Update is called once per frame
|
||||
void Update()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30f4ec0bd8dd71543ac8f08d62dcb010
|
||||
@@ -603,13 +603,12 @@ public class teamUIController : MonoBehaviour
|
||||
|
||||
// Calculate total max HP for all recognized enemies
|
||||
// Note: totalMaxHP is a class member and is intended to be the fixed maximum HP for the entire encounter.
|
||||
totalMaxHP = 0;
|
||||
if (recognizedEnemySOs != null)
|
||||
RecalculateTotalMaxHP();
|
||||
|
||||
if (recognizedEnemySOs.Length > 0)
|
||||
{
|
||||
foreach (var so in recognizedEnemySOs)
|
||||
{
|
||||
if (so != null) totalMaxHP += so.enemy_maxHP;
|
||||
}
|
||||
enemyCurrentCount = 0;
|
||||
SpawnNextEnemy();
|
||||
}
|
||||
|
||||
// If enemies were populated and we have some, start spawning from the beginning
|
||||
@@ -681,7 +680,9 @@ public class teamUIController : MonoBehaviour
|
||||
[Tooltip("当前总分")]
|
||||
public TextMeshProUGUI currentTotalScore;
|
||||
[Header("谱面总分")]
|
||||
public TextMeshProUGUI allSum_pmScore;
|
||||
public TextMeshProUGUI allSum_pmScore;
|
||||
[Header("爱豆总分")]
|
||||
public TextMeshProUGUI allSum_idolScore;
|
||||
|
||||
// new: runtime enemy instance and UI sync fields
|
||||
private EnemyCombatant enemyCombatantInstance;
|
||||
@@ -743,6 +744,8 @@ public class teamUIController : MonoBehaviour
|
||||
[SerializeField] private int teammate01_maxMana;
|
||||
[Header("pmScore")]
|
||||
public TextMeshProUGUI red_pmScore_sum;
|
||||
[Header("idolScore")]
|
||||
public TextMeshProUGUI red_idolScore_sum;
|
||||
|
||||
[Header("teammate02")]
|
||||
[Tooltip("父物体-控制整体显隐")]
|
||||
@@ -779,6 +782,8 @@ public class teamUIController : MonoBehaviour
|
||||
[SerializeField] private int teammate02_maxMana;
|
||||
[Header("pmScore")]
|
||||
public TextMeshProUGUI green_pmScore_sum;
|
||||
[Header("idolScore")]
|
||||
public TextMeshProUGUI green_idolScore_sum;
|
||||
|
||||
[Header("teammate03")]
|
||||
[Tooltip("父物体-控制整体显隐")]
|
||||
@@ -815,6 +820,8 @@ public class teamUIController : MonoBehaviour
|
||||
[SerializeField] private int teammate03_maxMana;
|
||||
[Header("pmScore")]
|
||||
public TextMeshProUGUI yellow_pmScore_sum;
|
||||
[Header("idolScore")]
|
||||
public TextMeshProUGUI yellow_idolScore_sum;
|
||||
|
||||
[Header("teammate04")]
|
||||
[Tooltip("父物体-控制整体显隐")]
|
||||
@@ -851,6 +858,8 @@ public class teamUIController : MonoBehaviour
|
||||
[SerializeField] private int teammate04_maxMana;
|
||||
[Header("pmScore")]
|
||||
public TextMeshProUGUI purple_pmScore_sum;
|
||||
[Header("idolScore")]
|
||||
public TextMeshProUGUI purple_idolScore_sum;
|
||||
|
||||
[Header("teammate05")]
|
||||
[Tooltip("父物体-控制整体显隐")]
|
||||
@@ -887,6 +896,8 @@ public class teamUIController : MonoBehaviour
|
||||
[SerializeField] private int teammate05_maxMana;
|
||||
[Header("pmScore")]
|
||||
public TextMeshProUGUI blue_pmScore_sum;
|
||||
[Header("idolScore")]
|
||||
public TextMeshProUGUI blue_idolScore_sum;
|
||||
|
||||
[Header("currentEnemy")]
|
||||
[Tooltip("父物体-控制整体显隐")]
|
||||
@@ -1388,6 +1399,70 @@ public class teamUIController : MonoBehaviour
|
||||
UpdateEnemyUIImmediate();
|
||||
}
|
||||
|
||||
// teamUIController.cs - 【新增】
|
||||
|
||||
// =========================================================
|
||||
// 供 BeatmapManager 调用的方法:设置单个敌人 HP
|
||||
// =========================================================
|
||||
/// <summary>
|
||||
/// 将计算得到的单个敌人最大生命值写入到所有已识别的敌人 Scriptable Object 中。
|
||||
/// </summary>
|
||||
public void ApplyCalculatedEnemyHP(int individualMaxHP)
|
||||
{
|
||||
// 确保 recognizedEnemySOs 数组不为空
|
||||
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0)
|
||||
{
|
||||
Debug.Log($"[teamUIController] Applying calculated HP: {individualMaxHP} to {recognizedEnemySOs.Length} recognized enemies.");
|
||||
|
||||
// 1. 修改 SO 数据:遍历所有已识别的敌人 SO,并设置其最大生命值
|
||||
foreach (var so in recognizedEnemySOs)
|
||||
{
|
||||
// 假设 EnemyData_SO 包含 public int enemy_maxHP; 字段
|
||||
if (so != null)
|
||||
{
|
||||
so.enemy_maxHP = individualMaxHP;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 重新计算总血量并更新总血条
|
||||
RecalculateTotalMaxHP();
|
||||
|
||||
// 3. 强制刷新当前生成的敌人实例
|
||||
// 因为 Populate 已经在 Apply 之前生成了旧数据的敌人,所以需要重置
|
||||
if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length)
|
||||
{
|
||||
var currentSO = recognizedEnemySOs[enemyCurrentCount];
|
||||
if (currentSO != null)
|
||||
{
|
||||
Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {individualMaxHP}");
|
||||
enemyCombatantInstance.InitializeFromSO(currentSO); // 重新使用新的 SO 数据初始化实例
|
||||
UpdateEnemyUIImmediate(); // 立即刷新 UI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 【新增】总生命值重新计算方法(封装原有的累加逻辑)
|
||||
// =========================================================
|
||||
/// <summary>
|
||||
/// 遍历所有已识别敌人的 SO,累加它们的生命值,更新 totalMaxHP,并立即刷新总血条。
|
||||
/// </summary>
|
||||
private void RecalculateTotalMaxHP()
|
||||
{
|
||||
totalMaxHP = 0;
|
||||
if (recognizedEnemySOs != null)
|
||||
{
|
||||
foreach (var so in recognizedEnemySOs)
|
||||
{
|
||||
if (so != null) totalMaxHP += so.enemy_maxHP;
|
||||
}
|
||||
}
|
||||
Debug.Log($"[teamUIController] Total Max HP has been recalculated to: {totalMaxHP}");
|
||||
// 刷新总血条上限显示,防止出现延迟
|
||||
UpdateAllEnemyTotalHealthUIImmediate();
|
||||
}
|
||||
|
||||
// 新增:在当前敌人 HP 变化时,更新总血条 UI
|
||||
private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user