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

@@ -27,7 +27,14 @@ public partial class BuildInfo
public Func<bool> _getIsAbleToAttack;
public Func<bool> _getIsUnableToAttackClass;
public Func<HandCardFrameEffectType> getHandCardFrameEffectType;
public BuildInfo(IReadOnlyBattleCardInfo cardInfo, BattlePlayerReadOnlyInfoPair playerInfoPair, GameObject gameObject, BattleCamera battleCamera, BackGroundBase backGround, IBattleResourceMgr resourceMgr, Func<bool> getIsTouchable, Func<bool> getIsMovable, Func<bool> getIsOnMove, Func<int, bool> getIsFixedUseEnable, Func<bool> getIsActionCard, Func<bool> getIsAbleToAttack, Func<bool> getIsUnableToAttackClass, Func<HandCardFrameEffectType> getHandCardFrameEffectType) { }
// HEADLESS-FIX (M-HC-4a): store cardInfo so the headless BattleCardView can expose CardInfo (the
// backing card) — the receive ATTACK path reads BattleCardView.CardInfo.IsClass via
// AttackSelectControl.IsCardTranslatable. (Generated stub body was empty; re-apply on regen.)
public BuildInfo(IReadOnlyBattleCardInfo cardInfo, BattlePlayerReadOnlyInfoPair playerInfoPair, GameObject gameObject, BattleCamera battleCamera, BackGroundBase backGround, IBattleResourceMgr resourceMgr, Func<bool> getIsTouchable, Func<bool> getIsMovable, Func<bool> getIsOnMove, Func<int, bool> getIsFixedUseEnable, Func<bool> getIsActionCard, Func<bool> getIsAbleToAttack, Func<bool> getIsUnableToAttackClass, Func<HandCardFrameEffectType> getHandCardFrameEffectType)
{
this.cardInfo = cardInfo;
this._playerInfoPair = playerInfoPair;
}
}
}
}

View File

@@ -9,7 +9,7 @@ public partial class EnemyClassBattleCardView
private readonly PlayerClassCharacter _classCharacter;
public IClassCharacter ClassCharacter { get; set; }
public float OriginalRootYPosition { get; set; }
public EnemyClassBattleCardView(BuildInfo buildInfo) { }
public EnemyClassBattleCardView(BuildInfo buildInfo) : base(buildInfo) { } // HEADLESS-FIX (M-HC-4a): chain BuildInfo so the leader view's CardInfo resolves (attack-on-leader targetting)
public void StartOutFrame() { }
public void StartIntoFrame() { }
public float GetCurrentClipTime() => default!;

View File

@@ -9,7 +9,7 @@ public partial class PlayerClassBattleCardView
private readonly PlayerClassCharacter _classCharacter;
public IClassCharacter ClassCharacter { get; set; }
public float OriginalRootYPosition { get; set; }
public PlayerClassBattleCardView(BuildInfo buildInfo) { }
public PlayerClassBattleCardView(BuildInfo buildInfo) : base(buildInfo) { } // HEADLESS-FIX (M-HC-4a): chain BuildInfo so the leader view's CardInfo resolves (attack-on-leader targetting)
public void StartOutFrame() { }
public void StartIntoFrame() { }
public float GetCurrentClipTime() => default!;

View File

