feat(battlenode): emit engine-resolved cost on every knownList entry (M-HC-3)
The opponent-facing PlayActions knownList now carries the engine-RESOLVED play-time cost (KnownCardEntry.cost), sourced from the headless shadow engine's PlayedCost on the just-resolved card. This closes the spellboost cost-desync BY CONSTRUCTION: the engine already knows the true discounted cost (spellboost + board modifiers folded in), so no bookkeeping is needed. - DTO: add non-nullable cost to KnownCardEntry (prod emits cost 45/45). - SessionBattleEngine.PlayedCardCost(seat, idx, fallback): finds the resolved card by engine Index across in-play/cemetery/hand zones and returns PlayedCost (captured by PlayCard at resolution == discounted Cost), degrading to fallback when the engine is not owned/ready. - PlayActionsHandler sources the played card's cost from ctx.Engine (ShadowIngest already resolved the play before the handler runs). Spellboost-map plumbing stays for now; Task 6 (M-HC-3b) retires it. - Validation: engine-read test (charge-seeded reducer 101314020: base 5, cost 5/1/0 at charge 0/4/5) + handler-emit test asserting knownList[0].cost == 1 (discounted, not base 5) with non-vacuity. Board-dependent (when_evolve_other) case deferred to M-HC-4 (evolve not yet headless); cost is read off the resolved engine so board modifiers are captured by construction once their ops resolve. - Harness: promote alt vanilla follower id (101211120) to AltVanillaFollowerId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,21 @@ internal sealed class PlayActionsHandler : IFrameHandler
|
||||
ctx.State.RecordCopyTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
|
||||
var deckMap = ctx.State.GetOrSeedDeckMap(ctx.From);
|
||||
// Spellboost count rides the played card's knownList (prod-faithful; the client reads it into the
|
||||
// card's cost model). Read the CURRENT map (state before this frame's grant) for the emit, then
|
||||
// fold THIS frame's alter ops in afterwards — a card's cost is fixed as it leaves hand, and a play
|
||||
// that grants spellboost (e.g. Fate's Hand) targets the REST of the hand, not the card just played.
|
||||
var played = KnownListBuilder.BuildPlayedCard(deckMap, playIdx, orderList, ctx.State.GetSpellboostMap(ctx.From));
|
||||
// The ENGINE-RESOLVED play-time cost (M-HC-3a). The conductor's ShadowIngest already ran
|
||||
// engine.Receive for THIS frame before this handler runs, so the engine has resolved the play and
|
||||
// PlayedCardCost reads the discounted cost it actually charged (spellboost + board modifiers folded
|
||||
// in BY CONSTRUCTION — no bookkeeping). Sender's seat == ctx.A (BattleSession.ShadowIngest uses the
|
||||
// same ReferenceEquals(from, A) mapping). Degrades to 0 when the engine isn't owned/ready for this
|
||||
// session (single-active-engine gate) so a non-engine session never crashes.
|
||||
bool senderSeat = ReferenceEquals(ctx.From, ctx.A);
|
||||
int playedCost = ctx.Engine.PlayedCardCost(senderSeat, playIdx, fallback: 0);
|
||||
|
||||
// Spellboost count still rides the played card's knownList (prod-faithful; Task 6 retires this
|
||||
// bookkeeping now that cost is engine-sourced). Read the CURRENT map (state before this frame's
|
||||
// grant) for the emit, then fold THIS frame's alter ops in afterwards — a play that grants
|
||||
// spellboost (e.g. Fate's Hand) targets the REST of the hand, not the card just played.
|
||||
var played = KnownListBuilder.BuildPlayedCard(
|
||||
deckMap, playIdx, orderList, ctx.State.GetSpellboostMap(ctx.From), cost: playedCost);
|
||||
ctx.State.RecordSpellboostFrom(ctx.From, ctx.Other, orderList);
|
||||
var oppoTargets = KnownListBuilder.RenameTargets(entries.GetValueOrDefault(WireKeys.TargetList));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user