feat: massive AI generation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user