Squashes 146 commits from battle-engine-extraction. Net: 2,045 files changed, +11,896 / -158,687 lines. Ships engine passes 4-7 (dead-code cull, view-layer stub, receive-path shrink) plus the Phase-5 AsyncLocal ambient deletion that turns concurrent battles into a type-system property rather than a scope contract. ## What landed **Passes 4-7 (chunks 1-34):** Extended the Phase-4 const-false collapse into a cascading cull across the skill graph, view layer, and receive-path periphery. Six mode flags (IsWatchBattle/IsReplayBattle/IsAdmin/IsAdminWatch/IsPuzzleQuest/ IsAINetwork) became `const false`, every guarded block deleted. Field*.cs subclass ctors + BackGroundBase + ObjectChecker culled to no-ops. Mulligan family reworked to take a mgr param through IMulliganMgr.InitMulligan. Emotion/Recovery/Resource clusters null-stubbed. Prediction/OperationSimulator/ skill filters converted from static ambient reads to per-mgr reads via SkillPrm.ownerCard.SelfBattlePlayer.BattleMgr / ins.BattleMgr / this.BattleMgr. **Phase-5 ambient rip (chunks 35-47):** Deleted BattleAmbient / BattleAmbient- Context / TestBattleScope in full. Every per-battle mutable slot now lives on the mgr instance itself: mgr.InstanceIsForecast / InstanceIsRandomDraw / InstanceRecoveryInfo / InstanceViewerId / InstanceNetworkAgent / GameMgr BattleManagerBase.GetIns() returns null unconditionally; the residual static flags + 3 façades (Certification.ViewerId, Data.BattleRecoveryInfo, ToolboxGame.RealTimeNetworkAgent) are null-tolerant defaults kept for the handful of engine-internal readers that still reference their types. Zero BattleAmbient references anywhere in engine + node + tests. Added pre-seeded GameMgr ctor overload threaded through the mgr chain (BattleManagerBase → SingleBattleMgr / NetworkBattleManagerBase → NetworkStandard- BattleMgr → HeadlessBattleMgr / HeadlessNetworkBattleMgr). Fixtures build a GameMgr, seed it via HeadlessEngineEnv.SeedCharaIds/SeedNetUser, and pass it to the mgr's ctor — no ambient reach. Node side (SVSim.BattleNode/SessionBattleEngine): _ctx replaced with a plain GameMgr field; 34 `using var _ambient = BattleAmbient.Enter(_ctx)` scope wraps ripped from every accessor and mutator; EngineGlobalInit.WirePerSessionGameMgr takes GameMgr as a param and runs from SessionBattleEngine.SetupInternal BEFORE mgr construction. Test side: TestBattleScope deleted; 18 fixture [SetUp]s migrated to `HeadlessEngineEnv.EnsureProcessGlobals()`; MultiInstanceEngineTests rewritten around per-mgr construction (GetIns() → null is the pinned invariant). ## Regression fixes - **chunk-48** (MulliganCtrl): chunk-35's `= null` stubs on card lookups broke the live receive-driven Deal path (BattlePlayerBase.DrawCard NRE'd downstream of NetworkPlayerMulliganCtrl.StartMulliganVfx). Restored the three lookups via `_battlePlayer.BattleMgr.GetBattleCardIdx`. Engine tests were satisfied by the WireMulliganPhase seam; unit tests exposed the live-path gap. ## Ship state - SVSim.BattleEngine.Tests: 56/56 pass, 2 skip - SVSim.UnitTests: 1554/1554 pass (was 1523/31-fail before chunk 48) - Solution build: 0 source warnings (40 pre-existing NU1902 MessagePack CVEs in SVSim.EmulatedEntrypoint, unrelated) - Sequential PVP smoke: verified live (two back-to-back battles, no regression on cleanup/spinup) - Concurrent PVP smoke: verified live Adds tools/engine-port/ClosureAnalyzer/ — the Roslyn transitive-type-closure analyzer needed to make future cascade cleanup safe (per feedback memory "Engine cleanup needs closure tool" from the 2026-06-28 pass-3 failure). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
270 lines
11 KiB
C#
270 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Wizard.Battle.View.Vfx;
|
|
|
|
namespace Wizard;
|
|
|
|
public static class AIAttachEventToBattleModuleUtility
|
|
{
|
|
public static void SetupEventWhenInitGame(BattlePlayerBase ally, BattlePlayerBase opponent, OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
SetupOnAddCemeteryEvent(ally, opponent, ai);
|
|
SetupOnTurnStartComplete(ally, opponent, ai);
|
|
SetupOnTurnEndStart(ally, opponent, ai);
|
|
SetupOperateMgrEvent(operateMgr, ai);
|
|
}
|
|
|
|
private static void SetupOnAddCemeteryEvent(BattlePlayerBase ally, BattlePlayerBase opponent, EnemyAI ai)
|
|
{
|
|
ai.PlayerBattleEvent.AllyAddCemeteryEvent = delegate(BattleCardBase card, BattlePlayerBase.CEMETERY_TYPE cemeteryType, bool isOpen, SkillBase skill)
|
|
{
|
|
if (cemeteryType == BattlePlayerBase.CEMETERY_TYPE.NORMAL && !(card is NullBattleCard) && card.SelfBattlePlayer.ClassAndInPlayCardList.Contains(card) && card.HasSkillWhenDestroy && ai.LatestAction != null)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(card, (SkillBase skillBase) => skillBase.IsWhenDestroySkill && RegisterTool.IsSkillRandom(skillBase));
|
|
}
|
|
};
|
|
ally.OnAddCemeteryEvent += ai.PlayerBattleEvent.AllyAddCemeteryEvent;
|
|
ai.PlayerBattleEvent.OpponentAddCemeteryEvent = delegate(BattleCardBase card, BattlePlayerBase.CEMETERY_TYPE cemeteryType, bool isOpen, SkillBase skill)
|
|
{
|
|
if (cemeteryType == BattlePlayerBase.CEMETERY_TYPE.NORMAL && !(card is NullBattleCard) && card.SelfBattlePlayer.ClassAndInPlayCardList.Contains(card))
|
|
{
|
|
RecordOpponentBarbarossa(card, ai);
|
|
if (card.HasSkillWhenDestroy && ai.LatestAction != null)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(card, (SkillBase skillBase) => skillBase.IsWhenDestroySkill && RegisterTool.IsSkillRandom(skillBase));
|
|
}
|
|
}
|
|
};
|
|
opponent.OnAddCemeteryEvent += ai.PlayerBattleEvent.OpponentAddCemeteryEvent;
|
|
}
|
|
|
|
private static void SetupOnTurnStartComplete(BattlePlayerBase ally, BattlePlayerBase opponent, EnemyAI ai)
|
|
{
|
|
ai.PlayerBattleEvent.AllyTurnStartCompleteEvent = delegate
|
|
{
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
};
|
|
ally.OnTurnStartComplete = (Action)Delegate.Combine(ally.OnTurnStartComplete, ai.PlayerBattleEvent.AllyTurnStartCompleteEvent);
|
|
ai.PlayerBattleEvent.OpponentTurnStartCompleteEvent = delegate
|
|
{
|
|
if (!ai.IsBattleEnd)
|
|
{
|
|
if (ai.IsOpponentBarbarossaDestroyed)
|
|
{
|
|
ai.IsOpponentBarbarossaDestroyed = false;
|
|
}
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
}
|
|
};
|
|
opponent.OnTurnStartComplete = (Action)Delegate.Combine(opponent.OnTurnStartComplete, ai.PlayerBattleEvent.OpponentTurnStartCompleteEvent);
|
|
}
|
|
|
|
private static void SetupOnTurnEndStart(BattlePlayerBase ally, BattlePlayerBase opponent, EnemyAI ai)
|
|
{
|
|
ai.PlayerBattleEvent.AllySelfTurnEndEvent = delegate
|
|
{
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.TURNEND, ally.Class, ally.Class, null);
|
|
ai.SaveBeforeLatestActionInformation();
|
|
for (int i = 0; i < ally.ClassAndInPlayCardList.Count; i++)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(ally.ClassAndInPlayCardList[i], (SkillBase skill) => skill.OnSelfTurnEndStart != 0 && RegisterTool.IsSkillRandom(skill));
|
|
}
|
|
for (int num = 0; num < opponent.ClassAndInPlayCardList.Count; num++)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(opponent.ClassAndInPlayCardList[num], (SkillBase skill) => skill.OnOpponentTurnEndStart != 0 && RegisterTool.IsSkillRandom(skill));
|
|
}
|
|
};
|
|
ally.OnTurnEndStart += ai.PlayerBattleEvent.AllySelfTurnEndEvent;
|
|
ai.PlayerBattleEvent.OpponentSelfTurnEndEvent = delegate
|
|
{
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.TURNEND, opponent.Class, opponent.Class, null);
|
|
ai.SaveBeforeLatestActionInformation();
|
|
for (int i = 0; i < opponent.ClassAndInPlayCardList.Count; i++)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(opponent.ClassAndInPlayCardList[i], (SkillBase skill) => skill.OnSelfTurnEndStart != 0 && RegisterTool.IsSkillRandom(skill));
|
|
}
|
|
for (int num = 0; num < ally.ClassAndInPlayCardList.Count; num++)
|
|
{
|
|
ai.LatestAction.CreateRandomTargetInformation(ally.ClassAndInPlayCardList[num], (SkillBase skill) => skill.OnOpponentTurnEndStart != 0 && RegisterTool.IsSkillRandom(skill));
|
|
}
|
|
};
|
|
opponent.OnTurnEndStart += ai.PlayerBattleEvent.OpponentSelfTurnEndEvent;
|
|
}
|
|
|
|
private static void SetupVirtualPairOnAfterPickCard(BattlePlayerPair pair, AIOperationSimulatorAccessor accessor)
|
|
{
|
|
pair.Self.OnAfterPickCard += delegate
|
|
{
|
|
accessor.UpdateCurrentField(pair, EnemyAI.EmptyPlayPtn);
|
|
};
|
|
pair.Opponent.OnAfterPickCard += delegate
|
|
{
|
|
accessor.UpdateCurrentField(pair, EnemyAI.EmptyPlayPtn);
|
|
};
|
|
}
|
|
|
|
private static void SetupOperateMgrEvent(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
SetupOperateMgrOnSetCardSuccess(operateMgr, ai);
|
|
SetupOperateMgrOnSetCardExecuted(operateMgr, ai);
|
|
SetupOperateMgrOnEvolveSuccess(operateMgr, ai);
|
|
SetupOperateMgrOnEvolveComplete(operateMgr, ai);
|
|
SetupOperateMgrOnBeforeAttack(operateMgr, ai);
|
|
SetupOperateMgrOnAttackExecuted(operateMgr, ai);
|
|
SetupOperateMgrOnBeforeFusion(operateMgr, ai);
|
|
SetupOperateMgrOnAfterFusion(operateMgr, ai);
|
|
}
|
|
|
|
private static void SetupOperateMgrOnSetCardSuccess(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnSetCardSuccessEvent = delegate(BattleCardBase originalCard, BattleCardBase playCard, IEnumerable<BattleCardBase> selectedCards)
|
|
{
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.PLAY, playCard, originalCard, selectedCards);
|
|
ai.SaveBeforeLatestActionInformation();
|
|
ai.LatestAction.CreateRandomTargetInformation(playCard, (SkillBase skill) => skill.IsWhenPlaySkill && RegisterTool.IsSkillRandom(skill));
|
|
};
|
|
operateMgr.OnSetCardSuccess += ai.OprMgrBattleEvent.OnSetCardSuccessEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnSetCardExecuted(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnSetCardExecutedEvent = delegate
|
|
{
|
|
if (ai.IsBattleEnd)
|
|
{
|
|
return NullVfx.GetInstance();
|
|
}
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
return NullVfx.GetInstance();
|
|
};
|
|
operateMgr.OnSetCardExecuted += ai.OprMgrBattleEvent.OnSetCardExecutedEvent;
|
|
}
|
|
|
|
private static void RecordOpponentBarbarossa(BattleCardBase destroyCard, EnemyAI ai)
|
|
{
|
|
if (destroyCard.BaseParameter.BaseCardId == 106241020)
|
|
{
|
|
ai.IsOpponentBarbarossaDestroyed = true;
|
|
}
|
|
}
|
|
|
|
private static void SetupOperateMgrOnEvolveSuccess(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnEvolveSuccessEvent = delegate(BattleCardBase originalEvolver, BattleCardBase realEvolver, IEnumerable<BattleCardBase> selectedTargets)
|
|
{
|
|
ai.SaveBeforeLatestActionInformation();
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.EVOLVE, realEvolver, originalEvolver, selectedTargets);
|
|
ai.LatestAction.CreateRandomTargetInformation(realEvolver, (SkillBase skill) => skill.IsWhenEvolveSkill && RegisterTool.IsSkillRandom(skill), forceCheckEvolveSkills: true);
|
|
};
|
|
operateMgr.OnEvolveSuccess += ai.OprMgrBattleEvent.OnEvolveSuccessEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnEvolveComplete(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnEvolveCompleteEvent = delegate
|
|
{
|
|
if (!ai.IsBattleEnd)
|
|
{
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
}
|
|
return NullVfx.GetInstance();
|
|
};
|
|
operateMgr.OnEvoleComplete += ai.OprMgrBattleEvent.OnEvolveCompleteEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnBeforeAttack(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnBeforeAttackEvent = delegate(BattleCardBase attacker, BattleCardBase target, SkillProcessor skillProcessorOneTime)
|
|
{
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.ATTACK, attacker, attacker, new List<BattleCardBase> { target });
|
|
ai.SaveBeforeLatestActionInformation();
|
|
ai.LatestAction.CreateRandomTargetInformation(attacker, (SkillBase skill) => skill.IsBeforAttackSkill && RegisterTool.IsSkillRandom(skill));
|
|
return NullVfx.GetInstance();
|
|
};
|
|
operateMgr.OnBeforeAttack += ai.OprMgrBattleEvent.OnBeforeAttackEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnAttackExecuted(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnAttackExecutedEvent = delegate
|
|
{
|
|
if (!ai.IsBattleEnd)
|
|
{
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
}
|
|
return NullVfx.GetInstance();
|
|
};
|
|
operateMgr.OnAttackExecuted += ai.OprMgrBattleEvent.OnAttackExecutedEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnBeforeFusion(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnBeforeFusionEvent = delegate(BattleCardBase fusionActor, IEnumerable<BattleCardBase> targets)
|
|
{
|
|
ai.LatestAction = new AIRealActionInformation(AIOperationType.FUSION, fusionActor, fusionActor, targets);
|
|
ai.SaveBeforeLatestActionInformation();
|
|
};
|
|
operateMgr.OnBeforeFusion += ai.OprMgrBattleEvent.OnBeforeFusionEvent;
|
|
}
|
|
|
|
private static void SetupOperateMgrOnAfterFusion(OperateMgr operateMgr, EnemyAI ai)
|
|
{
|
|
ai.OprMgrBattleEvent.OnAfterFusionEvent = delegate
|
|
{
|
|
if (!ai.IsBattleEnd)
|
|
{
|
|
ai.UpdateAICurrentVirtualField();
|
|
AIVirtualField currentVirtualField = ai.CurrentVirtualField;
|
|
if (ai.LatestAction != null && ai.BeforeLatestActionField != null)
|
|
{
|
|
currentVirtualField.UpdateTagInformationFromLatestAction(ai.LatestAction, ai.BeforeLatestActionField);
|
|
}
|
|
ai.LatestAction = null;
|
|
ai.BeforeLatestActionField = null;
|
|
}
|
|
return NullVfx.GetInstance();
|
|
};
|
|
operateMgr.OnAfterFusion += ai.OprMgrBattleEvent.OnAfterFusionEvent;
|
|
}
|
|
|
|
public static void SetupVirtualPairEventForOperationSimulator(BattlePlayerPair pair, AIOperationSimulatorAccessor accessor)
|
|
{
|
|
SetupVirtualPairOnAfterPickCard(pair, accessor);
|
|
}
|
|
}
|