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
+203 -15
View File
@@ -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)
+2
View File
@@ -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:
+38
View File
@@ -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
+47
View File
@@ -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()
+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)
{
+4 -2
View File
@@ -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.")]