using SpaceGame.Api.Industry.Planning; using SpaceGame.Api.Stations.Simulation; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Factions.AI; internal sealed class CommanderPlanningService { private const float FactionCommanderReplanInterval = 8f; private const float FleetCommanderReplanInterval = 4f; private const float StationCommanderReplanInterval = 5f; private const float ShipCommanderReplanInterval = 3f; private const int MaxDecisionLogEntries = 40; private const int MaxOutcomeEntries = 32; private const int MaxAiOrdersPerShip = 2; internal void UpdateCommanders(SimulationWorld world, float deltaSeconds, ICollection events) { EnsureHierarchy(world); foreach (var commander in world.Commanders) { if (!commander.IsAlive) { continue; } if (commander.ReplanTimer > 0f) { commander.ReplanTimer = MathF.Max(0f, commander.ReplanTimer - deltaSeconds); } } foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Faction).ToList()) { if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) { continue; } if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { continue; } UpdateFactionCommander(world, commander, events); } foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Fleet).ToList()) { if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) { continue; } if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { continue; } UpdateFleetCommander(world, commander); } foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Station).ToList()) { if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) { continue; } if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { continue; } UpdateStationCommander(world, commander); } foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Ship).ToList()) { if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) { continue; } if (commander.ReplanTimer > 0f && !commander.NeedsReplan) { continue; } UpdateShipCommander(world, commander, events); } } internal static CommanderRuntime? FindFactionCommander(SimulationWorld world, string factionId) => world.Commanders.FirstOrDefault(commander => commander.Kind == CommanderKind.Faction && commander.FactionId == factionId); internal static FactionRuntime? FindFaction(SimulationWorld world, string factionId) => world.Factions.FirstOrDefault(faction => string.Equals(faction.Id, factionId, StringComparison.Ordinal)); internal static FactionStrategicStateRuntime? FindFactionStrategicState(SimulationWorld world, string factionId) => FindFaction(world, factionId)?.StrategicState; internal static FactionEconomicAssessmentRuntime? FindFactionEconomicAssessment(SimulationWorld world, string factionId) => FindFactionStrategicState(world, factionId)?.EconomicAssessment; internal static FactionThreatAssessmentRuntime? FindFactionThreatAssessment(SimulationWorld world, string factionId) => FindFactionStrategicState(world, factionId)?.ThreatAssessment; private static void EnsureHierarchy(SimulationWorld world) { var commandersById = world.Commanders.ToDictionary(commander => commander.Id, StringComparer.Ordinal); var factionCommanders = world.Commanders .Where(commander => commander.Kind == CommanderKind.Faction) .ToDictionary(commander => commander.FactionId, StringComparer.Ordinal); foreach (var faction in world.Factions) { EnsureFactionStateDefaults(world, faction); if (!factionCommanders.TryGetValue(faction.Id, out var commander)) { commander = new CommanderRuntime { Id = $"commander-faction-{faction.Id}", Kind = CommanderKind.Faction, FactionId = faction.Id, ControlledEntityId = faction.Id, PolicySetId = faction.DefaultPolicySetId, Doctrine = "strategic-control", Skills = new CommanderSkillProfileRuntime { Leadership = 5, Coordination = 4, Strategy = 5 }, }; world.Commanders.Add(commander); commandersById[commander.Id] = commander; factionCommanders[faction.Id] = commander; } } foreach (var commander in world.Commanders) { commander.SubordinateCommanderIds.Clear(); } var stationCommanders = new Dictionary(StringComparer.Ordinal); foreach (var station in world.Stations) { if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander)) { continue; } var commander = world.Commanders.FirstOrDefault(candidate => candidate.Kind == CommanderKind.Station && string.Equals(candidate.ControlledEntityId, station.Id, StringComparison.Ordinal)); if (commander is null) { commander = new CommanderRuntime { Id = $"commander-station-{station.Id}", Kind = CommanderKind.Station, FactionId = station.FactionId, ControlledEntityId = station.Id, Doctrine = "station-control", Skills = new CommanderSkillProfileRuntime { Leadership = 3, Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5), Strategy = 3, }, }; world.Commanders.Add(commander); } commander.ParentCommanderId = parentCommander.Id; commander.PolicySetId = parentCommander.PolicySetId; station.CommanderId = commander.Id; station.PolicySetId ??= parentCommander.PolicySetId; stationCommanders[station.Id] = commander; } foreach (var ship in world.Ships) { if (!factionCommanders.TryGetValue(ship.FactionId, out var factionCommander)) { continue; } var commander = world.Commanders.FirstOrDefault(candidate => candidate.Kind == CommanderKind.Ship && string.Equals(candidate.ControlledEntityId, ship.Id, StringComparison.Ordinal)); if (commander is null) { commander = new CommanderRuntime { Id = $"commander-ship-{ship.Id}", Kind = CommanderKind.Ship, FactionId = ship.FactionId, ControlledEntityId = ship.Id, Doctrine = "ship-control", Skills = new CommanderSkillProfileRuntime { Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5), Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5), Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5), }, }; world.Commanders.Add(commander); } var parentCommander = ResolveShipParentCommander(world, ship, factionCommander, stationCommanders); commander.ParentCommanderId = parentCommander.Id; commander.PolicySetId = parentCommander.PolicySetId; ship.CommanderId = commander.Id; ship.PolicySetId ??= parentCommander.PolicySetId; } foreach (var commander in world.Commanders) { if (commander.ParentCommanderId is not null && commandersById.TryGetValue(commander.ParentCommanderId, out var parent)) { parent.SubordinateCommanderIds.Add(commander.Id); } } foreach (var faction in world.Factions) { faction.CommanderIds.Clear(); } foreach (var commander in world.Commanders) { if (world.Factions.FirstOrDefault(faction => faction.Id == commander.FactionId) is { } faction) { faction.CommanderIds.Add(commander.Id); } } } private static void EnsureFactionStateDefaults(SimulationWorld world, FactionRuntime faction) { faction.Doctrine.StrategicPosture = string.IsNullOrWhiteSpace(faction.Doctrine.StrategicPosture) ? "balanced" : faction.Doctrine.StrategicPosture; faction.Doctrine.ExpansionPosture = string.IsNullOrWhiteSpace(faction.Doctrine.ExpansionPosture) ? "measured" : faction.Doctrine.ExpansionPosture; faction.Doctrine.MilitaryPosture = string.IsNullOrWhiteSpace(faction.Doctrine.MilitaryPosture) ? "defensive" : faction.Doctrine.MilitaryPosture; faction.Doctrine.EconomicPosture = string.IsNullOrWhiteSpace(faction.Doctrine.EconomicPosture) ? "self-sufficient" : faction.Doctrine.EconomicPosture; faction.Doctrine.DesiredControlledSystems = Math.Max(2, Math.Min(world.Systems.Count, faction.Doctrine.DesiredControlledSystems <= 0 ? 3 : faction.Doctrine.DesiredControlledSystems)); faction.Doctrine.DesiredMilitaryPerFront = Math.Max(1, faction.Doctrine.DesiredMilitaryPerFront); faction.Doctrine.DesiredMinersPerSystem = Math.Max(1, faction.Doctrine.DesiredMinersPerSystem); faction.Doctrine.DesiredTransportsPerSystem = Math.Max(1, faction.Doctrine.DesiredTransportsPerSystem); faction.Doctrine.DesiredConstructors = Math.Max(1, faction.Doctrine.DesiredConstructors); faction.Doctrine.ReserveCreditsRatio = ClampRatio(faction.Doctrine.ReserveCreditsRatio, 0.2f); faction.Doctrine.ExpansionBudgetRatio = ClampRatio(faction.Doctrine.ExpansionBudgetRatio, 0.25f); faction.Doctrine.WarBudgetRatio = ClampRatio(faction.Doctrine.WarBudgetRatio, 0.35f); faction.Doctrine.ReserveMilitaryRatio = ClampRatio(faction.Doctrine.ReserveMilitaryRatio, 0.2f); faction.Doctrine.OffensiveReadinessThreshold = ClampRatio(faction.Doctrine.OffensiveReadinessThreshold, 0.62f); faction.Doctrine.SupplySecurityBias = ClampRatio(faction.Doctrine.SupplySecurityBias, 0.55f); faction.Doctrine.FailureAversion = ClampRatio(faction.Doctrine.FailureAversion, 0.45f); faction.Doctrine.ReinforcementLeadPerFront = Math.Max(1, faction.Doctrine.ReinforcementLeadPerFront); } private static float ClampRatio(float value, float fallback) => value is >= 0f and <= 1f ? value : fallback; private static CommanderRuntime ResolveShipParentCommander( SimulationWorld world, ShipRuntime ship, CommanderRuntime factionCommander, IReadOnlyDictionary stationCommanders) { if (string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)) { return factionCommander; } var stationCommander = world.Stations .Where(station => string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)) .OrderByDescending(station => station.SystemId == ship.SystemId ? 1 : 0) .ThenBy(station => station.Position.DistanceTo(ship.Position)) .Select(station => station.CommanderId is not null && stationCommanders.TryGetValue(station.Id, out var commander) ? commander : null) .FirstOrDefault(candidate => candidate is not null); return stationCommander ?? factionCommander; } private static void UpdateFactionCommander(SimulationWorld world, CommanderRuntime commander, ICollection events) { var faction = FindFaction(world, commander.FactionId); if (faction is null) { commander.IsAlive = false; return; } commander.ReplanTimer = FactionCommanderReplanInterval; commander.NeedsReplan = false; commander.PlanningCycle += 1; EnsureFactionStateDefaults(world, faction); var nowUtc = DateTimeOffset.UtcNow; var previousTheaters = faction.StrategicState.Theaters.ToDictionary(theater => theater.Id, StringComparer.Ordinal); var previousCampaigns = faction.StrategicState.Campaigns.ToDictionary(campaign => campaign.Id, StringComparer.Ordinal); var previousObjectives = faction.StrategicState.Objectives.ToDictionary(objective => objective.Id, StringComparer.Ordinal); var previousPrograms = faction.StrategicState.ProductionPrograms.ToDictionary(program => program.Id, StringComparer.Ordinal); var economy = FactionEconomyAnalyzer.Build(world, faction.Id); var expansionProject = ResolveExpansionProject(world, faction); var threatAssessment = BuildThreatAssessment(world, faction, commander, nowUtc); var economicAssessment = BuildEconomicAssessment(world, faction, commander, economy, expansionProject, threatAssessment, nowUtc); UpdateDoctrine(world, faction, threatAssessment, economicAssessment, expansionProject); UpdateBudget(faction, threatAssessment, economicAssessment); UpdateMemory(world, faction, threatAssessment, economicAssessment, nowUtc); if (expansionProject is not null && economicAssessment.ConstructorShipCount > 0 && faction.StrategicState.Budget.ExpansionCredits > 0f) { FactionIndustryPlanner.EnsureExpansionSite(world, faction.Id, expansionProject); expansionProject = FactionIndustryPlanner.GetActiveExpansionProject(world, faction.Id) ?? expansionProject; economicAssessment.PrimaryExpansionSiteId = expansionProject.SiteId; economicAssessment.PrimaryExpansionSystemId = expansionProject.SystemId; } var theaters = BuildTheaters(world, faction, threatAssessment, economicAssessment, expansionProject, nowUtc); var campaigns = BuildCampaigns(world, faction, theaters, threatAssessment, economicAssessment, expansionProject, previousCampaigns, nowUtc); var theatersById = theaters.ToDictionary(theater => theater.Id, StringComparer.Ordinal); foreach (var campaign in campaigns) { if (campaign.TheaterId is not null && theatersById.TryGetValue(campaign.TheaterId, out var theater)) { theater.CampaignIds.Add(campaign.Id); } } var objectives = BuildObjectives(world, faction, theaters, campaigns, economicAssessment, threatAssessment, expansionProject, previousObjectives, nowUtc); var reservations = BuildReservations(world, faction, objectives, nowUtc); var programs = BuildProductionPrograms(faction, theaters, campaigns, economicAssessment, threatAssessment, expansionProject, previousPrograms); ReconcileCampaignLifecycle(world, faction, previousCampaigns, campaigns, economicAssessment, threatAssessment, nowUtc); ReconcileObjectiveLifecycle(faction, previousObjectives, objectives, nowUtc); ReconcileTheaterLifecycle(faction, previousTheaters, theaters, nowUtc); ReconcileProgramLifecycle(faction, previousPrograms, programs, nowUtc); faction.Memory.LastPlanCycle = commander.PlanningCycle; faction.Memory.UpdatedAtUtc = nowUtc; faction.StrategicState.PlanCycle = commander.PlanningCycle; faction.StrategicState.UpdatedAtUtc = nowUtc; faction.StrategicState.Status = ResolveStrategicStatus(theaters, campaigns, economicAssessment, threatAssessment); faction.StrategicState.EconomicAssessment = economicAssessment; faction.StrategicState.ThreatAssessment = threatAssessment; faction.StrategicState.Theaters.Clear(); faction.StrategicState.Theaters.AddRange(theaters); faction.StrategicState.Campaigns.Clear(); faction.StrategicState.Campaigns.AddRange(campaigns); faction.StrategicState.Objectives.Clear(); faction.StrategicState.Objectives.AddRange(objectives); faction.StrategicState.Reservations.Clear(); faction.StrategicState.Reservations.AddRange(reservations); faction.StrategicState.ProductionPrograms.Clear(); faction.StrategicState.ProductionPrograms.AddRange(programs); ApplyDelegation(world, faction, commander, events, nowUtc); } private static void UpdateStationCommander(SimulationWorld world, CommanderRuntime commander) { commander.ReplanTimer = StationCommanderReplanInterval; commander.NeedsReplan = false; commander.PlanningCycle += 1; commander.ActiveObjectiveIds.Clear(); var station = world.Stations.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId); if (station is null) { commander.IsAlive = false; commander.Assignment = null; return; } var faction = FindFaction(world, commander.FactionId); if (faction is null) { commander.Assignment = null; return; } var objective = faction.StrategicState.Objectives .Where(candidate => string.Equals(candidate.CommanderId, commander.Id, StringComparison.Ordinal)) .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); if (objective is not null) { commander.ActiveObjectiveIds.Add(objective.Id); commander.Assignment = ToAssignment(objective); return; } var activeSite = world.ConstructionSites .Where(site => site.FactionId == station.FactionId && site.State is ConstructionSiteStateKinds.Active or ConstructionSiteStateKinds.Planned && (string.Equals(site.StationId, station.Id, StringComparison.Ordinal) || string.Equals(site.SystemId, station.SystemId, StringComparison.Ordinal))) .OrderByDescending(site => string.Equals(site.StationId, station.Id, StringComparison.Ordinal) ? 1 : 0) .ThenBy(site => site.Id, StringComparer.Ordinal) .FirstOrDefault(); var strategicAssignment = BuildStationFocusAssignment(world, faction, station, activeSite); commander.Assignment = strategicAssignment; } private static void UpdateShipCommander(SimulationWorld world, CommanderRuntime commander, ICollection events) { commander.ReplanTimer = ShipCommanderReplanInterval; commander.NeedsReplan = false; commander.PlanningCycle += 1; commander.ActiveObjectiveIds.Clear(); var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId); if (ship is null) { commander.IsAlive = false; commander.Assignment = null; return; } var faction = FindFaction(world, commander.FactionId); if (faction is null) { commander.Assignment = null; return; } var assignedObjective = faction.StrategicState.Objectives .Where(candidate => string.Equals(candidate.CommanderId, commander.Id, StringComparison.Ordinal)) .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); var nextAssignment = assignedObjective is null ? null : ToAssignment(assignedObjective); if (assignedObjective is not null) { commander.ActiveObjectiveIds.Add(assignedObjective.Id); } if (!AssignmentsEqual(commander.Assignment, nextAssignment)) { commander.Assignment = nextAssignment; ship.NeedsReplan = true; events.Add(new SimulationEventRecord( "ship", ship.Id, nextAssignment is null ? "assignment-cleared" : "assignment-updated", nextAssignment is null ? $"{ship.Definition.Label} returned to default behavior." : $"{ship.Definition.Label} assigned to {nextAssignment.Kind}.", DateTimeOffset.UtcNow)); } } private static IndustryExpansionProject? ResolveExpansionProject(SimulationWorld world, FactionRuntime faction) => FactionIndustryPlanner.GetActiveExpansionProject(world, faction.Id) ?? FactionIndustryPlanner.AnalyzeExpansionNeed(world, faction.Id) ?? FactionIndustryPlanner.AnalyzeShipyardNeed(world, faction.Id); private static FactionThreatAssessmentRuntime BuildThreatAssessment( SimulationWorld world, FactionRuntime faction, CommanderRuntime commander, DateTimeOffset nowUtc) { var assessment = new FactionThreatAssessmentRuntime { PlanCycle = commander.PlanningCycle, UpdatedAtUtc = nowUtc, EnemyFactionCount = world.Factions.Count(candidate => candidate.Id != faction.Id && GeopoliticalSimulationService.HasHostileRelation(world, faction.Id, candidate.Id)), EnemyShipCount = world.Ships.Count(ship => ship.Health > 0f && ship.FactionId != faction.Id && GeopoliticalSimulationService.HasHostileRelation(world, faction.Id, ship.FactionId)), EnemyStationCount = world.Stations.Count(station => station.FactionId != faction.Id && GeopoliticalSimulationService.HasHostileRelation(world, faction.Id, station.FactionId)), }; var controlledSystems = GeopoliticalSimulationService.GetControlledSystems(world, faction.Id) .ToHashSet(StringComparer.Ordinal); var factionStationSystems = world.Stations .Where(station => station.FactionId == faction.Id) .Select(station => station.SystemId) .ToHashSet(StringComparer.Ordinal); var borderTensions = world.Geopolitics?.Diplomacy.BorderTensions .Where(tension => string.Equals(tension.FactionAId, faction.Id, StringComparison.Ordinal) || string.Equals(tension.FactionBId, faction.Id, StringComparison.Ordinal)) .ToList() ?? []; var activeWars = world.Geopolitics?.Diplomacy.Wars .Where(war => war.Status == "active" && (string.Equals(war.FactionAId, faction.Id, StringComparison.Ordinal) || string.Equals(war.FactionBId, faction.Id, StringComparison.Ordinal))) .ToList() ?? []; var threatSignals = world.Systems .Select(system => { var controlState = GeopoliticalSimulationService.GetSystemControlState(world, system.Definition.Id); var strategicProfile = FindStrategicProfile(world, system.Definition.Id); var territoryPressure = FindTerritoryPressure(world, faction.Id, system.Definition.Id); var systemTensions = borderTensions .Where(tension => tension.SystemIds.Contains(system.Definition.Id, StringComparer.Ordinal)) .ToList(); var enemyShips = world.Ships .Where(ship => ship.Health > 0f && ship.FactionId != faction.Id && GeopoliticalSimulationService.HasHostileRelation(world, faction.Id, ship.FactionId) && ship.SystemId == system.Definition.Id) .ToList(); var enemyStations = world.Stations .Where(station => station.FactionId != faction.Id && GeopoliticalSimulationService.HasHostileRelation(world, faction.Id, station.FactionId) && station.SystemId == system.Definition.Id) .ToList(); if (enemyShips.Count == 0 && enemyStations.Count == 0 && systemTensions.Count == 0 && (territoryPressure?.PressureScore ?? 0f) < 0.08f) { return null; } var primaryEnemyFactionId = enemyShips .GroupBy(ship => ship.FactionId, StringComparer.Ordinal) .Select(group => (FactionId: group.Key, Weight: group.Count() * 10)) .Concat(enemyStations .GroupBy(station => station.FactionId, StringComparer.Ordinal) .Select(group => (FactionId: group.Key, Weight: group.Count() * 25))) .GroupBy(entry => entry.FactionId, StringComparer.Ordinal) .OrderByDescending(group => group.Sum(entry => entry.Weight)) .ThenBy(group => group.Key, StringComparer.Ordinal) .Select(group => group.Key) .FirstOrDefault(); var warBias = activeWars.Any(war => string.Equals(war.FactionAId, primaryEnemyFactionId, StringComparison.Ordinal) || string.Equals(war.FactionBId, primaryEnemyFactionId, StringComparison.Ordinal)) ? 24f : 0f; var priorityBias = controlledSystems.Contains(system.Definition.Id) ? 40 : factionStationSystems.Contains(system.Definition.Id) ? 25 : 0; var diplomacyBias = systemTensions.Sum(tension => tension.TensionScore * 26f) + systemTensions.Sum(tension => tension.AccessFriction * 12f); var territoryBias = (territoryPressure?.PressureScore ?? 0f) * 35f + ((strategicProfile?.IsContested ?? false) ? 24f : 0f) + ((strategicProfile?.ZoneKind == "frontier" ? 1f : 0f) * 12f); var scopeKind = controlState?.IsContested == true || (factionStationSystems.Contains(system.Definition.Id) && !controlledSystems.Contains(system.Definition.Id)) ? "contested-system" : controlledSystems.Contains(system.Definition.Id) ? "controlled-system" : "hostile-system"; return new { Signal = new FactionThreatSignalRuntime { ScopeId = system.Definition.Id, ScopeKind = scopeKind, EnemyShipCount = enemyShips.Count, EnemyStationCount = enemyStations.Count, EnemyFactionId = primaryEnemyFactionId, }, Score = (enemyStations.Count * 30) + (enemyShips.Count * 10) + priorityBias + diplomacyBias + territoryBias + warBias, }; }) .Where(entry => entry is not null) .Select(entry => entry!) .OrderByDescending(entry => entry.Score) .ThenBy(entry => entry.Signal.ScopeId, StringComparer.Ordinal) .ToList(); assessment.ThreatSignals.AddRange(threatSignals.Select(entry => entry.Signal)); assessment.PrimaryThreatSystemId = threatSignals.FirstOrDefault()?.Signal.ScopeId; assessment.PrimaryThreatFactionId = threatSignals.FirstOrDefault()?.Signal.EnemyFactionId; return assessment; } private static FactionEconomicAssessmentRuntime BuildEconomicAssessment( SimulationWorld world, FactionRuntime faction, CommanderRuntime commander, FactionEconomySnapshot economy, IndustryExpansionProject? expansionProject, FactionThreatAssessmentRuntime threatAssessment, DateTimeOffset nowUtc) { var controlledSystems = StationSimulationService.GetFactionControlledSystemsCount(world, faction.Id); var frontCount = Math.Max(1, threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind is "controlled-system" or "contested-system") + (expansionProject is null ? 0 : 1)); var militaryShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "military"); var minerShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && HasShipCapabilities(ship.Definition, "mining")); var transportShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "transport"); var constructorShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "construction"); var hasShipyard = world.Stations.Any(station => string.Equals(station.FactionId, faction.Id, StringComparison.Ordinal) && station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)); var hasWarIndustrySupplyChain = HasOperationalWarIndustry(economy); var factionRegions = world.Geopolitics?.EconomyRegions.Regions .Where(region => string.Equals(region.FactionId, faction.Id, StringComparison.Ordinal)) .ToList() ?? []; var regionSecurity = world.Geopolitics?.EconomyRegions.SecurityAssessments .Where(assessment => factionRegions.Any(region => region.Id == assessment.RegionId)) .ToList() ?? []; var regionEconomics = world.Geopolitics?.EconomyRegions.EconomicAssessments .Where(assessment => factionRegions.Any(region => region.Id == assessment.RegionId)) .ToList() ?? []; var regionalBottlenecks = world.Geopolitics?.EconomyRegions.Bottlenecks .Where(bottleneck => factionRegions.Any(region => region.Id == bottleneck.RegionId)) .ToList() ?? []; var corridorRisk = world.Geopolitics?.EconomyRegions.Corridors .Where(corridor => string.Equals(corridor.FactionId, faction.Id, StringComparison.Ordinal)) .DefaultIfEmpty() .Average(corridor => corridor?.RiskScore ?? 0f) ?? 0f; var regionalSupplyRisk = regionSecurity.Count == 0 ? 0f : regionSecurity.Average(assessment => assessment.SupplyRisk); var regionalSustainment = regionEconomics.Count == 0 ? 1f : regionEconomics.Average(assessment => assessment.SustainmentScore); var assessment = new FactionEconomicAssessmentRuntime { PlanCycle = commander.PlanningCycle, UpdatedAtUtc = nowUtc, MilitaryShipCount = militaryShipCount, MinerShipCount = minerShipCount, TransportShipCount = transportShipCount, ConstructorShipCount = constructorShipCount, ControlledSystemCount = controlledSystems, TargetMilitaryShipCount = Math.Max(frontCount * faction.Doctrine.DesiredMilitaryPerFront, controlledSystems + 2), TargetMinerShipCount = Math.Max(controlledSystems * faction.Doctrine.DesiredMinersPerSystem, 2), TargetTransportShipCount = Math.Max(controlledSystems * faction.Doctrine.DesiredTransportsPerSystem, 2), TargetConstructorShipCount = faction.Doctrine.DesiredConstructors, HasShipyard = hasShipyard, HasWarIndustrySupplyChain = hasWarIndustrySupplyChain, PrimaryExpansionSiteId = expansionProject?.SiteId, PrimaryExpansionSystemId = expansionProject?.SystemId, ReplacementPressure = MathF.Max(0f, (faction.ShipsLost - faction.Memory.LastObservedShipsLost) * 6f) + MathF.Max(0f, frontCount - Math.Max(1, militaryShipCount)) + (regionalSupplyRisk * 4f), }; assessment.CommoditySignals.AddRange( economy.Commodities .OrderBy(entry => entry.Key, StringComparer.Ordinal) .Select(entry => new FactionCommoditySignalRuntime { ItemId = entry.Key, AvailableStock = entry.Value.AvailableStock, OnHand = entry.Value.OnHand, ProductionRatePerSecond = entry.Value.ProductionRatePerSecond, CommittedProductionRatePerSecond = entry.Value.CommittedProductionRatePerSecond, UsageRatePerSecond = entry.Value.OperationalUsageRatePerSecond, NetRatePerSecond = entry.Value.NetRatePerSecond, ProjectedNetRatePerSecond = entry.Value.ProjectedNetRatePerSecond, LevelSeconds = entry.Value.LevelSeconds, Level = entry.Value.Level.ToString().ToLowerInvariant(), ProjectedProductionRatePerSecond = entry.Value.ProjectedProductionRatePerSecond, BuyBacklog = entry.Value.BuyBacklog, ReservedForConstruction = entry.Value.ReservedForConstruction, })); assessment.CriticalShortageCount = assessment.CommoditySignals.Count(signal => signal.Level is "critical" or "low") + regionalBottlenecks.Count(bottleneck => bottleneck.Severity >= 2.5f); assessment.IndustrialBottleneckItemId = assessment.CommoditySignals .OrderByDescending(ComputeCommodityPriority) .ThenBy(signal => signal.ItemId, StringComparer.Ordinal) .Select(signal => signal.ItemId) .FirstOrDefault() ?? regionalBottlenecks .OrderByDescending(bottleneck => bottleneck.Severity) .ThenBy(bottleneck => bottleneck.ItemId, StringComparer.Ordinal) .Select(bottleneck => bottleneck.ItemId) .FirstOrDefault(); if (assessment.PrimaryExpansionSystemId is null) { assessment.PrimaryExpansionSystemId = factionRegions .Join(regionEconomics, region => region.Id, economicState => economicState.RegionId, (region, economicState) => new { region, economicState }) .OrderByDescending(entry => entry.economicState.ConstructionPressure) .ThenByDescending(entry => entry.economicState.CorridorDependency) .ThenBy(entry => entry.region.Id, StringComparer.Ordinal) .Select(entry => entry.region.CoreSystemId) .FirstOrDefault(); } var transportCoverage = assessment.TargetTransportShipCount <= 0 ? 1f : Math.Clamp(assessment.TransportShipCount / (float)assessment.TargetTransportShipCount, 0f, 1.35f); var minerCoverage = assessment.TargetMinerShipCount <= 0 ? 1f : Math.Clamp(assessment.MinerShipCount / (float)assessment.TargetMinerShipCount, 0f, 1.35f); var constructorCoverage = assessment.TargetConstructorShipCount <= 0 ? 1f : Math.Clamp(assessment.ConstructorShipCount / (float)assessment.TargetConstructorShipCount, 0f, 1.35f); var shortagePenalty = MathF.Min(0.55f, assessment.CriticalShortageCount * 0.08f); var replacementPenalty = MathF.Min(0.45f, assessment.ReplacementPressure / MathF.Max(12f, assessment.TargetMilitaryShipCount * 8f)); assessment.LogisticsSecurityScore = Math.Clamp( (transportCoverage * 0.45f) + (minerCoverage * 0.2f) + (constructorCoverage * 0.1f) + (hasWarIndustrySupplyChain ? 0.2f : 0.05f) - shortagePenalty - (regionalSupplyRisk * 0.3f) - (corridorRisk * 0.18f), 0f, 1f); assessment.SustainmentScore = Math.Clamp( (assessment.LogisticsSecurityScore * 0.55f) + ((hasShipyard ? 0.15f : 0f) + (hasWarIndustrySupplyChain ? 0.15f : 0f)) + ((assessment.MilitaryShipCount >= assessment.TargetMilitaryShipCount ? 0.2f : 0.08f)) + (regionalSustainment * 0.18f) - replacementPenalty, 0f, 1f); return assessment; } private static bool HasOperationalWarIndustry(FactionEconomySnapshot economy) { var energy = economy.GetCommodity("energycells"); var refined = economy.GetCommodity("refinedmetals"); var hullparts = economy.GetCommodity("hullparts"); var claytronics = economy.GetCommodity("claytronics"); return CommodityOperationalSignal.IsOperational(energy, 180f) && CommodityOperationalSignal.IsOperational(refined, 180f) && CommodityOperationalSignal.IsOperational(hullparts, 180f) && CommodityOperationalSignal.IsOperational(claytronics, 180f); } private static void UpdateDoctrine( SimulationWorld world, FactionRuntime faction, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment, IndustryExpansionProject? expansionProject) { var controlledThreats = threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind == "controlled-system"); var contestedThreats = threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind == "contested-system"); var expansionPressure = StationSimulationService.GetFactionExpansionPressure(world, faction.Id); var shortagePressure = economicAssessment.CriticalShortageCount; var activeWars = world.Geopolitics?.Diplomacy.Wars.Count(war => war.Status == "active" && (string.Equals(war.FactionAId, faction.Id, StringComparison.Ordinal) || string.Equals(war.FactionBId, faction.Id, StringComparison.Ordinal))) ?? 0; var borderTension = world.Geopolitics?.Diplomacy.BorderTensions .Where(tension => string.Equals(tension.FactionAId, faction.Id, StringComparison.Ordinal) || string.Equals(tension.FactionBId, faction.Id, StringComparison.Ordinal)) .DefaultIfEmpty() .Average(tension => tension?.TensionScore ?? 0f) ?? 0f; faction.Doctrine.StrategicPosture = activeWars > 0 || controlledThreats > 1 ? "fortify-and-recover" : controlledThreats > 0 || borderTension > 0.45f ? expansionProject is null ? "contested" : "defend-and-delay" : expansionProject is not null && economicAssessment.SustainmentScore >= faction.Doctrine.OffensiveReadinessThreshold ? "growth-through-pressure" : shortagePressure > 0 ? "economic-recovery" : "stable-growth"; faction.Doctrine.MilitaryPosture = activeWars + controlledThreats + contestedThreats switch { >= 3 => "mobilized", > 0 when economicAssessment.SustainmentScore >= faction.Doctrine.OffensiveReadinessThreshold && economicAssessment.MilitaryShipCount >= economicAssessment.TargetMilitaryShipCount => "counteroffensive", > 0 => "defensive", _ => economicAssessment.MilitaryShipCount > economicAssessment.TargetMilitaryShipCount && economicAssessment.SustainmentScore >= faction.Doctrine.OffensiveReadinessThreshold ? "expeditionary" : "defensive", }; faction.Doctrine.ExpansionPosture = expansionProject is not null ? controlledThreats > 0 || economicAssessment.SustainmentScore < 0.58f ? "cautious" : "active" : expansionPressure > 0.2f && economicAssessment.SustainmentScore >= 0.55f ? "measured" : "consolidating"; faction.Doctrine.EconomicPosture = shortagePressure >= 3 ? "stabilizing" : economicAssessment.HasWarIndustrySupplyChain && economicAssessment.SustainmentScore >= 0.7f ? "surplus" : "self-sufficient"; } private static void UpdateBudget( FactionRuntime faction, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment) { var reserveCredits = faction.Credits * faction.Doctrine.ReserveCreditsRatio; var discretionary = MathF.Max(0f, faction.Credits - reserveCredits); var warRatio = threatAssessment.ThreatSignals.Count > 0 ? MathF.Max(faction.Doctrine.WarBudgetRatio, 0.4f) : faction.Doctrine.WarBudgetRatio * 0.5f; var expansionRatio = economicAssessment.PrimaryExpansionSystemId is not null ? MathF.Max(faction.Doctrine.ExpansionBudgetRatio, 0.25f) : faction.Doctrine.ExpansionBudgetRatio * 0.5f; faction.StrategicState.Budget = new FactionBudgetRuntime { ReservedCredits = reserveCredits, WarCredits = discretionary * Math.Clamp(warRatio, 0f, 1f), ExpansionCredits = discretionary * Math.Clamp(expansionRatio, 0f, 1f), ReservedMilitaryAssets = Math.Max(1, (int)MathF.Ceiling(economicAssessment.TargetMilitaryShipCount * faction.Doctrine.ReserveMilitaryRatio)), ReservedLogisticsAssets = Math.Max(economicAssessment.TargetTransportShipCount, economicAssessment.TransportShipCount), ReservedConstructionAssets = Math.Max(economicAssessment.TargetConstructorShipCount, economicAssessment.ConstructorShipCount), }; } private static void UpdateMemory( SimulationWorld world, FactionRuntime faction, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment, DateTimeOffset nowUtc) { foreach (var station in world.Stations.Where(station => station.FactionId == faction.Id)) { faction.Memory.KnownSystemIds.Add(station.SystemId); } foreach (var ship in world.Ships.Where(ship => ship.FactionId == faction.Id)) { faction.Memory.KnownSystemIds.Add(ship.SystemId); } foreach (var signal in threatAssessment.ThreatSignals) { faction.Memory.KnownSystemIds.Add(signal.ScopeId); if (!string.IsNullOrWhiteSpace(signal.EnemyFactionId)) { faction.Memory.KnownEnemyFactionIds.Add(signal.EnemyFactionId); } var systemMemory = faction.Memory.SystemMemories.FirstOrDefault(candidate => candidate.SystemId == signal.ScopeId); if (systemMemory is null) { systemMemory = new FactionSystemMemoryRuntime { SystemId = signal.ScopeId }; faction.Memory.SystemMemories.Add(systemMemory); } systemMemory.LastSeenAtUtc = nowUtc; systemMemory.LastEnemyShipCount = signal.EnemyShipCount; systemMemory.LastEnemyStationCount = signal.EnemyStationCount; systemMemory.ControlledByFaction = signal.ScopeKind == "controlled-system"; systemMemory.LastRole = signal.ScopeKind; var strategicProfile = FindStrategicProfile(world, signal.ScopeId); var territoryPressure = FindTerritoryPressure(world, faction.Id, signal.ScopeId); systemMemory.FrontierPressure = ((signal.EnemyStationCount * 0.9f) + (signal.EnemyShipCount * 0.35f)) + ((territoryPressure?.PressureScore ?? 0f) * 1.4f) + ((strategicProfile?.ZoneKind == "frontier" ? 1f : 0f) * 0.35f); systemMemory.RouteRisk = MathF.Max( GeopoliticalSimulationService.GetSystemRouteRisk(world, signal.ScopeId, faction.Id), signal.ScopeKind is "controlled-system" or "contested-system" ? MathF.Min(1f, (signal.EnemyShipCount * 0.08f) + (signal.EnemyStationCount * 0.16f)) : MathF.Min(1f, (signal.EnemyShipCount * 0.05f) + (signal.EnemyStationCount * 0.1f))); if (signal.ScopeKind is "controlled-system" or "contested-system") { systemMemory.LastContestedAtUtc = nowUtc; } } foreach (var systemId in faction.Memory.KnownSystemIds.OrderBy(id => id, StringComparer.Ordinal)) { var systemMemory = faction.Memory.SystemMemories.FirstOrDefault(candidate => candidate.SystemId == systemId); if (systemMemory is not null) { continue; } faction.Memory.SystemMemories.Add(new FactionSystemMemoryRuntime { SystemId = systemId, LastSeenAtUtc = nowUtc, ControlledByFaction = FactionControlsSystem(world, faction.Id, systemId), LastRole = FactionControlsSystem(world, faction.Id, systemId) ? "controlled-system" : "observed-system", RouteRisk = GeopoliticalSimulationService.GetSystemRouteRisk(world, systemId, faction.Id), }); } foreach (var zone in world.Geopolitics?.Territory.Zones.Where(zone => string.Equals(zone.FactionId, faction.Id, StringComparison.Ordinal)) ?? []) { faction.Memory.KnownSystemIds.Add(zone.SystemId); var systemMemory = faction.Memory.SystemMemories.FirstOrDefault(candidate => candidate.SystemId == zone.SystemId); if (systemMemory is null) { continue; } systemMemory.LastRole = zone.Kind switch { "contested" => "contested-system", "frontier" => "controlled-system", "corridor" => "controlled-system", _ => systemMemory.LastRole, }; systemMemory.RouteRisk = MathF.Max(systemMemory.RouteRisk, GeopoliticalSimulationService.GetSystemRouteRisk(world, zone.SystemId, faction.Id)); } foreach (var systemMemory in faction.Memory.SystemMemories) { if (threatAssessment.ThreatSignals.All(signal => signal.ScopeId != systemMemory.SystemId)) { systemMemory.FrontierPressure *= 0.92f; systemMemory.RouteRisk *= 0.9f; } } foreach (var signal in economicAssessment.CommoditySignals) { var commodityMemory = faction.Memory.CommodityMemories.FirstOrDefault(candidate => candidate.ItemId == signal.ItemId); if (commodityMemory is null) { commodityMemory = new FactionCommodityMemoryRuntime { ItemId = signal.ItemId }; faction.Memory.CommodityMemories.Add(commodityMemory); } commodityMemory.LastObservedBacklog = signal.BuyBacklog; commodityMemory.UpdatedAtUtc = nowUtc; commodityMemory.HistoricalShortageScore = (commodityMemory.HistoricalShortageScore * 0.85f) + (signal.Level is "critical" ? 1.2f : signal.Level is "low" ? 0.65f : 0f) + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 0.1f); commodityMemory.HistoricalSurplusScore = (commodityMemory.HistoricalSurplusScore * 0.85f) + (signal.Level == "surplus" ? 0.4f : 0f); if (signal.Level is "critical" or "low") { commodityMemory.LastCriticalAtUtc = nowUtc; } var impactedSystems = world.Stations .Where(station => station.FactionId == faction.Id && station.MarketOrderIds.Any(orderId => world.MarketOrders.Any(order => order.Id == orderId && order.Kind == MarketOrderKinds.Buy && order.ItemId == signal.ItemId && order.RemainingAmount > 0.01f))) .Select(station => station.SystemId) .Distinct(StringComparer.Ordinal) .ToList(); foreach (var systemId in impactedSystems) { var systemMemory = faction.Memory.SystemMemories.FirstOrDefault(candidate => candidate.SystemId == systemId); if (systemMemory is null) { continue; } systemMemory.HistoricalShortagePressure = (systemMemory.HistoricalShortagePressure * 0.82f) + (signal.Level is "critical" ? 0.7f : signal.Level is "low" ? 0.35f : 0f); if (signal.Level is "critical" or "low") { systemMemory.LastShortageAtUtc = nowUtc; } } } faction.Memory.SystemMemories.Sort((left, right) => string.Compare(left.SystemId, right.SystemId, StringComparison.Ordinal)); faction.Memory.CommodityMemories.Sort((left, right) => string.Compare(left.ItemId, right.ItemId, StringComparison.Ordinal)); if (faction.ShipsBuilt != faction.Memory.LastObservedShipsBuilt) { AppendOutcome(faction, new FactionOutcomeRecordRuntime { Id = $"outcome-built-{faction.Memory.LastPlanCycle}-{faction.ShipsBuilt}", Kind = "ships-built", Summary = $"{faction.Label} has built {faction.ShipsBuilt} ships.", OccurredAtUtc = nowUtc, }); faction.Memory.LastObservedShipsBuilt = faction.ShipsBuilt; } if (faction.ShipsLost != faction.Memory.LastObservedShipsLost) { AppendOutcome(faction, new FactionOutcomeRecordRuntime { Id = $"outcome-lost-{faction.Memory.LastPlanCycle}-{faction.ShipsLost}", Kind = "ships-lost", Summary = $"{faction.Label} has lost {faction.ShipsLost} ships.", OccurredAtUtc = nowUtc, }); faction.Memory.LastObservedShipsLost = faction.ShipsLost; } faction.Memory.LastObservedCredits = faction.Credits; } private static List BuildTheaters( SimulationWorld world, FactionRuntime faction, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment, IndustryExpansionProject? expansionProject, DateTimeOffset nowUtc) { var theaters = new List(); var systemMemories = faction.Memory.SystemMemories.ToDictionary(memory => memory.SystemId, StringComparer.Ordinal); foreach (var frontLine in (world.Geopolitics?.Territory.FrontLines .Where(front => front.FactionIds.Contains(faction.Id, StringComparer.Ordinal)) .OrderBy(front => front.Id, StringComparer.Ordinal) ?? Enumerable.Empty())) { var ownedSystemId = frontLine.SystemIds .FirstOrDefault(systemId => GeopoliticalSimulationService.FactionControlsSystem(world, faction.Id, systemId)) ?? frontLine.SystemIds.FirstOrDefault(systemId => world.Stations.Any(station => station.FactionId == faction.Id && station.SystemId == systemId)) ?? frontLine.SystemIds.FirstOrDefault(); if (ownedSystemId is null) { continue; } if (theaters.Any(existing => string.Equals(existing.SystemId, ownedSystemId, StringComparison.Ordinal) && existing.Kind == "defense-front")) { continue; } var pressure = FindTerritoryPressure(world, faction.Id, ownedSystemId); var security = FindRegionalSecurityAssessment(world, faction.Id, ownedSystemId); theaters.Add(new FactionTheaterRuntime { Id = $"theater-defense-{ownedSystemId}", Kind = "defense-front", SystemId = ownedSystemId, Status = "active", Priority = 96f + ((pressure?.PressureScore ?? 0f) * 24f) + ((security?.BorderPressure ?? 0f) * 20f), SupplyRisk = MathF.Max(pressure?.CorridorRisk ?? 0f, security?.SupplyRisk ?? 0f), FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, ownedSystemId), TargetFactionId = frontLine.FactionIds.FirstOrDefault(id => !string.Equals(id, faction.Id, StringComparison.Ordinal)), AnchorEntityId = frontLine.Id, AnchorPosition = ResolveSystemAnchor(world, ownedSystemId), UpdatedAtUtc = nowUtc, }); } foreach (var signal in threatAssessment.ThreatSignals .Where(candidate => candidate.ScopeKind is "controlled-system" or "contested-system") .OrderByDescending(candidate => (candidate.EnemyStationCount * 30) + (candidate.EnemyShipCount * 10)) .ThenBy(candidate => candidate.ScopeId, StringComparer.Ordinal)) { if (theaters.Any(existing => string.Equals(existing.SystemId, signal.ScopeId, StringComparison.Ordinal) && existing.Kind == "defense-front")) { continue; } var memory = systemMemories.GetValueOrDefault(signal.ScopeId); theaters.Add(new FactionTheaterRuntime { Id = $"theater-defense-{signal.ScopeId}", Kind = "defense-front", SystemId = signal.ScopeId, Status = "active", Priority = 90f + (signal.EnemyStationCount * 12f) + (signal.EnemyShipCount * 4f) + ((memory?.DefensiveFailures ?? 0) * 6f), SupplyRisk = memory?.RouteRisk ?? 0f, FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, signal.ScopeId), TargetFactionId = signal.EnemyFactionId, AnchorPosition = ResolveSystemAnchor(world, signal.ScopeId), UpdatedAtUtc = nowUtc, }); } foreach (var memory in faction.Memory.SystemMemories .Where(candidate => candidate.ControlledByFaction && (candidate.FrontierPressure > 0.35f || candidate.RouteRisk > 0.3f || candidate.HistoricalShortagePressure > 0.45f) && theaters.All(existing => existing.SystemId != candidate.SystemId || existing.Kind != "defense-front")) .OrderByDescending(candidate => (candidate.FrontierPressure * 50f) + (candidate.RouteRisk * 30f) + (candidate.HistoricalShortagePressure * 25f)) .ThenBy(candidate => candidate.SystemId, StringComparer.Ordinal) .Take(2)) { theaters.Add(new FactionTheaterRuntime { Id = $"theater-frontier-{memory.SystemId}", Kind = "defense-front", SystemId = memory.SystemId, Status = "active", Priority = 58f + (memory.FrontierPressure * 22f) + (memory.RouteRisk * 14f) + (memory.HistoricalShortagePressure * 10f), SupplyRisk = memory.RouteRisk, FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, memory.SystemId), AnchorPosition = ResolveSystemAnchor(world, memory.SystemId), UpdatedAtUtc = nowUtc, }); } if (CanRunOffensivePosture(faction, economicAssessment, threatAssessment)) { foreach (var target in SelectOffensiveTargets(world, faction, threatAssessment, economicAssessment) .Take(2)) { theaters.Add(new FactionTheaterRuntime { Id = $"theater-offense-{target.SystemId}", Kind = "offense-front", SystemId = target.SystemId, Status = "active", Priority = target.Priority, SupplyRisk = target.SupplyRisk, FriendlyAssetValue = target.Value, TargetFactionId = target.TargetFactionId, AnchorEntityId = target.AnchorEntityId, AnchorPosition = target.AnchorPosition, UpdatedAtUtc = nowUtc, }); } } if (expansionProject is not null && CanSupportExpansion(faction, economicAssessment, threatAssessment)) { theaters.Add(new FactionTheaterRuntime { Id = $"theater-expansion-{expansionProject.SystemId}-{expansionProject.CelestialId}", Kind = "expansion-front", SystemId = expansionProject.SystemId, Status = "active", Priority = 65f + (economicAssessment.HasShipyard ? 0f : 15f), SupplyRisk = ComputeSystemRisk(world, faction, expansionProject.SystemId), FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, expansionProject.SystemId), AnchorEntityId = expansionProject.SiteId ?? expansionProject.CelestialId, AnchorPosition = ResolveExpansionAnchor(world, expansionProject), UpdatedAtUtc = nowUtc, }); } foreach (var signal in economicAssessment.CommoditySignals .Where(candidate => candidate.Level is "critical" or "low" || candidate.ProjectedNetRatePerSecond < -0.01f || faction.Memory.CommodityMemories.FirstOrDefault(memory => memory.ItemId == candidate.ItemId) is { HistoricalShortageScore: > 0.8f }) .OrderByDescending(candidate => ComputeCommodityPriority(candidate)) .ThenBy(candidate => candidate.ItemId, StringComparer.Ordinal) .Take(3)) { var systemId = FindPrimaryCommoditySystem(world, faction.Id, signal.ItemId) ?? economicAssessment.PrimaryExpansionSystemId ?? world.Systems.First().Definition.Id; theaters.Add(new FactionTheaterRuntime { Id = $"theater-economy-{signal.ItemId}", Kind = "economic-front", SystemId = systemId, Status = "active", Priority = 45f + ComputeCommodityPriority(signal), SupplyRisk = ComputeSystemRisk(world, faction, systemId), FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, systemId), AnchorEntityId = ResolveCommodityAnchorStation(world, faction.Id, signal.ItemId)?.Id, UpdatedAtUtc = nowUtc, }); } theaters.Sort((left, right) => { var priority = right.Priority.CompareTo(left.Priority); return priority != 0 ? priority : string.Compare(left.Id, right.Id, StringComparison.Ordinal); }); return theaters; } private static List BuildCampaigns( SimulationWorld world, FactionRuntime faction, IReadOnlyList theaters, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment, IndustryExpansionProject? expansionProject, IReadOnlyDictionary previousCampaigns, DateTimeOffset nowUtc) { var campaigns = new List(); foreach (var theater in theaters) { var id = theater.Kind switch { "defense-front" => $"campaign-defense-{theater.SystemId}", "offense-front" => $"campaign-offense-{theater.SystemId}", "expansion-front" => $"campaign-expansion-{theater.SystemId}", "economic-front" => $"campaign-economy-{ResolveCommodityFromTheaterId(theater.Id) ?? theater.Id}", _ => $"campaign-{theater.Id}", }; previousCampaigns.TryGetValue(id, out var previous); var systemMemory = theater.SystemId is null ? null : faction.Memory.SystemMemories.FirstOrDefault(memory => memory.SystemId == theater.SystemId); var supplyAdequacy = ComputeCampaignSupplyAdequacy(faction, theater, economicAssessment); var continuationScore = ComputeCampaignContinuationScore(faction, theater, economicAssessment, systemMemory); var pauseReason = ResolveCampaignPauseReason(faction, theater, economicAssessment, threatAssessment, systemMemory); var effectiveContinuationScore = previous is not null && previous.ContinuationScore > 0f ? MathF.Max(previous.ContinuationScore * 0.7f, continuationScore) : continuationScore; var campaign = new FactionCampaignRuntime { Id = id, Kind = theater.Kind switch { "defense-front" => "defense", "offense-front" => "offense", "expansion-front" => "expansion", "economic-front" => "economic-stabilization", _ => theater.Kind, }, Status = pauseReason is null ? "active" : "paused", Priority = theater.Priority, TheaterId = theater.Id, TargetFactionId = theater.TargetFactionId, TargetSystemId = theater.SystemId, TargetEntityId = theater.AnchorEntityId, CommodityId = theater.Kind == "economic-front" ? ResolveCommodityFromTheaterId(theater.Id) : expansionProject?.CommodityId, SupportStationId = expansionProject?.SupportStationId, CurrentStepIndex = previous?.CurrentStepIndex ?? 0, Summary = BuildCampaignSummary(theater, expansionProject), UpdatedAtUtc = nowUtc, PauseReason = pauseReason, ContinuationScore = effectiveContinuationScore, SupplyAdequacy = supplyAdequacy, ReplacementPressure = economicAssessment.ReplacementPressure, FailureCount = previous?.FailureCount ?? GetCampaignFailureCount(systemMemory, theater.Kind), SuccessCount = previous?.SuccessCount ?? GetCampaignSuccessCount(systemMemory, theater.Kind), RequiresReinforcement = RequiresReinforcement(theater, economicAssessment, threatAssessment), }; campaign.Steps.AddRange(BuildCampaignSteps(campaign, expansionProject)); if (previous is not null) { campaign.CreatedAtUtc = previous.CreatedAtUtc; campaign.FleetCommanderId = previous.FleetCommanderId; } campaigns.Add(campaign); } if (economicAssessment.MilitaryShipCount < economicAssessment.TargetMilitaryShipCount || economicAssessment.ReplacementPressure > 0.5f || !economicAssessment.HasWarIndustrySupplyChain) { previousCampaigns.TryGetValue("campaign-force-build-up", out var previous); var campaign = new FactionCampaignRuntime { Id = "campaign-force-build-up", Kind = "force-build-up", Status = "active", Priority = 60f + ((economicAssessment.TargetMilitaryShipCount - economicAssessment.MilitaryShipCount) * 5f) + (economicAssessment.ReplacementPressure * 10f), CurrentStepIndex = previous?.CurrentStepIndex ?? 0, Summary = "Expand warfighting capacity.", UpdatedAtUtc = nowUtc, ContinuationScore = 0.9f, SupplyAdequacy = economicAssessment.SustainmentScore, ReplacementPressure = economicAssessment.ReplacementPressure, FailureCount = previous?.FailureCount ?? 0, SuccessCount = previous?.SuccessCount ?? 0, RequiresReinforcement = true, }; campaign.Steps.AddRange( [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-step-1", Kind = "stabilize-war-industry", Status = "active", Summary = "Stabilize core war industry inputs." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-step-2", Kind = "increase-warship-output", Status = "planned", Summary = "Increase military ship production." }, ]); if (previous is not null) { campaign.CreatedAtUtc = previous.CreatedAtUtc; } campaigns.Add(campaign); } return campaigns .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .ToList(); } private static List BuildCampaignSteps(FactionCampaignRuntime campaign, IndustryExpansionProject? expansionProject) { return campaign.Kind switch { "defense" => [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-stage", Kind = "stage-defense", Status = "active", Summary = "Stage defenders into the contested system." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-hold", Kind = "hold-space", Status = "planned", Summary = "Hold the system and protect friendly assets." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-sustain", Kind = "sustain-defense", Status = "planned", Summary = "Sustain logistics and restore security." }, ], "offense" => [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-stage", Kind = "stage-offense", Status = "active", Summary = "Stage assault forces at the target frontier." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-strike", Kind = "strike-targets", Status = "planned", Summary = "Engage hostile military and stations." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-hold", Kind = "hold-gains", Status = "planned", Summary = "Maintain pressure and deny recovery." }, ], "expansion" => [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-claim", Kind = "secure-claim", Status = "active", Summary = $"Secure {expansionProject?.CelestialId ?? campaign.TargetEntityId} for construction." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-supply", Kind = "supply-site", Status = "planned", Summary = "Move construction materials to the site." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-guard", Kind = "guard-site", Status = "planned", Summary = "Defend the expansion site until operational." }, ], "economic-stabilization" => [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-source", Kind = "secure-supply", Status = "active", Summary = "Secure incoming production and mining sources." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-move", Kind = "move-goods", Status = "planned", Summary = "Route logistics toward shortage points." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-stabilize", Kind = "stabilize-market", Status = "planned", Summary = "Restore stable stock levels." }, ], _ => [ new FactionPlanStepRuntime { Id = $"{campaign.Id}-step", Kind = "maintain", Status = "active", Summary = campaign.Summary }, ], }; } private static List BuildObjectives( SimulationWorld world, FactionRuntime faction, IReadOnlyList theaters, IReadOnlyList campaigns, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment, IndustryExpansionProject? expansionProject, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { var objectives = new List(); var campaignsById = campaigns.ToDictionary(campaign => campaign.Id, StringComparer.Ordinal); foreach (var campaign in campaigns) { var theater = theaters.FirstOrDefault(candidate => candidate.Id == campaign.TheaterId); switch (campaign.Kind) { case "defense": AddDefenseObjectives(world, faction, campaign, theater, objectives, previousObjectives, nowUtc); break; case "offense": if (campaign.Status == "active") { AddOffenseObjectives(world, faction, campaign, theater, objectives, previousObjectives, nowUtc); } break; case "expansion": if (campaign.Status == "active") { AddExpansionObjectives(world, faction, campaign, theater, expansionProject, objectives, previousObjectives, nowUtc); } break; case "economic-stabilization": AddEconomicObjectives(world, faction, campaign, theater, economicAssessment, objectives, previousObjectives, nowUtc); break; case "force-build-up": AddForceBuildUpObjectives(world, faction, campaign, economicAssessment, objectives, previousObjectives, nowUtc); break; } } foreach (var objective in objectives) { if (campaignsById.TryGetValue(objective.CampaignId, out var campaign)) { campaign.ObjectiveIds.Add(objective.Id); } } return objectives .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .ToList(); } private static void AddDefenseObjectives( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionTheaterRuntime? theater, ICollection objectives, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { var stations = world.Stations .Where(station => station.FactionId == faction.Id && station.SystemId == campaign.TargetSystemId) .OrderByDescending(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal) ? 1 : 0) .ThenBy(station => station.Id, StringComparer.Ordinal) .Take(2) .ToList(); foreach (var station in stations) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-protect-station-{station.Id}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "protect-station", DelegationKind = "ship", BehaviorKind = "protect-station", Status = "active", Priority = campaign.Priority + 8f, HomeSystemId = station.SystemId, HomeStationId = station.Id, TargetSystemId = station.SystemId, TargetEntityId = station.Id, Notes = $"Protect {station.Label}", UpdatedAtUtc = nowUtc, }, nowUtc)); } objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-patrol-{campaign.TargetSystemId}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "patrol-front", DelegationKind = "ship", BehaviorKind = "patrol", Status = "active", Priority = campaign.Priority + 2f, HomeSystemId = campaign.TargetSystemId, TargetSystemId = campaign.TargetSystemId, TargetPosition = theater?.AnchorPosition ?? ResolveSystemAnchor(world, campaign.TargetSystemId), Notes = "Patrol defensive front", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = campaign.RequiresReinforcement ? 2 : 1, }, nowUtc)); if ((theater?.SupplyRisk ?? 0f) > 0.25f) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-police-{campaign.TargetSystemId}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "police-front", DelegationKind = "ship", BehaviorKind = "police", Status = "active", Priority = campaign.Priority + 1f, HomeSystemId = campaign.TargetSystemId, TargetSystemId = campaign.TargetSystemId, TargetPosition = theater?.AnchorPosition ?? ResolveSystemAnchor(world, campaign.TargetSystemId), Notes = "Police frontier logistics routes", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 1, }, nowUtc)); } } private static void AddOffenseObjectives( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionTheaterRuntime? theater, ICollection objectives, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { var enemyStation = world.Stations .Where(station => station.FactionId != faction.Id && station.SystemId == campaign.TargetSystemId) .OrderBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); if (enemyStation is not null) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-strike-station-{enemyStation.Id}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "strike-station", DelegationKind = "ship", BehaviorKind = "attack-target", Status = "active", Priority = campaign.Priority + 10f, TargetSystemId = enemyStation.SystemId, TargetEntityId = enemyStation.Id, TargetPosition = enemyStation.Position, Notes = $"Strike {enemyStation.Label}", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 2, }, nowUtc)); } objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-hold-front", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "hold-front", DelegationKind = "ship", BehaviorKind = "protect-position", Status = "active", Priority = campaign.Priority + 3f, TargetSystemId = campaign.TargetSystemId, TargetPosition = theater?.AnchorPosition ?? ResolveSystemAnchor(world, campaign.TargetSystemId), Notes = "Maintain assault formation", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 2, }, nowUtc)); objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-fleet-supply", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "fleet-sustainment", DelegationKind = "ship", BehaviorKind = "supply-fleet", Status = "active", Priority = campaign.Priority + 1.5f, HomeSystemId = campaign.TargetSystemId, TargetSystemId = campaign.TargetSystemId, ItemId = "hullparts", Notes = "Sustain frontline fleet operations", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 1, }, nowUtc)); } private static void AddExpansionObjectives( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionTheaterRuntime? theater, IndustryExpansionProject? expansionProject, ICollection objectives, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { if (expansionProject is null) { return; } objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-construct-site", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "construct-site", DelegationKind = "ship", BehaviorKind = "construct-station", Status = "active", Priority = campaign.Priority + 8f, HomeSystemId = expansionProject.SystemId, HomeStationId = expansionProject.SupportStationId, TargetSystemId = expansionProject.SystemId, TargetEntityId = expansionProject.SiteId, TargetPosition = ResolveExpansionAnchor(world, expansionProject), Notes = $"Construct {expansionProject.ModuleId}", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 2, }, nowUtc)); objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-supply-site", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "supply-site", DelegationKind = "ship", BehaviorKind = "find-build-tasks", Status = "active", Priority = campaign.Priority + 4f, HomeSystemId = expansionProject.SystemId, HomeStationId = expansionProject.SupportStationId, TargetSystemId = expansionProject.SystemId, TargetEntityId = expansionProject.SiteId, ItemId = expansionProject.CommodityId, Notes = "Supply expansion site", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 1, }, nowUtc)); objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-guard-site", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "guard-site", DelegationKind = "ship", BehaviorKind = "protect-position", Status = "active", Priority = campaign.Priority + 2f, TargetSystemId = expansionProject.SystemId, TargetPosition = ResolveExpansionAnchor(world, expansionProject), TargetEntityId = expansionProject.SiteId, Notes = "Guard construction frontier", UpdatedAtUtc = nowUtc, UseOrders = true, StagingOrderKind = ShipOrderKinds.Move, ReinforcementLevel = 1, }, nowUtc)); if (CanMineItem(world, expansionProject.CommodityId)) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-mine-expansion-input", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "mine-expansion-input", DelegationKind = "ship", BehaviorKind = "expert-auto-mine", Status = "active", Priority = campaign.Priority + 1f, HomeSystemId = expansionProject.SystemId, HomeStationId = expansionProject.SupportStationId, TargetSystemId = expansionProject.SystemId, ItemId = expansionProject.CommodityId, Notes = $"Mine {expansionProject.CommodityId} for frontier build-up", UpdatedAtUtc = nowUtc, ReinforcementLevel = 1, }, nowUtc)); } } private static void AddEconomicObjectives( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionTheaterRuntime? theater, FactionEconomicAssessmentRuntime economicAssessment, ICollection objectives, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { var itemId = campaign.CommodityId ?? ResolveCommodityFromTheaterId(theater?.Id); if (string.IsNullOrWhiteSpace(itemId)) { return; } var anchorStation = ResolveCommodityAnchorStation(world, faction.Id, itemId); objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-trade-{itemId}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "trade-shortage", DelegationKind = "ship", BehaviorKind = "fill-shortages", Status = "active", Priority = campaign.Priority + 5f, HomeSystemId = anchorStation?.SystemId, HomeStationId = anchorStation?.Id, TargetSystemId = anchorStation?.SystemId ?? campaign.TargetSystemId, TargetEntityId = anchorStation?.Id, ItemId = itemId, Notes = $"Stabilize {itemId} shortages", UpdatedAtUtc = nowUtc, ReinforcementLevel = campaign.RequiresReinforcement ? 2 : 1, }, nowUtc)); if (CanMineItem(world, itemId)) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-mine-{itemId}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "mine-shortage", DelegationKind = "ship", BehaviorKind = "expert-auto-mine", Status = "active", Priority = campaign.Priority + 3f, HomeSystemId = anchorStation?.SystemId, HomeStationId = anchorStation?.Id, TargetSystemId = campaign.TargetSystemId, ItemId = itemId, Notes = $"Mine {itemId} for economic recovery", UpdatedAtUtc = nowUtc, ReinforcementLevel = 1, }, nowUtc)); } objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-revisit-{itemId}", CampaignId = campaign.Id, TheaterId = theater?.Id, Kind = "revisit-stations", DelegationKind = "ship", BehaviorKind = "revisit-known-stations", Status = "active", Priority = campaign.Priority + 0.5f, HomeSystemId = anchorStation?.SystemId, HomeStationId = anchorStation?.Id, TargetSystemId = anchorStation?.SystemId ?? campaign.TargetSystemId, ItemId = itemId, Notes = $"Refresh station trade knowledge for {itemId}", UpdatedAtUtc = nowUtc, ReinforcementLevel = 1, }, nowUtc)); } private static void AddForceBuildUpObjectives( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionEconomicAssessmentRuntime economicAssessment, ICollection objectives, IReadOnlyDictionary previousObjectives, DateTimeOffset nowUtc) { var shipyard = world.Stations .Where(station => station.FactionId == faction.Id && station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)) .OrderBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); if (shipyard is null) { return; } objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-feed-shipyard", CampaignId = campaign.Id, Kind = "feed-shipyard", DelegationKind = "ship", BehaviorKind = "fill-shortages", Status = "active", Priority = campaign.Priority + 4f, HomeSystemId = shipyard.SystemId, HomeStationId = shipyard.Id, TargetSystemId = shipyard.SystemId, TargetEntityId = shipyard.Id, ItemId = economicAssessment.IndustrialBottleneckItemId ?? "hullparts", Notes = "Keep shipyard supplied for fleet growth", UpdatedAtUtc = nowUtc, ReinforcementLevel = 2, }, nowUtc)); if (!string.IsNullOrWhiteSpace(economicAssessment.IndustrialBottleneckItemId) && CanMineItem(world, economicAssessment.IndustrialBottleneckItemId)) { objectives.Add(CreateObjective( previousObjectives, new FactionOperationalObjectiveRuntime { Id = $"{campaign.Id}-mine-bottleneck", CampaignId = campaign.Id, Kind = "mine-bottleneck", DelegationKind = "ship", BehaviorKind = "expert-auto-mine", Status = "active", Priority = campaign.Priority + 2f, HomeSystemId = shipyard.SystemId, HomeStationId = shipyard.Id, TargetSystemId = shipyard.SystemId, ItemId = economicAssessment.IndustrialBottleneckItemId, Notes = $"Mine {economicAssessment.IndustrialBottleneckItemId} for replacement pressure relief", UpdatedAtUtc = nowUtc, ReinforcementLevel = 1, }, nowUtc)); } } private static FactionOperationalObjectiveRuntime CreateObjective( IReadOnlyDictionary previousObjectives, FactionOperationalObjectiveRuntime objective, DateTimeOffset nowUtc) { if (previousObjectives.TryGetValue(objective.Id, out var previous)) { objective.CreatedAtUtc = previous.CreatedAtUtc; objective.CurrentStepIndex = previous.CurrentStepIndex; objective.CommanderId = previous.CommanderId; objective.Status = previous.Status; objective.ReservedAssetIds.AddRange(previous.ReservedAssetIds.OrderBy(id => id, StringComparer.Ordinal)); } objective.UpdatedAtUtc = nowUtc; objective.Steps.AddRange(BuildObjectiveSteps(objective)); return objective; } private static IEnumerable BuildObjectiveSteps(FactionOperationalObjectiveRuntime objective) { return [ new FactionPlanStepRuntime { Id = $"{objective.Id}-step-1", Kind = "deploy", Status = objective.Status == "paused" ? "paused" : objective.CommanderId is null ? "planned" : "active", Summary = "Deploy assigned asset.", }, new FactionPlanStepRuntime { Id = $"{objective.Id}-step-2", Kind = "execute", Status = objective.Status == "paused" ? "paused" : "planned", Summary = objective.Notes ?? objective.BehaviorKind, }, ]; } private static List BuildReservations( SimulationWorld world, FactionRuntime faction, IReadOnlyList objectives, DateTimeOffset nowUtc) { var reservations = new List(); var commanders = world.Commanders .Where(commander => commander.IsAlive && commander.FactionId == faction.Id && commander.Kind is CommanderKind.Station or CommanderKind.Ship) .OrderBy(commander => commander.Kind, StringComparer.Ordinal) .ThenBy(commander => commander.Id, StringComparer.Ordinal) .ToList(); var reservedCommanderIds = new HashSet(StringComparer.Ordinal); var availableMilitaryCommanders = commanders.Count(commander => commander.Kind == CommanderKind.Ship && world.Ships.FirstOrDefault(ship => ship.Id == commander.ControlledEntityId) is { Definition.Kind: "military", Health: > 0f }); var committedMilitaryCommanders = 0; foreach (var objective in objectives .OrderByDescending(candidate => candidate.Priority + (candidate.ReinforcementLevel * 4f)) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal)) { if (IsCombatObjective(objective) && objective.Priority < 95f && availableMilitaryCommanders - committedMilitaryCommanders <= faction.StrategicState.Budget.ReservedMilitaryAssets) { objective.Status = "reserved"; continue; } var commander = SelectCommanderForObjective(world, objective, commanders, reservedCommanderIds); if (commander is null) { continue; } reservedCommanderIds.Add(commander.Id); objective.CommanderId = commander.Id; objective.ReservedAssetIds.Clear(); objective.ReservedAssetIds.Add(commander.ControlledEntityId ?? commander.Id); objective.Status = "active"; objective.CurrentStepIndex = 0; if (IsCombatObjective(objective)) { committedMilitaryCommanders += 1; } reservations.Add(new FactionAssetReservationRuntime { Id = $"reservation-{objective.Id}-{commander.Id}", ObjectiveId = objective.Id, CampaignId = objective.CampaignId, AssetKind = commander.Kind == CommanderKind.Station ? "station-commander" : "ship-commander", AssetId = commander.Id, Priority = objective.Priority, UpdatedAtUtc = nowUtc, }); } return reservations; } private static CommanderRuntime? SelectCommanderForObjective( SimulationWorld world, FactionOperationalObjectiveRuntime objective, IReadOnlyList commanders, IReadOnlySet reservedCommanderIds) { return commanders .Where(commander => !reservedCommanderIds.Contains(commander.Id) && IsCommanderEligibleForObjective(world, commander, objective)) .OrderByDescending(commander => ScoreCommanderForObjective(world, commander, objective)) .ThenBy(commander => commander.Id, StringComparer.Ordinal) .FirstOrDefault(); } private static bool IsCommanderEligibleForObjective(SimulationWorld world, CommanderRuntime commander, FactionOperationalObjectiveRuntime objective) { if (objective.DelegationKind == "station") { return commander.Kind == CommanderKind.Station && (objective.HomeStationId is null || string.Equals(commander.ControlledEntityId, objective.HomeStationId, StringComparison.Ordinal)); } if (commander.Kind != CommanderKind.Ship) { return false; } var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId); if (ship is null || ship.Health <= 0f) { return false; } return objective.BehaviorKind switch { "construct-station" => string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal), "find-build-tasks" => string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal), "fill-shortages" or "advanced-auto-trade" or "revisit-known-stations" or "supply-fleet" => string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal), "local-auto-mine" or "advanced-auto-mine" or "expert-auto-mine" => HasShipCapabilities(ship.Definition, "mining"), "patrol" or "police" or "protect-position" or "protect-ship" or "protect-station" or "attack-target" => string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal), _ => true, }; } private static float ScoreCommanderForObjective(SimulationWorld world, CommanderRuntime commander, FactionOperationalObjectiveRuntime objective) { var skillScore = commander.Skills.Leadership + commander.Skills.Coordination + commander.Skills.Strategy; var homeBias = 0f; if (commander.Kind == CommanderKind.Station) { if (string.Equals(commander.ControlledEntityId, objective.HomeStationId, StringComparison.Ordinal)) { homeBias += 25f; } if (world.Stations.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId) is { } station && string.Equals(station.SystemId, objective.TargetSystemId, StringComparison.Ordinal)) { homeBias += 12f; } return homeBias + skillScore; } var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId); if (ship is null) { return float.MinValue; } if (string.Equals(ship.SystemId, objective.TargetSystemId, StringComparison.Ordinal)) { homeBias += 30f; } else if (string.Equals(ship.SystemId, objective.HomeSystemId, StringComparison.Ordinal)) { homeBias += 18f; } if (ship.CommanderId == objective.CommanderId) { homeBias += 8f; } var distancePenalty = objective.TargetPosition is null ? 0f : ship.Position.DistanceTo(objective.TargetPosition.Value); return homeBias + skillScore - distancePenalty; } private static List BuildProductionPrograms( FactionRuntime faction, IReadOnlyList theaters, IReadOnlyList campaigns, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment, IndustryExpansionProject? expansionProject, IReadOnlyDictionary previousPrograms) { var programs = new List(); programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-military", Kind = "military-fleet", Status = economicAssessment.MilitaryShipCount >= economicAssessment.TargetMilitaryShipCount ? "stable" : "active", Priority = 80f + (threatAssessment.ThreatSignals.Count * 4f), ShipKind = "military", TargetCount = economicAssessment.TargetMilitaryShipCount, CurrentCount = economicAssessment.MilitaryShipCount, Notes = "Maintain enough military hulls for all active fronts.", })); programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-miners", Kind = "mining-fleet", Status = economicAssessment.MinerShipCount >= economicAssessment.TargetMinerShipCount ? "stable" : "active", Priority = 60f, ShipKind = "mining", TargetCount = economicAssessment.TargetMinerShipCount, CurrentCount = economicAssessment.MinerShipCount, Notes = "Maintain raw resource extraction capacity.", })); programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-transports", Kind = "logistics-fleet", Status = economicAssessment.TransportShipCount >= economicAssessment.TargetTransportShipCount ? "stable" : "active", Priority = 62f, ShipKind = "transport", TargetCount = economicAssessment.TargetTransportShipCount, CurrentCount = economicAssessment.TransportShipCount, Notes = "Maintain logistics throughput across stations and fronts.", })); programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-constructors", Kind = "construction-fleet", Status = economicAssessment.ConstructorShipCount >= economicAssessment.TargetConstructorShipCount ? "stable" : "active", Priority = expansionProject is null ? 35f : 68f, ShipKind = "construction", TargetCount = economicAssessment.TargetConstructorShipCount, CurrentCount = economicAssessment.ConstructorShipCount, Notes = "Maintain construction capacity for frontier growth.", })); if (!economicAssessment.HasWarIndustrySupplyChain) { programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-war-industry", Kind = "war-industry", Status = "active", Priority = 78f, CommodityId = "hullparts", TargetCount = 1, CurrentCount = economicAssessment.HasWarIndustrySupplyChain ? 1 : 0, Notes = "Stabilize war industry bottlenecks.", })); } if (expansionProject is not null) { programs.Add(CreateProductionProgram(previousPrograms, new FactionProductionProgramRuntime { Id = $"program-{faction.Id}-expansion", Kind = "expansion", Status = "active", Priority = 72f, CampaignId = campaigns.FirstOrDefault(candidate => candidate.Kind == "expansion")?.Id, CommodityId = expansionProject.CommodityId, ModuleId = expansionProject.ModuleId, TargetSystemId = expansionProject.SystemId, TargetCount = 1, CurrentCount = economicAssessment.PrimaryExpansionSiteId is null ? 0 : 1, Notes = $"Expand into {expansionProject.SystemId}.", })); } return programs .OrderByDescending(program => program.Priority) .ThenBy(program => program.Id, StringComparer.Ordinal) .ToList(); } private static FactionProductionProgramRuntime CreateProductionProgram( IReadOnlyDictionary previousPrograms, FactionProductionProgramRuntime program) { if (previousPrograms.TryGetValue(program.Id, out var previous)) { program.CampaignId ??= previous.CampaignId; } return program; } private static void ReconcileCampaignLifecycle( SimulationWorld world, FactionRuntime faction, IReadOnlyDictionary previousCampaigns, IReadOnlyCollection campaigns, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment, DateTimeOffset nowUtc) { var activeIds = campaigns.Select(campaign => campaign.Id).ToHashSet(StringComparer.Ordinal); foreach (var campaign in campaigns) { if (!previousCampaigns.ContainsKey(campaign.Id)) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{campaign.Id}-created", Kind = "campaign-created", Summary = $"Activated {campaign.Kind} campaign {campaign.Id}.", RelatedEntityId = campaign.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } } foreach (var previous in previousCampaigns.Values.Where(candidate => !activeIds.Contains(candidate.Id))) { UpdateCampaignMemoryFromOutcome(world, faction, previous, economicAssessment, threatAssessment, nowUtc); AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{previous.Id}-completed-{faction.StrategicState.PlanCycle}", Kind = "campaign-completed", Summary = $"Closed {previous.Kind} campaign {previous.Id}.", RelatedEntityId = previous.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); AppendOutcome(faction, new FactionOutcomeRecordRuntime { Id = $"outcome-{previous.Id}-{faction.StrategicState.PlanCycle}", Kind = "campaign-completed", Summary = $"Campaign {previous.Id} left the active strategic set.", RelatedCampaignId = previous.Id, OccurredAtUtc = nowUtc, }); } } private static void ReconcileObjectiveLifecycle( FactionRuntime faction, IReadOnlyDictionary previousObjectives, IReadOnlyCollection objectives, DateTimeOffset nowUtc) { var activeIds = objectives.Select(objective => objective.Id).ToHashSet(StringComparer.Ordinal); foreach (var objective in objectives.Where(candidate => !previousObjectives.ContainsKey(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{objective.Id}-created", Kind = "objective-created", Summary = $"Delegated objective {objective.Kind}.", RelatedEntityId = objective.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } foreach (var previous in previousObjectives.Values.Where(candidate => !activeIds.Contains(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{previous.Id}-retired-{faction.StrategicState.PlanCycle}", Kind = "objective-retired", Summary = $"Retired objective {previous.Kind}.", RelatedEntityId = previous.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } } private static void ReconcileTheaterLifecycle( FactionRuntime faction, IReadOnlyDictionary previousTheaters, IReadOnlyCollection theaters, DateTimeOffset nowUtc) { var activeIds = theaters.Select(theater => theater.Id).ToHashSet(StringComparer.Ordinal); foreach (var theater in theaters.Where(candidate => !previousTheaters.ContainsKey(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{theater.Id}-opened", Kind = "theater-opened", Summary = $"Opened {theater.Kind} in {theater.SystemId}.", RelatedEntityId = theater.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } foreach (var previous in previousTheaters.Values.Where(candidate => !activeIds.Contains(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{previous.Id}-closed-{faction.StrategicState.PlanCycle}", Kind = "theater-closed", Summary = $"Closed {previous.Kind} in {previous.SystemId}.", RelatedEntityId = previous.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } } private static void ReconcileProgramLifecycle( FactionRuntime faction, IReadOnlyDictionary previousPrograms, IReadOnlyCollection programs, DateTimeOffset nowUtc) { var activeIds = programs.Select(program => program.Id).ToHashSet(StringComparer.Ordinal); foreach (var program in programs.Where(candidate => !previousPrograms.ContainsKey(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{program.Id}-started", Kind = "program-started", Summary = $"Started production program {program.Kind}.", RelatedEntityId = program.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } foreach (var previous in previousPrograms.Values.Where(candidate => !activeIds.Contains(candidate.Id))) { AppendDecision(faction, new FactionDecisionLogEntryRuntime { Id = $"decision-{previous.Id}-stopped-{faction.StrategicState.PlanCycle}", Kind = "program-stopped", Summary = $"Stopped production program {previous.Kind}.", RelatedEntityId = previous.Id, PlanCycle = faction.StrategicState.PlanCycle, OccurredAtUtc = nowUtc, }); } } private static void ApplyDelegation( SimulationWorld world, FactionRuntime faction, CommanderRuntime factionCommander, ICollection events, DateTimeOffset nowUtc) { foreach (var commander in world.Commanders.Where(candidate => candidate.FactionId == faction.Id)) { commander.ActiveObjectiveIds.Clear(); } foreach (var objective in faction.StrategicState.Objectives.Where(candidate => candidate.CommanderId is not null)) { if (world.Commanders.FirstOrDefault(candidate => candidate.Id == objective.CommanderId) is not { } commander) { continue; } commander.ActiveObjectiveIds.Add(objective.Id); } var fleetCommanders = EnsureFleetCommanders(world, faction, factionCommander, nowUtc); var focusCampaign = faction.StrategicState.Campaigns .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); factionCommander.Assignment = new CommanderAssignmentRuntime { ObjectiveId = focusCampaign?.Id ?? $"objective-strategic-{faction.Id}", CampaignId = focusCampaign?.Id, TheaterId = focusCampaign?.TheaterId, Kind = "strategic-executive", BehaviorKind = "strategic-executive", Status = "active", Priority = 100f, HomeSystemId = focusCampaign?.TargetSystemId ?? faction.StrategicState.EconomicAssessment.PrimaryExpansionSystemId, TargetSystemId = focusCampaign?.TargetSystemId ?? faction.StrategicState.ThreatAssessment.PrimaryThreatSystemId ?? faction.StrategicState.EconomicAssessment.PrimaryExpansionSystemId, TargetEntityId = focusCampaign?.TargetEntityId, Notes = focusCampaign?.Summary ?? faction.StrategicState.Status, UpdatedAtUtc = nowUtc, }; foreach (var commander in world.Commanders.Where(candidate => candidate.FactionId == faction.Id && candidate.Kind is CommanderKind.Ship or CommanderKind.Station)) { var objective = faction.StrategicState.Objectives .Where(candidate => string.Equals(candidate.CommanderId, commander.Id, StringComparison.Ordinal)) .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); if (commander.Kind == CommanderKind.Ship && world.Ships.FirstOrDefault(ship => ship.Id == commander.ControlledEntityId) is { } ship) { if (ApplyShipControlSurface(world, faction, factionCommander, commander, ship, objective, fleetCommanders, nowUtc)) { ship.NeedsReplan = true; ship.LastReplanReason = objective is null ? "faction-fallback-updated" : "faction-objective-updated"; } } if (objective is not null) { commander.NeedsReplan = true; } } RefreshCommanderHierarchy(world, faction.Id); events.Add(new SimulationEventRecord( "faction", faction.Id, "strategic-cycle", $"{faction.Label} strategic cycle {faction.StrategicState.PlanCycle} updated {faction.StrategicState.Campaigns.Count} campaigns across {faction.StrategicState.Theaters.Count} theaters.", nowUtc)); } private static void UpdateFleetCommander(SimulationWorld world, CommanderRuntime commander) { commander.ReplanTimer = FleetCommanderReplanInterval; commander.NeedsReplan = false; commander.PlanningCycle += 1; commander.ActiveObjectiveIds.Clear(); var faction = FindFaction(world, commander.FactionId); var campaign = faction?.StrategicState.Campaigns.FirstOrDefault(candidate => candidate.Id == commander.ControlledEntityId); if (faction is null || campaign is null) { commander.IsAlive = false; commander.Assignment = null; return; } var objectives = faction.StrategicState.Objectives .Where(candidate => candidate.CampaignId == campaign.Id && candidate.CommanderId is not null) .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .ToList(); foreach (var objective in objectives) { commander.ActiveObjectiveIds.Add(objective.Id); } commander.Assignment = new CommanderAssignmentRuntime { ObjectiveId = campaign.Id, CampaignId = campaign.Id, TheaterId = campaign.TheaterId, Kind = "fleet-command", BehaviorKind = campaign.Kind switch { "offense" => "attack-target", "defense" => "protect-position", "expansion" => "protect-position", _ => "patrol", }, Status = campaign.Status, Priority = campaign.Priority, HomeSystemId = campaign.TargetSystemId, TargetSystemId = campaign.TargetSystemId, TargetEntityId = campaign.TargetEntityId, Notes = campaign.Summary, UpdatedAtUtc = DateTimeOffset.UtcNow, }; } private static CommanderAssignmentRuntime BuildStationFocusAssignment( SimulationWorld world, FactionRuntime faction, StationRuntime station, ConstructionSiteRuntime? activeSite) { var economic = faction.StrategicState.EconomicAssessment; var role = StationSimulationService.DetermineStationRole(station); var bottleneckItem = economic.IndustrialBottleneckItemId; var nowUtc = DateTimeOffset.UtcNow; if (HasStationModules(station, "module_gen_build_l_01") && economic.MilitaryShipCount < economic.TargetMilitaryShipCount) { return new CommanderAssignmentRuntime { ObjectiveId = $"objective-station-{station.Id}-ship-production", Kind = "ship-production-focus", BehaviorKind = "fill-shortages", Status = "active", Priority = 55f, HomeSystemId = station.SystemId, HomeStationId = station.Id, TargetSystemId = station.SystemId, TargetEntityId = station.Id, ItemId = bottleneckItem ?? "hullparts", Notes = "Prioritize military replacement output", UpdatedAtUtc = nowUtc, }; } if (!string.IsNullOrWhiteSpace(bottleneckItem) && StationCanProduceItem(world, station, bottleneckItem)) { return new CommanderAssignmentRuntime { ObjectiveId = $"objective-station-{station.Id}-commodity-focus-{bottleneckItem}", Kind = "commodity-focus", BehaviorKind = "fill-shortages", Status = "active", Priority = 45f, HomeSystemId = station.SystemId, HomeStationId = station.Id, TargetSystemId = station.SystemId, TargetEntityId = station.Id, ItemId = bottleneckItem, Notes = $"Stabilize {bottleneckItem} production", UpdatedAtUtc = nowUtc, }; } if (activeSite is not null) { return new CommanderAssignmentRuntime { ObjectiveId = $"objective-station-{station.Id}-expansion-support", Kind = "expansion-support", BehaviorKind = "find-build-tasks", Status = "active", Priority = 40f, HomeSystemId = station.SystemId, HomeStationId = station.Id, TargetSystemId = activeSite.SystemId, TargetEntityId = activeSite.Id, ItemId = economic.IndustrialBottleneckItemId, Notes = $"Support {activeSite.BlueprintId}", UpdatedAtUtc = nowUtc, }; } return new CommanderAssignmentRuntime { ObjectiveId = $"objective-station-{station.Id}-oversight", Kind = "station-oversight", BehaviorKind = "fill-shortages", Status = "active", Priority = 30f, HomeSystemId = station.SystemId, HomeStationId = station.Id, TargetSystemId = station.SystemId, TargetEntityId = station.Id, ItemId = bottleneckItem, Notes = role, UpdatedAtUtc = nowUtc, }; } private static IReadOnlyDictionary EnsureFleetCommanders( SimulationWorld world, FactionRuntime faction, CommanderRuntime factionCommander, DateTimeOffset nowUtc) { var activeCampaignIds = faction.StrategicState.Campaigns .Where(campaign => campaign.Status == "active" && faction.StrategicState.Objectives.Any(objective => objective.CampaignId == campaign.Id && objective.CommanderId is not null && (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, "supply-fleet", StringComparison.Ordinal)))) .Select(campaign => campaign.Id) .ToHashSet(StringComparer.Ordinal); world.Commanders.RemoveAll(commander => commander.Kind == CommanderKind.Fleet && commander.FactionId == faction.Id && (commander.ControlledEntityId is null || !activeCampaignIds.Contains(commander.ControlledEntityId))); var fleetCommanders = new Dictionary(StringComparer.Ordinal); foreach (var campaign in faction.StrategicState.Campaigns.Where(campaign => activeCampaignIds.Contains(campaign.Id))) { var commander = world.Commanders.FirstOrDefault(candidate => candidate.Kind == CommanderKind.Fleet && candidate.FactionId == faction.Id && string.Equals(candidate.ControlledEntityId, campaign.Id, StringComparison.Ordinal)); if (commander is null) { commander = new CommanderRuntime { Id = $"commander-fleet-{campaign.Id}", Kind = CommanderKind.Fleet, FactionId = faction.Id, ParentCommanderId = factionCommander.Id, ControlledEntityId = campaign.Id, PolicySetId = factionCommander.PolicySetId, Doctrine = "fleet-control", Skills = new CommanderSkillProfileRuntime { Leadership = Math.Clamp(factionCommander.Skills.Leadership, 3, 5), Coordination = Math.Clamp(factionCommander.Skills.Coordination + 1, 3, 5), Strategy = Math.Clamp(factionCommander.Skills.Strategy, 3, 5), }, ReplanTimer = 0f, NeedsReplan = true, }; world.Commanders.Add(commander); } commander.ParentCommanderId = factionCommander.Id; commander.PolicySetId = factionCommander.PolicySetId; commander.IsAlive = true; commander.Assignment = new CommanderAssignmentRuntime { ObjectiveId = campaign.Id, CampaignId = campaign.Id, TheaterId = campaign.TheaterId, Kind = "fleet-command", BehaviorKind = campaign.Kind switch { "offense" => "attack-target", "defense" => "protect-position", "expansion" => "protect-position", _ => "patrol", }, Status = campaign.Status, Priority = campaign.Priority + 1f, HomeSystemId = campaign.TargetSystemId, TargetSystemId = campaign.TargetSystemId, TargetEntityId = campaign.TargetEntityId, Notes = campaign.Summary, UpdatedAtUtc = nowUtc, }; campaign.FleetCommanderId = commander.Id; fleetCommanders[campaign.Id] = commander; } return fleetCommanders; } private static bool ApplyShipControlSurface( SimulationWorld world, FactionRuntime faction, CommanderRuntime factionCommander, CommanderRuntime commander, ShipRuntime ship, FactionOperationalObjectiveRuntime? objective, IReadOnlyDictionary fleetCommanders, DateTimeOffset nowUtc) { var desiredParentCommanderId = ResolveDelegatedParent(world, factionCommander, commander, objective, fleetCommanders); var parentChanged = !string.Equals(commander.ParentCommanderId, desiredParentCommanderId, StringComparison.Ordinal); var policyChanged = !string.Equals(commander.PolicySetId, factionCommander.PolicySetId, StringComparison.Ordinal); commander.ParentCommanderId = desiredParentCommanderId; commander.PolicySetId = factionCommander.PolicySetId; ship.PolicySetId = commander.PolicySetId; var desiredBehavior = objective is null ? BuildFallbackBehavior(world, ship) : BuildBehaviorForObjective(world, ship, objective); var behaviorChanged = !DefaultBehaviorsEqual(ship.DefaultBehavior, desiredBehavior); if (behaviorChanged) { ApplyBehavior(ship.DefaultBehavior, desiredBehavior); } var desiredOrder = BuildAiOrderForObjective(world, ship, objective, nowUtc); var ordersChanged = ReconcileAiOrders(ship, desiredOrder); var desiredControlSourceKind = objective is null ? "faction-fallback" : "faction-objective"; var desiredControlSourceId = objective?.Id ?? commander.Id; var desiredControlReason = objective?.Notes ?? objective?.Kind ?? desiredBehavior.Kind; var controlChanged = !string.Equals(ship.ControlSourceKind, desiredControlSourceKind, StringComparison.Ordinal) || !string.Equals(ship.ControlSourceId, desiredControlSourceId, StringComparison.Ordinal) || !string.Equals(ship.ControlReason, desiredControlReason, StringComparison.Ordinal); ship.ControlSourceKind = desiredControlSourceKind; ship.ControlSourceId = desiredControlSourceId; ship.ControlReason = desiredControlReason; ship.LastDeltaSignature = parentChanged || controlChanged ? string.Empty : ship.LastDeltaSignature; return policyChanged || behaviorChanged || ordersChanged; } private static string ResolveDelegatedParent( SimulationWorld world, CommanderRuntime factionCommander, CommanderRuntime commander, FactionOperationalObjectiveRuntime? objective, IReadOnlyDictionary fleetCommanders) { if (objective?.CampaignId is not null && fleetCommanders.TryGetValue(objective.CampaignId, out var fleetCommander) && (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, "supply-fleet", StringComparison.Ordinal))) { return fleetCommander.Id; } if (objective?.HomeStationId is not null && world.Stations.FirstOrDefault(station => station.Id == objective.HomeStationId)?.CommanderId is { } stationCommanderId) { return stationCommanderId; } return factionCommander.Id; } private static DefaultBehaviorRuntime BuildFallbackBehavior(SimulationWorld world, ShipRuntime ship) { var homeStation = ResolveFallbackHomeStation(world, ship); if (HasShipCapabilities(ship.Definition, "mining")) { return new DefaultBehaviorRuntime { Kind = ship.Definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine", HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, PreferredItemId = null, Radius = 24f, MaxSystemRange = ship.Definition.CargoCapacity >= 120f ? 3 : 1, }; } if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal)) { return new DefaultBehaviorRuntime { Kind = "advanced-auto-trade", HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, Radius = 24f, MaxSystemRange = 2, }; } if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal)) { return new DefaultBehaviorRuntime { Kind = "construct-station", HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, Radius = 28f, MaxSystemRange = 2, }; } if (string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)) { var anchor = homeStation?.Position ?? ship.Position; var patrolRadius = (homeStation?.Radius ?? 30f) + 90f; return new DefaultBehaviorRuntime { Kind = "patrol", HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, TargetPosition = anchor, PatrolPoints = BuildPatrolPoints(anchor, patrolRadius), PatrolIndex = ship.DefaultBehavior.PatrolIndex, Radius = patrolRadius, MaxSystemRange = 1, WaitSeconds = 2f, RepeatIndex = ship.DefaultBehavior.RepeatIndex, }; } return new DefaultBehaviorRuntime { Kind = "idle", HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, }; } private static StationRuntime? ResolveFallbackHomeStation(SimulationWorld world, ShipRuntime ship) => world.Stations .Where(station => station.FactionId == ship.FactionId) .OrderByDescending(station => station.SystemId == ship.SystemId ? 1 : 0) .ThenBy(station => station.Position.DistanceTo(ship.Position)) .ThenBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); private static DefaultBehaviorRuntime BuildBehaviorForObjective( SimulationWorld world, ShipRuntime ship, FactionOperationalObjectiveRuntime objective) { var fallback = BuildFallbackBehavior(world, ship); var areaSystemId = objective.TargetSystemId ?? objective.HomeSystemId ?? fallback.AreaSystemId ?? ship.SystemId; var radius = objective.BehaviorKind switch { "protect-position" or "protect-station" or "patrol" or "police" => MathF.Max(80f, fallback.Radius), "follow-ship" or "protect-ship" => MathF.Max(18f, fallback.Radius * 0.6f), "fill-shortages" or "advanced-auto-trade" or "find-build-tasks" => MathF.Max(20f, fallback.Radius), _ => fallback.Radius, }; var maxRange = objective.BehaviorKind switch { "attack-target" or "protect-position" or "protect-station" or "protect-ship" or "patrol" or "police" => Math.Max(1, fallback.MaxSystemRange), "fill-shortages" or "advanced-auto-trade" or "find-build-tasks" or "supply-fleet" => Math.Max(2, fallback.MaxSystemRange), _ => fallback.MaxSystemRange, }; return new DefaultBehaviorRuntime { Kind = objective.BehaviorKind, HomeSystemId = objective.HomeSystemId ?? fallback.HomeSystemId ?? ship.SystemId, HomeStationId = objective.HomeStationId ?? fallback.HomeStationId, AreaSystemId = areaSystemId, TargetEntityId = objective.TargetEntityId, PreferredItemId = objective.ItemId ?? fallback.PreferredItemId, PreferredNodeId = fallback.PreferredNodeId, PreferredConstructionSiteId = objective.Kind is "construct-site" or "supply-site" ? objective.TargetEntityId : fallback.PreferredConstructionSiteId, PreferredModuleId = fallback.PreferredModuleId, TargetPosition = objective.TargetPosition ?? fallback.TargetPosition, WaitSeconds = objective.BehaviorKind == "supply-fleet" ? 4f : fallback.WaitSeconds, Radius = radius, MaxSystemRange = maxRange, KnownStationsOnly = objective.BehaviorKind == "revisit-known-stations", PatrolPoints = objective.BehaviorKind == "patrol" ? BuildPatrolPoints(objective.TargetPosition ?? fallback.TargetPosition ?? ship.Position, radius) : [], PatrolIndex = ship.DefaultBehavior.PatrolIndex, RepeatOrders = [], RepeatIndex = ship.DefaultBehavior.RepeatIndex, }; } private static void ApplyBehavior(DefaultBehaviorRuntime target, DefaultBehaviorRuntime source) { target.Kind = source.Kind; target.HomeSystemId = source.HomeSystemId; target.HomeStationId = source.HomeStationId; target.AreaSystemId = source.AreaSystemId; target.TargetEntityId = source.TargetEntityId; target.PreferredItemId = source.PreferredItemId; target.PreferredNodeId = source.PreferredNodeId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredModuleId = source.PreferredModuleId; target.TargetPosition = source.TargetPosition; target.WaitSeconds = source.WaitSeconds; target.Radius = source.Radius; target.MaxSystemRange = source.MaxSystemRange; target.KnownStationsOnly = source.KnownStationsOnly; target.PatrolPoints = source.PatrolPoints.ToList(); target.PatrolIndex = source.PatrolIndex; target.RepeatOrders = source.RepeatOrders.ToList(); target.RepeatIndex = source.RepeatIndex; } private static bool DefaultBehaviorsEqual(DefaultBehaviorRuntime left, DefaultBehaviorRuntime right) => string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.HomeSystemId, right.HomeSystemId, StringComparison.Ordinal) && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.PreferredItemId, right.PreferredItemId, StringComparison.Ordinal) && string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly && left.PatrolPoints.SequenceEqual(right.PatrolPoints) && left.RepeatOrders.Count == right.RepeatOrders.Count && left.RepeatOrders.Zip(right.RepeatOrders, ShipOrderTemplatesEqual).All(equal => equal); private static bool ShipOrderTemplatesEqual(ShipOrderTemplateRuntime left, ShipOrderTemplateRuntime right) => string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.Label, right.Label, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly; private static ShipOrderRuntime? BuildAiOrderForObjective( SimulationWorld world, ShipRuntime ship, FactionOperationalObjectiveRuntime? objective, DateTimeOffset nowUtc) { if (objective is null || !objective.UseOrders || string.IsNullOrWhiteSpace(objective.StagingOrderKind)) { return null; } var targetSystemId = objective.TargetSystemId ?? objective.HomeSystemId ?? ship.SystemId; var targetPosition = objective.TargetPosition ?? ResolveEntityPosition(world, objective.TargetEntityId) ?? ship.Position; var alreadyInSystem = string.Equals(ship.SystemId, targetSystemId, StringComparison.Ordinal); var inPosition = alreadyInSystem && ship.Position.DistanceTo(targetPosition) <= MathF.Max(14f, objective.ReinforcementLevel * 18f); if (inPosition) { return null; } return new ShipOrderRuntime { Id = $"ai-order-{objective.Id}", Kind = objective.StagingOrderKind, Priority = 90 + objective.ReinforcementLevel, InterruptCurrentPlan = true, Label = $"{objective.Kind} staging", TargetEntityId = objective.TargetEntityId, TargetSystemId = targetSystemId, TargetPosition = targetPosition, DestinationStationId = objective.BehaviorKind == "dock-and-wait" ? objective.TargetEntityId : null, ItemId = objective.ItemId, WaitSeconds = 0f, Radius = MathF.Max(12f, objective.ReinforcementLevel * 18f), MaxSystemRange = null, KnownStationsOnly = false, }; } private static Vector3? ResolveEntityPosition(SimulationWorld world, string? entityId) { if (entityId is null) { return null; } var shipPosition = world.Ships.FirstOrDefault(ship => ship.Id == entityId)?.Position; if (shipPosition is not null) { return shipPosition; } var stationPosition = world.Stations.FirstOrDefault(station => station.Id == entityId)?.Position; if (stationPosition is not null) { return stationPosition; } var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId); if (site?.CelestialId is { } celestialId) { return world.Celestials.FirstOrDefault(celestial => celestial.Id == celestialId)?.Position; } return null; } private static bool ReconcileAiOrders(ShipRuntime ship, ShipOrderRuntime? desiredOrder) { var changed = ship.OrderQueue.RemoveAll(order => order.Id.StartsWith("ai-order-", StringComparison.Ordinal) && (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal))) > 0; if (desiredOrder is null) { return changed; } var existing = ship.OrderQueue.FirstOrDefault(order => string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)); if (existing is not null) { if (ShipOrdersEqual(existing, desiredOrder)) { return changed; } ship.OrderQueue.Remove(existing); changed = true; } if (ship.OrderQueue.Count >= MaxAiOrdersPerShip) { changed |= ship.OrderQueue.RemoveAll(order => order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) > 0; } if (ship.OrderQueue.Count < 8) { ship.OrderQueue.Add(desiredOrder); changed = true; } return changed; } private static bool ShipOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) => string.Equals(left.Id, right.Id, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && left.Priority == right.Priority && left.InterruptCurrentPlan == right.InterruptCurrentPlan && string.Equals(left.Label, right.Label, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly; private static void RefreshCommanderHierarchy(SimulationWorld world, string factionId) { foreach (var commander in world.Commanders.Where(candidate => candidate.FactionId == factionId)) { commander.SubordinateCommanderIds.Clear(); } var commandersById = world.Commanders.ToDictionary(commander => commander.Id, StringComparer.Ordinal); foreach (var commander in world.Commanders.Where(candidate => candidate.FactionId == factionId && candidate.ParentCommanderId is not null)) { if (commander.ParentCommanderId is { } parentCommanderId && commandersById.TryGetValue(parentCommanderId, out var parent)) { parent.SubordinateCommanderIds.Add(commander.Id); } } } private static bool IsCombatObjective(FactionOperationalObjectiveRuntime objective) => objective.BehaviorKind is "attack-target" or "protect-position" or "protect-ship" or "protect-station" or "patrol" or "police"; private static float EstimateFriendlyAssetValue(SimulationWorld world, string factionId, string systemId) { var stationValue = world.Stations.Count(station => station.FactionId == factionId && station.SystemId == systemId) * 35f; var shipValue = world.Ships.Count(ship => ship.FactionId == factionId && ship.SystemId == systemId && ship.Health > 0f) * 6f; return stationValue + shipValue; } private static bool CanRunOffensivePosture( FactionRuntime faction, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment) { var defenseLoad = threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind is "controlled-system" or "contested-system"); var militarySurplus = economicAssessment.MilitaryShipCount - Math.Max(1, economicAssessment.TargetMilitaryShipCount - faction.Doctrine.ReinforcementLeadPerFront); return economicAssessment.SustainmentScore >= faction.Doctrine.OffensiveReadinessThreshold && economicAssessment.LogisticsSecurityScore >= faction.Doctrine.SupplySecurityBias && militarySurplus > 0 && defenseLoad <= Math.Max(1, economicAssessment.ControlledSystemCount / 2); } private static bool CanSupportExpansion( FactionRuntime faction, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment) => economicAssessment.ConstructorShipCount > 0 && economicAssessment.SustainmentScore >= 0.52f && threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind == "controlled-system") <= 1 && faction.StrategicState.Budget.ExpansionCredits > 0f; private static float ComputeSystemRisk(SimulationWorld world, FactionRuntime faction, string systemId) => MathF.Max( faction.Memory.SystemMemories.FirstOrDefault(memory => memory.SystemId == systemId)?.RouteRisk ?? 0f, GeopoliticalSimulationService.GetSystemRouteRisk(world, systemId, faction.Id)); private static SectorStrategicProfileRuntime? FindStrategicProfile(SimulationWorld world, string systemId) => world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => string.Equals(profile.SystemId, systemId, StringComparison.Ordinal)); private static TerritoryPressureRuntime? FindTerritoryPressure(SimulationWorld world, string factionId, string systemId) => world.Geopolitics?.Territory.Pressures .Where(pressure => string.Equals(pressure.SystemId, systemId, StringComparison.Ordinal) && (pressure.FactionId is null || string.Equals(pressure.FactionId, factionId, StringComparison.Ordinal))) .OrderByDescending(pressure => pressure.PressureScore + pressure.CorridorRisk) .ThenBy(pressure => pressure.Id, StringComparer.Ordinal) .FirstOrDefault(); private static RegionalSecurityAssessmentRuntime? FindRegionalSecurityAssessment(SimulationWorld world, string factionId, string systemId) { var regionId = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, systemId)?.Id; return regionId is null ? null : world.Geopolitics?.EconomyRegions.SecurityAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, regionId, StringComparison.Ordinal)); } private static IEnumerable SelectOffensiveTargets( SimulationWorld world, FactionRuntime faction, FactionThreatAssessmentRuntime threatAssessment, FactionEconomicAssessmentRuntime economicAssessment) { return threatAssessment.ThreatSignals .Where(signal => signal.ScopeKind == "hostile-system") .Select(signal => { var memory = faction.Memory.SystemMemories.FirstOrDefault(candidate => candidate.SystemId == signal.ScopeId); var distancePenalty = faction.Memory.SystemMemories .Where(candidate => candidate.ControlledByFaction) .Select(candidate => GetSystemDistanceTier(world, candidate.SystemId, signal.ScopeId)) .DefaultIfEmpty(world.Systems.Count) .Min(); var failurePenalty = ((memory?.OffensiveFailures ?? 0) * 16f) + ((memory?.DefensiveFailures ?? 0) * 4f); var value = (signal.EnemyStationCount * 28f) + (signal.EnemyShipCount * 8f); var pressureBias = MathF.Max(0f, economicAssessment.SustainmentScore - 0.5f) * 25f; var supplyRisk = memory?.RouteRisk ?? 0.1f; var priority = 62f + value + pressureBias - (distancePenalty * 9f) - failurePenalty - (supplyRisk * 18f); return new OffensiveTargetCandidate( signal.ScopeId, signal.EnemyFactionId, ResolvePrimaryOffensiveAnchor(world, faction.Id, signal.ScopeId), ResolveSystemAnchor(world, signal.ScopeId), MathF.Max(0f, priority), supplyRisk, value); }) .Where(candidate => candidate.Priority > 35f) .OrderByDescending(candidate => candidate.Priority) .ThenBy(candidate => candidate.SystemId, StringComparer.Ordinal); } private static string? ResolvePrimaryOffensiveAnchor(SimulationWorld world, string factionId, string systemId) => world.Stations .Where(station => station.FactionId != factionId && station.SystemId == systemId) .OrderByDescending(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal) ? 1 : 0) .ThenBy(station => station.Id, StringComparer.Ordinal) .Select(station => station.Id) .FirstOrDefault(); private static bool StationCanProduceItem(SimulationWorld world, StationRuntime station, string itemId) => world.Recipes.Values.Any(recipe => StationSimulationService.RecipeAppliesToStation(station, recipe) && recipe.Outputs.Any(output => string.Equals(output.ItemId, itemId, StringComparison.Ordinal))); private static float ComputeCampaignSupplyAdequacy( FactionRuntime faction, FactionTheaterRuntime theater, FactionEconomicAssessmentRuntime economicAssessment) { var riskPenalty = theater.SupplyRisk * 0.45f; var assetBias = MathF.Min(0.25f, theater.FriendlyAssetValue / 200f); return Math.Clamp((economicAssessment.SustainmentScore * 0.75f) + assetBias - riskPenalty, 0f, 1f); } private static float ComputeCampaignContinuationScore( FactionRuntime faction, FactionTheaterRuntime theater, FactionEconomicAssessmentRuntime economicAssessment, FactionSystemMemoryRuntime? systemMemory) { var successBias = theater.Kind switch { "offense-front" => (systemMemory?.OffensiveSuccesses ?? 0) * 0.08f, _ => (systemMemory?.DefensiveSuccesses ?? 0) * 0.06f, }; var failurePenalty = theater.Kind switch { "offense-front" => (systemMemory?.OffensiveFailures ?? 0) * (0.09f + faction.Doctrine.FailureAversion * 0.1f), _ => (systemMemory?.DefensiveFailures ?? 0) * 0.06f, }; return Math.Clamp( 0.35f + (theater.Priority / 160f) + (economicAssessment.SustainmentScore * 0.4f) + successBias - failurePenalty - (theater.SupplyRisk * 0.3f), 0f, 1.4f); } private static string? ResolveCampaignPauseReason( FactionRuntime faction, FactionTheaterRuntime theater, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment, FactionSystemMemoryRuntime? systemMemory) { return theater.Kind switch { "offense-front" when !CanRunOffensivePosture(faction, economicAssessment, threatAssessment) => "offensive-readiness-insufficient", "offense-front" when (systemMemory?.OffensiveFailures ?? 0) >= 2 && economicAssessment.SustainmentScore < 0.75f => "recover-after-failure", "offense-front" when economicAssessment.ReplacementPressure > Math.Max(4f, economicAssessment.TargetMilitaryShipCount * 0.4f) => "replacement-pressure", "expansion-front" when !CanSupportExpansion(faction, economicAssessment, threatAssessment) => "expansion-delayed", "economic-front" when economicAssessment.CriticalShortageCount == 0 && economicAssessment.SustainmentScore > 0.7f => "economy-stable", _ => null, }; } private static int GetCampaignFailureCount(FactionSystemMemoryRuntime? systemMemory, string theaterKind) => theaterKind switch { "offense-front" => systemMemory?.OffensiveFailures ?? 0, _ => systemMemory?.DefensiveFailures ?? 0, }; private static int GetCampaignSuccessCount(FactionSystemMemoryRuntime? systemMemory, string theaterKind) => theaterKind switch { "offense-front" => systemMemory?.OffensiveSuccesses ?? 0, _ => systemMemory?.DefensiveSuccesses ?? 0, }; private static bool RequiresReinforcement( FactionTheaterRuntime theater, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment) => theater.Kind == "defense-front" ? theater.Priority > 105f || threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind == "controlled-system") > 1 : theater.Kind == "offense-front" ? economicAssessment.SustainmentScore > 0.7f && economicAssessment.MilitaryShipCount > economicAssessment.TargetMilitaryShipCount : theater.Kind == "economic-front" ? economicAssessment.CriticalShortageCount > 2 : false; private static void UpdateCampaignMemoryFromOutcome( SimulationWorld world, FactionRuntime faction, FactionCampaignRuntime campaign, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment, DateTimeOffset nowUtc) { var systemMemory = campaign.TargetSystemId is null ? null : faction.Memory.SystemMemories.FirstOrDefault(memory => memory.SystemId == campaign.TargetSystemId); var success = campaign.Kind switch { "defense" => campaign.TargetSystemId is not null && FactionControlsSystem(world, faction.Id, campaign.TargetSystemId) && threatAssessment.ThreatSignals.All(signal => signal.ScopeId != campaign.TargetSystemId || signal.ScopeKind == "hostile-system"), "offense" => campaign.TargetSystemId is not null && (FactionControlsSystem(world, faction.Id, campaign.TargetSystemId) || world.Stations.All(station => station.FactionId == faction.Id || station.SystemId != campaign.TargetSystemId)), "expansion" => campaign.TargetSystemId is not null && (world.ConstructionSites.Any(site => site.FactionId == faction.Id && site.SystemId == campaign.TargetSystemId) || world.Stations.Any(station => station.FactionId == faction.Id && station.SystemId == campaign.TargetSystemId)), "economic-stabilization" => campaign.CommodityId is not null && economicAssessment.CommoditySignals.FirstOrDefault(signal => signal.ItemId == campaign.CommodityId) is { Level: not ("critical" or "low") }, "force-build-up" => economicAssessment.MilitaryShipCount >= economicAssessment.TargetMilitaryShipCount, _ => true, }; if (systemMemory is not null) { if (campaign.Kind == "offense") { if (success) { systemMemory.OffensiveSuccesses += 1; } else { systemMemory.OffensiveFailures += 1; } } else if (campaign.Kind == "defense") { if (success) { systemMemory.DefensiveSuccesses += 1; systemMemory.FrontierPressure *= 0.75f; systemMemory.RouteRisk *= 0.8f; } else { systemMemory.DefensiveFailures += 1; } } } AppendOutcome(faction, new FactionOutcomeRecordRuntime { Id = $"outcome-{campaign.Id}-{nowUtc.ToUnixTimeMilliseconds()}", Kind = success ? "campaign-success" : "campaign-failure", Summary = $"{campaign.Kind} campaign {campaign.Id} {(success ? "succeeded" : "failed")}.", RelatedCampaignId = campaign.Id, OccurredAtUtc = nowUtc, }); } private static int GetSystemDistanceTier(SimulationWorld world, string originSystemId, string targetSystemId) { if (string.Equals(originSystemId, targetSystemId, StringComparison.Ordinal)) { return 0; } var originPosition = world.Systems.FirstOrDefault(system => system.Definition.Id == originSystemId)?.Position ?? Vector3.Zero; return world.Systems .OrderBy(system => system.Position.DistanceTo(originPosition)) .ThenBy(system => system.Definition.Id, StringComparer.Ordinal) .Select(system => system.Definition.Id) .TakeWhile(systemId => !string.Equals(systemId, targetSystemId, StringComparison.Ordinal)) .Count(); } private static List BuildPatrolPoints(Vector3 anchor, float radius) => [ new Vector3(anchor.X + radius, anchor.Y, anchor.Z), new Vector3(anchor.X, anchor.Y, anchor.Z + radius), new Vector3(anchor.X - radius, anchor.Y, anchor.Z), new Vector3(anchor.X, anchor.Y, anchor.Z - radius), ]; private sealed record OffensiveTargetCandidate( string SystemId, string? TargetFactionId, string? AnchorEntityId, Vector3 AnchorPosition, float Priority, float SupplyRisk, float Value); private static CommanderAssignmentRuntime ToAssignment(FactionOperationalObjectiveRuntime objective) => new() { ObjectiveId = objective.Id, CampaignId = objective.CampaignId, TheaterId = objective.TheaterId, Kind = objective.Kind, BehaviorKind = objective.BehaviorKind, Status = objective.Status, Priority = objective.Priority, HomeSystemId = objective.HomeSystemId, HomeStationId = objective.HomeStationId, TargetSystemId = objective.TargetSystemId, TargetEntityId = objective.TargetEntityId, TargetPosition = objective.TargetPosition, ItemId = objective.ItemId, Notes = objective.Notes, UpdatedAtUtc = objective.UpdatedAtUtc, }; private static bool AssignmentsEqual(CommanderAssignmentRuntime? left, CommanderAssignmentRuntime? right) { if (ReferenceEquals(left, right)) { return true; } if (left is null || right is null) { return false; } return string.Equals(left.ObjectiveId, right.ObjectiveId, StringComparison.Ordinal) && string.Equals(left.CampaignId, right.CampaignId, StringComparison.Ordinal) && string.Equals(left.TheaterId, right.TheaterId, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.BehaviorKind, right.BehaviorKind, StringComparison.Ordinal) && string.Equals(left.Status, right.Status, StringComparison.Ordinal) && string.Equals(left.HomeSystemId, right.HomeSystemId, StringComparison.Ordinal) && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && left.Priority.Equals(right.Priority); } private static void AppendDecision(FactionRuntime faction, FactionDecisionLogEntryRuntime entry) { if (faction.DecisionLog.Any(candidate => candidate.Id == entry.Id)) { return; } faction.DecisionLog.Add(entry); if (faction.DecisionLog.Count > MaxDecisionLogEntries) { faction.DecisionLog.RemoveRange(0, faction.DecisionLog.Count - MaxDecisionLogEntries); } } private static void AppendOutcome(FactionRuntime faction, FactionOutcomeRecordRuntime entry) { if (faction.Memory.RecentOutcomes.Any(candidate => candidate.Id == entry.Id)) { return; } faction.Memory.RecentOutcomes.Add(entry); if (faction.Memory.RecentOutcomes.Count > MaxOutcomeEntries) { faction.Memory.RecentOutcomes.RemoveRange(0, faction.Memory.RecentOutcomes.Count - MaxOutcomeEntries); } } private static string ResolveStrategicStatus( IReadOnlyCollection theaters, IReadOnlyCollection campaigns, FactionEconomicAssessmentRuntime economicAssessment, FactionThreatAssessmentRuntime threatAssessment) { if (threatAssessment.ThreatSignals.Any(signal => signal.ScopeKind == "controlled-system")) { return "defending"; } if (campaigns.Any(campaign => campaign.Kind == "offense")) { return "offensive"; } if (campaigns.Any(campaign => campaign.Kind == "expansion")) { return "expanding"; } if (economicAssessment.CommoditySignals.Any(signal => signal.Level is "critical" or "low")) { return "stabilizing"; } return theaters.Count == 0 ? "stable" : "active"; } private static float ComputeCommodityPriority(FactionCommoditySignalRuntime signal) { var levelBias = signal.Level switch { "critical" => 60f, "low" => 35f, _ => 10f, }; return levelBias + signal.BuyBacklog + signal.ReservedForConstruction + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 50f); } private static bool CanMineItem(SimulationWorld world, string itemId) => world.Nodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal)); private static string? FindPrimaryCommoditySystem(SimulationWorld world, string factionId, string itemId) => world.Geopolitics?.EconomyRegions.Bottlenecks .Where(bottleneck => string.Equals(bottleneck.ItemId, itemId, StringComparison.Ordinal)) .Join( world.Geopolitics.EconomyRegions.Regions.Where(region => string.Equals(region.FactionId, factionId, StringComparison.Ordinal)), bottleneck => bottleneck.RegionId, region => region.Id, (bottleneck, region) => new { bottleneck, region }) .OrderByDescending(entry => entry.bottleneck.Severity) .ThenBy(entry => entry.region.Id, StringComparer.Ordinal) .Select(entry => entry.region.CoreSystemId) .FirstOrDefault() ?? world.Stations .Where(station => station.FactionId == factionId && GetInventoryAmount(station.Inventory, itemId) > 0.01f) .OrderByDescending(station => GetInventoryAmount(station.Inventory, itemId)) .ThenBy(station => station.Id, StringComparer.Ordinal) .Select(station => station.SystemId) .FirstOrDefault(); private static StationRuntime? ResolveCommodityAnchorStation(SimulationWorld world, string factionId, string itemId) => world.Stations .Where(station => station.FactionId == factionId) .OrderByDescending(station => GetInventoryAmount(station.Inventory, itemId)) .ThenByDescending(station => station.MarketOrderIds.Count(orderId => world.MarketOrders.Any(order => order.Id == orderId && order.ItemId == itemId && order.Kind == MarketOrderKinds.Buy))) .ThenBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); private static string BuildCampaignSummary(FactionTheaterRuntime theater, IndustryExpansionProject? expansionProject) => theater.Kind switch { "defense-front" => $"Defend {theater.SystemId} from hostile pressure.", "offense-front" => $"Project force into {theater.SystemId}.", "expansion-front" => $"Expand into {expansionProject?.CelestialId ?? theater.SystemId}.", "economic-front" => $"Stabilize commodity shortages around {theater.AnchorEntityId ?? theater.SystemId}.", _ => theater.Kind, }; private static string? ResolveCommodityFromTheaterId(string? theaterId) { if (string.IsNullOrWhiteSpace(theaterId)) { return null; } const string prefix = "theater-economy-"; return theaterId.StartsWith(prefix, StringComparison.Ordinal) ? theaterId[prefix.Length..] : null; } private static Vector3 ResolveSystemAnchor(SimulationWorld world, string? systemId) { if (systemId is null) { return Vector3.Zero; } var station = world.Stations .Where(candidate => candidate.SystemId == systemId) .OrderBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); if (station is not null) { return station.Position; } var celestial = world.Celestials .Where(candidate => candidate.SystemId == systemId) .OrderBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); return celestial?.Position ?? Vector3.Zero; } private static Vector3 ResolveExpansionAnchor(SimulationWorld world, IndustryExpansionProject project) { if (project.SiteId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == project.SiteId) is { } site && world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId) is { } siteCelestial) { return siteCelestial.Position; } return world.Celestials.FirstOrDefault(candidate => candidate.Id == project.CelestialId)?.Position ?? ResolveSystemAnchor(world, project.SystemId); } private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) => GeopoliticalSimulationService.FactionControlsSystem(world, factionId, systemId); }