using Wizard.Battle.Phase; using Wizard.Battle.Recovery; using Wizard.Battle.Replay; using Wizard.Battle.Resource; using Wizard.Battle.View.Vfx; using Wizard.BattleMgr; namespace SVSim.BattleEngine.Tests { // Initializes the global engine state a headless battle assumes exists. In the real client this // is populated from /load/index at login; here we author the minimum the resolution path reads. public static class HeadlessEngineEnv { // Simplest zero-skill vanilla follower in cards.json: neutral (clan 0), cost 1, 1/2, no skill. public const int FollowerId = 100011010; // M3 next-hardest deterministic card: a fixed-damage spell. 900124030 is an ELF (clan 1, matches // PlayerClassId) cost-3 spell whose sole skill is `when_play` `damage=3` to `card_type=class` // (the enemy leader) — auto-targeted (no select_count), no RNG. Deterministic burn to the face. public const int SpellId = 900124030; private static bool _done; public static void EnsureInitialized() { if (_done) return; // Wizard.Data.Load: static /load/index snapshot. The ctor's CreateBackgroundId reads // Data.Load.data._userTutorial (LoadDetail self-inits _userTutorial). Suppress VFX too. Wizard.Data.Load = new Load { data = new LoadDetail() }; // CardParameter(CardCSVData) reads Data.Crossover.RestrictedCard for deck-limit calc; // an empty Crossover returns the default count (no restriction). Private setter -> reflect. typeof(Wizard.Data).GetProperty("Crossover", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) .SetValue(null, new Wizard.Crossover()); BattleManagerBase.IsForecast = true; // CardMaster must be non-null before construction (the leader/class card looks up id 0). // Load the M2 vanilla follower + the M3 fixed-damage spell so each oracle can create + // look up its real stats. HeadlessCardMaster.Load(FollowerId, SpellId); // Master reference data (class-character list) for leader/class card resolution. HeadlessMasterData.Install(); // Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master). // Set the backing fields directly: the public SetPlayerCharaId() also pulls MyRotation/ // AvatarBattle info (more null statics) which the resolution path doesn't need (the // TryGet* accessors are null-tolerant). var dm = GameMgr.GetIns().GetDataMgr(); SetField(dm, "_playerCharaId", HeadlessMasterData.PlayerCharaId); SetField(dm, "_enemyCharaId", HeadlessMasterData.EnemyCharaId); _done = true; } // Seed each leader's starting life on a freshly-constructed mgr. The engine does this in // BattleManagerBase.SetupInitialGameState -> InitializeClassLife (InitBaseMaxLife per leader), // but the full SetupInitialGameState also cascades into rotation/avatar/turn-panel UI init // that is irrelevant (and hostile) to a headless resolution test, so apply just the // InitializeClassLife subset. Without this a leader's BaseMaxLife defaults to 0 — which reads // as already-dead/game-over and silently blocks any card play (the M2 follower oracle never // noticed because it only asserted leader life *unchanged*, and 0 == 0). public const int DefaultLeaderLife = 20; public static void InitLeaderLife(BattleManagerBase mgr, int life = DefaultLeaderLife) { ((ClassBattleCardBase)mgr.BattlePlayer.Class).InitBaseMaxLife(life); ((ClassBattleCardBase)mgr.BattleEnemy.Class).InitBaseMaxLife(life); } private static void SetField(object obj, string name, object value) { var f = obj.GetType().GetField(name, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public); if (f == null) throw new System.InvalidOperationException( $"{obj.GetType().Name} has no field '{name}'"); f.SetValue(obj, value); } } // Test-side replica of the engine's own StandardBattleMgrContentsCreator (the practice/solo // init path: GameMgr.cs:244 `new SingleBattleMgr(new StandardBattleMgrContentsCreator(null, null))`). // Authored here (not copied) so we control the seed deterministically; uses the real engine // managers verbatim. The real StandardBattleMgrContentsCreator + SingleBattlePhaseCreator were // cut from the M1 copy set (entry-point constructors), so we reproduce them minimally. public sealed class HeadlessContentsCreator : IBattleMgrContentsCreator { public int RandomSeed => 12345; // fixed; vanilla follower has no RNG so value is irrelevant // No-op managers (vs the practice path's file-backed SingleBattleRecoveryRecordManager): // the ctor's FirstRecoverySetting/FirstReplaySetting dereference these, and recovery/replay // recording is irrelevant to the M2 oracle, so use the engine's own null implementations. public IRecoveryManager RecoveryManager { get; } = new NullRecoveryManager(); public IRecoveryRecordManager RecoveryRecordManager { get; } = new NullRecoveryRecordManager(); public IReplayRecordManager ReplayRecordManager { get; } = new NullReplayRecordManager(); public IBattleResourceMgr CreateResourceMgr() => new BattleResourceMgr(); public VfxMgr CreateVfxMgr() => new VfxMgr(); public IPhaseCreator CreatePhaseCreator(BattleManagerBase battleMgr) => new HeadlessPhaseCreator(battleMgr); } // Equivalent of the engine's SingleBattlePhaseCreator: inherits PhaseCreatorBase wholesale. public sealed class HeadlessPhaseCreator : PhaseCreatorBase { public HeadlessPhaseCreator(BattleManagerBase battleMgr) : base(battleMgr) { } } }