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