refactor(battlenode): retire spellboost bookkeeping, engine owns cost+spellboost (M-HC-3)
The headless engine accumulates spell-charge for real on the receive path (each spell play runs the played card's own AddSpellChargeCount) and resolves the discounted cost by construction, so the wire-derived spellboost-count bookkeeping is redundant. Engine-source the knownList spellboost COUNT too (prod-faithful) via a new SessionBattleEngine.PlayedCardSpellboost, using the same persist-post-play zone search as PlayedCardCost (SpellChargeCount survives PlayCard; only ctor/ReturnCard zero it). - Delete IdxToSpellboost/SpellboostMap/GetSpellboostMap/RecordSpellboostFrom (BattleSessionState) and MineAlterSpellboosts (KnownListBuilder); token/choice/ copy identity maps are untouched. - BuildPlayedCard takes an engine-sourced spellboost int (drops spellboostMap). - Seed BattleLogManager fusion lists headless (the per-frame filter cleanup NREs on null EnemyFusionCard when a fanfare card registers a CalledCreateFilter) so real spell-charge grantor plays resolve. - Add committed real-charge regression tests (no SeedHandCardSpellboostCost seam): one grantor play accumulates +1 on the reducer -> cost 5->4, count 1, persisting post-play; handler emits cost 4 + spellboost 1 engine-sourced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ using UIWidget = engine::UIWidget;
|
||||
using UISprite = engine::UISprite;
|
||||
using NullDetailPanelControl = engine::NullDetailPanelControl;
|
||||
using DetailPanelControl = engine::DetailPanelControl;
|
||||
using BattleLogManager = engine::Wizard.Battle.UI.BattleLogManager;
|
||||
|
||||
namespace SVSim.BattleNode.Sessions.Engine;
|
||||
|
||||
@@ -94,6 +95,7 @@ internal sealed class SessionBattleEngine
|
||||
InitLeaderLife(mgr); // a 0-life leader reads as game-over and silently blocks plays
|
||||
InitCardTemplates(mgr); // play/draw resolution touches the (no-op) card view layer
|
||||
InitHeadlessViews(mgr); // turn/play cycle dereferences UI-container + emotion refs
|
||||
SeedBattleLogManager(); // per-frame filter cleanup reads BattleLogManager fusion lists
|
||||
InstallHeadlessNetworkAgent(); // turn-flow resolve reads ToolboxGame.RealTimeNetworkAgent
|
||||
|
||||
// Per-session leader class: chara_id == class_id for 1..8 in the all-8-class ClassCharacterList,
|
||||
@@ -162,6 +164,11 @@ internal sealed class SessionBattleEngine
|
||||
/// a card dealt from the seeded deck.</summary>
|
||||
public int HandCardIndex(bool playerSeat, int handPos) => Seat(playerSeat).HandCardList[handPos].Index;
|
||||
|
||||
/// <summary>The real <c>CardId</c> (wire identity) of the hand card at <paramref name="handPos"/>. Lets a
|
||||
/// test locate a specific card in a SHUFFLED opening hand by identity (then read its <see cref="HandCardIndex"/>
|
||||
/// to drive a play), without depending on which shuffled position the card landed at.</summary>
|
||||
public int HandCardId(bool playerSeat, int handPos) => Seat(playerSeat).HandCardList[handPos].CardId;
|
||||
|
||||
/// <summary>The real <c>CardId</c> (wire identity) of the in-play follower at <paramref name="boardPos"/>
|
||||
/// (0-based, skipping the leader/Class card at ClassAndInPlayCardList[0] — same convention as
|
||||
/// <see cref="BoardCount"/>). Used to assert an opponent reveal seated the substituted card with its
|
||||
@@ -196,6 +203,28 @@ internal sealed class SessionBattleEngine
|
||||
return card.PlayedCost >= 0 ? card.PlayedCost : card.Cost;
|
||||
}
|
||||
|
||||
/// <summary>The engine-RESOLVED spellboost (spell-charge) COUNT of the card whose engine <c>Index</c> ==
|
||||
/// <paramref name="idx"/> on <paramref name="playerSeat"/> (M-HC-3b). The engine accumulates this count
|
||||
/// for real on the receive path (each spell play that targets the card runs the card's own
|
||||
/// <c>Skill_spell_charge.AddSpellChargeCount</c>), so this is the same authoritative count prod sends —
|
||||
/// emitted on the opponent-facing knownList so the wire stays prod-faithful now that the wire-derived
|
||||
/// spellboost bookkeeping is retired (cost itself is engine-sourced via <see cref="PlayedCardCost"/>).
|
||||
/// <para>READ-MOMENT (persist-post-play): <see cref="BattleCardBase.SpellChargeCount"/> is set to 0 only
|
||||
/// in the ctor (re-init, BattleCardBase.cs:2042) and in <c>ReturnCard</c> (bounce-to-hand,
|
||||
/// BattleCardBase.cs:2681); <see cref="BattleCardBase.PlayCard"/> never touches it. So the count PERSISTS
|
||||
/// on the played card object after it leaves the hand (follower in-play, spell in cemetery) — the same
|
||||
/// persist-after-play property <see cref="BattleCardBase.PlayedCost"/> has. We therefore use the SAME
|
||||
/// post-resolution zone search (<see cref="FindByIndex"/>: in-play → cemetery → hand) and read
|
||||
/// <c>SpellChargeCount</c> directly — no separate receive-capture is needed.</para>
|
||||
/// <para>Degrades to <paramref name="fallback"/> when the engine is not set up or the idx resolves to no
|
||||
/// card — so a non-engine session never crashes and a vanilla play emits 0 via the caller's fallback.</para></summary>
|
||||
public int PlayedCardSpellboost(bool playerSeat, int idx, int fallback = 0)
|
||||
{
|
||||
if (_mgr is null) return fallback;
|
||||
var card = FindByIndex(Seat(playerSeat), idx);
|
||||
return card?.SpellChargeCount ?? fallback;
|
||||
}
|
||||
|
||||
// Locate the card with the given engine Index across the seat's post-resolution zones. Order matters
|
||||
// only for disambiguation; Index is unique per card so the first hit is the card. In-play (followers)
|
||||
// and cemetery (spells) are where a just-resolved play lands; hand is the pre-resolution fallback.
|
||||
@@ -399,6 +428,19 @@ internal sealed class SessionBattleEngine
|
||||
return card;
|
||||
}
|
||||
|
||||
// The per-frame skill-filter cleanup (BattleManagerBase.RemoveUnUseCalledFilterDictionary, run on
|
||||
// EVERY receive) reads BattleLogManager.GetInstance().EnemyFusionCard.Contains(...) when a card with a
|
||||
// registered CalledCreateFilter is alive — e.g. a follower with a when_play spell_charge/fanfare skill
|
||||
// (BattleManagerBase.cs:155). The shim BattleLogManager singleton leaves PlayerFusionCard/EnemyFusionCard
|
||||
// null (no UI ran SetUp), so that .Contains NREs. Seed both to empty lists — a pure no-op view-state
|
||||
// seed (the fusion log is cosmetic; nothing headless adds to it). Process-global like the other seeds.
|
||||
private static void SeedBattleLogManager()
|
||||
{
|
||||
var log = BattleLogManager.GetInstance();
|
||||
log.PlayerFusionCard ??= new List<BattleCardBase>();
|
||||
log.EnemyFusionCard ??= new List<BattleCardBase>();
|
||||
}
|
||||
|
||||
// The turn-flow + emit bookkeeping reads the global ToolboxGame.RealTimeNetworkAgent (e.g.
|
||||
// RealTimeNetworkAgent.GetIsFirstPlayer/GetTurnState, which delegate to GameMgr's
|
||||
// NetworkUserInfoData.TurnState; AddActionSequence touches _gungnir). Headless there is no socket
|
||||
|
||||
Reference in New Issue
Block a user