feat(battle-engine-port): M3 COMPLETE — fixed-damage spell resolves headless (leader-life-delta oracle passes)

Card 900124030 (ELF cost-3, when_play damage=3 to enemy leader) resolves to
correct authoritative state headless via the IsForecast/IsRecovery +
ActionProcessor.PlayCard path. New oracle dimension (opponent leader-life delta)
passes; 3/3 tests green; engine still 0 errors; check_drift clean.

Four headless gaps, each mechanical (no logic/Unity wall):
- Data seam: InitLeaderLife (SetupInitialGameState->InitializeClassLife subset);
  leader BaseMaxLife was 0 => game-over => play silently rejected. M2 missed it
  (only asserted leader life unchanged: 0==0).
- Runtime cast: re-attach IClassBattleCardView on the generated
  NullClassBattleCardView stub (members already present; base-clause recovery
  stripped the decl). Compiled fine -> M1 loop never surfaced it.
- M1 mis-cut: copy NullVfxWithLoading verbatim (its GetInstance() lazy singleton
  was stubbed to default!/null). Same pattern as M2 NullCardVfxCreator.
- Card events: CreateHeadlessHandCard now calls SetupCardEvent so a spell's
  OnPlay->RemoveSpellCardFromHand / OnFinishWhenPlaySkill->AddSpellCardToCemetery
  fire (the bare CreateCardWithoutResources seam skips them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-06 02:19:54 -04:00
parent 171f07ec74
commit c47ae93027
7 changed files with 180 additions and 19 deletions

View File

@@ -3311,3 +3311,4 @@ llField.cs llField.cs a0e0eaed3f22a8c4ce47f82fa80346e3b99e3ac0a6765e1ad4ade3a87c
Wizard.Battle.View/CardIconControl.cs Wizard.Battle.View/CardIconControl.cs affd5a289a04bc9f446f3e892403dd9cd560ee557de1e5cd743dcb031dab280c 0
Wizard.Battle.View.Vfx/NullCardVfxCreator.cs Wizard.Battle.View.Vfx/NullCardVfxCreator.cs bf8f34d27f41df0dc728c47f874465869649299a5195c2d616597d7a37c581f5 0
Wizard.Battle.View.Vfx/NotEmptyNullVfx.cs Wizard.Battle.View.Vfx/NotEmptyNullVfx.cs fd471e4254bde6dded2c1447714e605a9889b97b01a834bc616585bcff738825 0
Wizard.Battle.View.Vfx/NullVfxWithLoading.cs Wizard.Battle.View.Vfx/NullVfxWithLoading.cs c297c7c7e53fc9a41e46b5f52baa65f905bc51943d6a0fbe683a98fc0668b9b9 0
1 # engine-relpath source-relpath sha256 patched(0|1)
3311 Wizard.Battle.View/CardIconControl.cs Wizard.Battle.View/CardIconControl.cs affd5a289a04bc9f446f3e892403dd9cd560ee557de1e5cd743dcb031dab280c 0
3312 Wizard.Battle.View.Vfx/NullCardVfxCreator.cs Wizard.Battle.View.Vfx/NullCardVfxCreator.cs bf8f34d27f41df0dc728c47f874465869649299a5195c2d616597d7a37c581f5 0
3313 Wizard.Battle.View.Vfx/NotEmptyNullVfx.cs Wizard.Battle.View.Vfx/NotEmptyNullVfx.cs fd471e4254bde6dded2c1447714e605a9889b97b01a834bc616585bcff738825 0
3314 Wizard.Battle.View.Vfx/NullVfxWithLoading.cs Wizard.Battle.View.Vfx/NullVfxWithLoading.cs c297c7c7e53fc9a41e46b5f52baa65f905bc51943d6a0fbe683a98fc0668b9b9 0

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace Wizard.Battle.View.Vfx;
public class NullVfxWithLoading : VfxWithLoading
{
private static NullVfxWithLoading _instance;
public override VfxBase LoadingVfx => NullVfx.GetInstance();
public override VfxBase MainVfx => NullVfx.GetInstance();
public override bool IsEnd => true;
public static NullVfxWithLoading GetInstance()
{
if (_instance == null)
{
_instance = new NullVfxWithLoading();
}
return _instance;
}
public override void Play()
{
}
public override void Update(float dt, List<IEffectVfx> effectVfxList)
{
}
public override bool IsVfxNonEmpty()
{
return false;
}
}

View File

@@ -1,16 +0,0 @@
// AUTO-GENERATED no-op stubs (m1_stub_gen) from Shadowverse_Code_2026-05-23\Wizard.Battle.View.Vfx\NullVfxWithLoading.cs
using System.Collections.Generic;
namespace Wizard.Battle.View.Vfx
{
public partial class NullVfxWithLoading
{
private static NullVfxWithLoading _instance;
public VfxBase LoadingVfx { get; set; }
public VfxBase MainVfx { get; set; }
public bool IsEnd { get; set; }
public static NullVfxWithLoading GetInstance() => default!;
public void Play() { }
public void Update(float dt, List<IEffectVfx> effectVfxList) { }
public bool IsVfxNonEmpty() => default!;
}
}

View File

@@ -101,7 +101,6 @@ namespace Wizard.Battle.View { public partial class NullClassBattleCardView : Nu
namespace Wizard.Battle.View { public partial class NullEnemyBattleView : BattleEnemyView { } }
namespace Wizard.Battle.View { public partial class NullFieldBattleCardView : FieldBattleCardView { } }
namespace Wizard.Battle.View { public partial class NullPlayerBattleView : BattlePlayerView { } }
namespace Wizard.Battle.View.Vfx { public partial class NullVfxWithLoading : VfxWithLoading { } }
namespace Wizard.Battle.View.Vfx { public partial class OneShotHeavenlyAegisPlayVfx : SequentialVfxPlayer { } }
namespace Wizard.Battle.View.Vfx { public partial class OpenCardFromHandVfx : SequentialVfxPlayer { } }
namespace Wizard.Battle.View.Vfx { public partial class OpenCardVfx : SequentialVfxPlayer { } }

View File

@@ -61,6 +61,16 @@ namespace Wizard.Battle.View
public virtual void ClearSpineObject() { }
}
public class NullBattleCardView : BattleCardView { public NullBattleCardView() { } public NullBattleCardView(BuildInfo buildInfo) { } public static void ReleaseSharedDummy() { } }
// The decomp NullClassBattleCardView is `: NullBattleCardView, IClassBattleCardView, IBattleCardView`;
// base-clause recovery kept only the base class. IBattleCardView is satisfied via the BattleCardView
// base, but IClassBattleCardView was dropped. The generated NullClassBattleCardView stub already
// provides that interface's members (public no-ops), so just re-attach the dropped interface here.
// The resolution path's VirtualClone (createNullView) -> ClassBattleCardBase.Setup casts the null
// view to IClassBattleCardView, which throws InvalidCastException at runtime without this (M3,
// fixed-damage spell: Skill_damage.TakeDamageSingle clones the leader before applying damage).
// Compiles fine without it (it's a cast, not a member call), so the M1 loop never surfaced it.
public partial class NullClassBattleCardView : IClassBattleCardView { }
}
namespace Wizard.Battle.View.Vfx