using SpaceGame.Simulation.Api.Contracts; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { private const float FactionCommanderReplanInterval = 10f; private const float ShipCommanderReplanInterval = 5f; private static readonly GoapPlanner _factionPlanner = new(s => s.Clone()); private static readonly GoapPlanner _shipPlanner = new(s => s.Clone()); private static readonly IReadOnlyList> _factionGoals = [ new ExpandTerritoryGoal(), new EnsureWarFleetGoal(), new EnsureMiningCapacityGoal(), new EnsureConstructionCapacityGoal(), ]; private static readonly IReadOnlyList> _shipActions = [ new SetMiningObjectiveAction(), new SetPatrolObjectiveAction(), new SetConstructionObjectiveAction(), new SetIdleObjectiveAction(), ]; private static readonly GoapGoal _shipGoal = new AssignObjectiveGoal(); private void UpdateCommanders(SimulationWorld world, float deltaSeconds, ICollection events) { // Faction commanders run first so their directives are available to ship commanders in the same tick. foreach (var commander in world.Commanders) { if (!commander.IsAlive || commander.Kind != CommanderKind.Faction) { continue; } TickCommander(commander, deltaSeconds); UpdateFactionCommander(world, commander); } foreach (var commander in world.Commanders) { if (!commander.IsAlive || commander.Kind != CommanderKind.Ship) { continue; } TickCommander(commander, deltaSeconds); UpdateShipCommander(world, commander); } } private static void TickCommander(CommanderRuntime commander, float deltaSeconds) { if (commander.ReplanTimer > 0f) { commander.ReplanTimer = MathF.Max(0f, commander.ReplanTimer - deltaSeconds); } } private void UpdateFactionCommander(SimulationWorld world, CommanderRuntime commander) { if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { return; } commander.ReplanTimer = FactionCommanderReplanInterval; commander.NeedsReplan = false; var state = BuildFactionPlanningState(world, commander.FactionId); var actions = BuildFactionActions(world); // Clear stale directives — actions will re-assert what is still needed. commander.ActiveDirectives.Clear(); var rankedGoals = _factionGoals .Select(g => (goal: g, priority: g.ComputePriority(state, world, commander))) .Where(x => x.priority > 0f) .OrderByDescending(x => x.priority) .ToList(); commander.LastPlanningState = state; commander.LastGoalPriorities = rankedGoals.Select(x => (x.goal.Name, x.priority)).ToList(); // Execute the first action of each active goal's plan (top 3 to avoid conflicts). foreach (var (goal, _) in rankedGoals.Take(3)) { var plan = _factionPlanner.Plan(state, goal, actions); plan?.CurrentAction?.Execute(this, world, commander); } } private void UpdateShipCommander(SimulationWorld world, CommanderRuntime commander) { if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { return; } commander.ReplanTimer = ShipCommanderReplanInterval; commander.NeedsReplan = false; var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId); if (ship is null) { return; } var state = BuildShipPlanningState(world, ship, commander); var plan = _shipPlanner.Plan(state, _shipGoal, _shipActions); if (plan?.CurrentAction is { } action) { commander.ActiveGoalName = _shipGoal.Name; commander.ActiveActionName = action.Name; action.Execute(this, world, commander); } } internal FactionPlanningState BuildFactionPlanningState(SimulationWorld world, string factionId) { var stations = world.Stations.Where(s => s.FactionId == factionId).ToList(); return new FactionPlanningState { MilitaryShipCount = world.Ships.Count(s => s.FactionId == factionId && string.Equals(s.Definition.Kind, "military", StringComparison.Ordinal)), MinerShipCount = world.Ships.Count(s => s.FactionId == factionId && string.Equals(s.Definition.Kind, "mining", StringComparison.Ordinal)), TransportShipCount = world.Ships.Count(s => s.FactionId == factionId && string.Equals(s.Definition.Kind, "transport", StringComparison.Ordinal)), ConstructorShipCount = world.Ships.Count(s => s.FactionId == factionId && string.Equals(s.Definition.Kind, "construction", StringComparison.Ordinal)), ControlledSystemCount = GetFactionControlledSystemsCount(world, factionId), TargetSystemCount = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count)), HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)), OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")), RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")), }; } private static ShipPlanningState BuildShipPlanningState( SimulationWorld world, ShipRuntime ship, CommanderRuntime commander) { var factionCommander = world.Commanders.FirstOrDefault(c => c.FactionId == commander.FactionId && string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal)); return new ShipPlanningState { ShipKind = ship.Definition.Kind, HasMiningCapability = HasShipCapabilities(ship.Definition, "mining"), FactionWantsOre = true, FactionWantsExpansion = factionCommander?.ActiveDirectives .Contains("expand-territory", StringComparer.Ordinal) ?? false, }; } private static IReadOnlyList> BuildFactionActions(SimulationWorld world) { var actions = new List>(); foreach (var (shipId, def) in world.ShipDefinitions) { actions.Add(new OrderShipProductionAction(def.Kind, shipId)); } actions.Add(new ExpandToSystemAction()); return actions; } internal static bool FactionCommanderHasDirective(SimulationWorld world, string factionId, string directive) => world.Commanders.FirstOrDefault(c => c.FactionId == factionId && string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal)) ?.ActiveDirectives.Contains(directive, StringComparer.Ordinal) ?? false; }