1170 lines
43 KiB
C#
1170 lines
43 KiB
C#
namespace SpaceGame.Api.Factions.AI;
|
|
|
|
internal sealed class FactionObjectivePlanner
|
|
{
|
|
private readonly ObjectiveDependencyResolver _dependencyResolver = new();
|
|
private readonly ObjectiveStepFactory _stepFactory = new();
|
|
|
|
internal FactionBlackboardRuntime UpdateBlackboard(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionPlanningState state)
|
|
{
|
|
var blackboard = commander.FactionBlackboard ?? new FactionBlackboardRuntime();
|
|
var economy = FactionEconomyAnalyzer.Build(world, commander.FactionId);
|
|
var activeProject = FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId);
|
|
|
|
blackboard.PlanCycle = commander.PlanningCycle;
|
|
blackboard.UpdatedAtUtc = world.GeneratedAtUtc;
|
|
blackboard.TargetWarshipCount = FactionPlanningState.ComputeTargetWarships(state);
|
|
blackboard.HasWarIndustrySupplyChain = state.HasWarIndustrySupplyChain;
|
|
blackboard.HasShipyard = state.HasShipFactory;
|
|
blackboard.HasActiveExpansionProject = activeProject is not null;
|
|
blackboard.ActiveExpansionCommodityId = activeProject?.CommodityId;
|
|
blackboard.ActiveExpansionModuleId = activeProject?.ModuleId;
|
|
blackboard.ActiveExpansionSiteId = activeProject?.SiteId;
|
|
blackboard.ActiveExpansionSystemId = activeProject?.SystemId;
|
|
blackboard.EnemyFactionCount = state.EnemyFactionCount;
|
|
blackboard.EnemyShipCount = state.EnemyShipCount;
|
|
blackboard.EnemyStationCount = state.EnemyStationCount;
|
|
blackboard.MilitaryShipCount = state.MilitaryShipCount;
|
|
blackboard.MinerShipCount = state.MinerShipCount;
|
|
blackboard.TransportShipCount = state.TransportShipCount;
|
|
blackboard.ConstructorShipCount = state.ConstructorShipCount;
|
|
blackboard.ControlledSystemCount = state.ControlledSystemCount;
|
|
blackboard.AvailableShipIds.Clear();
|
|
foreach (var ship in world.Ships.Where(ship =>
|
|
ship.Health > 0f
|
|
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)))
|
|
{
|
|
blackboard.AvailableShipIds.Add(ship.Id);
|
|
}
|
|
|
|
blackboard.CommoditySignals.Clear();
|
|
foreach (var commodityId in EnumerateTrackedCommodityIds())
|
|
{
|
|
var commodity = economy.GetCommodity(commodityId);
|
|
blackboard.CommoditySignals.Add(new FactionCommoditySignalRuntime
|
|
{
|
|
ItemId = commodityId,
|
|
AvailableStock = commodity.AvailableStock,
|
|
OnHand = commodity.OnHand,
|
|
ProductionRatePerSecond = commodity.ProductionRatePerSecond,
|
|
CommittedProductionRatePerSecond = commodity.CommittedProductionRatePerSecond,
|
|
UsageRatePerSecond = commodity.OperationalUsageRatePerSecond,
|
|
NetRatePerSecond = commodity.NetRatePerSecond,
|
|
ProjectedNetRatePerSecond = commodity.ProjectedNetRatePerSecond,
|
|
LevelSeconds = commodity.LevelSeconds,
|
|
Level = commodity.Level.ToString().ToLowerInvariant(),
|
|
ProjectedProductionRatePerSecond = commodity.ProjectedProductionRatePerSecond,
|
|
BuyBacklog = commodity.BuyBacklog,
|
|
ReservedForConstruction = commodity.ReservedForConstruction,
|
|
});
|
|
}
|
|
|
|
blackboard.ThreatSignals.Clear();
|
|
foreach (var threat in world.Systems
|
|
.Select(system => new FactionThreatSignalRuntime
|
|
{
|
|
ScopeId = system.Definition.Id,
|
|
ScopeKind = "system",
|
|
EnemyShipCount = world.Ships.Count(ship =>
|
|
ship.Health > 0f
|
|
&& string.Equals(ship.SystemId, system.Definition.Id, StringComparison.Ordinal)
|
|
&& !string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)),
|
|
EnemyStationCount = world.Stations.Count(station =>
|
|
string.Equals(station.SystemId, system.Definition.Id, StringComparison.Ordinal)
|
|
&& !string.Equals(station.FactionId, commander.FactionId, StringComparison.Ordinal)),
|
|
})
|
|
.Where(threat => threat.EnemyShipCount > 0 || threat.EnemyStationCount > 0))
|
|
{
|
|
blackboard.ThreatSignals.Add(threat);
|
|
}
|
|
|
|
commander.FactionBlackboard = blackboard;
|
|
return blackboard;
|
|
}
|
|
|
|
internal void RefreshObjectives(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionPlanningState state,
|
|
IReadOnlyList<(string GoalName, float Priority)> rankedGoals)
|
|
{
|
|
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are refreshed.");
|
|
|
|
var objectiveIndex = commander.Objectives.ToDictionary(objective => objective.MergeKey, StringComparer.Ordinal);
|
|
var touchedObjectiveIds = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var enemyFaction in world.Factions.Where(faction =>
|
|
!string.Equals(faction.Id, commander.FactionId, StringComparison.Ordinal)))
|
|
{
|
|
var priority = rankedGoals.FirstOrDefault(goal => string.Equals(goal.GoalName, "exterminate-rival", StringComparison.Ordinal)).Priority;
|
|
var objective = GetOrCreateObjective(
|
|
commander,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
mergeKey: $"destroy-faction:{enemyFaction.Id}",
|
|
kind: FactionObjectiveKind.DestroyFaction,
|
|
priority: MathF.Max(priority, 1f),
|
|
configure: current =>
|
|
{
|
|
current.TargetFactionId = enemyFaction.Id;
|
|
current.State = enemyFaction.ShipsLost >= 0 ? current.State : FactionObjectiveState.Planned;
|
|
});
|
|
}
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.BootstrapWarIndustry,
|
|
"bootstrap-war-industry",
|
|
"ensure-war-industry");
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.BuildShipyard,
|
|
"build-shipyard",
|
|
"ensure-war-industry");
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.BuildAttackFleet,
|
|
"build-attack-fleet",
|
|
"ensure-war-fleet");
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.EnsureWaterSecurity,
|
|
"secure-water",
|
|
"ensure-water-security");
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.EnsureMiningCapacity,
|
|
"ensure-mining-capacity",
|
|
"ensure-mining-capacity");
|
|
|
|
RefreshSupportObjective(
|
|
commander,
|
|
state,
|
|
rankedGoals,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
FactionObjectiveKind.EnsureConstructionCapacity,
|
|
"ensure-construction-capacity",
|
|
"ensure-construction-capacity");
|
|
|
|
for (var index = 0; index < commander.Objectives.Count; index += 1)
|
|
{
|
|
var objective = commander.Objectives[index];
|
|
if (!touchedObjectiveIds.Contains(objective.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_stepFactory.EnsureObjectiveSteps(objective, blackboard, state);
|
|
_dependencyResolver.ResolveDependencies(world, commander, objective, blackboard, state, objectiveIndex, touchedObjectiveIds);
|
|
}
|
|
|
|
foreach (var objective in commander.Objectives)
|
|
{
|
|
objective.UpdatedAtCycle = commander.PlanningCycle;
|
|
if (!touchedObjectiveIds.Contains(objective.Id) && objective.State is not FactionObjectiveState.Complete and not FactionObjectiveState.Cancelled)
|
|
{
|
|
objective.State = FactionObjectiveState.Cancelled;
|
|
objective.InvalidationReason = "Objective no longer relevant to the current faction plan.";
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RefreshSupportObjective(
|
|
CommanderRuntime commander,
|
|
FactionPlanningState state,
|
|
IReadOnlyList<(string GoalName, float Priority)> rankedGoals,
|
|
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
|
|
ISet<string> touchedObjectiveIds,
|
|
FactionObjectiveKind kind,
|
|
string mergeKey,
|
|
string goalName)
|
|
{
|
|
var priority = rankedGoals.FirstOrDefault(goal => string.Equals(goal.GoalName, goalName, StringComparison.Ordinal)).Priority;
|
|
if (priority <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var objective = GetOrCreateObjective(
|
|
commander,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
mergeKey,
|
|
kind,
|
|
priority,
|
|
configure: _ => { });
|
|
}
|
|
|
|
private static FactionObjectiveRuntime GetOrCreateObjective(
|
|
CommanderRuntime commander,
|
|
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
|
|
ISet<string> touchedObjectiveIds,
|
|
string mergeKey,
|
|
FactionObjectiveKind kind,
|
|
float priority,
|
|
Action<FactionObjectiveRuntime> configure)
|
|
{
|
|
if (!objectiveIndex.TryGetValue(mergeKey, out var objective))
|
|
{
|
|
objective = new FactionObjectiveRuntime
|
|
{
|
|
Id = $"obj-{commander.FactionId}-{commander.Objectives.Count + 1}",
|
|
MergeKey = mergeKey,
|
|
Kind = kind,
|
|
Priority = priority,
|
|
CreatedAtCycle = commander.PlanningCycle,
|
|
UpdatedAtCycle = commander.PlanningCycle,
|
|
};
|
|
commander.Objectives.Add(objective);
|
|
objectiveIndex[mergeKey] = objective;
|
|
}
|
|
|
|
objective.Priority = MathF.Max(objective.Priority, priority);
|
|
if (objective.State is FactionObjectiveState.Cancelled or FactionObjectiveState.Failed)
|
|
{
|
|
objective.State = FactionObjectiveState.Planned;
|
|
objective.InvalidationReason = null;
|
|
}
|
|
|
|
configure(objective);
|
|
touchedObjectiveIds.Add(objective.Id);
|
|
return objective;
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateTrackedCommodityIds()
|
|
{
|
|
yield return "ore";
|
|
yield return "refinedmetals";
|
|
yield return "hullparts";
|
|
yield return "claytronics";
|
|
yield return "water";
|
|
yield return "energycells";
|
|
}
|
|
}
|
|
|
|
internal sealed class ObjectiveDependencyResolver
|
|
{
|
|
internal void ResolveDependencies(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionBlackboardRuntime blackboard,
|
|
FactionPlanningState state,
|
|
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
|
|
ISet<string> touchedObjectiveIds)
|
|
{
|
|
switch (objective.Kind)
|
|
{
|
|
case FactionObjectiveKind.DestroyFaction:
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "bootstrap-war-industry", FactionObjectiveKind.BootstrapWarIndustry, objective.Priority - 10f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-shipyard", FactionObjectiveKind.BuildShipyard, objective.Priority - 12f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-attack-fleet", FactionObjectiveKind.BuildAttackFleet, objective.Priority - 6f);
|
|
break;
|
|
case FactionObjectiveKind.BootstrapWarIndustry:
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "refinedmetals", objective.Priority - 2f);
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "hullparts", objective.Priority - 4f);
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "claytronics", objective.Priority - 4f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "secure-water", FactionObjectiveKind.EnsureWaterSecurity, objective.Priority - 15f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-mining-capacity", FactionObjectiveKind.EnsureMiningCapacity, objective.Priority - 8f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-construction-capacity", FactionObjectiveKind.EnsureConstructionCapacity, objective.Priority - 9f);
|
|
break;
|
|
case FactionObjectiveKind.BuildShipyard:
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "refinedmetals", objective.Priority - 2f);
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "hullparts", objective.Priority - 1f);
|
|
EnsureCommodityDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "claytronics", objective.Priority - 1f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-construction-capacity", FactionObjectiveKind.EnsureConstructionCapacity, objective.Priority - 5f);
|
|
break;
|
|
case FactionObjectiveKind.BuildAttackFleet:
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "build-shipyard", FactionObjectiveKind.BuildShipyard, objective.Priority - 2f);
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "bootstrap-war-industry", FactionObjectiveKind.BootstrapWarIndustry, objective.Priority - 3f);
|
|
break;
|
|
case FactionObjectiveKind.EnsureCommoditySupply:
|
|
if (string.Equals(objective.CommodityId, "refinedmetals", StringComparison.Ordinal))
|
|
{
|
|
AddDependency(commander, objective, objectiveIndex, touchedObjectiveIds, "ensure-mining-capacity", FactionObjectiveKind.EnsureMiningCapacity, objective.Priority - 2f);
|
|
}
|
|
break;
|
|
}
|
|
|
|
foreach (var step in objective.Steps)
|
|
{
|
|
step.DependencyStepIds.Clear();
|
|
foreach (var dependencyId in objective.PrerequisiteObjectiveIds)
|
|
{
|
|
if (commander.Objectives.FirstOrDefault(candidate => candidate.Id == dependencyId) is not { } dependencyObjective)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var dependencyStep in dependencyObjective.Steps)
|
|
{
|
|
if (dependencyStep.Status != FactionPlanStepStatus.Complete)
|
|
{
|
|
step.DependencyStepIds.Add(dependencyStep.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void EnsureCommodityDependency(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime parent,
|
|
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
|
|
ISet<string> touchedObjectiveIds,
|
|
string commodityId,
|
|
float priority)
|
|
{
|
|
var dependency = AddDependency(
|
|
commander,
|
|
parent,
|
|
objectiveIndex,
|
|
touchedObjectiveIds,
|
|
$"ensure-commodity:{commodityId}",
|
|
FactionObjectiveKind.EnsureCommoditySupply,
|
|
priority);
|
|
dependency.CommodityId = commodityId;
|
|
}
|
|
|
|
private static FactionObjectiveRuntime AddDependency(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime parent,
|
|
IDictionary<string, FactionObjectiveRuntime> objectiveIndex,
|
|
ISet<string> touchedObjectiveIds,
|
|
string mergeKey,
|
|
FactionObjectiveKind kind,
|
|
float priority)
|
|
{
|
|
if (!objectiveIndex.TryGetValue(mergeKey, out var dependency))
|
|
{
|
|
dependency = new FactionObjectiveRuntime
|
|
{
|
|
Id = $"obj-{commander.FactionId}-{commander.Objectives.Count + 1}",
|
|
MergeKey = mergeKey,
|
|
Kind = kind,
|
|
Priority = MathF.Max(1f, priority),
|
|
CreatedAtCycle = commander.PlanningCycle,
|
|
UpdatedAtCycle = commander.PlanningCycle,
|
|
ParentObjectiveId = parent.Id,
|
|
};
|
|
commander.Objectives.Add(dependency);
|
|
objectiveIndex[mergeKey] = dependency;
|
|
}
|
|
else
|
|
{
|
|
dependency.Priority = MathF.Max(dependency.Priority, priority);
|
|
}
|
|
|
|
dependency.ParentObjectiveId ??= parent.Id;
|
|
parent.PrerequisiteObjectiveIds.Add(dependency.Id);
|
|
touchedObjectiveIds.Add(dependency.Id);
|
|
return dependency;
|
|
}
|
|
}
|
|
|
|
internal sealed class ObjectiveStepFactory
|
|
{
|
|
internal void EnsureObjectiveSteps(
|
|
FactionObjectiveRuntime objective,
|
|
FactionBlackboardRuntime blackboard,
|
|
FactionPlanningState state)
|
|
{
|
|
if (objective.Steps.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (objective.Kind)
|
|
{
|
|
case FactionObjectiveKind.DestroyFaction:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.AttackFactionAssets, objective.Priority, targetFactionId: objective.TargetFactionId));
|
|
break;
|
|
case FactionObjectiveKind.BootstrapWarIndustry:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.MonitorExpansionProject, objective.Priority, notes: "Maintain the war-industry support program until the full chain exists."));
|
|
break;
|
|
case FactionObjectiveKind.BuildShipyard:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureShipyardSite, objective.Priority, moduleId: "module_gen_build_l_01"));
|
|
break;
|
|
case FactionObjectiveKind.BuildAttackFleet:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.ProduceFleet, objective.Priority));
|
|
break;
|
|
case FactionObjectiveKind.EnsureCommoditySupply:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureCommodityProduction, objective.Priority, commodityId: objective.CommodityId));
|
|
break;
|
|
case FactionObjectiveKind.EnsureWaterSecurity:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureWaterSupply, objective.Priority, commodityId: "water"));
|
|
break;
|
|
case FactionObjectiveKind.EnsureMiningCapacity:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureMiningCapacity, objective.Priority));
|
|
break;
|
|
case FactionObjectiveKind.EnsureConstructionCapacity:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureConstructionCapacity, objective.Priority));
|
|
break;
|
|
case FactionObjectiveKind.EnsureTransportCapacity:
|
|
objective.Steps.Add(CreateStep(objective, FactionPlanStepKind.EnsureTransportCapacity, objective.Priority));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static FactionPlanStepRuntime CreateStep(
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepKind kind,
|
|
float priority,
|
|
string? commodityId = null,
|
|
string? moduleId = null,
|
|
string? targetFactionId = null,
|
|
string? notes = null)
|
|
{
|
|
return new FactionPlanStepRuntime
|
|
{
|
|
Id = $"{objective.Id}-step-{objective.Steps.Count + 1}",
|
|
ObjectiveId = objective.Id,
|
|
Kind = kind,
|
|
Priority = priority,
|
|
CommodityId = commodityId,
|
|
ModuleId = moduleId,
|
|
TargetFactionId = targetFactionId,
|
|
Notes = notes,
|
|
};
|
|
}
|
|
}
|
|
|
|
internal sealed class FactionObjectiveExecutor
|
|
{
|
|
internal void Execute(
|
|
SimulationEngine engine,
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionPlanningState state)
|
|
{
|
|
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are executed.");
|
|
|
|
commander.ActiveGoalName = null;
|
|
commander.ActiveActionName = null;
|
|
ResetRuntimeAssignments(commander);
|
|
var touchedTaskIds = new HashSet<string>(StringComparer.Ordinal);
|
|
var assignedAssetIds = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var objective in commander.Objectives
|
|
.Where(objective => objective.State is not FactionObjectiveState.Cancelled and not FactionObjectiveState.Failed)
|
|
.OrderByDescending(objective => objective.Priority))
|
|
{
|
|
EvaluateObjective(world, commander, objective);
|
|
foreach (var step in objective.Steps.OrderByDescending(step => step.Priority))
|
|
{
|
|
EvaluateStep(world, commander, objective, step, blackboard, state);
|
|
EmitTasks(engine, world, commander, objective, step, blackboard, state, touchedTaskIds, assignedAssetIds);
|
|
}
|
|
}
|
|
|
|
ReconcileStaleTasks(commander, touchedTaskIds);
|
|
}
|
|
|
|
private static void ResetRuntimeAssignments(CommanderRuntime commander)
|
|
{
|
|
foreach (var objective in commander.Objectives)
|
|
{
|
|
objective.AssignedAssetIds.Clear();
|
|
foreach (var step in objective.Steps)
|
|
{
|
|
step.AssignedAssetIds.Clear();
|
|
step.IssuedTaskIds.Clear();
|
|
}
|
|
}
|
|
|
|
foreach (var task in commander.IssuedTasks)
|
|
{
|
|
task.AssignedAssetIds.Clear();
|
|
}
|
|
}
|
|
|
|
private static void EvaluateObjective(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective)
|
|
{
|
|
objective.BlockingReason = null;
|
|
objective.InvalidationReason = null;
|
|
|
|
var incompleteSteps = objective.Steps.Where(step => step.Status != FactionPlanStepStatus.Complete).ToList();
|
|
if (incompleteSteps.Count == 0)
|
|
{
|
|
objective.State = FactionObjectiveState.Complete;
|
|
return;
|
|
}
|
|
|
|
if (incompleteSteps.All(step => step.Status == FactionPlanStepStatus.Blocked))
|
|
{
|
|
objective.State = FactionObjectiveState.Blocked;
|
|
objective.BlockingReason = incompleteSteps.FirstOrDefault(step => !string.IsNullOrWhiteSpace(step.BlockingReason))?.BlockingReason;
|
|
return;
|
|
}
|
|
|
|
objective.State = FactionObjectiveState.Active;
|
|
}
|
|
|
|
private static void EvaluateStep(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
FactionBlackboardRuntime blackboard,
|
|
FactionPlanningState state)
|
|
{
|
|
step.LastEvaluatedCycle = commander.PlanningCycle;
|
|
step.BlockingReason = null;
|
|
|
|
if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step))
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = "Waiting for prerequisite objective steps to complete.";
|
|
return;
|
|
}
|
|
|
|
switch (step.Kind)
|
|
{
|
|
case FactionPlanStepKind.EnsureCommodityProduction:
|
|
EvaluateCommodityStep(step, blackboard);
|
|
break;
|
|
case FactionPlanStepKind.EnsureShipyardSite:
|
|
if (state.HasShipFactory)
|
|
{
|
|
step.Status = FactionPlanStepStatus.Complete;
|
|
step.ProducedFacts.Add("shipyard-online");
|
|
}
|
|
else
|
|
{
|
|
step.Status = blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionModuleId, step.ModuleId, StringComparison.Ordinal)
|
|
? FactionPlanStepStatus.Running
|
|
: FactionPlanStepStatus.Ready;
|
|
}
|
|
break;
|
|
case FactionPlanStepKind.ProduceFleet:
|
|
step.Status = state.MilitaryShipCount >= blackboard.TargetWarshipCount
|
|
? FactionPlanStepStatus.Complete
|
|
: FactionPlanStepStatus.Running;
|
|
break;
|
|
case FactionPlanStepKind.AttackFactionAssets:
|
|
if (blackboard.EnemyFactionCount <= 0)
|
|
{
|
|
step.Status = FactionPlanStepStatus.Complete;
|
|
}
|
|
else if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = "Insufficient military strength to commit to a faction attack objective.";
|
|
}
|
|
else
|
|
{
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
}
|
|
break;
|
|
case FactionPlanStepKind.EnsureWaterSupply:
|
|
step.Status = IsCommodityOperational(blackboard, "water", 300f)
|
|
? FactionPlanStepStatus.Complete
|
|
: blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionCommodityId, "water", StringComparison.Ordinal)
|
|
? FactionPlanStepStatus.Running
|
|
: FactionPlanStepStatus.Ready;
|
|
break;
|
|
case FactionPlanStepKind.EnsureMiningCapacity:
|
|
step.Status = state.MinerShipCount >= 2 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
break;
|
|
case FactionPlanStepKind.EnsureConstructionCapacity:
|
|
step.Status = state.ConstructorShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
break;
|
|
case FactionPlanStepKind.EnsureTransportCapacity:
|
|
step.Status = state.TransportShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
break;
|
|
case FactionPlanStepKind.MonitorExpansionProject:
|
|
step.Status = blackboard.HasWarIndustrySupplyChain ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void EvaluateCommodityStep(
|
|
FactionPlanStepRuntime step,
|
|
FactionBlackboardRuntime blackboard)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(step.CommodityId))
|
|
{
|
|
step.Status = FactionPlanStepStatus.Failed;
|
|
step.BlockingReason = "Commodity planning step is missing a target commodity.";
|
|
return;
|
|
}
|
|
|
|
var completed = IsCommodityOperational(blackboard, step.CommodityId, 240f);
|
|
|
|
step.Status = completed ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Ready;
|
|
if (completed)
|
|
{
|
|
step.ProducedFacts.Add($"commodity-online:{step.CommodityId}");
|
|
}
|
|
}
|
|
|
|
private static bool IsCommodityOperational(
|
|
FactionBlackboardRuntime blackboard,
|
|
string commodityId,
|
|
float minimumLevelSeconds)
|
|
{
|
|
var signal = blackboard.CommoditySignals.FirstOrDefault(candidate =>
|
|
string.Equals(candidate.ItemId, commodityId, StringComparison.Ordinal));
|
|
if (signal is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return signal.ProjectedProductionRatePerSecond > 0.01f
|
|
&& signal.ProjectedNetRatePerSecond >= -0.01f
|
|
&& signal.LevelSeconds >= minimumLevelSeconds
|
|
&& signal.Level is "stable" or "surplus";
|
|
}
|
|
|
|
private static void EmitTasks(
|
|
SimulationEngine engine,
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
FactionBlackboardRuntime blackboard,
|
|
FactionPlanningState state,
|
|
ISet<string> touchedTaskIds,
|
|
ISet<string> assignedAssetIds)
|
|
{
|
|
if (step.Status is FactionPlanStepStatus.Complete or FactionPlanStepStatus.Cancelled or FactionPlanStepStatus.Failed)
|
|
{
|
|
SyncTerminalTasks(commander, step, touchedTaskIds);
|
|
return;
|
|
}
|
|
|
|
commander.ActiveGoalName ??= objective.Kind.ToString();
|
|
commander.ActiveActionName ??= step.Kind.ToString();
|
|
|
|
switch (step.Kind)
|
|
{
|
|
case FactionPlanStepKind.EnsureCommodityProduction:
|
|
if (blackboard.HasActiveExpansionProject)
|
|
{
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: blackboard.ActiveExpansionCommodityId ?? step.CommodityId,
|
|
moduleId: blackboard.ActiveExpansionModuleId,
|
|
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes ?? "Expansion project already active for faction.");
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
return;
|
|
}
|
|
|
|
if (step.CommodityId is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var project = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId);
|
|
if (project is not null)
|
|
{
|
|
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
|
step.TargetSiteId = project.SiteId;
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
step.Notes = $"Queued expansion project for {project.CommodityId}.";
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: project.CommodityId,
|
|
moduleId: project.ModuleId,
|
|
targetSystemId: project.SystemId,
|
|
targetSiteId: project.SiteId,
|
|
blockingReason: null,
|
|
notes: step.Notes);
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
}
|
|
else
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = $"Unable to derive an expansion project for {step.CommodityId}.";
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: step.CommodityId,
|
|
moduleId: step.ModuleId,
|
|
targetSystemId: null,
|
|
targetSiteId: null,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes);
|
|
}
|
|
break;
|
|
case FactionPlanStepKind.EnsureShipyardSite:
|
|
if (blackboard.HasActiveExpansionProject)
|
|
{
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: blackboard.ActiveExpansionCommodityId,
|
|
moduleId: blackboard.ActiveExpansionModuleId ?? step.ModuleId,
|
|
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes ?? "Shipyard support project waiting on current expansion site.");
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
return;
|
|
}
|
|
|
|
var shipyardProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId);
|
|
if (shipyardProject is not null)
|
|
{
|
|
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, shipyardProject);
|
|
step.TargetSiteId = shipyardProject.SiteId;
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
step.Notes = "Queued shipyard foundation project.";
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: shipyardProject.CommodityId,
|
|
moduleId: shipyardProject.ModuleId,
|
|
targetSystemId: shipyardProject.SystemId,
|
|
targetSiteId: shipyardProject.SiteId,
|
|
blockingReason: null,
|
|
notes: step.Notes);
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
}
|
|
else
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = "Unable to identify a viable shipyard foundation project.";
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: step.CommodityId,
|
|
moduleId: step.ModuleId,
|
|
targetSystemId: null,
|
|
targetSiteId: null,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes);
|
|
}
|
|
break;
|
|
case FactionPlanStepKind.ProduceFleet:
|
|
if (!blackboard.HasShipyard)
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = "Fleet production requires an online shipyard.";
|
|
}
|
|
UpsertShipProductionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
shipRole: "military",
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes ?? "Maintain military ship production until war fleet target is satisfied.");
|
|
AssignShipyardAssets(world, commander, objective, step);
|
|
break;
|
|
case FactionPlanStepKind.AttackFactionAssets:
|
|
UpsertAttackTask(commander, objective, step, touchedTaskIds);
|
|
AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
break;
|
|
case FactionPlanStepKind.EnsureWaterSupply:
|
|
if (blackboard.HasActiveExpansionProject)
|
|
{
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: blackboard.ActiveExpansionCommodityId,
|
|
moduleId: blackboard.ActiveExpansionModuleId,
|
|
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes ?? "Water support project waiting on current expansion site.");
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
return;
|
|
}
|
|
|
|
var waterProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water");
|
|
if (waterProject is not null)
|
|
{
|
|
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, waterProject);
|
|
step.Status = FactionPlanStepStatus.Running;
|
|
step.Notes = "Queued water expansion project.";
|
|
step.TargetSiteId = waterProject.SiteId;
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: waterProject.CommodityId,
|
|
moduleId: waterProject.ModuleId,
|
|
targetSystemId: waterProject.SystemId,
|
|
targetSiteId: waterProject.SiteId,
|
|
blockingReason: null,
|
|
notes: step.Notes);
|
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
}
|
|
else
|
|
{
|
|
step.Status = FactionPlanStepStatus.Blocked;
|
|
step.BlockingReason = "Unable to derive an expansion project for water.";
|
|
UpsertExpansionTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
commodityId: "water",
|
|
moduleId: step.ModuleId,
|
|
targetSystemId: null,
|
|
targetSiteId: null,
|
|
blockingReason: step.BlockingReason,
|
|
notes: step.Notes);
|
|
}
|
|
break;
|
|
case FactionPlanStepKind.EnsureMiningCapacity:
|
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "mining", step.BlockingReason, "Maintain mining ship production until logistical capacity is healthy.");
|
|
AssignShipyardAssets(world, commander, objective, step);
|
|
break;
|
|
case FactionPlanStepKind.EnsureConstructionCapacity:
|
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "construction", step.BlockingReason, "Maintain construction ship production until expansion support is healthy.");
|
|
AssignShipyardAssets(world, commander, objective, step);
|
|
break;
|
|
case FactionPlanStepKind.EnsureTransportCapacity:
|
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "transport", step.BlockingReason, "Maintain transport ship production until logistical throughput is healthy.");
|
|
AssignShipyardAssets(world, commander, objective, step);
|
|
break;
|
|
case FactionPlanStepKind.MonitorExpansionProject:
|
|
UpsertWarIndustryTask(commander, objective, step, touchedTaskIds);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void ReconcileStaleTasks(CommanderRuntime commander, ISet<string> touchedTaskIds)
|
|
{
|
|
foreach (var task in commander.IssuedTasks)
|
|
{
|
|
task.UpdatedAtCycle = commander.PlanningCycle;
|
|
if (touchedTaskIds.Contains(task.Id))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (task.State is FactionIssuedTaskState.Complete or FactionIssuedTaskState.Cancelled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
task.State = FactionIssuedTaskState.Cancelled;
|
|
task.BlockingReason = "Task no longer backed by an active faction plan step.";
|
|
task.AssignedAssetIds.Clear();
|
|
}
|
|
}
|
|
|
|
private static void SyncTerminalTasks(
|
|
CommanderRuntime commander,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds)
|
|
{
|
|
foreach (var task in commander.IssuedTasks.Where(candidate =>
|
|
string.Equals(candidate.StepId, step.Id, StringComparison.Ordinal)))
|
|
{
|
|
task.State = step.Status switch
|
|
{
|
|
FactionPlanStepStatus.Complete => FactionIssuedTaskState.Complete,
|
|
FactionPlanStepStatus.Failed => FactionIssuedTaskState.Cancelled,
|
|
FactionPlanStepStatus.Cancelled => FactionIssuedTaskState.Cancelled,
|
|
_ => task.State,
|
|
};
|
|
task.BlockingReason = step.BlockingReason;
|
|
task.UpdatedAtCycle = commander.PlanningCycle;
|
|
step.IssuedTaskIds.Add(task.Id);
|
|
touchedTaskIds.Add(task.Id);
|
|
}
|
|
}
|
|
|
|
private static void UpsertExpansionTask(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds,
|
|
string? commodityId,
|
|
string? moduleId,
|
|
string? targetSystemId,
|
|
string? targetSiteId,
|
|
string? blockingReason,
|
|
string? notes)
|
|
{
|
|
var task = GetOrCreateTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
$"expand-industry:{objective.Id}:{step.Id}:{commodityId ?? moduleId ?? "general"}",
|
|
FactionIssuedTaskKind.ExpandIndustry);
|
|
task.CommodityId = commodityId;
|
|
task.ModuleId = moduleId;
|
|
task.TargetSystemId = targetSystemId;
|
|
task.TargetSiteId = targetSiteId;
|
|
task.Notes = notes;
|
|
task.BlockingReason = blockingReason;
|
|
task.State = MapStepStatus(step.Status);
|
|
}
|
|
|
|
private static void UpsertShipProductionTask(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds,
|
|
string shipRole,
|
|
string? blockingReason,
|
|
string? notes)
|
|
{
|
|
var task = GetOrCreateTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
$"produce-ships:{shipRole}",
|
|
FactionIssuedTaskKind.ProduceShips);
|
|
task.ShipRole = shipRole;
|
|
task.BlockingReason = blockingReason;
|
|
task.Notes = notes;
|
|
task.State = MapStepStatus(step.Status);
|
|
}
|
|
|
|
private static void UpsertAttackTask(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds)
|
|
{
|
|
var task = GetOrCreateTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
$"attack-faction:{step.TargetFactionId ?? objective.TargetFactionId ?? "unknown"}",
|
|
FactionIssuedTaskKind.AttackFactionAssets);
|
|
task.TargetFactionId = step.TargetFactionId ?? objective.TargetFactionId;
|
|
task.Notes = step.Notes ?? "Commit combat ships against the hostile faction.";
|
|
task.BlockingReason = step.BlockingReason;
|
|
task.State = MapStepStatus(step.Status);
|
|
}
|
|
|
|
private static void UpsertWarIndustryTask(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds)
|
|
{
|
|
var task = GetOrCreateTask(
|
|
commander,
|
|
objective,
|
|
step,
|
|
touchedTaskIds,
|
|
"sustain-war-industry",
|
|
FactionIssuedTaskKind.SustainWarIndustry);
|
|
task.Notes = step.Notes ?? "Maintain the faction war-industry bootstrap program.";
|
|
task.BlockingReason = step.BlockingReason;
|
|
task.State = MapStepStatus(step.Status);
|
|
}
|
|
|
|
private static FactionIssuedTaskRuntime GetOrCreateTask(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
ISet<string> touchedTaskIds,
|
|
string mergeKey,
|
|
FactionIssuedTaskKind kind)
|
|
{
|
|
var task = commander.IssuedTasks.FirstOrDefault(candidate =>
|
|
string.Equals(candidate.MergeKey, mergeKey, StringComparison.Ordinal));
|
|
if (task is null)
|
|
{
|
|
task = new FactionIssuedTaskRuntime
|
|
{
|
|
Id = $"task-{commander.FactionId}-{commander.IssuedTasks.Count + 1}",
|
|
MergeKey = mergeKey,
|
|
Kind = kind,
|
|
ObjectiveId = objective.Id,
|
|
StepId = step.Id,
|
|
Priority = step.Priority,
|
|
CreatedAtCycle = commander.PlanningCycle,
|
|
UpdatedAtCycle = commander.PlanningCycle,
|
|
};
|
|
commander.IssuedTasks.Add(task);
|
|
}
|
|
|
|
task.Priority = MathF.Max(task.Priority, step.Priority);
|
|
task.UpdatedAtCycle = commander.PlanningCycle;
|
|
task.BlockingReason = null;
|
|
task.Notes = step.Notes;
|
|
step.IssuedTaskIds.Add(task.Id);
|
|
touchedTaskIds.Add(task.Id);
|
|
return task;
|
|
}
|
|
|
|
private static FactionIssuedTaskState MapStepStatus(FactionPlanStepStatus status) =>
|
|
status switch
|
|
{
|
|
FactionPlanStepStatus.Planned => FactionIssuedTaskState.Planned,
|
|
FactionPlanStepStatus.Ready => FactionIssuedTaskState.Active,
|
|
FactionPlanStepStatus.Running => FactionIssuedTaskState.Active,
|
|
FactionPlanStepStatus.Blocked => FactionIssuedTaskState.Blocked,
|
|
FactionPlanStepStatus.Complete => FactionIssuedTaskState.Complete,
|
|
_ => FactionIssuedTaskState.Cancelled,
|
|
};
|
|
|
|
private static void AssignCombatAssets(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
IReadOnlyCollection<string> taskIds,
|
|
ISet<string> assignedAssetIds)
|
|
{
|
|
var availableShips = world.Ships
|
|
.Where(ship =>
|
|
ship.Health > 0f
|
|
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)
|
|
&& string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)
|
|
&& assignedAssetIds.Add(ship.Id))
|
|
.OrderBy(ship => ship.SystemId, StringComparer.Ordinal)
|
|
.ThenBy(ship => ship.Id, StringComparer.Ordinal)
|
|
.Take(4)
|
|
.ToList();
|
|
ApplyAssignments(commander, objective, step, taskIds, availableShips.Select(ship => ship.Id));
|
|
}
|
|
|
|
private static void AssignConstructionAssets(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
IReadOnlyCollection<string> taskIds,
|
|
ISet<string> assignedAssetIds)
|
|
{
|
|
var availableShips = world.Ships
|
|
.Where(ship =>
|
|
ship.Health > 0f
|
|
&& string.Equals(ship.FactionId, commander.FactionId, StringComparison.Ordinal)
|
|
&& string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal)
|
|
&& assignedAssetIds.Add(ship.Id))
|
|
.OrderBy(ship => ship.SystemId, StringComparer.Ordinal)
|
|
.ThenBy(ship => ship.Id, StringComparer.Ordinal)
|
|
.Take(2)
|
|
.ToList();
|
|
ApplyAssignments(commander, objective, step, taskIds, availableShips.Select(ship => ship.Id));
|
|
}
|
|
|
|
private static void AssignShipyardAssets(
|
|
SimulationWorld world,
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step)
|
|
{
|
|
var stationIds = world.Stations
|
|
.Where(station =>
|
|
string.Equals(station.FactionId, commander.FactionId, StringComparison.Ordinal)
|
|
&& station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal))
|
|
.Select(station => station.Id)
|
|
.OrderBy(id => id, StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
foreach (var stationId in stationIds)
|
|
{
|
|
objective.AssignedAssetIds.Add(stationId);
|
|
step.AssignedAssetIds.Add(stationId);
|
|
}
|
|
|
|
foreach (var taskId in step.IssuedTaskIds)
|
|
{
|
|
if (commander.IssuedTasks.FirstOrDefault(candidate => candidate.Id == taskId) is not { } task)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var stationId in stationIds)
|
|
{
|
|
task.AssignedAssetIds.Add(stationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ApplyAssignments(
|
|
CommanderRuntime commander,
|
|
FactionObjectiveRuntime objective,
|
|
FactionPlanStepRuntime step,
|
|
IReadOnlyCollection<string> taskIds,
|
|
IEnumerable<string> assetIds)
|
|
{
|
|
var materializedAssetIds = assetIds.ToList();
|
|
foreach (var assetId in materializedAssetIds)
|
|
{
|
|
objective.AssignedAssetIds.Add(assetId);
|
|
step.AssignedAssetIds.Add(assetId);
|
|
}
|
|
|
|
foreach (var taskId in taskIds)
|
|
{
|
|
if (commander.IssuedTasks.FirstOrDefault(candidate => candidate.Id == taskId) is not { } task)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var assetId in materializedAssetIds)
|
|
{
|
|
task.AssignedAssetIds.Add(assetId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool HasIncompleteDependencies(CommanderRuntime commander, FactionPlanStepRuntime step) =>
|
|
step.DependencyStepIds
|
|
.Select(dependencyId => commander.Objectives
|
|
.SelectMany(objective => objective.Steps)
|
|
.FirstOrDefault(candidate => candidate.Id == dependencyId))
|
|
.Any(dependency => dependency is not null && dependency.Status != FactionPlanStepStatus.Complete);
|
|
}
|