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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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!; }
|
||||
|
||||
38
SVSim.BattleEngine/Shim/View/HeadlessAttackSelectControl.cs
Normal file
38
SVSim.BattleEngine/Shim/View/HeadlessAttackSelectControl.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// AUTHORED SHIM (not copied) — headless no-op AttackSelectControl for the receive ATTACK path.
|
||||
//
|
||||
// The attack conductor dereferences BattlePlayer.BattleView.AttackSelectControl twice on the resolve
|
||||
// path: InPlayCardReflection.RegisterPairToAttackSelectControl (isPlayer attacks only) calls
|
||||
// IsCardTranslatable(targetView) + RegisterAttackPair(pair); ActionProcessor.Attack's non-proceeding
|
||||
// arm calls ResetCardAfterAttack(view). Headless those touch the (UI-only) attack-pair animation state
|
||||
// — purely cosmetic translate/idle tweening — so a no-op subclass keeps authoritative state intact.
|
||||
//
|
||||
// IsCardTranslatable is NOT virtual (it reads cardView.CardInfo.IsClass), so it is left to the BASE
|
||||
// impl; it resolves correctly headless because the headless BattleCardView's CardInfo is wired to the
|
||||
// backing card (see Shim/View/ViewUiTouchStubs.cs + Generated/_IfaceImpl.g.cs HEADLESS-FIX). The
|
||||
// virtual mutating-cosmetic methods are overridden to no-ops here so they never deref the null
|
||||
// _attackTargetSelectInfo._attackPairsCardIsInvolvedIn animation queue.
|
||||
|
||||
using Wizard.Battle.View.Vfx;
|
||||
|
||||
namespace Wizard.Battle.View
|
||||
{
|
||||
/// <summary>A no-op <see cref="AttackSelectControl"/> seeded as the headless player view's
|
||||
/// AttackSelectControl. Overrides only the cosmetic attack-pair animation entry points the receive
|
||||
/// ATTACK path invokes; all authoritative damage/death resolution stays in ActionProcessor.Attack.</summary>
|
||||
public sealed class HeadlessAttackSelectControl : AttackSelectControl
|
||||
{
|
||||
public static readonly HeadlessAttackSelectControl Instance = new();
|
||||
|
||||
// The receive path enqueues an attack pair for the translate-up animation. No UI headless, so
|
||||
// skip it entirely (the base would deref the card view's null _attackTargetSelectInfo queue).
|
||||
public override void RegisterAttackPair(AttackPair attackPair) { }
|
||||
|
||||
// Post-attack card reset is a position tween; no-op headless.
|
||||
public override VfxBase ResetCardAfterAttack(IBattleCardView cardToReset) => NullVfx.GetInstance();
|
||||
|
||||
public override VfxBase ResetCardAfterAttackOnReplay() => NullVfx.GetInstance();
|
||||
|
||||
// Idle pingpong tween; no-op headless.
|
||||
public override void StartCardIdling(IBattleCardView battleCardView) { }
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,11 @@ namespace Wizard.Battle.View
|
||||
// interface here with no-op members (the leaf PlayerClassBattleCardView inherits them).
|
||||
public abstract class ClassBattleCardViewBase : BattleCardView, IClassBattleCardView
|
||||
{
|
||||
// HEADLESS-FIX (M-HC-4a): forward the BuildInfo to the BattleCardView base so a class (leader)
|
||||
// view's CardInfo resolves to its backing card — the receive ATTACK path reads CardInfo.IsClass
|
||||
// (true for a leader) via AttackSelectControl.IsCardTranslatable when an attack targets the leader.
|
||||
protected ClassBattleCardViewBase() { }
|
||||
protected ClassBattleCardViewBase(BuildInfo buildInfo) : base(buildInfo) { }
|
||||
public virtual Wizard.Battle.Player.ClassCharacter.IClassCharacter ClassCharacter => null;
|
||||
public virtual void StartOutFrame() { }
|
||||
public virtual void StartIntoFrame() { }
|
||||
@@ -60,7 +65,7 @@ namespace Wizard.Battle.View
|
||||
public virtual bool GetCurrentClipIsName(global::ClassCharaPrm.MotionType motionType) => false;
|
||||
public virtual void ClearSpineObject() { }
|
||||
}
|
||||
public class NullBattleCardView : BattleCardView { public NullBattleCardView() { } public NullBattleCardView(BuildInfo buildInfo) { } public static void ReleaseSharedDummy() { } }
|
||||
public class NullBattleCardView : BattleCardView { public NullBattleCardView() { } public NullBattleCardView(BuildInfo buildInfo) : base(buildInfo) { } public static void ReleaseSharedDummy() { } } // HEADLESS-FIX (M-HC-4a): chain BuildInfo so a null-view card's CardInfo still resolves
|
||||
|
||||
// The decomp NullClassBattleCardView is `: NullBattleCardView, IClassBattleCardView, IBattleCardView`;
|
||||
// base-clause recovery kept only the base class. IBattleCardView is satisfied via the BattleCardView
|
||||
|
||||
@@ -14,7 +14,19 @@ namespace Wizard.Battle.View
|
||||
// Parameterless ctor lets the no-op subclass hand stubs (ClassBattleCardViewBase,
|
||||
// NullBattleCardView) and any non-chaining stub satisfy their implicit base() call.
|
||||
public BattleCardView() { }
|
||||
public BattleCardView(BuildInfo buildInfo) { }
|
||||
public BattleCardView(BuildInfo buildInfo) { _buildInfo = buildInfo; }
|
||||
|
||||
// HEADLESS-FIX (M-HC-4a): the receive ATTACK path reads BattleCardView.CardInfo (the backing
|
||||
// card) and BattleCardView._inPlayFrameEffect on the resolve path (InPlayCardReflection /
|
||||
// ActionProcessor.Attack). The interface getters in Generated/_IfaceImpl.g.cs surface these two
|
||||
// fields. CardInfo comes from the stored BuildInfo (cardInfo == the card, IReadOnlyBattleCardInfo,
|
||||
// so IsClass etc. are authentic); _inPlayFrameEffect is a non-null no-op frame-effect control
|
||||
// whose HideFrameEffect/UpdateCanAttackEffect are empty (Generated/InPlayCardFrameEffectControl.g.cs).
|
||||
internal BuildInfo _buildInfo;
|
||||
internal IReadOnlyBattleCardInfo HeadlessCardInfo => _buildInfo?.cardInfo;
|
||||
internal InPlayCardFrameEffectControl _headlessInPlayFrameEffect =
|
||||
new InPlayCardFrameEffectControl(null, null, null);
|
||||
|
||||
// AttackTargetSelectInfo provided by Generated/BattleCardView_AttackTargetSelectInfo.g.cs
|
||||
public virtual UnityEngine.GameObject GameObject { get; protected set; }
|
||||
public HandCardFrameEffectControl HandFrameEffect { get; private set; }
|
||||
|
||||
Reference in New Issue
Block a user