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 { 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() }; BattleManagerBase.IsForecast = true; // CardMaster must be non-null before construction (the leader/class card looks up id 0). // Empty load suffices for construction; the oracle reloads with the follower's real id. HeadlessCardMaster.Load(); // 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; } 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) { } } }