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:
@@ -52,6 +52,12 @@ internal static class EngineGlobalInit
|
||||
private const int PlayerCharaId = 1;
|
||||
private const int EnemyCharaId = 2;
|
||||
|
||||
/// <summary>The headless engine's "self" viewer id (seeded into <c>Certification.viewer_id</c>). Any
|
||||
/// stable nonzero value works; it only has to be DISTINCT from the vid an attack/evolve stamps for a
|
||||
/// target on the OTHER seat so the IsRecovery target parse resolves owners correctly. Exposed for the
|
||||
/// node-native harness to build attack frames whose target vid matches this perspective.</summary>
|
||||
internal const int ThisViewerId = 1001;
|
||||
|
||||
public static void EnsureInitialized()
|
||||
{
|
||||
if (_done) return;
|
||||
@@ -134,6 +140,22 @@ internal static class EngineGlobalInit
|
||||
if (string.IsNullOrEmpty(udidField.GetValue(null) as string))
|
||||
udidField.SetValue(null, "headless-udid");
|
||||
|
||||
// --- Cute.Certification.viewer_id ------------------------------------------------------
|
||||
// The IsRecovery target parse (NetworkBattleReceiver.CreateTargetList, isWatch branch) derives
|
||||
// a target's owner from `vid != PlayerStaticData.UserViewerID`, where
|
||||
// UserViewerID => Certification.ViewerId, whose getter LAZILY reads
|
||||
// Toolbox.SavedataManager.GetInt("VIEWER_ID") when its backing field is 0 — and that savedata
|
||||
// read throws headless. The exception is SWALLOWED by ConvertReceiveDataToMakeData's blanket
|
||||
// catch, which (with the node's checkBreakData:false ingest) silently drops the parsed
|
||||
// targetList, leaving an attack/evolve with an EMPTY target list -> the action throws on
|
||||
// targetList[0]. Seed the backing field with a stable nonzero id so the getter short-circuits.
|
||||
// It defines the engine's "player" perspective: a target vid == ThisViewerId resolves on
|
||||
// BattlePlayer (engine seat A), vid != it on BattleEnemy (seat B). Only set when 0 (coexistence).
|
||||
var viewerIdField = typeof(Certification).GetField("viewer_id",
|
||||
BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
if ((int)(viewerIdField.GetValue(null) ?? 0) == 0)
|
||||
viewerIdField.SetValue(null, ThisViewerId);
|
||||
|
||||
_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user