feat(battlenode): attack resolves on engine state via view-untangle (M-HC-4a)

Drive ATTACK frames through the headless receive conductor and assert on engine
board state (node-native harness). Two cases: follower -> enemy leader (leader
life drops by atk, attacker spent) and a lethal follower-vs-follower trade (both
removed). ATTACK opcode confirmed = 10 (NetworkBattleDefine.PlayActionType).

Headless view-untangle (no Engine logic edits; drift clean):
- IBattlePlayerView.AttackSelectControl -> non-null HeadlessAttackSelectControl
  (no-op RegisterAttackPair/ResetCardAfterAttack); IsCardTranslatable left to base.
- IBattleCardView.CardInfo -> backing card via BuildInfo (so IsCardTranslatable
  reads authentic IsClass); class/null view ctors now chain : base(buildInfo).
- IBattleCardView._inPlayFrameEffect -> non-null no-op control.
- Seed Certification.viewer_id headless so the IsRecovery target parse
  (vid != UserViewerID) does not throw inside SavedataManager and silently drop
  the parsed targetList.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-06 22:48:26 -04:00
parent 0d7136787a
commit c5a511e4fe
11 changed files with 291 additions and 9 deletions

View File

@@ -177,6 +177,30 @@ internal sealed class SessionBattleEngine
public int InPlayCardId(bool playerSeat, int boardPos) =>
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].CardId;
/// <summary>The engine <c>Index</c> of the in-play follower at <paramref name="boardPos"/> (0-based,
/// leader excluded — same convention as <see cref="BoardCount"/>/<see cref="InPlayCardId"/>). An ATTACK
/// frame addresses the attacker by this in-play Index (the wire <c>playIdx</c>), so a test reads it after
/// a follower resolves onto the board to build the attack (M-HC-4a).</summary>
public int InPlayCardIndex(bool playerSeat, int boardPos) =>
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Index;
/// <summary>The current life/health of the in-play follower at <paramref name="boardPos"/> (0-based,
/// leader excluded). Reads <see cref="BattleCardBase.Life"/> (skill-resolved current health). Lets an
/// attack test assert a follower took the attacker's damage (M-HC-4a follower-vs-follower trade).</summary>
public int InPlayCardLife(bool playerSeat, int boardPos) =>
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Life;
/// <summary>The attack stat of the in-play follower at <paramref name="boardPos"/> (skill-resolved
/// <see cref="BattleCardBase.Atk"/>). The damage it deals when it attacks.</summary>
public int InPlayCardAtk(bool playerSeat, int boardPos) =>
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Atk;
/// <summary>True when the in-play follower at <paramref name="boardPos"/> can still attack this turn
/// (<see cref="BattleCardBase.Attackable"/>). After it attacks (consuming its single attack) this reads
/// false — the "attacker is spent" assertion (M-HC-4a).</summary>
public bool InPlayCardAttackable(bool playerSeat, int boardPos) =>
Seat(playerSeat).ClassAndInPlayCardList[boardPos + 1].Attackable;
/// <summary>The engine-RESOLVED play-time cost of the card whose engine <c>Index</c> == <paramref name="idx"/>
/// on <paramref name="playerSeat"/> (M-HC-3a). This is the discounted cost the play actually paid —
/// spellboost reduction, board-dependent modifiers and all — read straight off the engine, so the