feat: massive AI generation

This commit is contained in:
2026-03-21 02:21:05 -04:00
parent 3b56785f9a
commit 6ccc708ae1
80 changed files with 16929 additions and 5427 deletions

View File

@@ -1,11 +0,0 @@
namespace SpaceGame.Api.Ships.AI;
internal interface IShipBehaviorState
{
string Kind { get; }
void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world);
void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent);
}

View File

@@ -1,41 +0,0 @@
namespace SpaceGame.Api.Ships.AI;
internal sealed class ShipBehaviorStateMachine
{
private readonly IReadOnlyDictionary<string, IShipBehaviorState> states;
private readonly IShipBehaviorState fallbackState;
private ShipBehaviorStateMachine(IReadOnlyDictionary<string, IShipBehaviorState> states, IShipBehaviorState fallbackState)
{
this.states = states;
this.fallbackState = fallbackState;
}
public static ShipBehaviorStateMachine CreateDefault()
{
var idleState = new IdleShipBehaviorState();
var knownStates = new IShipBehaviorState[]
{
idleState,
new PatrolShipBehaviorState(),
new AttackTargetShipBehaviorState(),
new TradeHaulShipBehaviorState(),
new ResourceHarvestShipBehaviorState("auto-mine", null, "mining"),
new ConstructStationShipBehaviorState(),
};
return new ShipBehaviorStateMachine(
knownStates.ToDictionary(state => state.Kind, StringComparer.Ordinal),
idleState);
}
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
Resolve(ship.DefaultBehavior.Kind).Plan(engine, ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) =>
Resolve(ship.DefaultBehavior.Kind).ApplyEvent(engine, ship, world, controllerEvent);
private IShipBehaviorState Resolve(string kind) =>
states.TryGetValue(kind, out var state) ? state : fallbackState;
}

View File

