notebook实装功能,galgame继续,一些bug修复

This commit is contained in:
FloatGaming
2026-03-06 02:08:43 +08:00
parent 9011fa022e
commit 1f4077cf21
468 changed files with 50175 additions and 11402 deletions
+278 -1
View File
@@ -72,9 +72,19 @@ public class SkillBuilder : MonoBehaviour
private readonly Dictionary<string, Coroutine> _refreshOnlyTimedEffectCoroutines = new Dictionary<string, Coroutine>(32);
private readonly Dictionary<string, RefreshOnlyTimedState> _refreshOnlyTimedEffectStates = new Dictionary<string, RefreshOnlyTimedState>(32);
private readonly Dictionary<string, Coroutine> _refreshOnlyOverTimeCoroutines = new Dictionary<string, Coroutine>(16);
private readonly Dictionary<int, ManaFullLongingState> _manaFullLongingStateBySlot = new Dictionary<int, ManaFullLongingState>(8);
private int _cachedAlliesFrame = -1;
private readonly List<GameObject> _cachedAllies = new List<GameObject>(8);
private const string SkillIdYuetaoLongingDamage = "44109";
private const string SkillIdYuetaoLongingCost = "44109_2";
private struct ManaFullLongingState
{
public int frame;
public int consumedHp;
}
private static readonly HashSet<string> s_refreshOnlyTimedSkillIds = new HashSet<string>
{
// Lock
@@ -859,6 +869,7 @@ public class SkillBuilder : MonoBehaviour
case EffectType.IncreaseAttack:
case EffectType.DecreaseAttack:
case EffectType.RedirectNextDamageToSelf:
case EffectType.RedirectSelfDamageToAdjacent:
return true;
default:
return false;
@@ -1182,10 +1193,11 @@ public class SkillBuilder : MonoBehaviour
int groupId = 0;
string smallSkillName = null;
Sprite smallSkillIcon = null;
AllyHero_SO heroSoForName = null;
// Resolve group/icon metadata for the ally HUD skill icon queue.
try
{
var heroSoForName = GetAllyHeroSOBySlot(slotIndex);
heroSoForName = GetAllyHeroSOBySlot(slotIndex);
// Documentation text normalized.
if (heroSoForName != null && heroSoForName.skillGroups != null)
@@ -1308,6 +1320,11 @@ ResolvedGroup:
// Log useful debug info for diagnosing OnEnemyDead->Self issues
LogVerbose($"[SkillBuilder] UseSkillDefinition: casting skill {def.skillId} for slot {slotIndex} (caster={(caster?caster.name:"null")}) selector={def.defaultSelector} operateDirectly={def.operateDirectly} inputValue={inputValue} specificTarget={(specificTarget?specificTarget.name:"null")}");
if (TryHandleYuetaoLongingSkill(def, slotIndex, caster, heroSoForName, specificTarget))
{
return;
}
float amount = 0f;
// If caller provided explicit inputValue use it
if (inputValue != -1f)
@@ -1372,6 +1389,9 @@ ResolvedGroup:
vars["currentScore"] = casterAlly != null ? casterAlly.currentScore : 0f;
vars["idolScore"] = vars["currentScore"];
vars["currentHP"] = casterAlly != null ? casterAlly.currentHP : 0f;
vars["noteBaseScore"] = casterAlly != null
? (casterAlly.bmm != null ? Mathf.Max(0, casterAlly.bmm.perNoteScore) : Mathf.Max(0, casterAlly.baseTrackScore))
: 0f;
if (string.IsNullOrWhiteSpace(def.formula))
{
@@ -1438,6 +1458,26 @@ ResolvedGroup:
_lastSkillTriggerTime[repeatKey] = now;
}
try
{
string casterDisplayName = ResolveCasterDisplayNameForLog(slotIndex, heroSoForName, caster);
string skillDisplayNameForLog = string.IsNullOrWhiteSpace(smallSkillName)
? (!string.IsNullOrWhiteSpace(def.displayName) ? def.displayName : (!string.IsNullOrWhiteSpace(def.skillId) ? def.skillId : def.name))
: smallSkillName;
string effectSummary = BuildSkillEffectSummaryForLog(def, amountPerTick, amountTotal);
string targetSummary = BuildSkillTargetSummaryForLog(def.defaultSelector, specificTarget);
GameplaySkillLogger.RecordSkillRelease(
casterDisplayName,
skillDisplayNameForLog,
def.effectType.ToString(),
effectSummary,
targetSummary);
}
catch (System.Exception ex)
{
LogVerbose("[SkillBuilder] GameplaySkillLogger failed: " + ex.Message);
}
if (def.operateDirectly)
{
// Resolve targets as GameObjects but operate on their AllyCombatant / ICombatant data directly
@@ -1800,6 +1840,79 @@ ResolvedGroup:
}
}
private bool TryHandleYuetaoLongingSkill(SkillDefinition def, int slotIndex, GameObject caster, AllyHero_SO heroSoForName, GameObject specificTarget)
{
if (def == null || string.IsNullOrWhiteSpace(def.skillId)) return false;
if (def.skillId != SkillIdYuetaoLongingDamage && def.skillId != SkillIdYuetaoLongingCost) return false;
var casterAlly = caster != null ? caster.GetComponent<AllyCombatant>() : null;
if (casterAlly == null)
{
Debug.LogWarning($"[SkillBuilder] Yuetao Longing custom handler skipped: caster ally missing for slot {slotIndex}");
return false;
}
int consumedHp = 0;
int nowFrame = Time.frameCount;
if (_manaFullLongingStateBySlot.TryGetValue(slotIndex, out var state) && state.frame == nowFrame)
{
consumedHp = state.consumedHp;
}
else
{
consumedHp = ConsumeYuetaoLongingHpCost(casterAlly);
_manaFullLongingStateBySlot[slotIndex] = new ManaFullLongingState { frame = nowFrame, consumedHp = consumedHp };
}
string casterDisplayName = ResolveCasterDisplayNameForLog(slotIndex, heroSoForName, caster);
string skillDisplayName = !string.IsNullOrWhiteSpace(def.displayName) ? def.displayName : (!string.IsNullOrWhiteSpace(def.skillId) ? def.skillId : def.name);
string targetSummary = BuildSkillTargetSummaryForLog(def.defaultSelector, specificTarget);
if (def.skillId == SkillIdYuetaoLongingCost)
{
try
{
string effectSummary = "生命值" + FormatSignedIntForLog(-Mathf.Abs(consumedHp)) + " (上限3%, 保底>20%)";
GameplaySkillLogger.RecordSkillRelease(casterDisplayName, skillDisplayName, def.effectType.ToString(), effectSummary, targetSummary);
}
catch { }
return true;
}
float attackBonus = Mathf.Max(0f, casterAlly.attack) * 0.1f;
float damage = consumedHp + attackBonus;
if (damage > 0f)
{
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, damage, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
}
try
{
string effectSummary = "重击伤害=" + damage.ToString("0.###") + " (耗血" + consumedHp + "+攻击10%:" + attackBonus.ToString("0.###") + ")";
GameplaySkillLogger.RecordSkillRelease(casterDisplayName, skillDisplayName, def.effectType.ToString(), effectSummary, targetSummary);
}
catch { }
return true;
}
private static int ConsumeYuetaoLongingHpCost(AllyCombatant ally)
{
if (ally == null || ally.maxHP <= 0 || ally.currentHP <= 0) return 0;
int rawCost = Mathf.Max(0, Mathf.CeilToInt(ally.maxHP * 0.03f));
if (rawCost <= 0) return 0;
// "cannot drop to 20% max HP or below" => keep HP strictly greater than 20%.
int minRemainExclusive = Mathf.FloorToInt(ally.maxHP * 0.2f) + 1;
int maxAllowedCost = Mathf.Max(0, ally.currentHP - minRemainExclusive);
int finalCost = Mathf.Clamp(rawCost, 0, maxAllowedCost);
if (finalCost <= 0) return 0;
ally.ModifyHP(-finalCost, true);
return finalCost;
}
// Use the selected skill index from an AllyHero_SO for a given slot (calls UseSkillDefinition)
public void UseSelectedSkillForSlot(int slotIndex, int selectedSkillIndex, float inputValue = -1f, GameObject specificTarget = null)
{
@@ -2584,6 +2697,170 @@ ResolvedGroup:
}
}
private static string ResolveCasterDisplayNameForLog(int slotIndex, AllyHero_SO heroSo, GameObject caster)
{
if (heroSo != null && !string.IsNullOrWhiteSpace(heroSo.ally_heroName))
{
return heroSo.ally_heroName;
}
if (caster != null)
{
var ally = caster.GetComponent<AllyCombatant>() ?? caster.GetComponentInChildren<AllyCombatant>(true);
if (ally != null && !string.IsNullOrWhiteSpace(ally.allyName))
{
return ally.allyName;
}
if (!string.IsNullOrWhiteSpace(caster.name))
{
return caster.name;
}
}
return "slot" + (slotIndex + 1);
}
private static string BuildSkillTargetSummaryForLog(Selector selector, GameObject specificTarget)
{
if (specificTarget != null)
{
return selector + " -> " + specificTarget.name;
}
return selector.ToString();
}
private static string BuildSkillEffectSummaryForLog(SkillDefinition def, float amountPerTick, float amountTotal)
{
if (def == null) return "-";
bool isOverTime = IsOverTimeEffectForLog(def.effectType) && def.defaultDuration > 0f;
string summary;
switch (def.effectType)
{
case EffectType.DamageSingleEnemy:
case EffectType.DamageSingleAlly:
summary = "生命值" + FormatSignedFloatForLog(-Mathf.Abs(amountTotal));
break;
case EffectType.DamageOverTimeEnemy:
case EffectType.DamageOverTimeAlly:
if (isOverTime) summary = "生命值" + FormatSignedFloatForLog(-Mathf.Abs(amountPerTick)) + "/tick, 总计" + FormatSignedFloatForLog(-Mathf.Abs(amountTotal));
else summary = "生命值" + FormatSignedFloatForLog(-Mathf.Abs(amountTotal));
break;
case EffectType.HealSingleEnemy:
case EffectType.HealSingleSelf:
case EffectType.HealGroupSingle:
summary = "生命值" + FormatSignedFloatForLog(Mathf.Abs(amountTotal));
break;
case EffectType.HealOverTimeEnemy:
case EffectType.HealOverTimeSelf:
case EffectType.HealGroupOverTime:
if (isOverTime) summary = "生命值" + FormatSignedFloatForLog(Mathf.Abs(amountPerTick)) + "/tick, 总计" + FormatSignedFloatForLog(Mathf.Abs(amountTotal));
else summary = "生命值" + FormatSignedFloatForLog(Mathf.Abs(amountTotal));
break;
case EffectType.IncreaseManaOverTime:
if (isOverTime) summary = "法力值" + FormatSignedFloatForLog(amountPerTick) + "/tick, 总计" + FormatSignedFloatForLog(amountTotal);
else summary = "法力值" + FormatSignedFloatForLog(amountTotal);
break;
case EffectType.ReduceEnemyHealOverTime:
summary = "受疗倍率-" + Mathf.RoundToInt(Mathf.Abs(amountPerTick) * 100f) + "%";
break;
case EffectType.ScoreMultiplier:
summary = "得分倍率x" + amountTotal.ToString("0.###");
break;
case EffectType.AddScore:
summary = "偶像分数" + FormatSignedFloatForLog(amountTotal);
break;
case EffectType.IncreaseMaxHP:
summary = "最大生命值" + FormatSignedIntForLog(Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.DecreaseMaxHP:
summary = "最大生命值" + FormatSignedIntForLog(-Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.IncreaseMaxMana:
summary = "最大法力值" + FormatSignedIntForLog(Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.DecreaseMaxMana:
summary = "最大法力值" + FormatSignedIntForLog(-Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.IncreaseScoreEfficiency:
summary = "得分效率" + FormatSignedFloatForLog(Mathf.Abs(amountTotal));
break;
case EffectType.DecreaseScoreEfficiency:
summary = "得分效率" + FormatSignedFloatForLog(-Mathf.Abs(amountTotal));
break;
case EffectType.IncreaseDamageResistance:
summary = "伤害抗性" + FormatSignedFloatForLog(Mathf.Abs(amountTotal));
break;
case EffectType.DecreaseDamageResistance:
summary = "伤害抗性" + FormatSignedFloatForLog(-Mathf.Abs(amountTotal));
break;
case EffectType.IncreaseAttack:
summary = "攻击力" + FormatSignedIntForLog(Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.DecreaseAttack:
summary = "攻击力" + FormatSignedIntForLog(-Mathf.CeilToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.RedirectNextDamageToSelf:
summary = "下一次承伤转移到自身";
break;
case EffectType.RedirectSelfDamageToAdjacent:
summary = "下一次承伤转移到相邻偶像";
break;
case EffectType.GrantExtraPerfect:
summary = "额外Perfect判定+" + Mathf.Max(1, Mathf.RoundToInt(Mathf.Abs(amountTotal)));
break;
case EffectType.RewriteNonMissToPerfect:
summary = "非Miss判定改写为Perfect";
break;
case EffectType.BuffDuration:
summary = "增益效果倍率x" + amountPerTick.ToString("0.###");
break;
case EffectType.DebuffDuration:
summary = "减益效果倍率x" + amountPerTick.ToString("0.###");
break;
default:
summary = "数值" + FormatSignedFloatForLog(amountTotal);
break;
}
if (def.defaultDuration > 0f)
{
summary += ",持续" + def.defaultDuration.ToString("0.##") + "s";
if (isOverTime)
{
float tick = Mathf.Max(0.01f, def.GetEffectiveTickInterval());
summary += ",间隔" + tick.ToString("0.##") + "s";
}
}
return summary;
}
private static bool IsOverTimeEffectForLog(EffectType effectType)
{
return effectType == EffectType.DamageOverTimeEnemy
|| effectType == EffectType.DamageOverTimeAlly
|| effectType == EffectType.HealOverTimeEnemy
|| effectType == EffectType.HealOverTimeSelf
|| effectType == EffectType.HealGroupOverTime
|| effectType == EffectType.IncreaseManaOverTime;
}
private static string FormatSignedFloatForLog(float value)
{
if (value >= 0f) return "+" + value.ToString("0.###");
return value.ToString("0.###");
}
private static string FormatSignedIntForLog(int value)
{
if (value >= 0) return "+" + value.ToString();
return value.ToString();
}
// Helper: map judge string to numeric quality
private int JudgeQualityFromString(string judge)
{