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:
@@ -104,48 +104,6 @@ internal sealed class BattleSessionState
|
||||
RecordToken(isSelf == CardOwner.Self ? from : other, idx, cardId);
|
||||
}
|
||||
|
||||
/// <summary>Per-side idx->spellboost COUNT, accumulated from <c>orderList</c> <c>alter</c> ops via
|
||||
/// <see cref="RecordSpellboostFrom"/>. Separate from <see cref="IdxToCardId"/> because spellboost is a
|
||||
/// mutable counter, not an identity. Surfaced by <c>BuildPlayedCard</c> as the played card's
|
||||
/// <c>knownList.spellboost</c> so the opponent computes its discounted cost (see that method).</summary>
|
||||
public Dictionary<IBattleParticipant, Dictionary<int, int>> IdxToSpellboost { get; } = new();
|
||||
|
||||
private Dictionary<int, int> SpellboostMap(IBattleParticipant side)
|
||||
{
|
||||
if (!IdxToSpellboost.TryGetValue(side, out var map))
|
||||
IdxToSpellboost[side] = map = new Dictionary<int, int>();
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>The side's idx->spellboost map (empty if nothing recorded yet). Read by
|
||||
/// <c>PlayActionsHandler</c> to feed <c>BuildPlayedCard</c>.</summary>
|
||||
public IReadOnlyDictionary<int, int> GetSpellboostMap(IBattleParticipant side) => SpellboostMap(side);
|
||||
|
||||
/// <summary>Apply a frame's spellboost <c>alter</c> ops to the per-side maps. Routed by <c>isSelf</c>
|
||||
/// (the sender's perspective) exactly like <see cref="RecordTokensFrom"/>: <c>isSelf:1</c> → the
|
||||
/// sender's own hand (<paramref name="from"/>); <c>isSelf:0</c> → the opponent's hand
|
||||
/// (<paramref name="other"/>) for the rare cross-side spellboost. Ops: <c>'a'</c> add, <c>'s'</c> set,
|
||||
/// <c>'h'</c> half. Call this AFTER <c>BuildPlayedCard</c> for the same frame: a card's cost is fixed
|
||||
/// when it leaves hand, so the played card's emitted count must reflect state BEFORE this frame's
|
||||
/// grant (Fate's Hand plays, then spellboosts the rest of the hand). Recorded only from the
|
||||
/// authoritative PlayActions, never the Echo, to avoid double-counting the same alter.
|
||||
/// Known gap: a card bounced back to hand keeps its stale count (no reset on zone-exit) — not yet
|
||||
/// observed in capture, left for when a bounce desync actually shows up.</summary>
|
||||
public void RecordSpellboostFrom(IBattleParticipant from, IBattleParticipant other, object? orderList)
|
||||
{
|
||||
foreach (var (idx, isSelf, op, amount) in KnownListBuilder.MineAlterSpellboosts(orderList))
|
||||
{
|
||||
var map = SpellboostMap(isSelf == CardOwner.Self ? from : other);
|
||||
map.TryGetValue(idx, out var cur);
|
||||
map[idx] = op switch
|
||||
{
|
||||
's' => amount, // set
|
||||
'h' => cur / 2, // half
|
||||
_ => cur + amount, // 'a' add (the only form seen in capture)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mine + record copy/clone-token identities (<see cref="KnownListBuilder.MineCopyTokens"/>)
|
||||
/// into the correct side's map. A copy's source lives at <c>baseIdx</c> in the actor's own index
|
||||
/// space, so the resolution side == the record side, both selected by the same <c>isSelf</c> routing
|
||||
|
||||
Reference in New Issue
Block a user