编队系统重置 选角页面基本完成 等待存入playerprefs

This commit is contained in:
FloatGaming
2025-12-07 12:04:21 +08:00
parent 941b294fce
commit c0a822b958
134 changed files with 14796 additions and 198 deletions
+278 -76
View File
@@ -44,10 +44,16 @@ public class SkillBuilder : MonoBehaviour
// Placeholder: if you want to route damage through EffectSystem later, use this.
public void DealDamageFromAllyToEnemies(int slotIndex, float amount)
{
Debug.Log($"[SkillBuilder] DealDamageFromAllyToEnemies (placeholder): slot={slotIndex} amount={amount}");
// Future: integrate with EffectSystem/Enemy system here. Example (commented):
// var caster = GetAllyObjectBySlot(slotIndex);
// global::EffectSystem.Instance?.ApplyEffect(Selector.CurrentEnemies, EffectType.DamageSingleEnemy, amount, 0f, caster, null);
// Resolve caster GameObject and route damage to EffectSystem so enemies receive damage with resistances etc.
var caster = GetAllyObjectBySlot(slotIndex);
if (global::EffectSystem.Instance == null)
{
Debug.LogWarning($"DealDamageFromAllyToEnemies: EffectSystem.Instance is null. slot={slotIndex} amount={amount}");
return;
}
// Apply as single-instance damage to current enemies; EffectSystem will pick first applicable enemy for DamageSingleEnemy
global::EffectSystem.Instance.ApplyEffect(Selector.CurrentEnemies, EffectType.DamageSingleEnemy, amount, 0f, caster, null);
Debug.Log($"[SkillBuilder] DealDamageFromAllyToEnemies: applied {amount} from slot {slotIndex} via EffectSystem");
}
private void ApplySharedOnNoteHit(int trackIndex, string judgeResult)
@@ -224,7 +230,7 @@ public class SkillBuilder : MonoBehaviour
// 直接修改全局总分(立即生效)
public void ModifyTotalScore(int delta)
{
if (ScoreManager.Instance == null) { Debug.LogWarning("ModifyTotalScore: ScoreManager.Instance == null"); return; }
if (ScoreManager.Instance == null) { Debug.LogWarning("ModifyTotalScore: ScoreManager.Instance is null"); return; }
// adjust total and attempt to update UI
ScoreManager.Instance.totalScore += delta;
// try to force UI update via ScoreManager.RecalculateTotal() is not appropriate because it recalculates from allies
@@ -320,6 +326,36 @@ public class SkillBuilder : MonoBehaviour
ExecuteSkill("DealSingleEnemyDamage", EffectType.DamageSingleEnemy, amount, Selector.CurrentEnemies, caster, enemyTarget);
}
// New: heal enemy directly (single)
public void HealSingleEnemy(GameObject caster, GameObject enemyTarget, float amount)
{
if (enemyTarget == null) { Debug.LogWarning("HealSingleEnemy: enemyTarget == null"); return; }
ExecuteSkill("HealSingleEnemy", EffectType.HealSingleEnemy, amount, Selector.CurrentEnemies, caster, enemyTarget);
}
// New: heal enemy over time
public void ApplyHealOverTimeToEnemies(GameObject caster, Selector selector, float totalAmount, float duration, float tickInterval = 1f, GameObject specificTarget = null)
{
if (duration <= 0f) duration = defaultDuration;
if (tickInterval <= 0f) tickInterval = defaultTickInterval;
ExecuteSkill("HealOverTimeEnemy", EffectType.HealOverTimeEnemy, totalAmount, selector, caster, specificTarget, duration, tickInterval);
}
// New: damage allies directly (single)
public void DealSingleAllyDamage(GameObject caster, GameObject allyTarget, float amount)
{
if (allyTarget == null) { Debug.LogWarning("DealSingleAllyDamage: allyTarget == null"); return; }
ExecuteSkill("DealSingleAllyDamage", EffectType.DamageSingleAlly, amount, Selector.AllAllies, caster, allyTarget);
}
// New: damage allies over time
public void ApplyDamageOverTimeToAllies(GameObject caster, Selector selector, float totalAmount, float duration, float tickInterval = 1f, GameObject specificTarget = null)
{
if (duration <= 0f) duration = defaultDuration;
if (tickInterval <= 0f) tickInterval = defaultTickInterval;
ExecuteSkill("DamageOverTimeAlly", EffectType.DamageOverTimeAlly, totalAmount, selector, caster, specificTarget, duration, tickInterval);
}
public void ApplyDamageOverTimeToEnemies(GameObject caster, Selector selector, float totalAmount, float duration, float tickInterval = 1f, GameObject specificTarget = null)
{
if (duration <= 0f) duration = defaultDuration;
@@ -636,6 +672,37 @@ public class SkillBuilder : MonoBehaviour
if (def.effectType == EffectType.BuffDuration) b.scoreMultiplier = 1.5f;
if (ic != null) ic.ApplyBuff(b, caster);
break;
// 新增 effect type 处理
case EffectType.IncreaseMaxHP:
if (ally != null) ally.SetMaxHP(ally.maxHP + Mathf.CeilToInt(amountTotal), false);
else if (t.TryGetComponent<EnemyCombatant>(out var enemy)) enemy.maxHP += Mathf.CeilToInt(amountTotal);
break;
case EffectType.DecreaseMaxHP:
if (ally != null) ally.SetMaxHP(Mathf.Max(1, ally.maxHP - Mathf.CeilToInt(amountTotal)), false);
else if (t.TryGetComponent<EnemyCombatant>(out var enemy2)) enemy2.maxHP = Mathf.Max(1, enemy2.maxHP - Mathf.CeilToInt(amountTotal));
break;
case EffectType.IncreaseMaxMana:
if (ally != null) ally.SetMaxMana(ally.maxMana + Mathf.CeilToInt(amountTotal), false);
else if (t.TryGetComponent<EnemyCombatant>(out var enemy3)) enemy3.maxMana += Mathf.CeilToInt(amountTotal);
break;
case EffectType.DecreaseMaxMana:
if (ally != null) ally.SetMaxMana(Mathf.Max(1, ally.maxMana - Mathf.CeilToInt(amountTotal)), false);
else if (t.TryGetComponent<EnemyCombatant>(out var enemy4)) enemy4.maxMana = Mathf.Max(1, enemy4.maxMana - Mathf.CeilToInt(amountTotal));
break;
case EffectType.IncreaseScoreEfficiency:
if (ally != null) ally.scoreEfficiency += amountTotal;
break;
case EffectType.DecreaseScoreEfficiency:
if (ally != null) ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amountTotal);
break;
case EffectType.IncreaseAttack:
if (ally != null) ally.attack += Mathf.CeilToInt(amountTotal);
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5)) enemy5.attack += Mathf.CeilToInt(amountTotal);
break;
case EffectType.DecreaseAttack:
if (ally != null) ally.attack = Mathf.Max(0, ally.attack - Mathf.CeilToInt(amountTotal));
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6)) enemy6.attack = Mathf.Max(0, enemy6.attack - Mathf.CeilToInt(amountTotal));
break;
default:
// non-direct path handled below, but keep compatibility
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, amountTotal, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
@@ -663,6 +730,7 @@ public class SkillBuilder : MonoBehaviour
}
// Use the primary skill configured in the AllyHero_SO for this slot (primarySkillIndex dropdown)
// Updated: runtime activation now prefers equippedSkillGroupIDs on the SO. Only skills inside equipped groups are considered active.
public void UsePrimarySkillForSlot(int slotIndex, float inputValue = -1f, GameObject specificTarget = null)
{
var so = GetAllyHeroSOBySlot(slotIndex);
@@ -671,11 +739,51 @@ public class SkillBuilder : MonoBehaviour
Debug.LogWarning($"UsePrimarySkillForSlot: no SO for slot {slotIndex}");
return;
}
// If SO defines equipped group IDs, cast skills from those groups (these represent active/owned skills at runtime)
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (var gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++)
{
var g = so.skillGroups[k];
if (g != null && g.skillGroupID == gid) { group = g; break; }
}
if (group == null) continue;
UseSkillGroupForSlot(group, slotIndex, inputValue, specificTarget);
}
return;
}
// Fallback: prefer any defined primary group (SO-level) via GetPrimarySkillGroup(), otherwise use primarySkillIndex
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null)
{
UseSkillGroupForSlot(fallbackGroup, slotIndex, inputValue, specificTarget);
return;
}
int idx = so.primarySkillIndex;
if (idx < 0) { Debug.LogWarning($"UsePrimarySkillForSlot: primarySkillIndex not set for slot {slotIndex}"); return; }
UseSelectedSkillForSlot(slotIndex, idx, inputValue, specificTarget);
}
// Cast all non-null skills in a SkillGroup for a given slotIndex. Each skill is invoked via UseSkillDefinition.
public void UseSkillGroupForSlot(SkillGroup group, int slotIndex, float inputValue = -1f, GameObject specificTarget = null)
{
if (group == null) return;
for (int i = 0; i < group.skills.Length; i++)
{
var s = group.skills[i];
if (s == null) continue;
UseSkillDefinition(s, slotIndex, inputValue, specificTarget);
Debug.Log($"[SkillBuilder] UseSkillGroupForSlot: cast skill {s.skillId} from group '{group.groupName}' for slot {slotIndex}");
}
}
// Called at game start to trigger any allies whose primary skill is set to trigger on game start
public void TriggerOnGameStart()
{
@@ -685,6 +793,45 @@ public class SkillBuilder : MonoBehaviour
{
var so = GetAllyHeroSOBySlot(i);
if (so == null) continue;
// If equipped groups exist, iterate them and cast group skills whose triggerCondition == OnGameStart
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (int gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++) { var g = so.skillGroups[k]; if (g != null && g.skillGroupID == gid) { group = g; break; } }
if (group == null) continue;
foreach (var sk in group.skills)
{
if (sk == null) continue;
if (sk.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
{
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting equipped group skill {sk.skillId} for slot {i}");
UseSkillDefinition(sk, i, -1f, null);
}
}
}
continue;
}
// Fallback: check SO-level primary group via GetPrimarySkillGroup()
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null)
{
foreach (var sk in fallbackGroup.skills)
{
if (sk == null) continue;
if (sk.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
{
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting group skill {sk.skillId} for slot {i}");
UseSkillDefinition(sk, i, -1f, null);
}
}
continue;
}
var def = so.GetPrimarySkill();
if (def == null) continue;
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
@@ -697,14 +844,44 @@ public class SkillBuilder : MonoBehaviour
private Dictionary<string, float> _lastOnNoteHitTriggerTime = new Dictionary<string, float>();
// track processed unique note IDs (e.g. long-hold note id) so shared effects (mana/hp) are applied only once
private Dictionary<string, float> _processedNoteHitTimestamps = new Dictionary<string, float>();
// Called when a note on a particular track is hit (judgeResult e.g. "Perfect"/"Great"/"Good").
// Will cast the primary skill for the ally in that track if its primary skill is configured to trigger on note hit.
// noteType indicates whether the hit event came from a Tap or Hold (tail)
public bool NotifyNoteHit(int trackIndex, string judgeResult, SkillDefinition.NoteTypeTrigger noteType = SkillDefinition.NoteTypeTrigger.Tap)
public bool NotifyNoteHit(int trackIndex, string judgeResult, SkillDefinition.NoteTypeTrigger noteType = SkillDefinition.NoteTypeTrigger.Tap, string uniqueNoteId = null)
{
// Always apply shared per-note behavior (mana gain / miss penalty / reserved damage) regardless of SO-defined primary skill
try { ApplySharedOnNoteHit(trackIndex, judgeResult); } catch (System.Exception ex) { Debug.LogError($"[SkillBuilder] ApplySharedOnNoteHit threw: {ex}"); }
Debug.Log($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType}");
Debug.Log($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType} uniqueId={uniqueNoteId}");
// cleanup old processed ids (> 10s)
float nowCleanup = Time.time;
var toRemove = new List<string>();
foreach (var kv in _processedNoteHitTimestamps)
{
if (nowCleanup - kv.Value > 10f) toRemove.Add(kv.Key);
}
foreach (var k in toRemove) _processedNoteHitTimestamps.Remove(k);
// Determine whether to apply shared effects (mana/H P loss) for this event.
bool applyShared = true;
if (!string.IsNullOrEmpty(uniqueNoteId))
{
if (_processedNoteHitTimestamps.ContainsKey(uniqueNoteId))
{
applyShared = false; // already applied for this hold note
}
else
{
_processedNoteHitTimestamps[uniqueNoteId] = Time.time;
}
}
// Always apply shared per-note behavior (mana gain / miss penalty / reserved damage) unless this unique note id was already processed
if (applyShared)
{
try { ApplySharedOnNoteHit(trackIndex, judgeResult); } catch (System.Exception ex) { Debug.LogError($"[SkillBuilder] ApplySharedOnNoteHit threw: {ex}"); }
}
if (trackIndex < 0) { Debug.Log("[SkillBuilder] NotifyNoteHit: invalid trackIndex"); return false; }
var so = GetAllyHeroSOBySlot(trackIndex);
if (so == null)
@@ -744,100 +921,113 @@ public class SkillBuilder : MonoBehaviour
return false;
}
}
var def = so.GetPrimarySkill();
if (def == null) { Debug.Log($"[SkillBuilder] NotifyNoteHit: no primary skill for slot {trackIndex}"); return false; }
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill={def.skillId} noteTriggerType={def.noteTriggerType} onNoteHitMinThreshold={def.onNoteHitMinThreshold} cooldown={def.onNoteHitCooldown}");
bool anyTriggered = false;
// Treat Hold events as Tap-equivalent by default to improve compatibility: many skills expect Tap/Either
var effectiveNoteType = noteType;
if (noteType == SkillDefinition.NoteTypeTrigger.Hold)
// If equipped groups exist, iterate equipped groups and check each skill for OnNoteHit
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
// Only convert to Tap-equivalent for checking; keep original in logs
effectiveNoteType = SkillDefinition.NoteTypeTrigger.Tap;
Debug.Log($"[SkillBuilder] NotifyNoteHit: treating incoming Hold event as Tap-equivalent for skill checks (track={trackIndex})");
foreach (int gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++) { var g = so.skillGroups[k]; if (g != null && g.skillGroupID == gid) { group = g; break; } }
if (group == null) continue;
foreach (var def in group.skills)
{
if (def == null) continue;
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnNoteHit) continue;
// Respect note type (Tap/Hold/Either) - treat Hold as Tap-equivalent for checking as before
var effectiveNoteType = noteType;
if (noteType == SkillDefinition.NoteTypeTrigger.Hold) effectiveNoteType = SkillDefinition.NoteTypeTrigger.Tap;
if (def.noteTriggerType == SkillDefinition.NoteTypeTrigger.Tap && effectiveNoteType != SkillDefinition.NoteTypeTrigger.Tap) continue;
if (def.noteTriggerType == SkillDefinition.NoteTypeTrigger.Hold && noteType != SkillDefinition.NoteTypeTrigger.Hold) continue;
int quality = JudgeQualityFromString(judgeResult);
if (def.onNoteHitMinThreshold == SkillDefinition.NoteTriggerThreshold.Miss)
{
if (quality != 0) continue;
}
else
{
int required = (int)def.onNoteHitMinThreshold;
if (quality < required) continue;
}
// cooldown per slot+skill
string key = $"{trackIndex}:{def.skillId}";
float now = Time.time;
if (def.onNoteHitCooldown > 0f)
{
if (_lastOnNoteHitTriggerTime.TryGetValue(key, out float last))
{
if (now - last < def.onNoteHitCooldown) continue;
}
}
// trigger
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering equipped skill {def.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
UseSkillDefinition(def, trackIndex, -1f, null);
_lastOnNoteHitTriggerTime[key] = now;
anyTriggered = true;
}
}
return anyTriggered;
}
// Respect note type (Tap/Hold/Either)
switch (def.noteTriggerType)
// Fallback to previous behavior using primary skill (availableSkills)
var defPrimary = so.GetPrimarySkill();
if (defPrimary == null) { Debug.Log($"[SkillBuilder] NotifyNoteHit: no primary skill for slot {trackIndex}"); return false; }
if (defPrimary.triggerCondition != SkillDefinition.SkillTrigger.OnNoteHit)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: primary skill {defPrimary.skillId} for slot {trackIndex} is not set to OnNoteHit (actual={defPrimary.triggerCondition}), rejecting");
return false;
}
// previous checks preserved
var effectiveNoteTypePrimary = noteType;
if (noteType == SkillDefinition.NoteTypeTrigger.Hold) effectiveNoteTypePrimary = SkillDefinition.NoteTypeTrigger.Tap;
switch (defPrimary.noteTriggerType)
{
case SkillDefinition.NoteTypeTrigger.Tap:
// accept Tap (and treat Hold as Tap-equivalent)
if (effectiveNoteType != SkillDefinition.NoteTypeTrigger.Tap)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires Tap but event is {noteType}, rejecting");
return false;
}
if (effectiveNoteTypePrimary != SkillDefinition.NoteTypeTrigger.Tap) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Tap but event is {noteType}, rejecting"); return false; }
break;
case SkillDefinition.NoteTypeTrigger.Hold:
if (noteType != SkillDefinition.NoteTypeTrigger.Hold)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires Hold but event is {noteType}, rejecting");
return false;
}
if (noteType != SkillDefinition.NoteTypeTrigger.Hold) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Hold but event is {noteType}, rejecting"); return false; }
break;
case SkillDefinition.NoteTypeTrigger.Either:
// accept both
break;
}
// Map judgeResult to numeric quality
int quality = JudgeQualityFromString(judgeResult);
Debug.Log($"[SkillBuilder] NotifyNoteHit: judgeResult='{judgeResult}' mappedQuality={quality}");
// Special case: if threshold == Miss, only trigger on Miss (quality == 0)
if (def.onNoteHitMinThreshold == SkillDefinition.NoteTriggerThreshold.Miss)
int qualityPrimary = JudgeQualityFromString(judgeResult);
if (defPrimary.onNoteHitMinThreshold == SkillDefinition.NoteTriggerThreshold.Miss)
{
if (quality != 0)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} is Miss-only but quality={quality}, rejecting");
return false;
}
if (qualityPrimary != 0) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} is Miss-only but quality={qualityPrimary}, rejecting"); return false; }
}
else
{
int required = (int)def.onNoteHitMinThreshold; // Good=1, Great=2, Perfect=3
if (quality < required)
int required = (int)defPrimary.onNoteHitMinThreshold;
if (qualityPrimary < required) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires quality>={required} but got {qualityPrimary}, rejecting"); return false; }
}
string keyPrimary = $"{trackIndex}:{defPrimary.skillId}";
float nowPrimary = Time.time;
if (defPrimary.onNoteHitCooldown > 0f && _lastOnNoteHitTriggerTime.TryGetValue(keyPrimary, out float lastPrimary))
{
if (nowPrimary - lastPrimary < defPrimary.onNoteHitCooldown)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} requires quality>={required} but got {quality}, rejecting");
// not high enough
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} on cooldown for slot {trackIndex}");
return false;
}
}
// cooldown: per slot+skill
string key = $"{trackIndex}:{def.skillId}";
float now = Time.time;
if (def.onNoteHitCooldown > 0f)
{
if (_lastOnNoteHitTriggerTime.TryGetValue(key, out float last))
{
if (now - last < def.onNoteHitCooldown)
{
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {def.skillId} on cooldown for slot {trackIndex}");
// still cooling down
return false;
}
}
}
// Passed checks -> invoke primary skill for slot
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering primary skill {def.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering primary skill {defPrimary.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
UsePrimarySkillForSlot(trackIndex, -1f, null);
_lastOnNoteHitTriggerTime[key] = now;
_lastOnNoteHitTriggerTime[keyPrimary] = nowPrimary;
return true;
}
private int JudgeQualityFromString(string judge)
{
switch (judge)
{
case "Perfect": return 3;
case "Great": return 2;
case "Good": return 1;
default: return 0; // Miss or unknown
}
}
/*
Explanation of caster and enemyTarget:
- GameObject caster: the GameObject that performs (casts) the skill. For ally skills this should be the in-scene ally GameObject (usually name ally_01..ally_05) and should have AllyCombatant / ICombatant components. SkillBuilder uses slotIndex -> GetAllyObjectBySlot to resolve this.
@@ -935,4 +1125,16 @@ public class SkillBuilder : MonoBehaviour
elapsed += tickInterval;
}
}
// Helper: map judge string to numeric quality
private int JudgeQualityFromString(string judge)
{
switch (judge)
{
case "Perfect": return 3;
case "Great": return 2;
case "Good": return 1;
default: return 0; // Miss or unknown
}
}
}