Deepen faction economy and station planning

This commit is contained in:
2026-03-19 23:34:06 -04:00
parent 9a5040cf1f
commit cd1fe776a5
33 changed files with 3170 additions and 175 deletions

View File

@@ -12,17 +12,22 @@ internal sealed class CommanderPlanningService
private static readonly IReadOnlyList<GoapGoal<FactionPlanningState>> _factionGoals =
[
new ExterminateRivalGoal(),
new EnsureWarIndustryGoal(),
new ExpandTerritoryGoal(),
new EnsureWarFleetGoal(),
new EnsureWaterSecurityGoal(),
new EnsureMiningCapacityGoal(),
new EnsureConstructionCapacityGoal(),
];
private static readonly IReadOnlyList<GoapAction<ShipPlanningState>> _shipActions =
[
new SetAttackObjectiveAction(),
new SetMiningObjectiveAction(),
new SetPatrolObjectiveAction(),
new SetConstructionObjectiveAction(),
new SetTradeObjectiveAction(),
new SetIdleObjectiveAction(),
];
@@ -93,6 +98,19 @@ internal sealed class CommanderPlanningService
var plan = _factionPlanner.Plan(state, goal, actions);
plan?.CurrentAction?.Execute(engine, world, commander);
}
if (FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId) is null)
{
if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-war-industry", StringComparison.Ordinal)))
{
TryQueueFactionExpansionProject(world, commander, SelectGoalDrivenWarIndustryProject(world, state, commander.FactionId));
}
else if (rankedGoals.Any(entry => string.Equals(entry.goal.Name, "ensure-water-security", StringComparison.Ordinal)))
{
TryQueueFactionExpansionProject(world, commander, FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water"));
}
}
}
private void UpdateShipCommander(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
@@ -124,9 +142,20 @@ internal sealed class CommanderPlanningService
internal FactionPlanningState BuildFactionPlanningState(SimulationWorld world, string factionId)
{
var stations = world.Stations.Where(s => s.FactionId == factionId).ToList();
var economy = FactionEconomyAnalyzer.Build(world, factionId);
var refinedMetals = economy.GetCommodity("refinedmetals");
var hullparts = economy.GetCommodity("hullparts");
var claytronics = economy.GetCommodity("claytronics");
var water = economy.GetCommodity("water");
return new FactionPlanningState
{
EnemyFactionCount = world.Factions.Count(f => f.Id != factionId),
EnemyShipCount = world.Ships.Count(s =>
s.Health > 0f &&
!string.Equals(s.FactionId, factionId, StringComparison.Ordinal)),
EnemyStationCount = world.Stations.Count(s =>
!string.Equals(s.FactionId, factionId, StringComparison.Ordinal)),
MilitaryShipCount = world.Ships.Count(s =>
s.FactionId == factionId &&
string.Equals(s.Definition.Kind, "military", StringComparison.Ordinal)),
@@ -142,8 +171,19 @@ internal sealed class CommanderPlanningService
ControlledSystemCount = StationSimulationService.GetFactionControlledSystemsCount(world, factionId),
TargetSystemCount = Math.Max(1, Math.Min(StationSimulationService.StrategicControlTargetSystems, world.Systems.Count)),
HasShipFactory = stations.Any(s => s.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
OreStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "ore")),
RefinedMetalsStockpile = stations.Sum(s => GetInventoryAmount(s.Inventory, "refinedmetals")),
OreStockpile = economy.GetCommodity("ore").OnHand,
RefinedMetalsStockpile = refinedMetals.OnHand,
RefinedMetalsProductionRate = refinedMetals.ProjectedProductionRatePerSecond,
RefinedMetalsShortageHorizonSeconds = refinedMetals.ProjectedShortageHorizonSeconds,
HullpartsStockpile = hullparts.OnHand,
HullpartsProductionRate = hullparts.ProjectedProductionRatePerSecond,
HullpartsShortageHorizonSeconds = hullparts.ProjectedShortageHorizonSeconds,
ClaytronicsStockpile = claytronics.OnHand,
ClaytronicsProductionRate = claytronics.ProjectedProductionRatePerSecond,
ClaytronicsShortageHorizonSeconds = claytronics.ProjectedShortageHorizonSeconds,
WaterStockpile = water.OnHand,
WaterProductionRate = water.ProjectedProductionRatePerSecond,
WaterShortageHorizonSeconds = water.ProjectedShortageHorizonSeconds,
};
}
@@ -156,13 +196,52 @@ internal sealed class CommanderPlanningService
c.FactionId == commander.FactionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
var enemyTarget = SelectEnemyTarget(world, ship);
var tradeRoute = SelectTradeRoute(world, ship.FactionId);
var expansionProject = FactionIndustryPlanner.GetActiveExpansionProject(world, ship.FactionId);
if (commander.ActiveBehavior is not null)
{
commander.ActiveBehavior.AreaSystemId = enemyTarget?.SystemId;
commander.ActiveBehavior.TargetEntityId = enemyTarget?.EntityId;
if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal))
{
commander.ActiveBehavior.ItemId = tradeRoute?.ItemId;
commander.ActiveBehavior.StationId = tradeRoute?.SourceStationId;
commander.ActiveBehavior.TargetEntityId = tradeRoute?.DestinationStationId;
}
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal) && expansionProject is not null)
{
commander.ActiveBehavior.StationId = expansionProject.SupportStationId;
commander.ActiveBehavior.TargetEntityId = expansionProject.SiteId;
commander.ActiveBehavior.ModuleId = expansionProject.ModuleId;
commander.ActiveBehavior.AreaSystemId = expansionProject.SystemId;
}
else if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal))
{
commander.ActiveBehavior.TargetEntityId = null;
commander.ActiveBehavior.ModuleId = null;
commander.ActiveBehavior.AreaSystemId = ship.SystemId;
}
}
return new ShipPlanningState
{
ShipKind = ship.Definition.Kind,
HasMiningCapability = HasShipCapabilities(ship.Definition, "mining"),
FactionWantsOre = true,
FactionWantsCombat = factionCommander?.ActiveDirectives.Contains("attack-rival", StringComparer.Ordinal) ?? false,
FactionWantsExpansion = factionCommander?.ActiveDirectives
.Contains("expand-territory", StringComparer.Ordinal) ?? false,
FactionNeedsShipyard = !(factionCommander?.ActiveDirectives.Contains("bootstrap-war-industry", StringComparer.Ordinal) ?? false)
? false
: !world.Stations.Any(station =>
string.Equals(station.FactionId, ship.FactionId, StringComparison.Ordinal)
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
TargetEnemySystemId = enemyTarget?.SystemId,
TargetEnemyEntityId = enemyTarget?.EntityId,
TradeItemId = tradeRoute?.ItemId,
TradeSourceStationId = tradeRoute?.SourceStationId,
TradeDestinationStationId = tradeRoute?.DestinationStationId,
};
}
@@ -170,11 +249,15 @@ internal sealed class CommanderPlanningService
{
var actions = new List<GoapAction<FactionPlanningState>>();
actions.Add(new PlanWarIndustryAction());
actions.Add(new PlanCommoditySupplyAction("water"));
foreach (var (shipId, def) in world.ShipDefinitions)
{
actions.Add(new OrderShipProductionAction(def.Kind, shipId));
}
actions.Add(new LaunchExterminationCampaignAction());
actions.Add(new ExpandToSystemAction());
return actions;
}
@@ -184,4 +267,137 @@ internal sealed class CommanderPlanningService
c.FactionId == factionId &&
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal))
?.ActiveDirectives.Contains(directive, StringComparer.Ordinal) ?? false;
private static void TryQueueFactionExpansionProject(
SimulationWorld world,
CommanderRuntime commander,
IndustryExpansionProject? project)
{
if (project is null)
{
return;
}
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
commander.ActiveDirectives.Add($"expand-industry:{project.CommodityId}:{project.SystemId}:{project.CelestialId}");
}
private static IndustryExpansionProject? SelectGoalDrivenWarIndustryProject(
SimulationWorld world,
FactionPlanningState state,
string factionId)
{
if (!state.HasRefinedMetalsProduction || state.RefinedMetalsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "refinedmetals");
}
if (!state.HasHullpartsProduction || state.HullpartsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "hullparts");
}
if (!state.HasClaytronicsProduction || state.ClaytronicsShortageHorizonSeconds < 240f)
{
return FactionIndustryPlanner.AnalyzeCommodityNeed(world, factionId, "claytronics");
}
if (!state.HasShipFactory)
{
return FactionIndustryPlanner.CreateShipyardFoundationProject(world, factionId);
}
return null;
}
private static (string EntityId, string SystemId)? SelectEnemyTarget(SimulationWorld world, ShipRuntime ship)
{
var hostileShip = world.Ships
.Where(candidate =>
candidate.Health > 0f &&
!string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
.OrderBy(candidate => candidate.SystemId == ship.SystemId ? 0 : 1)
.ThenBy(candidate => candidate.Position.DistanceTo(ship.Position))
.Select(candidate => (candidate.Id, candidate.SystemId))
.FirstOrDefault();
if (hostileShip != default)
{
return hostileShip;
}
var hostileStation = world.Stations
.Where(candidate => !string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
.OrderBy(candidate => candidate.SystemId == ship.SystemId ? 0 : 1)
.ThenBy(candidate => candidate.Position.DistanceTo(ship.Position))
.Select(candidate => (candidate.Id, candidate.SystemId))
.FirstOrDefault();
return hostileStation == default ? null : hostileStation;
}
private static (string ItemId, string SourceStationId, string DestinationStationId)? SelectTradeRoute(SimulationWorld world, string factionId)
{
var stationsById = world.Stations
.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal))
.ToDictionary(station => station.Id, StringComparer.Ordinal);
foreach (var demand in world.MarketOrders
.Where(order =>
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
&& order.Kind == MarketOrderKinds.Buy
&& order.RemainingAmount > 0.01f
&& order.StationId is not null)
.OrderByDescending(order => order.Valuation))
{
if (!stationsById.TryGetValue(demand.StationId!, out var destination))
{
continue;
}
if (!CanStationAcceptAdditionalItem(world, destination, demand.ItemId))
{
continue;
}
var source = stationsById.Values
.Where(station =>
station.Id != destination.Id
&& GetInventoryAmount(station.Inventory, demand.ItemId) > 1f)
.OrderByDescending(station => GetInventoryAmount(station.Inventory, demand.ItemId))
.FirstOrDefault();
if (source is not null)
{
return (demand.ItemId, source.Id, destination.Id);
}
}
return null;
}
private static bool CanStationAcceptAdditionalItem(SimulationWorld world, StationRuntime station, string itemId)
{
if (!world.ItemDefinitions.TryGetValue(itemId, out var definition))
{
return false;
}
var requiredModule = GetStorageRequirement(definition.CargoKind);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return false;
}
var capacity = GetStationStorageCapacity(station, definition.CargoKind);
if (capacity <= 0.01f)
{
return false;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var item) && string.Equals(item.CargoKind, definition.CargoKind, StringComparison.Ordinal))
.Sum(entry => entry.Value);
return used <= capacity - 1f;
}
}