@@ -11,7 +11,7 @@ namespace Wizard.Battle.View {
Vector3 global::Wizard.Battle.View.IBattleCardView.ForecastIconPosition { get => default!; }
Vector3 global::Wizard.Battle.View.IBattleCardView.ForecastIconScale { get => default!; }
float global::Wizard.Battle.View.IBattleCardView.OriginalRootYPosition { get => default!; }
IReadOnlyBattleCardInfo global::Wizard.Battle.View.IBattleCardView.CardInfo { get => default!; }
IReadOnlyBattleCardInfo global::Wizard.Battle.View.IBattleCardView.CardInfo { get => HeadlessCardInfo; } // HEADLESS-FIX (M-HC-4a): the backing card (from BuildInfo) — AttackSelectControl.IsCardTranslatable reads CardInfo.IsClass
BattlePlayerReadOnlyInfoPair global::Wizard.Battle.View.IBattleCardView.PlayerInfoPair { get => default!; }
IReadOnlyVoiceInfo global::Wizard.Battle.View.IBattleCardView.VoiceInfo { get => global::Wizard.Battle.View.HeadlessVoiceInfo.Instance; } // HEADLESS-FIX (M7): non-null voice info for the death-voice tail
GameObject global::Wizard.Battle.View.IBattleCardView.GameObject { get => default!; }
@@ -27,7 +27,7 @@ namespace Wizard.Battle.View {
BackGroundBase global::Wizard.Battle.View.IBattleCardView.m_BackGround { get => default!; }
HandParameter global::Wizard.Battle.View.IBattleCardView.HandParam { get => default!; }
BattleCardView.AttackTargetSelectInfo global::Wizard.Battle.View.IBattleCardView._attackTargetSelectInfo { get => default!; set { } }
InPlayCardFrameEffectControl global::Wizard.Battle.View.IBattleCardView._inPlayFrameEffect { get => default!; set { } }
InPlayCardFrameEffectControl global::Wizard.Battle.View.IBattleCardView._inPlayFrameEffect { get => _headlessInPlayFrameEffect; set { _headlessInPlayFrameEffect = value; } } // HEADLESS-FIX (M-HC-4a): non-null no-op frame-effect control (HideFrameEffect/UpdateCanAttackEffect are no-ops) for the receive ATTACK path
bool global::Wizard.Battle.View.IBattleCardView.areArrowsForcedOff { get => default!; set { } }
bool global::Wizard.Battle.View.IBattleCardView._isCardQueuedToBePlayed { get => default!; set { } }
bool global::Wizard.Battle.View.IBattleCardView.isHiddenFromHandView { get => default!; set { } }
@@ -228,7 +228,7 @@ namespace Wizard.Battle.View {
// shared no-op PlayQueueViewBase so the presentation call is a safe NullVfx (the authoritative
// play mutation runs in PlayHandCardReflection.Play, not in this view).
PlayQueueViewBase global::Wizard.Battle.View.IBattlePlayerView.PlayQueueView { get => global::Wizard.Battle.View.HeadlessPlayQueueViewStub.Instance; }
AttackSelectControl global::Wizard.Battle.View.IBattlePlayerView.AttackSelectControl { get => default!; }
AttackSelectControl global::Wizard.Battle.View.IBattlePlayerView.AttackSelectControl { get => global::Wizard.Battle.View.HeadlessAttackSelectControl.Instance; } // HEADLESS-FIX (M-HC-4a): non-null no-op attack-select-control for the receive ATTACK path (RegisterPairToAttackSelectControl + ActionProcessor.Attack reset arm)
InPlayViewBase global::Wizard.Battle.View.IBattlePlayerView.InPlayView { get => default!; }
GameObject global::Wizard.Battle.View.IBattlePlayerView.StatusParentPanel { get => default!; }
GameObject global::Wizard.Battle.View.IBattlePlayerView.AnchorL { get => default!; }
@@ -335,7 +335,7 @@ namespace Wizard.Battle.View {
// shared no-op PlayQueueViewBase so the presentation call is a safe NullVfx (the authoritative
// play mutation runs in PlayHandCardReflection.Play, not in this view).
PlayQueueViewBase global::Wizard.Battle.View.IBattlePlayerView.PlayQueueView { get => global::Wizard.Battle.View.HeadlessPlayQueueViewStub.Instance; }
AttackSelectControl global::Wizard.Battle.View.IBattlePlayerView.AttackSelectControl { get => default!; }
AttackSelectControl global::Wizard.Battle.View.IBattlePlayerView.AttackSelectControl { get => global::Wizard.Battle.View.HeadlessAttackSelectControl.Instance; } // HEADLESS-FIX (M-HC-4a): non-null no-op attack-select-control for the receive ATTACK path (RegisterPairToAttackSelectControl + ActionProcessor.Attack reset arm)
InPlayViewBase global::Wizard.Battle.View.IBattlePlayerView.InPlayView { get => default!; }
GameObject global::Wizard.Battle.View.IBattlePlayerView.StatusParentPanel { get => default!; }
GameObject global::Wizard.Battle.View.IBattlePlayerView.AnchorL { get => default!; }