@@ -1,186 +0,0 @@
namespace SpaceGame.Api.Ships.AI;
internal sealed class IdleShipBehaviorState : IShipBehaviorState
{
public string Kind => "idle";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
{
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Idle,
Threshold = world.Balance.ArrivalThreshold,
Status = WorkStatus.Pending,
};
}
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
}
}
internal sealed class PatrolShipBehaviorState : IShipBehaviorState
{
public string Kind => "patrol";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
{
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
{
ship.DefaultBehavior.Kind = "idle";
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Idle,
Threshold = world.Balance.ArrivalThreshold,
Status = WorkStatus.Pending,
};
return;
}
ship.ControllerTask = new ControllerTaskRuntime
{
Kind = ControllerTaskKind.Travel,
TargetPosition = ship.DefaultBehavior.PatrolPoints[ship.DefaultBehavior.PatrolIndex],
TargetSystemId = ship.SystemId,
Threshold = 18f,
};
}
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
if (controllerEvent == "arrived" && ship.DefaultBehavior.PatrolPoints.Count > 0)
{
ship.DefaultBehavior.PatrolIndex = (ship.DefaultBehavior.PatrolIndex + 1) % ship.DefaultBehavior.PatrolPoints.Count;
}
}
}
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
{
private readonly string? resourceItemId;
private readonly string requiredModule;
public ResourceHarvestShipBehaviorState(string kind, string? resourceItemId, string requiredModule)
{
Kind = kind;
this.resourceItemId = resourceItemId;
this.requiredModule = requiredModule;
}
public string Kind { get; }
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{
case ("travel-to-node", "arrived"):
ship.DefaultBehavior.Phase = SimulationEngine.GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "travel-to-station" : "extract";
break;
case ("extract", "cargo-full"):
ship.DefaultBehavior.Phase = "travel-to-station";
break;
case ("extract", "node-depleted"):
ship.DefaultBehavior.Phase = "travel-to-node";
ship.DefaultBehavior.NodeId = null;
break;
case ("travel-to-station", "arrived"):
ship.DefaultBehavior.Phase = "dock";
break;
case ("dock", "docked"):
ship.DefaultBehavior.Phase = "unload";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = "undock";
break;
case ("undock", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-node";
ship.DefaultBehavior.NodeId = null;
break;
}
}
}
internal sealed class ConstructStationShipBehaviorState : IShipBehaviorState
{
public string Kind => "construct-station";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanStationConstruction(ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{
case ("travel-to-station", "arrived"):
ship.DefaultBehavior.Phase = "deliver-to-site";
break;
case ("deliver-to-site", "construction-delivered"):
ship.DefaultBehavior.Phase = "build-site";
break;
case ("construct-module", "module-constructed"):
case ("build-site", "site-constructed"):
ship.DefaultBehavior.Phase = "travel-to-station";
ship.DefaultBehavior.ModuleId = null;
break;
}
}
}
internal sealed class AttackTargetShipBehaviorState : IShipBehaviorState
{
public string Kind => "attack-target";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanAttackTarget(ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
if (controllerEvent is "target-destroyed" or "target-lost")
{
ship.DefaultBehavior.TargetEntityId = null;
}
}
}
internal sealed class TradeHaulShipBehaviorState : IShipBehaviorState
{
public string Kind => "trade-haul";
public void Plan(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) =>
engine.PlanTransportHaul(ship, world);
public void ApplyEvent(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
{
switch (ship.DefaultBehavior.Phase, controllerEvent)
{
case ("travel-to-source", "arrived"):
ship.DefaultBehavior.Phase = "dock-source";
break;
case ("dock-source", "docked"):
ship.DefaultBehavior.Phase = "load";
break;
case ("load", "loaded"):
ship.DefaultBehavior.Phase = "undock-from-source";
break;
case ("undock-from-source", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-destination";
break;
case ("travel-to-destination", "arrived"):
ship.DefaultBehavior.Phase = "dock-destination";
break;
case ("dock-destination", "docked"):
ship.DefaultBehavior.Phase = "unload";
break;
case ("unload", "unloaded"):
ship.DefaultBehavior.Phase = "undock-from-destination";
break;
case ("undock-from-destination", "undocked"):
ship.DefaultBehavior.Phase = "travel-to-source";
break;
}
}
}

View File

@@ -1,227 +0,0 @@
namespace SpaceGame.Api.Ships.AI;
// ─── Planning State ────────────────────────────────────────────────────────────
public sealed class ShipPlanningState
{
public string ShipKind { get; set; } = string.Empty;
public bool HasMiningCapability { get; set; }
public bool FactionWantsOre { get; set; }
public bool FactionWantsExpansion { get; set; }
public bool FactionWantsCombat { get; set; }
public bool FactionNeedsShipyard { get; set; }
public string? TargetEnemySystemId { get; set; }
public string? TargetEnemyEntityId { get; set; }
public string? TradeItemId { get; set; }
public string? TradeSourceStationId { get; set; }
public string? TradeDestinationStationId { get; set; }
public string? CurrentObjective { get; set; }
public ShipPlanningState Clone() => (ShipPlanningState)MemberwiseClone();
}
// ─── Goals ─────────────────────────────────────────────────────────────────────
// A ship should always have an assigned objective. The planner picks the best one.
public sealed class AssignObjectiveGoal : GoapGoal<ShipPlanningState>
{
public override string Name => "assign-objective";
public override bool IsSatisfied(ShipPlanningState state) => state.CurrentObjective is not null;
public override float ComputePriority(ShipPlanningState state, SimulationWorld world, CommanderRuntime commander) =>
100f;
}
// ─── Actions ───────────────────────────────────────────────────────────────────
public sealed class SetMiningObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-mining-objective";
public override float Cost => 1f;
public override bool CheckPreconditions(ShipPlanningState state) =>
state.HasMiningCapability && state.FactionWantsOre;
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "auto-mine";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "auto-mine", StringComparison.Ordinal))
{
return;
}
ship.DefaultBehavior.Kind = "auto-mine";
ship.DefaultBehavior.Phase = null;
ship.DefaultBehavior.NodeId = null;
}
}
public sealed class SetPatrolObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-patrol-objective";
public override float Cost => 2f;
public override bool CheckPreconditions(ShipPlanningState state) =>
string.Equals(state.ShipKind, "military", StringComparison.Ordinal);
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "patrol";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "patrol", StringComparison.Ordinal))
{
return;
}
if (ship.DefaultBehavior.PatrolPoints.Count == 0)
{
var station = world.Stations.FirstOrDefault(s =>
s.FactionId == ship.FactionId &&
string.Equals(s.SystemId, ship.SystemId, StringComparison.Ordinal));
if (station is not null)
{
var radius = station.Radius + 90f;
ship.DefaultBehavior.PatrolPoints.AddRange(
[
new Vector3(station.Position.X + radius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + radius),
new Vector3(station.Position.X - radius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - radius),
]);
}
}
ship.DefaultBehavior.Kind = "patrol";
}
}
public sealed class SetAttackObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-attack-objective";
public override float Cost => 1f;
public override bool CheckPreconditions(ShipPlanningState state) =>
string.Equals(state.ShipKind, "military", StringComparison.Ordinal)
&& state.FactionWantsCombat
&& state.TargetEnemyEntityId is not null;
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "attack-target";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null)
{
return;
}
ship.DefaultBehavior.Kind = "attack-target";
ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior?.AreaSystemId ?? ship.DefaultBehavior.AreaSystemId;
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
ship.DefaultBehavior.Phase = null;
}
}
public sealed class SetConstructionObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-construction-objective";
public override float Cost => 1f;
public override bool CheckPreconditions(ShipPlanningState state) =>
string.Equals(state.ShipKind, "construction", StringComparison.Ordinal)
&& (state.FactionWantsExpansion || state.FactionNeedsShipyard);
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "construct-station";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "construct-station", StringComparison.Ordinal))
{
return;
}
ship.DefaultBehavior.Kind = "construct-station";
ship.DefaultBehavior.Phase = null;
}
}
public sealed class SetTradeObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-trade-objective";
public override float Cost => 1f;
public override bool CheckPreconditions(ShipPlanningState state) =>
string.Equals(state.ShipKind, "transport", StringComparison.Ordinal)
&& state.TradeItemId is not null
&& state.TradeSourceStationId is not null
&& state.TradeDestinationStationId is not null;
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "trade-haul";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null || commander.ActiveBehavior is null)
{
return;
}
ship.DefaultBehavior.Kind = "trade-haul";
ship.DefaultBehavior.ItemId = commander.ActiveBehavior.ItemId;
ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId;
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior.TargetEntityId;
ship.DefaultBehavior.Phase ??= "travel-to-source";
}
}
public sealed class SetIdleObjectiveAction : GoapAction<ShipPlanningState>
{
public override string Name => "set-idle-objective";
public override float Cost => 10f;
public override bool CheckPreconditions(ShipPlanningState state) => true;
public override ShipPlanningState ApplyEffects(ShipPlanningState state)
{
state.CurrentObjective = "idle";
return state;
}
public override void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander)
{
var ship = world.Ships.FirstOrDefault(s => s.Id == commander.ControlledEntityId);
if (ship is null || string.Equals(ship.DefaultBehavior.Kind, "idle", StringComparison.Ordinal))
{
return;
}
ship.DefaultBehavior.Kind = "idle";
}
}