notebook实装功能,galgame继续,一些bug修复
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
@@ -132,6 +132,10 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private HashSet<string> _activeAttackBelowSkills = new HashSet<string>();
|
||||
private Dictionary<string, AppliedEffect> _appliedAttackBelowEffects = new Dictionary<string, AppliedEffect>();
|
||||
|
||||
// Track skills that are currently active because Attack is above a threshold (OnAttackAboveValue)
|
||||
private HashSet<string> _activeAttackAboveSkills = new HashSet<string>();
|
||||
private Dictionary<string, AppliedEffect> _appliedAttackAboveEffects = new Dictionary<string, AppliedEffect>();
|
||||
|
||||
// Prevent re-entrant mana-trigger loops, e.g. OnManaGained -> (skill adds mana) -> OnManaGained -> ...
|
||||
private bool _isTriggeringManaGained = false;
|
||||
private bool _isTriggeringManaLost = false;
|
||||
@@ -179,6 +183,8 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (_appliedAttackZeroEffects == null) _appliedAttackZeroEffects = new Dictionary<string, AppliedEffect>();
|
||||
if (_activeAttackBelowSkills == null) _activeAttackBelowSkills = new HashSet<string>();
|
||||
if (_appliedAttackBelowEffects == null) _appliedAttackBelowEffects = new Dictionary<string, AppliedEffect>();
|
||||
if (_activeAttackAboveSkills == null) _activeAttackAboveSkills = new HashSet<string>();
|
||||
if (_appliedAttackAboveEffects == null) _appliedAttackAboveEffects = new Dictionary<string, AppliedEffect>();
|
||||
}
|
||||
|
||||
private void LogVerbose(string message)
|
||||
@@ -304,6 +310,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
EvaluateHPPercentageTriggers(currentHP, true);
|
||||
EvaluateAttackZeroTriggers(true);
|
||||
EvaluateAttackBelowTriggers(true);
|
||||
EvaluateAttackAboveTriggers(true);
|
||||
}
|
||||
|
||||
// Allow editing in inspector to immediately reflect on UI (editor only)
|
||||
@@ -326,6 +333,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
EvaluateHPPercentageTriggers(currentHP);
|
||||
EvaluateAttackZeroTriggers();
|
||||
EvaluateAttackBelowTriggers();
|
||||
EvaluateAttackAboveTriggers();
|
||||
TryCastOnFullMana();
|
||||
}
|
||||
}
|
||||
@@ -1263,10 +1271,14 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
if (isDead) return; // dead characters cannot change mana
|
||||
int old = currentMana;
|
||||
int requestedMana = mana;
|
||||
bool incomingOverflowGainAtFull = mana > maxMana && old >= maxMana;
|
||||
currentMana = Mathf.Clamp(mana, 0, maxMana);
|
||||
UpdateManaVisuals(old, animateFade, flashVisuals);
|
||||
// Only auto-cast when we actually reach full mana from below.
|
||||
if (old < maxMana && currentMana >= maxMana)
|
||||
// Also handle positive mana input while already full (e.g. maxMana=1 and receiving mana from adjacent skills):
|
||||
// without this, mana can stay at 100% with no further "reach full from below" transition.
|
||||
if ((old < maxMana && currentMana >= maxMana) || incomingOverflowGainAtFull)
|
||||
{
|
||||
TryCastOnFullMana();
|
||||
}
|
||||
@@ -1308,6 +1320,44 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
_isTriggeringManaLost = false;
|
||||
}
|
||||
}
|
||||
|
||||
int finalMana = currentMana;
|
||||
if (requestedMana != old || incomingOverflowGainAtFull)
|
||||
{
|
||||
string manaEventType;
|
||||
if (incomingOverflowGainAtFull && finalMana == old && old >= maxMana)
|
||||
{
|
||||
manaEventType = "ManaOverflowAtFull";
|
||||
GameplaySkillLogger.RecordConflictHint(
|
||||
"AllyMana",
|
||||
"OverflowGainAtFull",
|
||||
"ally=" + allyName + " | slot=" + slotIndex + " | requested=" + requestedMana + " | current=" + finalMana + " | max=" + maxMana);
|
||||
}
|
||||
else if (finalMana > old)
|
||||
{
|
||||
manaEventType = "ManaGain";
|
||||
}
|
||||
else if (finalMana < old)
|
||||
{
|
||||
manaEventType = "ManaSpend";
|
||||
}
|
||||
else
|
||||
{
|
||||
manaEventType = "ManaClampNoDelta";
|
||||
}
|
||||
|
||||
GameplaySkillLogger.RecordAllyResourceEvent(
|
||||
allyName,
|
||||
slotIndex,
|
||||
"Mana",
|
||||
manaEventType,
|
||||
requestedMana - old,
|
||||
finalMana - old,
|
||||
old,
|
||||
finalMana,
|
||||
maxMana,
|
||||
"SetCurrentMana");
|
||||
}
|
||||
}
|
||||
|
||||
public void ModifyMana(int delta, bool animateFade = true, bool flashVisuals = true)
|
||||
@@ -1398,6 +1448,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
EvaluateAttackZeroTriggers();
|
||||
EvaluateAttackBelowTriggers();
|
||||
EvaluateAttackAboveTriggers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1597,6 +1648,18 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
ModifyHP(-delta, true, true);
|
||||
|
||||
int actual = oldHP - currentHP;
|
||||
GameplaySkillLogger.RecordAllyResourceEvent(
|
||||
allyName,
|
||||
slotIndex,
|
||||
"HP",
|
||||
"DamageTaken",
|
||||
amount,
|
||||
effective,
|
||||
oldHP,
|
||||
currentHP,
|
||||
maxHP,
|
||||
source != null ? source.name : "None");
|
||||
|
||||
if (actual != 0)
|
||||
{
|
||||
if (deferPopup)
|
||||
@@ -1618,6 +1681,18 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
int delta = Mathf.CeilToInt(amount * mult);
|
||||
ModifyHP(delta, true, true);
|
||||
int actual = currentHP - oldHP;
|
||||
GameplaySkillLogger.RecordAllyResourceEvent(
|
||||
allyName,
|
||||
slotIndex,
|
||||
"HP",
|
||||
"HealTaken",
|
||||
amount,
|
||||
amount * mult,
|
||||
oldHP,
|
||||
currentHP,
|
||||
maxHP,
|
||||
source != null ? source.name : "None");
|
||||
|
||||
if (actual != 0)
|
||||
{
|
||||
if (deferPopup)
|
||||
@@ -1936,6 +2011,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
vars["currentScore"] = currentScore;
|
||||
vars["idolScore"] = currentScore;
|
||||
vars["currentHP"] = currentHP;
|
||||
vars["noteBaseScore"] = bmm != null ? Mathf.Max(0, bmm.perNoteScore) : Mathf.Max(0, baseTrackScore);
|
||||
return vars;
|
||||
}
|
||||
// New: Evaluate HP-percent triggers and handle enter/leave transitions
|
||||
@@ -2336,6 +2412,131 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateAttackAboveTriggers(bool skipVFX = false)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (SkillBuilder.Instance == null) return;
|
||||
|
||||
var so = SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex);
|
||||
if (so == null) return;
|
||||
|
||||
int currAttack = attack;
|
||||
|
||||
var defs = _tmpSkillDefs;
|
||||
defs.Clear();
|
||||
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 || group.skills == null) continue;
|
||||
for (int i = 0; i < group.skills.Length; i++)
|
||||
{
|
||||
var def = group.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fallbackGroup = so.GetPrimarySkillGroup();
|
||||
if (fallbackGroup != null && fallbackGroup.skills != null)
|
||||
{
|
||||
for (int i = 0; i < fallbackGroup.skills.Length; i++)
|
||||
{
|
||||
var def = fallbackGroup.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
else if (so.availableSkills != null)
|
||||
{
|
||||
for (int i = 0; i < so.availableSkills.Length; i++)
|
||||
{
|
||||
var def = so.availableSkills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the closest satisfied threshold from below (largest attackTriggerValue such that attack > value).
|
||||
string selected = null;
|
||||
int selectedThreshold = int.MinValue;
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
var def = defs[i];
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnAttackAboveValue) continue;
|
||||
|
||||
int thr = def.attackTriggerValue;
|
||||
if (thr < 0) thr = 0;
|
||||
|
||||
if (currAttack > thr && thr >= selectedThreshold)
|
||||
{
|
||||
selectedThreshold = thr;
|
||||
selected = def.skillId;
|
||||
}
|
||||
}
|
||||
|
||||
var vars = BuildFormulaVars(so);
|
||||
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
var def = defs[i];
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnAttackAboveValue) continue;
|
||||
|
||||
int thr = def.attackTriggerValue;
|
||||
if (thr < 0) thr = 0;
|
||||
|
||||
bool wasActive = _activeAttackAboveSkills.Contains(def.skillId);
|
||||
bool nowActive = (selected != null && def.skillId == selected) && (currAttack > thr);
|
||||
|
||||
float amount = 0f;
|
||||
if (!string.IsNullOrWhiteSpace(def.formula))
|
||||
{
|
||||
if (SkillDefinition.TryEvaluateFormula(def.formula, vars, out float fresult))
|
||||
amount = fresult;
|
||||
}
|
||||
|
||||
bool isReversible = IsEffectReversible(def.effectType);
|
||||
if (!isReversible)
|
||||
{
|
||||
if (!wasActive && nowActive)
|
||||
{
|
||||
_activeAttackAboveSkills.Add(def.skillId);
|
||||
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null, 1f, skipVFX);
|
||||
}
|
||||
else if (wasActive && !nowActive)
|
||||
{
|
||||
_activeAttackAboveSkills.Remove(def.skillId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!wasActive && nowActive)
|
||||
{
|
||||
_activeAttackAboveSkills.Add(def.skillId);
|
||||
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null, 1f, skipVFX);
|
||||
_appliedAttackAboveEffects[def.skillId] = new AppliedEffect(def.effectType, amount);
|
||||
}
|
||||
else if (wasActive && !nowActive)
|
||||
{
|
||||
_activeAttackAboveSkills.Remove(def.skillId);
|
||||
if (_appliedAttackAboveEffects.TryGetValue(def.skillId, out var applied))
|
||||
{
|
||||
ApplyInverseEffect(def.defaultSelector, applied.effectType, applied.amount);
|
||||
_appliedAttackAboveEffects.Remove(def.skillId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ActivateNextDamageRedirect(AllyCombatant redirector, float duration = 0f)
|
||||
{
|
||||
if (redirector == null) return;
|
||||
@@ -2370,12 +2571,6 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
state.expireTime = expireTime;
|
||||
state.activationOrder = ++s_nextDamageRedirectOrderSeed;
|
||||
state.iconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(redirector, PlayerBudeffIconType.dmg_redirect_toSelf, 0f, duration);
|
||||
|
||||
// If adjacent allies already have buffs, transfer one now and consume this one-shot redirect.
|
||||
if (redirector.TryTransferExistingBuffFromAdjacentToSelf())
|
||||
{
|
||||
ConsumeGlobalRedirect(redirector);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanupGlobalRedirects()
|
||||
@@ -3040,13 +3235,6 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
_selfDamageRedirectToAdjacentIconId = null;
|
||||
}
|
||||
_selfDamageRedirectToAdjacentIconId = iBudeffPrefabController.Instance?.RegisterTimedEffect(this, PlayerBudeffIconType.dmg_redirect_toAdjacent, 0f, duration);
|
||||
|
||||
// "都给你!" rule: transfer to adjacent ally with the MOST buffs.
|
||||
var adj = FindAdjacentAllyByBuffCount(pickMost: true, requireAtLeastOneBuff: false);
|
||||
if (adj != null && TransferOneExistingBuffTo(adj))
|
||||
{
|
||||
ConsumeSelfDamageRedirectToAdjacent();
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateNonMissToPerfectRewrite(float duration)
|
||||
|
||||
@@ -1590,6 +1590,8 @@ public class EffectSystem : MonoBehaviour
|
||||
case EffectType.DecreaseDamageResistance:
|
||||
case EffectType.IncreaseAttack:
|
||||
case EffectType.DecreaseAttack:
|
||||
case EffectType.RedirectNextDamageToSelf:
|
||||
case EffectType.RedirectSelfDamageToAdjacent:
|
||||
case EffectType.RewriteNonMissToPerfect:
|
||||
return true;
|
||||
default:
|
||||
|
||||
@@ -70,6 +70,15 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
currentMana = 0;
|
||||
isDead = false;
|
||||
|
||||
GameplaySkillLogger.RecordEnemyLifeState(
|
||||
gameObject.name,
|
||||
"Spawned",
|
||||
currentHP,
|
||||
maxHP,
|
||||
currentMana,
|
||||
maxMana,
|
||||
sourceData != null ? sourceData.name : "None");
|
||||
|
||||
iBudeffPrefabController.Instance?.RegisterEnemyBaseline(this);
|
||||
|
||||
// notify listeners (UI/manager) that enemy revived
|
||||
@@ -150,6 +159,16 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
ModifyHP(-delta, true);
|
||||
|
||||
int actual = before - currentHP;
|
||||
GameplaySkillLogger.RecordEnemyHpEvent(
|
||||
gameObject.name,
|
||||
"Damage",
|
||||
amount,
|
||||
effective,
|
||||
before,
|
||||
currentHP,
|
||||
damageResistance,
|
||||
source != null ? source.name : "None");
|
||||
|
||||
if (actual != 0)
|
||||
{
|
||||
if (deferPopup)
|
||||
@@ -184,6 +203,16 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
int delta = Mathf.CeilToInt(amount * mult);
|
||||
ModifyHP(delta, true);
|
||||
int actual = currentHP - before;
|
||||
GameplaySkillLogger.RecordEnemyHpEvent(
|
||||
gameObject.name,
|
||||
"Heal",
|
||||
amount,
|
||||
amount * mult,
|
||||
before,
|
||||
currentHP,
|
||||
damageResistance,
|
||||
source != null ? source.name : "None");
|
||||
|
||||
if (actual != 0)
|
||||
{
|
||||
if (deferPopup)
|
||||
@@ -356,6 +385,15 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
if (isDead) return;
|
||||
|
||||
GameplaySkillLogger.RecordEnemyLifeState(
|
||||
gameObject.name,
|
||||
"Dead",
|
||||
currentHP,
|
||||
maxHP,
|
||||
currentMana,
|
||||
maxMana,
|
||||
_lastAttacker != null ? _lastAttacker.name : "None");
|
||||
|
||||
// award score to last attacker
|
||||
if (sourceData != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
public static class GameplaySkillLogger
|
||||
{
|
||||
private static readonly object s_fileLock = new object();
|
||||
private static readonly UTF8Encoding s_utf8NoBom = new UTF8Encoding(false);
|
||||
private static readonly Dictionary<string, float> s_lastSkillReleaseByKey = new Dictionary<string, float>(256);
|
||||
|
||||
private static string s_logFilePath;
|
||||
private static bool s_sessionActive;
|
||||
private static float s_sessionStartRealtime;
|
||||
private static int s_lastSecondBucket = -1;
|
||||
|
||||
private const float RapidDuplicateSkillThresholdSeconds = 0.05f;
|
||||
|
||||
public static string LogFilePath
|
||||
{
|
||||
get { return EnsureLogFilePath(); }
|
||||
}
|
||||
|
||||
public static void BeginSession(string sessionLabel)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
BeginSessionInternal(sessionLabel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordSkillRelease(
|
||||
string roleName,
|
||||
string skillName,
|
||||
string effectName,
|
||||
string attributeSummary,
|
||||
string targetSummary)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string role = Sanitize(roleName);
|
||||
string skill = Sanitize(skillName);
|
||||
string effect = Sanitize(effectName);
|
||||
string attr = Sanitize(attributeSummary);
|
||||
string target = Sanitize(targetSummary);
|
||||
|
||||
string payload = "role=" + role
|
||||
+ " | skill=" + skill
|
||||
+ " | effect=" + effect
|
||||
+ " | attr=" + attr
|
||||
+ " | target=" + target;
|
||||
float elapsed = AppendEventLineLocked("SKILL", payload);
|
||||
|
||||
string dedupeKey = role + "|" + skill + "|" + effect + "|" + target;
|
||||
if (s_lastSkillReleaseByKey.TryGetValue(dedupeKey, out float lastElapsed))
|
||||
{
|
||||
float interval = elapsed - lastElapsed;
|
||||
if (interval >= 0f && interval <= RapidDuplicateSkillThresholdSeconds)
|
||||
{
|
||||
AppendEventLineLocked(
|
||||
"WARN",
|
||||
"potential_duplicate_skill_trigger"
|
||||
+ " | role=" + role
|
||||
+ " | skill=" + skill
|
||||
+ " | intervalSec=" + FormatFloat(interval));
|
||||
}
|
||||
}
|
||||
s_lastSkillReleaseByKey[dedupeKey] = elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordJudgeResult(
|
||||
string noteType,
|
||||
string noteSegment,
|
||||
int trackIndex,
|
||||
string noteId,
|
||||
string judgeResult,
|
||||
float rawOffsetMs,
|
||||
bool rewrittenToPerfect,
|
||||
bool autoplay,
|
||||
float actionTime,
|
||||
float hitTime,
|
||||
float scheduledEndTime)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "noteType=" + Sanitize(noteType)
|
||||
+ " | segment=" + Sanitize(noteSegment)
|
||||
+ " | track=" + trackIndex
|
||||
+ " | noteId=" + Sanitize(noteId)
|
||||
+ " | judge=" + Sanitize(judgeResult)
|
||||
+ " | rawOffsetMs=" + FormatFloat(rawOffsetMs)
|
||||
+ " | rewrittenToPerfect=" + (rewrittenToPerfect ? "1" : "0")
|
||||
+ " | autoplay=" + (autoplay ? "1" : "0")
|
||||
+ " | actionTime=" + FormatFloat(actionTime)
|
||||
+ " | hitTime=" + FormatFloat(hitTime)
|
||||
+ " | scheduledEndTime=" + FormatFloat(scheduledEndTime);
|
||||
|
||||
AppendEventLineLocked("JUDGE", payload);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordScoreDelta(
|
||||
string source,
|
||||
int trackIndex,
|
||||
int pmRequestedDelta,
|
||||
int pmActualDelta,
|
||||
int idolRequestedDelta,
|
||||
int idolActualDelta,
|
||||
float scoreEfficiency,
|
||||
bool isSkillRelated,
|
||||
int trackPmSum,
|
||||
int trackIdolSum,
|
||||
int allPm,
|
||||
int allIdol,
|
||||
int totalScore)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "source=" + Sanitize(source)
|
||||
+ " | track=" + trackIndex
|
||||
+ " | pmReq=" + pmRequestedDelta
|
||||
+ " | pmAct=" + pmActualDelta
|
||||
+ " | idolReq=" + idolRequestedDelta
|
||||
+ " | idolAct=" + idolActualDelta
|
||||
+ " | efficiency=" + FormatFloat(scoreEfficiency)
|
||||
+ " | skillRelated=" + (isSkillRelated ? "1" : "0")
|
||||
+ " | trackPm=" + trackPmSum
|
||||
+ " | trackIdol=" + trackIdolSum
|
||||
+ " | allPm=" + allPm
|
||||
+ " | allIdol=" + allIdol
|
||||
+ " | total=" + totalScore;
|
||||
|
||||
AppendEventLineLocked("SCORE", payload);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordEnemyHpEvent(
|
||||
string enemyName,
|
||||
string eventType,
|
||||
float rawAmount,
|
||||
float effectiveAmount,
|
||||
int hpBefore,
|
||||
int hpAfter,
|
||||
float damageResistance,
|
||||
string sourceName)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "enemy=" + Sanitize(enemyName)
|
||||
+ " | event=" + Sanitize(eventType)
|
||||
+ " | raw=" + FormatFloat(rawAmount)
|
||||
+ " | effective=" + FormatFloat(effectiveAmount)
|
||||
+ " | hpBefore=" + hpBefore
|
||||
+ " | hpAfter=" + hpAfter
|
||||
+ " | hpDelta=" + (hpAfter - hpBefore)
|
||||
+ " | resist=" + FormatFloat(damageResistance)
|
||||
+ " | source=" + Sanitize(sourceName);
|
||||
|
||||
AppendEventLineLocked("ENEMY", payload);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordEnemyLifeState(
|
||||
string enemyName,
|
||||
string state,
|
||||
int hp,
|
||||
int maxHp,
|
||||
int mana,
|
||||
int maxMana,
|
||||
string source)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "enemy=" + Sanitize(enemyName)
|
||||
+ " | state=" + Sanitize(state)
|
||||
+ " | hp=" + hp + "/" + maxHp
|
||||
+ " | mana=" + mana + "/" + maxMana
|
||||
+ " | source=" + Sanitize(source);
|
||||
|
||||
AppendEventLineLocked("ENEMY", payload);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordAllyResourceEvent(
|
||||
string allyName,
|
||||
int slotIndex,
|
||||
string resource,
|
||||
string eventType,
|
||||
float requestedAmount,
|
||||
float effectiveAmount,
|
||||
int before,
|
||||
int after,
|
||||
int maxValue,
|
||||
string sourceName)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "ally=" + Sanitize(allyName)
|
||||
+ " | slot=" + slotIndex
|
||||
+ " | resource=" + Sanitize(resource)
|
||||
+ " | event=" + Sanitize(eventType)
|
||||
+ " | requested=" + FormatFloat(requestedAmount)
|
||||
+ " | effective=" + FormatFloat(effectiveAmount)
|
||||
+ " | before=" + before
|
||||
+ " | after=" + after
|
||||
+ " | delta=" + (after - before)
|
||||
+ " | max=" + maxValue
|
||||
+ " | source=" + Sanitize(sourceName);
|
||||
|
||||
AppendEventLineLocked("ALLY", payload);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordConflictHint(string subsystem, string issueType, string details)
|
||||
{
|
||||
lock (s_fileLock)
|
||||
{
|
||||
EnsureSessionLocked();
|
||||
|
||||
string payload = "subsystem=" + Sanitize(subsystem)
|
||||
+ " | issue=" + Sanitize(issueType)
|
||||
+ " | details=" + Sanitize(details);
|
||||
|
||||
AppendEventLineLocked("WARN", payload);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BeginSessionInternal(string sessionLabel)
|
||||
{
|
||||
s_sessionStartRealtime = Time.realtimeSinceStartup;
|
||||
s_lastSecondBucket = -1;
|
||||
s_sessionActive = true;
|
||||
s_lastSkillReleaseByKey.Clear();
|
||||
|
||||
string normalizedLabel = string.IsNullOrWhiteSpace(sessionLabel) ? "UnknownSession" : sessionLabel.Trim();
|
||||
string path = EnsureLogFilePath();
|
||||
string header =
|
||||
"# Gameplay Skill Timeline Log" + Environment.NewLine +
|
||||
"# Session: " + normalizedLabel + Environment.NewLine +
|
||||
"# StartedAt: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + Environment.NewLine +
|
||||
"# Format: [elapsedSec] [CATEGORY] key=value | key=value" + Environment.NewLine +
|
||||
Environment.NewLine;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, header, s_utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GameplaySkillLogger] Failed to create session log: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static float AppendEventLineLocked(string category, string payload)
|
||||
{
|
||||
float elapsed = Mathf.Max(0f, Time.realtimeSinceStartup - s_sessionStartRealtime);
|
||||
int secondBucket = Mathf.FloorToInt(elapsed);
|
||||
var builder = new StringBuilder(320);
|
||||
|
||||
if (secondBucket != s_lastSecondBucket)
|
||||
{
|
||||
if (s_lastSecondBucket >= 0) builder.AppendLine();
|
||||
builder.AppendLine("=== T+" + secondBucket.ToString("D4", CultureInfo.InvariantCulture) + "s ===");
|
||||
s_lastSecondBucket = secondBucket;
|
||||
}
|
||||
|
||||
builder.Append("[")
|
||||
.Append(elapsed.ToString("F3", CultureInfo.InvariantCulture))
|
||||
.Append("s] [")
|
||||
.Append(Sanitize(category))
|
||||
.Append("] ")
|
||||
.AppendLine(payload);
|
||||
|
||||
try
|
||||
{
|
||||
File.AppendAllText(EnsureLogFilePath(), builder.ToString(), s_utf8NoBom);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[GameplaySkillLogger] Failed to append log: " + ex.Message);
|
||||
}
|
||||
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
private static void EnsureSessionLocked()
|
||||
{
|
||||
if (!s_sessionActive)
|
||||
{
|
||||
BeginSessionInternal("AutoSession");
|
||||
}
|
||||
}
|
||||
|
||||
private static string EnsureLogFilePath()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(s_logFilePath))
|
||||
{
|
||||
return s_logFilePath;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
string logsDir = Path.Combine(projectRoot, "Logs");
|
||||
Directory.CreateDirectory(logsDir);
|
||||
s_logFilePath = Path.Combine(logsDir, "GameplaySkillTimeline.log");
|
||||
}
|
||||
catch
|
||||
{
|
||||
s_logFilePath = Path.Combine(Application.persistentDataPath, "GameplaySkillTimeline.log");
|
||||
}
|
||||
|
||||
return s_logFilePath;
|
||||
}
|
||||
|
||||
private static string FormatFloat(float value)
|
||||
{
|
||||
if (float.IsNaN(value)) return "NaN";
|
||||
if (float.IsPositiveInfinity(value)) return "Infinity";
|
||||
if (float.IsNegativeInfinity(value)) return "-Infinity";
|
||||
return value.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string Sanitize(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return "-";
|
||||
return value.Replace("\r", " ").Replace("\n", " ").Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52e3eb7e0c41c7141b410f678c7ec701
|
||||
@@ -269,6 +269,21 @@ public class ScoreManager : MonoBehaviour
|
||||
}
|
||||
|
||||
RecalculateTotal();
|
||||
|
||||
GameplaySkillLogger.RecordScoreDelta(
|
||||
"PerfectClearBonus",
|
||||
-1,
|
||||
perfectClearBonusPm,
|
||||
perfectClearBonusPm,
|
||||
0,
|
||||
0,
|
||||
float.NaN,
|
||||
false,
|
||||
-1,
|
||||
-1,
|
||||
allSum_pmScore,
|
||||
allSum_idolScore,
|
||||
totalScore);
|
||||
}
|
||||
|
||||
private void EnsureIdolscoreKeys()
|
||||
@@ -368,10 +383,12 @@ public class ScoreManager : MonoBehaviour
|
||||
TryHookPerfectBonusEvent();
|
||||
if (trackIndex < 0 || trackIndex >= pmScoreSums.Length) return;
|
||||
if (pmDelta == 0) return;
|
||||
int pmBefore = pmScoreSums[trackIndex];
|
||||
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;
|
||||
int pmActualDelta = pmScoreSums[trackIndex] - pmBefore;
|
||||
|
||||
int idolBefore = idolScoreSums[trackIndex];
|
||||
int idolDelta = Mathf.FloorToInt(pmDelta * scoreEfficiency);
|
||||
@@ -446,6 +463,21 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
// Update per-track progress keys (bgSPRITE/idolscoreKey_*).
|
||||
UpdateIdolscoreKeyScales();
|
||||
|
||||
GameplaySkillLogger.RecordScoreDelta(
|
||||
"AddPmScoreForTrack",
|
||||
trackIndex,
|
||||
pmDelta,
|
||||
pmActualDelta,
|
||||
idolDelta,
|
||||
idolActualDelta,
|
||||
scoreEfficiency,
|
||||
isSkillRelated,
|
||||
pmScoreSums[trackIndex],
|
||||
idolScoreSums[trackIndex],
|
||||
allSum_pmScore,
|
||||
allSum_idolScore,
|
||||
totalScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -504,6 +536,21 @@ public class ScoreManager : MonoBehaviour
|
||||
}
|
||||
|
||||
RecalculateTotal();
|
||||
|
||||
GameplaySkillLogger.RecordScoreDelta(
|
||||
"AddIdolScoreForTrack",
|
||||
trackIndex,
|
||||
0,
|
||||
0,
|
||||
idolDelta,
|
||||
actual,
|
||||
float.NaN,
|
||||
true,
|
||||
pmScoreSums[trackIndex],
|
||||
idolScoreSums[trackIndex],
|
||||
allSum_pmScore,
|
||||
allSum_idolScore,
|
||||
totalScore);
|
||||
}
|
||||
|
||||
public void RecalculateTotal()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,9 @@ public class SkillDefinition : ScriptableObject
|
||||
// New: triggers once when this ally is defeated (HP reaches 0 from above).
|
||||
OnSelfDefeated,
|
||||
// New: triggers on this ally when an adjacent ally releases a skill (casts on mana full).
|
||||
OnAdjacentAllySkillCast
|
||||
OnAdjacentAllySkillCast,
|
||||
// New: attack threshold trigger (attack > attackTriggerValue)
|
||||
OnAttackAboveValue
|
||||
}
|
||||
|
||||
[Header("Inspector")]
|
||||
@@ -98,7 +100,7 @@ public class SkillDefinition : ScriptableObject
|
||||
[Tooltip("Optional: for HP percent triggers, evaluate this formula (0..1) to get the threshold instead of hpTriggerPercent. Useful for per-level thresholds like 90/80/75/70.")]
|
||||
public string hpTriggerPercentFormula = "";
|
||||
|
||||
[Tooltip("For OnAttackBelowValue: triggers when attack < attackTriggerValue.")]
|
||||
[Tooltip("For OnAttackBelowValue/OnAttackAboveValue: threshold for attack comparison.")]
|
||||
public int attackTriggerValue = 0;
|
||||
|
||||
[Tooltip("Optional: if > 0 and repeatValue != 0, when the skill triggers again within this window, the computed amount is overridden by repeatValue.")]
|
||||
|
||||
Reference in New Issue
Block a user