feat: massive AI generation
This commit is contained in:
@@ -35,7 +35,11 @@ public sealed record PolicySetSnapshot(
|
||||
string TradeAccessPolicy,
|
||||
string DockingAccessPolicy,
|
||||
string ConstructionAccessPolicy,
|
||||
string OperationalRangePolicy);
|
||||
string OperationalRangePolicy,
|
||||
string CombatEngagementPolicy,
|
||||
bool AvoidHostileSystems,
|
||||
float FleeHullRatio,
|
||||
IReadOnlyList<string> BlacklistedSystemIds);
|
||||
|
||||
public sealed record PolicySetDelta(
|
||||
string Id,
|
||||
@@ -44,4 +48,8 @@ public sealed record PolicySetDelta(
|
||||
string TradeAccessPolicy,
|
||||
string DockingAccessPolicy,
|
||||
string ConstructionAccessPolicy,
|
||||
string OperationalRangePolicy);
|
||||
string OperationalRangePolicy,
|
||||
string CombatEngagementPolicy,
|
||||
bool AvoidHostileSystems,
|
||||
float FleeHullRatio,
|
||||
IReadOnlyList<string> BlacklistedSystemIds);
|
||||
|
||||
@@ -26,5 +26,9 @@ public sealed class PolicySetRuntime
|
||||
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
|
||||
public string ConstructionAccessPolicy { get; set; } = "owner-only";
|
||||
public string OperationalRangePolicy { get; set; } = "unrestricted";
|
||||
public string CombatEngagementPolicy { get; set; } = "defensive";
|
||||
public bool AvoidHostileSystems { get; set; } = true;
|
||||
public float FleeHullRatio { get; set; } = 0.35f;
|
||||
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
||||
|
||||
namespace SpaceGame.Api.Factions.AI;
|
||||
|
||||
// ─── Planning State ────────────────────────────────────────────────────────────
|
||||
|
||||
public sealed class FactionPlanningState
|
||||
{
|
||||
public int MilitaryShipCount { get; set; }
|
||||
public int MinerShipCount { get; set; }
|
||||
public int TransportShipCount { get; set; }
|
||||
public int ConstructorShipCount { get; set; }
|
||||
public int ControlledSystemCount { get; set; }
|
||||
public int TargetSystemCount { get; set; }
|
||||
public bool HasShipFactory { get; set; }
|
||||
public int EnemyFactionCount { get; set; }
|
||||
public int EnemyShipCount { get; set; }
|
||||
public int EnemyStationCount { get; set; }
|
||||
public float OreStockpile { get; set; }
|
||||
public float RefinedMetalsAvailableStock { get; set; }
|
||||
public float RefinedMetalsUsageRate { get; set; }
|
||||
public float RefinedMetalsProjectedProductionRate { get; set; }
|
||||
public float RefinedMetalsProjectedNetRate { get; set; }
|
||||
public float RefinedMetalsLevelSeconds { get; set; }
|
||||
public string RefinedMetalsLevel { get; set; } = "unknown";
|
||||
public float HullpartsAvailableStock { get; set; }
|
||||
public float HullpartsUsageRate { get; set; }
|
||||
public float HullpartsProjectedProductionRate { get; set; }
|
||||
public float HullpartsProjectedNetRate { get; set; }
|
||||
public float HullpartsLevelSeconds { get; set; }
|
||||
public string HullpartsLevel { get; set; } = "unknown";
|
||||
public float ClaytronicsAvailableStock { get; set; }
|
||||
public float ClaytronicsUsageRate { get; set; }
|
||||
public float ClaytronicsProjectedProductionRate { get; set; }
|
||||
public float ClaytronicsProjectedNetRate { get; set; }
|
||||
public float ClaytronicsLevelSeconds { get; set; }
|
||||
public string ClaytronicsLevel { get; set; } = "unknown";
|
||||
public float WaterAvailableStock { get; set; }
|
||||
public float WaterUsageRate { get; set; }
|
||||
public float WaterProjectedProductionRate { get; set; }
|
||||
public float WaterProjectedNetRate { get; set; }
|
||||
public float WaterLevelSeconds { get; set; }
|
||||
public string WaterLevel { get; set; } = "unknown";
|
||||
|
||||
public bool HasRefinedMetalsProduction => RefinedMetalsProjectedProductionRate > 0.01f;
|
||||
public bool HasHullpartsProduction => HullpartsProjectedProductionRate > 0.01f;
|
||||
public bool HasClaytronicsProduction => ClaytronicsProjectedProductionRate > 0.01f;
|
||||
public bool HasWaterProduction => WaterProjectedProductionRate > 0.01f;
|
||||
|
||||
public bool HasWarIndustrySupplyChain =>
|
||||
IsCommodityOperational(RefinedMetalsProjectedProductionRate, RefinedMetalsProjectedNetRate, RefinedMetalsLevelSeconds, RefinedMetalsLevel, 240f)
|
||||
&& IsCommodityOperational(HullpartsProjectedProductionRate, HullpartsProjectedNetRate, HullpartsLevelSeconds, HullpartsLevel, 240f)
|
||||
&& IsCommodityOperational(ClaytronicsProjectedProductionRate, ClaytronicsProjectedNetRate, ClaytronicsLevelSeconds, ClaytronicsLevel, 240f);
|
||||
|
||||
public FactionPlanningState Clone() => (FactionPlanningState)MemberwiseClone();
|
||||
|
||||
internal static int ComputeTargetWarships(FactionPlanningState state)
|
||||
{
|
||||
var expansionDeficit = Math.Max(0, state.TargetSystemCount - state.ControlledSystemCount);
|
||||
return Math.Max(3, (state.ControlledSystemCount * 2) + (expansionDeficit * 3) + Math.Min(4, state.EnemyFactionCount + state.EnemyStationCount));
|
||||
}
|
||||
|
||||
internal static bool IsCommodityOperational(
|
||||
float projectedProductionRate,
|
||||
float projectedNetRate,
|
||||
float levelSeconds,
|
||||
string level,
|
||||
float targetLevelSeconds) =>
|
||||
projectedProductionRate > 0.01f
|
||||
&& projectedNetRate >= -0.01f
|
||||
&& levelSeconds >= targetLevelSeconds
|
||||
&& (string.Equals(level, "stable", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(level, "surplus", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
internal static float ComputeCommodityNeed(
|
||||
float projectedProductionRate,
|
||||
float usageRate,
|
||||
float projectedNetRate,
|
||||
float levelSeconds,
|
||||
string level,
|
||||
float targetLevelSeconds)
|
||||
{
|
||||
var levelWeight = level switch
|
||||
{
|
||||
"critical" => 140f,
|
||||
"low" => 80f,
|
||||
"stable" => 20f,
|
||||
_ => 0f,
|
||||
};
|
||||
var rateDeficit = MathF.Max(0f, usageRate - projectedProductionRate);
|
||||
var levelDeficit = MathF.Max(0f, targetLevelSeconds - levelSeconds) / MathF.Max(targetLevelSeconds, 1f);
|
||||
var instability = projectedNetRate < 0f ? MathF.Abs(projectedNetRate) * 80f : 0f;
|
||||
return levelWeight + (rateDeficit * 140f) + (levelDeficit * 120f) + instability;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Goals ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
public sealed class EnsureWarIndustryGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
public override string Name => "ensure-war-industry";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||
state.EnemyFactionCount <= 0 || (state.HasWarIndustrySupplyChain && state.HasShipFactory);
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
if (state.EnemyFactionCount <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var missingStages =
|
||||
(FactionPlanningState.IsCommodityOperational(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f) ? 0 : 1) +
|
||||
(FactionPlanningState.IsCommodityOperational(state.HullpartsProjectedProductionRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f) ? 0 : 1) +
|
||||
(FactionPlanningState.IsCommodityOperational(state.ClaytronicsProjectedProductionRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f) ? 0 : 1) +
|
||||
(state.HasShipFactory ? 0 : 1);
|
||||
var supplyNeed =
|
||||
FactionPlanningState.ComputeCommodityNeed(state.RefinedMetalsProjectedProductionRate, state.RefinedMetalsUsageRate, state.RefinedMetalsProjectedNetRate, state.RefinedMetalsLevelSeconds, state.RefinedMetalsLevel, 240f)
|
||||
+ FactionPlanningState.ComputeCommodityNeed(state.HullpartsProjectedProductionRate, state.HullpartsUsageRate, state.HullpartsProjectedNetRate, state.HullpartsLevelSeconds, state.HullpartsLevel, 240f)
|
||||
+ FactionPlanningState.ComputeCommodityNeed(state.ClaytronicsProjectedProductionRate, state.ClaytronicsUsageRate, state.ClaytronicsProjectedNetRate, state.ClaytronicsLevelSeconds, state.ClaytronicsLevel, 240f);
|
||||
|
||||
return missingStages <= 0 && supplyNeed <= 0.01f ? 0f : 110f + (missingStages * 22f) + (supplyNeed * 0.18f);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnsureWaterSecurityGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
public override string Name => "ensure-water-security";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||
FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f);
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
if (FactionPlanningState.IsCommodityOperational(state.WaterProjectedProductionRate, state.WaterProjectedNetRate, state.WaterLevelSeconds, state.WaterLevel, 300f))
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return 55f + FactionPlanningState.ComputeCommodityNeed(
|
||||
state.WaterProjectedProductionRate,
|
||||
state.WaterUsageRate,
|
||||
state.WaterProjectedNetRate,
|
||||
state.WaterLevelSeconds,
|
||||
state.WaterLevel,
|
||||
300f) * 0.25f;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnsureWarFleetGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
public override string Name => "ensure-war-fleet";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||
state.MilitaryShipCount >= FactionPlanningState.ComputeTargetWarships(state);
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
var deficit = FactionPlanningState.ComputeTargetWarships(state) - state.MilitaryShipCount;
|
||||
return deficit <= 0 ? 0f : 50f + (deficit * 10f);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExterminateRivalGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
public override string Name => "exterminate-rival";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||
state.EnemyFactionCount <= 0 || (state.EnemyShipCount <= 0 && state.EnemyStationCount <= 0);
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
if (state.EnemyFactionCount <= 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return 140f + (state.EnemyStationCount * 25f) + (state.EnemyShipCount * 6f);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExpandTerritoryGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
public override string Name => "expand-territory";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) =>
|
||||
state.ControlledSystemCount >= state.TargetSystemCount;
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
var deficit = state.TargetSystemCount - state.ControlledSystemCount;
|
||||
return deficit <= 0 ? 0f : 80f + (deficit * 15f);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnsureMiningCapacityGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
private const int MinMiners = 2;
|
||||
|
||||
public override string Name => "ensure-mining-capacity";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) => state.MinerShipCount >= MinMiners;
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
var deficit = MinMiners - state.MinerShipCount;
|
||||
return deficit <= 0 ? 0f : 70f + (deficit * 12f);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnsureConstructionCapacityGoal : GoapGoal<FactionPlanningState>
|
||||
{
|
||||
private const int MinConstructors = 1;
|
||||
|
||||
public override string Name => "ensure-construction-capacity";
|
||||
|
||||
public override bool IsSatisfied(FactionPlanningState state) => state.ConstructorShipCount >= MinConstructors;
|
||||
|
||||
public override float ComputePriority(FactionPlanningState state, SimulationWorld world, CommanderRuntime commander)
|
||||
{
|
||||
var deficit = MinConstructors - state.ConstructorShipCount;
|
||||
return deficit <= 0 ? 0f : 60f + (deficit * 10f);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,76 @@
|
||||
namespace SpaceGame.Api.Factions.Contracts;
|
||||
|
||||
public sealed record FactionPlanningStateSnapshot(
|
||||
int MilitaryShipCount,
|
||||
int MinerShipCount,
|
||||
int TransportShipCount,
|
||||
int ConstructorShipCount,
|
||||
int ControlledSystemCount,
|
||||
int TargetSystemCount,
|
||||
bool HasShipFactory,
|
||||
float OreStockpile,
|
||||
float RefinedMetalsAvailableStock,
|
||||
float RefinedMetalsUsageRate,
|
||||
float RefinedMetalsProjectedProductionRate,
|
||||
float RefinedMetalsProjectedNetRate,
|
||||
float RefinedMetalsLevelSeconds,
|
||||
string RefinedMetalsLevel,
|
||||
float HullpartsAvailableStock,
|
||||
float HullpartsUsageRate,
|
||||
float HullpartsProjectedProductionRate,
|
||||
float HullpartsProjectedNetRate,
|
||||
float HullpartsLevelSeconds,
|
||||
string HullpartsLevel,
|
||||
float ClaytronicsAvailableStock,
|
||||
float ClaytronicsUsageRate,
|
||||
float ClaytronicsProjectedProductionRate,
|
||||
float ClaytronicsProjectedNetRate,
|
||||
float ClaytronicsLevelSeconds,
|
||||
string ClaytronicsLevel,
|
||||
float WaterAvailableStock,
|
||||
float WaterUsageRate,
|
||||
float WaterProjectedProductionRate,
|
||||
float WaterProjectedNetRate,
|
||||
float WaterLevelSeconds,
|
||||
string WaterLevel);
|
||||
public sealed record FactionDoctrineSnapshot(
|
||||
string StrategicPosture,
|
||||
string ExpansionPosture,
|
||||
string MilitaryPosture,
|
||||
string EconomicPosture,
|
||||
int DesiredControlledSystems,
|
||||
int DesiredMilitaryPerFront,
|
||||
int DesiredMinersPerSystem,
|
||||
int DesiredTransportsPerSystem,
|
||||
int DesiredConstructors,
|
||||
float ReserveCreditsRatio,
|
||||
float ExpansionBudgetRatio,
|
||||
float WarBudgetRatio,
|
||||
float ReserveMilitaryRatio,
|
||||
float OffensiveReadinessThreshold,
|
||||
float SupplySecurityBias,
|
||||
float FailureAversion,
|
||||
int ReinforcementLeadPerFront);
|
||||
|
||||
public sealed record FactionStrategicPrioritySnapshot(string GoalName, float Priority);
|
||||
public sealed record FactionSystemMemorySnapshot(
|
||||
string SystemId,
|
||||
DateTimeOffset LastSeenAtUtc,
|
||||
int LastEnemyShipCount,
|
||||
int LastEnemyStationCount,
|
||||
bool ControlledByFaction,
|
||||
string? LastRole,
|
||||
float FrontierPressure,
|
||||
float RouteRisk,
|
||||
float HistoricalShortagePressure,
|
||||
int OffensiveFailures,
|
||||
int DefensiveFailures,
|
||||
int OffensiveSuccesses,
|
||||
int DefensiveSuccesses,
|
||||
DateTimeOffset? LastContestedAtUtc,
|
||||
DateTimeOffset? LastShortageAtUtc);
|
||||
|
||||
public sealed record FactionCommodityMemorySnapshot(
|
||||
string ItemId,
|
||||
float HistoricalShortageScore,
|
||||
float HistoricalSurplusScore,
|
||||
float LastObservedBacklog,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
DateTimeOffset? LastCriticalAtUtc);
|
||||
|
||||
public sealed record FactionOutcomeRecordSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Summary,
|
||||
string? RelatedCampaignId,
|
||||
string? RelatedObjectiveId,
|
||||
DateTimeOffset OccurredAtUtc);
|
||||
|
||||
public sealed record FactionMemorySnapshot(
|
||||
int LastPlanCycle,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
int LastObservedShipsBuilt,
|
||||
int LastObservedShipsLost,
|
||||
float LastObservedCredits,
|
||||
IReadOnlyList<string> KnownSystemIds,
|
||||
IReadOnlyList<string> KnownEnemyFactionIds,
|
||||
IReadOnlyList<FactionSystemMemorySnapshot> Systems,
|
||||
IReadOnlyList<FactionCommodityMemorySnapshot> Commodities,
|
||||
IReadOnlyList<FactionOutcomeRecordSnapshot> RecentOutcomes);
|
||||
|
||||
public sealed record FactionBudgetSnapshot(
|
||||
float ReservedCredits,
|
||||
float ExpansionCredits,
|
||||
float WarCredits,
|
||||
int ReservedMilitaryAssets,
|
||||
int ReservedLogisticsAssets,
|
||||
int ReservedConstructionAssets);
|
||||
|
||||
public sealed record FactionCommoditySignalSnapshot(
|
||||
string ItemId,
|
||||
@@ -51,96 +87,185 @@ public sealed record FactionCommoditySignalSnapshot(
|
||||
float BuyBacklog,
|
||||
float ReservedForConstruction);
|
||||
|
||||
public sealed record FactionThreatSignalSnapshot(
|
||||
string ScopeId,
|
||||
string ScopeKind,
|
||||
int EnemyShipCount,
|
||||
int EnemyStationCount);
|
||||
|
||||
public sealed record FactionBlackboardSnapshot(
|
||||
public sealed record FactionEconomicAssessmentSnapshot(
|
||||
int PlanCycle,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
int TargetWarshipCount,
|
||||
bool HasWarIndustrySupplyChain,
|
||||
bool HasShipyard,
|
||||
bool HasActiveExpansionProject,
|
||||
string? ActiveExpansionCommodityId,
|
||||
string? ActiveExpansionModuleId,
|
||||
string? ActiveExpansionSiteId,
|
||||
string? ActiveExpansionSystemId,
|
||||
int EnemyFactionCount,
|
||||
int EnemyShipCount,
|
||||
int EnemyStationCount,
|
||||
int MilitaryShipCount,
|
||||
int MinerShipCount,
|
||||
int TransportShipCount,
|
||||
int ConstructorShipCount,
|
||||
int ControlledSystemCount,
|
||||
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals,
|
||||
int TargetMilitaryShipCount,
|
||||
int TargetMinerShipCount,
|
||||
int TargetTransportShipCount,
|
||||
int TargetConstructorShipCount,
|
||||
bool HasShipyard,
|
||||
bool HasWarIndustrySupplyChain,
|
||||
string? PrimaryExpansionSiteId,
|
||||
string? PrimaryExpansionSystemId,
|
||||
float ReplacementPressure,
|
||||
float SustainmentScore,
|
||||
float LogisticsSecurityScore,
|
||||
int CriticalShortageCount,
|
||||
string? IndustrialBottleneckItemId,
|
||||
IReadOnlyList<FactionCommoditySignalSnapshot> CommoditySignals);
|
||||
|
||||
public sealed record FactionThreatSignalSnapshot(
|
||||
string ScopeId,
|
||||
string ScopeKind,
|
||||
int EnemyShipCount,
|
||||
int EnemyStationCount,
|
||||
string? EnemyFactionId);
|
||||
|
||||
public sealed record FactionThreatAssessmentSnapshot(
|
||||
int PlanCycle,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
int EnemyFactionCount,
|
||||
int EnemyShipCount,
|
||||
int EnemyStationCount,
|
||||
string? PrimaryThreatFactionId,
|
||||
string? PrimaryThreatSystemId,
|
||||
IReadOnlyList<FactionThreatSignalSnapshot> ThreatSignals);
|
||||
|
||||
public sealed record FactionTheaterSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string SystemId,
|
||||
string Status,
|
||||
float Priority,
|
||||
float SupplyRisk,
|
||||
float FriendlyAssetValue,
|
||||
string? TargetFactionId,
|
||||
string? AnchorEntityId,
|
||||
Vector3Dto? AnchorPosition,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> CampaignIds);
|
||||
|
||||
public sealed record FactionPlanStepSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
float Priority,
|
||||
string? CommodityId,
|
||||
string? ModuleId,
|
||||
string? TargetFactionId,
|
||||
string? TargetSiteId,
|
||||
string? StatusReason,
|
||||
string? ExecutionBindingKind,
|
||||
string? ExecutionBindingTargetId,
|
||||
string? ExecutionBindingSummary,
|
||||
string? BlockingReason,
|
||||
string? Notes,
|
||||
int LastEvaluatedCycle,
|
||||
IReadOnlyList<string> DependencyStepIds,
|
||||
IReadOnlyList<string> RequiredFacts,
|
||||
IReadOnlyList<string> ProducedFacts,
|
||||
IReadOnlyList<string> AssignedAssets,
|
||||
IReadOnlyList<string> IssuedTaskIds);
|
||||
string? Summary,
|
||||
string? BlockingReason);
|
||||
|
||||
public sealed record FactionIssuedTaskSnapshot(
|
||||
public sealed record FactionCampaignSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string State,
|
||||
string ObjectiveId,
|
||||
string StepId,
|
||||
string Status,
|
||||
float Priority,
|
||||
string? ShipRole,
|
||||
string? CommodityId,
|
||||
string? ModuleId,
|
||||
string? TheaterId,
|
||||
string? TargetFactionId,
|
||||
string? TargetSystemId,
|
||||
string? TargetSiteId,
|
||||
int CreatedAtCycle,
|
||||
int UpdatedAtCycle,
|
||||
string? BlockingReason,
|
||||
string? Notes,
|
||||
IReadOnlyList<string> AssignedAssets);
|
||||
string? TargetEntityId,
|
||||
string? CommodityId,
|
||||
string? SupportStationId,
|
||||
int CurrentStepIndex,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
string? Summary,
|
||||
string? PauseReason,
|
||||
float ContinuationScore,
|
||||
float SupplyAdequacy,
|
||||
float ReplacementPressure,
|
||||
int FailureCount,
|
||||
int SuccessCount,
|
||||
string? FleetCommanderId,
|
||||
bool RequiresReinforcement,
|
||||
IReadOnlyList<FactionPlanStepSnapshot> Steps,
|
||||
IReadOnlyList<string> ObjectiveIds);
|
||||
|
||||
public sealed record FactionObjectiveSnapshot(
|
||||
string Id,
|
||||
string CampaignId,
|
||||
string? TheaterId,
|
||||
string Kind,
|
||||
string State,
|
||||
string DelegationKind,
|
||||
string BehaviorKind,
|
||||
string Status,
|
||||
float Priority,
|
||||
string? ParentObjectiveId,
|
||||
string? TargetFactionId,
|
||||
string? CommanderId,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? TargetSystemId,
|
||||
string? TargetSiteId,
|
||||
string? TargetRegionId,
|
||||
string? TargetEntityId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? ItemId,
|
||||
string? Notes,
|
||||
int CurrentStepIndex,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
int ReinforcementLevel,
|
||||
IReadOnlyList<FactionPlanStepSnapshot> Steps,
|
||||
IReadOnlyList<string> ReservedAssetIds);
|
||||
|
||||
public sealed record FactionReservationSnapshot(
|
||||
string Id,
|
||||
string ObjectiveId,
|
||||
string? CampaignId,
|
||||
string AssetKind,
|
||||
string AssetId,
|
||||
float Priority,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record FactionProductionProgramSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
float Priority,
|
||||
string? CampaignId,
|
||||
string? CommodityId,
|
||||
string? ModuleId,
|
||||
int BudgetWeight,
|
||||
int SlotCost,
|
||||
int CreatedAtCycle,
|
||||
int UpdatedAtCycle,
|
||||
string? InvalidationReason,
|
||||
string? BlockingReason,
|
||||
IReadOnlyList<string> PrerequisiteObjectiveIds,
|
||||
IReadOnlyList<string> AssignedAssets,
|
||||
IReadOnlyList<FactionPlanStepSnapshot> Steps);
|
||||
string? ShipKind,
|
||||
string? TargetSystemId,
|
||||
int TargetCount,
|
||||
int CurrentCount,
|
||||
string? Notes);
|
||||
|
||||
public sealed record FactionDecisionLogEntrySnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Summary,
|
||||
string? RelatedEntityId,
|
||||
int PlanCycle,
|
||||
DateTimeOffset OccurredAtUtc);
|
||||
|
||||
public sealed record FactionStrategicStateSnapshot(
|
||||
int PlanCycle,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
string Status,
|
||||
FactionBudgetSnapshot Budget,
|
||||
FactionEconomicAssessmentSnapshot EconomicAssessment,
|
||||
FactionThreatAssessmentSnapshot ThreatAssessment,
|
||||
IReadOnlyList<FactionTheaterSnapshot> Theaters,
|
||||
IReadOnlyList<FactionCampaignSnapshot> Campaigns,
|
||||
IReadOnlyList<FactionObjectiveSnapshot> Objectives,
|
||||
IReadOnlyList<FactionReservationSnapshot> Reservations,
|
||||
IReadOnlyList<FactionProductionProgramSnapshot> ProductionPrograms);
|
||||
|
||||
public sealed record CommanderAssignmentSnapshot(
|
||||
string CommanderId,
|
||||
string Kind,
|
||||
string BehaviorKind,
|
||||
string Status,
|
||||
string? ObjectiveId,
|
||||
string? CampaignId,
|
||||
string? TheaterId,
|
||||
string? ParentCommanderId,
|
||||
string? ControlledEntityId,
|
||||
float Priority,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? TargetSystemId,
|
||||
string? TargetEntityId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? ItemId,
|
||||
string? Notes,
|
||||
DateTimeOffset? UpdatedAtUtc,
|
||||
IReadOnlyList<string> ActiveObjectiveIds,
|
||||
IReadOnlyList<string> SubordinateCommanderIds);
|
||||
|
||||
public sealed record FactionSnapshot(
|
||||
string Id,
|
||||
@@ -153,11 +278,11 @@ public sealed record FactionSnapshot(
|
||||
int ShipsBuilt,
|
||||
int ShipsLost,
|
||||
string? DefaultPolicySetId,
|
||||
FactionPlanningStateSnapshot? StrategicAssessment,
|
||||
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
||||
FactionBlackboardSnapshot? Blackboard,
|
||||
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
||||
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
||||
FactionDoctrineSnapshot Doctrine,
|
||||
FactionMemorySnapshot Memory,
|
||||
FactionStrategicStateSnapshot StrategicState,
|
||||
IReadOnlyList<FactionDecisionLogEntrySnapshot> DecisionLog,
|
||||
IReadOnlyList<CommanderAssignmentSnapshot> Commanders);
|
||||
|
||||
public sealed record FactionDelta(
|
||||
string Id,
|
||||
@@ -170,8 +295,8 @@ public sealed record FactionDelta(
|
||||
int ShipsBuilt,
|
||||
int ShipsLost,
|
||||
string? DefaultPolicySetId,
|
||||
FactionPlanningStateSnapshot? StrategicAssessment,
|
||||
IReadOnlyList<FactionStrategicPrioritySnapshot>? StrategicPriorities,
|
||||
FactionBlackboardSnapshot? Blackboard,
|
||||
IReadOnlyList<FactionObjectiveSnapshot>? Objectives,
|
||||
IReadOnlyList<FactionIssuedTaskSnapshot>? IssuedTasks);
|
||||
FactionDoctrineSnapshot Doctrine,
|
||||
FactionMemorySnapshot Memory,
|
||||
FactionStrategicStateSnapshot StrategicState,
|
||||
IReadOnlyList<FactionDecisionLogEntrySnapshot> DecisionLog,
|
||||
IReadOnlyList<CommanderAssignmentSnapshot> Commanders);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace SpaceGame.Api.Factions.Runtime;
|
||||
|
||||
public sealed class FactionRuntime
|
||||
@@ -14,6 +13,10 @@ public sealed class FactionRuntime
|
||||
public int ShipsLost { get; set; }
|
||||
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||
public string? DefaultPolicySetId { get; set; }
|
||||
public FactionDoctrineRuntime Doctrine { get; set; } = new();
|
||||
public FactionMemoryRuntime Memory { get; set; } = new();
|
||||
public FactionStrategicStateRuntime StrategicState { get; set; } = new();
|
||||
public List<FactionDecisionLogEntryRuntime> DecisionLog { get; } = [];
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -26,183 +29,296 @@ public sealed class CommanderRuntime
|
||||
public string? ControlledEntityId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public string? Doctrine { get; set; }
|
||||
public List<string> Goals { get; } = [];
|
||||
public string? ActiveGoalName { get; set; }
|
||||
public string? ActiveActionName { get; set; }
|
||||
public float ReplanTimer { get; set; }
|
||||
public bool NeedsReplan { get; set; } = true;
|
||||
public CommanderBehaviorRuntime? ActiveBehavior { get; set; }
|
||||
public CommanderOrderRuntime? ActiveOrder { get; set; }
|
||||
public CommanderTaskRuntime? ActiveTask { get; set; }
|
||||
public CommanderAssignmentRuntime? Assignment { get; set; }
|
||||
public CommanderSkillProfileRuntime Skills { get; set; } = new();
|
||||
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ActiveObjectiveIds { get; } = new(StringComparer.Ordinal);
|
||||
public bool IsAlive { get; set; } = true;
|
||||
public FactionPlanningState? LastStrategicAssessment { get; set; }
|
||||
public IReadOnlyList<(string Name, float Priority)>? LastStrategicPriorities { get; set; }
|
||||
public FactionBlackboardRuntime? FactionBlackboard { get; set; }
|
||||
public List<FactionObjectiveRuntime> Objectives { get; } = [];
|
||||
public List<FactionIssuedTaskRuntime> IssuedTasks { get; } = [];
|
||||
public int PlanningCycle { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum FactionObjectiveKind
|
||||
public sealed class CommanderAssignmentRuntime
|
||||
{
|
||||
DestroyFaction,
|
||||
BootstrapWarIndustry,
|
||||
BuildShipyard,
|
||||
BuildAttackFleet,
|
||||
EnsureCommoditySupply,
|
||||
EnsureWaterSecurity,
|
||||
EnsureMiningCapacity,
|
||||
EnsureConstructionCapacity,
|
||||
EnsureTransportCapacity,
|
||||
}
|
||||
|
||||
public enum FactionObjectiveState
|
||||
{
|
||||
Planned,
|
||||
Active,
|
||||
Blocked,
|
||||
Complete,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
public enum FactionPlanStepKind
|
||||
{
|
||||
EnsureCommodityProduction,
|
||||
EnsureShipyardSite,
|
||||
ProduceFleet,
|
||||
AttackFactionAssets,
|
||||
EnsureWaterSupply,
|
||||
EnsureMiningCapacity,
|
||||
EnsureConstructionCapacity,
|
||||
EnsureTransportCapacity,
|
||||
MonitorExpansionProject,
|
||||
}
|
||||
|
||||
public enum FactionPlanStepStatus
|
||||
{
|
||||
Planned,
|
||||
Ready,
|
||||
Running,
|
||||
Blocked,
|
||||
Complete,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
public enum FactionIssuedTaskKind
|
||||
{
|
||||
ExpandIndustry,
|
||||
ProduceShips,
|
||||
AttackFactionAssets,
|
||||
SustainWarIndustry,
|
||||
}
|
||||
|
||||
public enum FactionIssuedTaskState
|
||||
{
|
||||
Planned,
|
||||
Active,
|
||||
Blocked,
|
||||
Complete,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
public sealed class FactionObjectiveRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string MergeKey { get; init; }
|
||||
public required FactionObjectiveKind Kind { get; init; }
|
||||
public FactionObjectiveState State { get; set; } = FactionObjectiveState.Planned;
|
||||
public required string ObjectiveId { get; set; }
|
||||
public string? CampaignId { get; set; }
|
||||
public string? TheaterId { get; set; }
|
||||
public required string Kind { get; set; }
|
||||
public required string BehaviorKind { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public float Priority { get; set; }
|
||||
public string? ParentObjectiveId { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? HomeStationId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetSiteId { get; set; }
|
||||
public string? TargetRegionId { get; set; }
|
||||
public string? CommodityId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public int BudgetWeight { get; set; }
|
||||
public int SlotCost { get; set; } = 1;
|
||||
public int CreatedAtCycle { get; init; }
|
||||
public int UpdatedAtCycle { get; set; }
|
||||
public string? InvalidationReason { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
public HashSet<string> PrerequisiteObjectiveIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||
public string? TargetEntityId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FactionPlanStepRuntime
|
||||
public sealed class CommanderSkillProfileRuntime
|
||||
{
|
||||
public int Leadership { get; set; } = 3;
|
||||
public int Coordination { get; set; } = 3;
|
||||
public int Strategy { get; set; } = 3;
|
||||
}
|
||||
|
||||
public sealed class FactionDoctrineRuntime
|
||||
{
|
||||
public string StrategicPosture { get; set; } = "balanced";
|
||||
public string ExpansionPosture { get; set; } = "measured";
|
||||
public string MilitaryPosture { get; set; } = "defensive";
|
||||
public string EconomicPosture { get; set; } = "self-sufficient";
|
||||
public int DesiredControlledSystems { get; set; } = 3;
|
||||
public int DesiredMilitaryPerFront { get; set; } = 2;
|
||||
public int DesiredMinersPerSystem { get; set; } = 1;
|
||||
public int DesiredTransportsPerSystem { get; set; } = 1;
|
||||
public int DesiredConstructors { get; set; } = 1;
|
||||
public float ReserveCreditsRatio { get; set; } = 0.2f;
|
||||
public float ExpansionBudgetRatio { get; set; } = 0.25f;
|
||||
public float WarBudgetRatio { get; set; } = 0.35f;
|
||||
public float ReserveMilitaryRatio { get; set; } = 0.2f;
|
||||
public float OffensiveReadinessThreshold { get; set; } = 0.62f;
|
||||
public float SupplySecurityBias { get; set; } = 0.55f;
|
||||
public float FailureAversion { get; set; } = 0.45f;
|
||||
public int ReinforcementLeadPerFront { get; set; } = 1;
|
||||
}
|
||||
|
||||
public sealed class FactionMemoryRuntime
|
||||
{
|
||||
public int LastPlanCycle { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public int LastObservedShipsBuilt { get; set; }
|
||||
public int LastObservedShipsLost { get; set; }
|
||||
public float LastObservedCredits { get; set; }
|
||||
public HashSet<string> KnownSystemIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> KnownEnemyFactionIds { get; } = new(StringComparer.Ordinal);
|
||||
public List<FactionSystemMemoryRuntime> SystemMemories { get; } = [];
|
||||
public List<FactionCommodityMemoryRuntime> CommodityMemories { get; } = [];
|
||||
public List<FactionOutcomeRecordRuntime> RecentOutcomes { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionSystemMemoryRuntime
|
||||
{
|
||||
public required string SystemId { get; init; }
|
||||
public DateTimeOffset LastSeenAtUtc { get; set; }
|
||||
public int LastEnemyShipCount { get; set; }
|
||||
public int LastEnemyStationCount { get; set; }
|
||||
public bool ControlledByFaction { get; set; }
|
||||
public string? LastRole { get; set; }
|
||||
public float FrontierPressure { get; set; }
|
||||
public float RouteRisk { get; set; }
|
||||
public float HistoricalShortagePressure { get; set; }
|
||||
public int OffensiveFailures { get; set; }
|
||||
public int DefensiveFailures { get; set; }
|
||||
public int OffensiveSuccesses { get; set; }
|
||||
public int DefensiveSuccesses { get; set; }
|
||||
public DateTimeOffset? LastContestedAtUtc { get; set; }
|
||||
public DateTimeOffset? LastShortageAtUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FactionCommodityMemoryRuntime
|
||||
{
|
||||
public required string ItemId { get; init; }
|
||||
public float HistoricalShortageScore { get; set; }
|
||||
public float HistoricalSurplusScore { get; set; }
|
||||
public float LastObservedBacklog { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public DateTimeOffset? LastCriticalAtUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FactionOutcomeRecordRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ObjectiveId { get; init; }
|
||||
public required FactionPlanStepKind Kind { get; init; }
|
||||
public FactionPlanStepStatus Status { get; set; } = FactionPlanStepStatus.Planned;
|
||||
public float Priority { get; set; }
|
||||
public string? CommodityId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public string? TargetSiteId { get; set; }
|
||||
public string? StatusReason { get; set; }
|
||||
public string? ExecutionBindingKind { get; set; }
|
||||
public string? ExecutionBindingTargetId { get; set; }
|
||||
public string? ExecutionBindingSummary { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int LastEvaluatedCycle { get; set; }
|
||||
public HashSet<string> DependencyStepIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> RequiredFacts { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ProducedFacts { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> IssuedTaskIds { get; } = new(StringComparer.Ordinal);
|
||||
public required string Kind { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public string? RelatedCampaignId { get; set; }
|
||||
public string? RelatedObjectiveId { get; set; }
|
||||
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FactionIssuedTaskRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string MergeKey { get; init; }
|
||||
public required FactionIssuedTaskKind Kind { get; init; }
|
||||
public required string ObjectiveId { get; init; }
|
||||
public required string StepId { get; init; }
|
||||
public FactionIssuedTaskState State { get; set; } = FactionIssuedTaskState.Planned;
|
||||
public float Priority { get; set; }
|
||||
public string? ShipRole { get; set; }
|
||||
public string? CommodityId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetSiteId { get; set; }
|
||||
public int CreatedAtCycle { get; init; }
|
||||
public int UpdatedAtCycle { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public HashSet<string> AssignedAssetIds { get; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class FactionBlackboardRuntime
|
||||
public sealed class FactionStrategicStateRuntime
|
||||
{
|
||||
public int PlanCycle { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public string Status { get; set; } = "stable";
|
||||
public FactionBudgetRuntime Budget { get; set; } = new();
|
||||
public FactionEconomicAssessmentRuntime EconomicAssessment { get; set; } = new();
|
||||
public FactionThreatAssessmentRuntime ThreatAssessment { get; set; } = new();
|
||||
public List<FactionTheaterRuntime> Theaters { get; } = [];
|
||||
public List<FactionCampaignRuntime> Campaigns { get; } = [];
|
||||
public List<FactionOperationalObjectiveRuntime> Objectives { get; } = [];
|
||||
public List<FactionAssetReservationRuntime> Reservations { get; } = [];
|
||||
public List<FactionProductionProgramRuntime> ProductionPrograms { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionBudgetRuntime
|
||||
{
|
||||
public float ReservedCredits { get; set; }
|
||||
public float ExpansionCredits { get; set; }
|
||||
public float WarCredits { get; set; }
|
||||
public int ReservedMilitaryAssets { get; set; }
|
||||
public int ReservedLogisticsAssets { get; set; }
|
||||
public int ReservedConstructionAssets { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FactionEconomicAssessmentRuntime
|
||||
{
|
||||
public int PlanCycle { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public int TargetWarshipCount { get; set; }
|
||||
public bool HasWarIndustrySupplyChain { get; set; }
|
||||
public bool HasShipyard { get; set; }
|
||||
public bool HasActiveExpansionProject { get; set; }
|
||||
public string? ActiveExpansionCommodityId { get; set; }
|
||||
public string? ActiveExpansionModuleId { get; set; }
|
||||
public string? ActiveExpansionSiteId { get; set; }
|
||||
public string? ActiveExpansionSystemId { get; set; }
|
||||
public int EnemyFactionCount { get; set; }
|
||||
public int EnemyShipCount { get; set; }
|
||||
public int EnemyStationCount { get; set; }
|
||||
public int MilitaryShipCount { get; set; }
|
||||
public int MinerShipCount { get; set; }
|
||||
public int TransportShipCount { get; set; }
|
||||
public int ConstructorShipCount { get; set; }
|
||||
public int ControlledSystemCount { get; set; }
|
||||
public int TargetMilitaryShipCount { get; set; }
|
||||
public int TargetMinerShipCount { get; set; }
|
||||
public int TargetTransportShipCount { get; set; }
|
||||
public int TargetConstructorShipCount { get; set; }
|
||||
public bool HasShipyard { get; set; }
|
||||
public bool HasWarIndustrySupplyChain { get; set; }
|
||||
public string? PrimaryExpansionSiteId { get; set; }
|
||||
public string? PrimaryExpansionSystemId { get; set; }
|
||||
public float ReplacementPressure { get; set; }
|
||||
public float SustainmentScore { get; set; }
|
||||
public float LogisticsSecurityScore { get; set; }
|
||||
public int CriticalShortageCount { get; set; }
|
||||
public string? IndustrialBottleneckItemId { get; set; }
|
||||
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionThreatAssessmentRuntime
|
||||
{
|
||||
public int PlanCycle { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public int EnemyFactionCount { get; set; }
|
||||
public int EnemyShipCount { get; set; }
|
||||
public int EnemyStationCount { get; set; }
|
||||
public string? PrimaryThreatFactionId { get; set; }
|
||||
public string? PrimaryThreatSystemId { get; set; }
|
||||
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
|
||||
public HashSet<string> AvailableShipIds { get; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class FactionTheaterRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public float Priority { get; set; }
|
||||
public float SupplyRisk { get; set; }
|
||||
public float FriendlyAssetValue { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public string? AnchorEntityId { get; set; }
|
||||
public Vector3? AnchorPosition { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> CampaignIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionCampaignRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "planned";
|
||||
public float Priority { get; set; }
|
||||
public string? TheaterId { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? CommodityId { get; set; }
|
||||
public string? SupportStationId { get; set; }
|
||||
public int CurrentStepIndex { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string? Summary { get; set; }
|
||||
public string? PauseReason { get; set; }
|
||||
public float ContinuationScore { get; set; }
|
||||
public float SupplyAdequacy { get; set; }
|
||||
public float ReplacementPressure { get; set; }
|
||||
public int FailureCount { get; set; }
|
||||
public int SuccessCount { get; set; }
|
||||
public string? FleetCommanderId { get; set; }
|
||||
public bool RequiresReinforcement { get; set; }
|
||||
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||
public List<string> ObjectiveIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionOperationalObjectiveRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string CampaignId { get; set; }
|
||||
public string? TheaterId { get; set; }
|
||||
public required string Kind { get; set; }
|
||||
public required string DelegationKind { get; set; }
|
||||
public required string BehaviorKind { get; set; }
|
||||
public string Status { get; set; } = "planned";
|
||||
public float Priority { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? HomeStationId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public int CurrentStepIndex { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public bool UseOrders { get; set; }
|
||||
public string? StagingOrderKind { get; set; }
|
||||
public int ReinforcementLevel { get; set; }
|
||||
public List<FactionPlanStepRuntime> Steps { get; } = [];
|
||||
public List<string> ReservedAssetIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class FactionPlanStepRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "planned";
|
||||
public string? Summary { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FactionAssetReservationRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string ObjectiveId { get; set; }
|
||||
public string? CampaignId { get; set; }
|
||||
public required string AssetKind { get; set; }
|
||||
public required string AssetId { get; set; }
|
||||
public float Priority { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FactionProductionProgramRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "planned";
|
||||
public float Priority { get; set; }
|
||||
public string? CampaignId { get; set; }
|
||||
public string? CommodityId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? ShipKind { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public int TargetCount { get; set; }
|
||||
public int CurrentCount { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FactionDecisionLogEntryRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public string? RelatedEntityId { get; set; }
|
||||
public int PlanCycle { get; set; }
|
||||
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FactionCommoditySignalRuntime
|
||||
@@ -228,38 +344,5 @@ public sealed class FactionThreatSignalRuntime
|
||||
public required string ScopeKind { get; init; }
|
||||
public int EnemyShipCount { get; set; }
|
||||
public int EnemyStationCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CommanderBehaviorRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public string? Phase { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? AreaSystemId { get; set; }
|
||||
public int PatrolIndex { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CommanderOrderRuntime
|
||||
{
|
||||
public required string Kind { get; init; }
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? DestinationNodeId { get; set; }
|
||||
public required string DestinationSystemId { get; init; }
|
||||
public required Vector3 DestinationPosition { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CommanderTaskRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetNodeId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
public string? EnemyFactionId { get; set; }
|
||||
}
|
||||
|
||||
283
apps/backend/Geopolitics/Contracts/Geopolitics.cs
Normal file
283
apps/backend/Geopolitics/Contracts/Geopolitics.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
namespace SpaceGame.Api.Geopolitics.Contracts;
|
||||
|
||||
public sealed record SystemRouteLinkSnapshot(
|
||||
string Id,
|
||||
string SourceSystemId,
|
||||
string DestinationSystemId,
|
||||
float Distance,
|
||||
bool IsPrimaryLane);
|
||||
|
||||
public sealed record DiplomaticRelationSnapshot(
|
||||
string Id,
|
||||
string FactionAId,
|
||||
string FactionBId,
|
||||
string Status,
|
||||
string Posture,
|
||||
float TrustScore,
|
||||
float TensionScore,
|
||||
float GrievanceScore,
|
||||
string TradeAccessPolicy,
|
||||
string MilitaryAccessPolicy,
|
||||
string? WarStateId,
|
||||
DateTimeOffset? CeasefireUntilUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> ActiveTreatyIds,
|
||||
IReadOnlyList<string> ActiveIncidentIds);
|
||||
|
||||
public sealed record TreatySnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string TradeAccessPolicy,
|
||||
string MilitaryAccessPolicy,
|
||||
string? Summary,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> FactionIds);
|
||||
|
||||
public sealed record DiplomaticIncidentSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string SourceFactionId,
|
||||
string TargetFactionId,
|
||||
string? SystemId,
|
||||
string? BorderEdgeId,
|
||||
string Summary,
|
||||
float Severity,
|
||||
float EscalationScore,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset LastObservedAtUtc);
|
||||
|
||||
public sealed record BorderTensionSnapshot(
|
||||
string Id,
|
||||
string RelationId,
|
||||
string BorderEdgeId,
|
||||
string FactionAId,
|
||||
string FactionBId,
|
||||
string Status,
|
||||
float TensionScore,
|
||||
float IncidentScore,
|
||||
float MilitaryPressure,
|
||||
float AccessFriction,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> SystemIds);
|
||||
|
||||
public sealed record WarStateSnapshot(
|
||||
string Id,
|
||||
string RelationId,
|
||||
string FactionAId,
|
||||
string FactionBId,
|
||||
string Status,
|
||||
string WarGoal,
|
||||
float EscalationScore,
|
||||
DateTimeOffset StartedAtUtc,
|
||||
DateTimeOffset? CeasefireUntilUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> ActiveFrontLineIds);
|
||||
|
||||
public sealed record DiplomaticStateSnapshot(
|
||||
IReadOnlyList<DiplomaticRelationSnapshot> Relations,
|
||||
IReadOnlyList<TreatySnapshot> Treaties,
|
||||
IReadOnlyList<DiplomaticIncidentSnapshot> Incidents,
|
||||
IReadOnlyList<BorderTensionSnapshot> BorderTensions,
|
||||
IReadOnlyList<WarStateSnapshot> Wars);
|
||||
|
||||
public sealed record TerritoryClaimSnapshot(
|
||||
string Id,
|
||||
string? SourceClaimId,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string Status,
|
||||
string ClaimKind,
|
||||
float ClaimStrength,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record TerritoryInfluenceSnapshot(
|
||||
string Id,
|
||||
string SystemId,
|
||||
string FactionId,
|
||||
float ClaimStrength,
|
||||
float AssetStrength,
|
||||
float LogisticsStrength,
|
||||
float TotalInfluence,
|
||||
bool IsContesting,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record TerritoryControlStateSnapshot(
|
||||
string SystemId,
|
||||
string? ControllerFactionId,
|
||||
string? PrimaryClaimantFactionId,
|
||||
string ControlKind,
|
||||
bool IsContested,
|
||||
float ControlScore,
|
||||
float StrategicValue,
|
||||
IReadOnlyList<string> ClaimantFactionIds,
|
||||
IReadOnlyList<string> InfluencingFactionIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record SectorStrategicProfileSnapshot(
|
||||
string SystemId,
|
||||
string? ControllerFactionId,
|
||||
string ZoneKind,
|
||||
bool IsContested,
|
||||
float StrategicValue,
|
||||
float SecurityRating,
|
||||
float TerritorialPressure,
|
||||
float LogisticsValue,
|
||||
string? EconomicRegionId,
|
||||
string? FrontLineId,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record BorderEdgeSnapshot(
|
||||
string Id,
|
||||
string SourceSystemId,
|
||||
string DestinationSystemId,
|
||||
string? SourceFactionId,
|
||||
string? DestinationFactionId,
|
||||
bool IsContested,
|
||||
string? RelationId,
|
||||
float TensionScore,
|
||||
float CorridorImportance,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record FrontLineSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string? AnchorSystemId,
|
||||
float PressureScore,
|
||||
float SupplyRisk,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> FactionIds,
|
||||
IReadOnlyList<string> SystemIds,
|
||||
IReadOnlyList<string> BorderEdgeIds);
|
||||
|
||||
public sealed record TerritoryZoneSnapshot(
|
||||
string Id,
|
||||
string SystemId,
|
||||
string? FactionId,
|
||||
string Kind,
|
||||
string Status,
|
||||
string? Reason,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record TerritoryPressureSnapshot(
|
||||
string Id,
|
||||
string SystemId,
|
||||
string? FactionId,
|
||||
string Kind,
|
||||
float PressureScore,
|
||||
float SecurityScore,
|
||||
float HostileInfluence,
|
||||
float CorridorRisk,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record TerritoryStateSnapshot(
|
||||
IReadOnlyList<TerritoryClaimSnapshot> Claims,
|
||||
IReadOnlyList<TerritoryInfluenceSnapshot> Influences,
|
||||
IReadOnlyList<TerritoryControlStateSnapshot> ControlStates,
|
||||
IReadOnlyList<SectorStrategicProfileSnapshot> StrategicProfiles,
|
||||
IReadOnlyList<BorderEdgeSnapshot> BorderEdges,
|
||||
IReadOnlyList<FrontLineSnapshot> FrontLines,
|
||||
IReadOnlyList<TerritoryZoneSnapshot> Zones,
|
||||
IReadOnlyList<TerritoryPressureSnapshot> Pressures);
|
||||
|
||||
public sealed record EconomicRegionSnapshot(
|
||||
string Id,
|
||||
string? FactionId,
|
||||
string Label,
|
||||
string Kind,
|
||||
string Status,
|
||||
string CoreSystemId,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> SystemIds,
|
||||
IReadOnlyList<string> StationIds,
|
||||
IReadOnlyList<string> FrontLineIds,
|
||||
IReadOnlyList<string> CorridorIds);
|
||||
|
||||
public sealed record SupplyNetworkSnapshot(
|
||||
string Id,
|
||||
string RegionId,
|
||||
float ThroughputScore,
|
||||
float RiskScore,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> StationIds,
|
||||
IReadOnlyList<string> ProducerItemIds,
|
||||
IReadOnlyList<string> ConsumerItemIds,
|
||||
IReadOnlyList<string> ConstructionItemIds);
|
||||
|
||||
public sealed record LogisticsCorridorSnapshot(
|
||||
string Id,
|
||||
string? FactionId,
|
||||
string Kind,
|
||||
string Status,
|
||||
float RiskScore,
|
||||
float ThroughputScore,
|
||||
string AccessState,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> SystemPathIds,
|
||||
IReadOnlyList<string> RegionIds,
|
||||
IReadOnlyList<string> BorderEdgeIds);
|
||||
|
||||
public sealed record RegionalProductionProfileSnapshot(
|
||||
string RegionId,
|
||||
string PrimaryIndustry,
|
||||
int ShipyardCount,
|
||||
int StationCount,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<string> ProducedItemIds,
|
||||
IReadOnlyList<string> ScarceItemIds);
|
||||
|
||||
public sealed record RegionalTradeBalanceSnapshot(
|
||||
string RegionId,
|
||||
int ImportsRequiredCount,
|
||||
int ExportsSurplusCount,
|
||||
int CriticalShortageCount,
|
||||
float NetTradeScore,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record RegionalBottleneckSnapshot(
|
||||
string Id,
|
||||
string RegionId,
|
||||
string ItemId,
|
||||
string Cause,
|
||||
string Status,
|
||||
float Severity,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record RegionalSecurityAssessmentSnapshot(
|
||||
string RegionId,
|
||||
float SupplyRisk,
|
||||
float BorderPressure,
|
||||
int ActiveWarCount,
|
||||
int HostileRelationCount,
|
||||
float AccessFriction,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record RegionalEconomicAssessmentSnapshot(
|
||||
string RegionId,
|
||||
float SustainmentScore,
|
||||
float ProductionDepth,
|
||||
float ConstructionPressure,
|
||||
float CorridorDependency,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record EconomyRegionStateSnapshot(
|
||||
IReadOnlyList<EconomicRegionSnapshot> Regions,
|
||||
IReadOnlyList<SupplyNetworkSnapshot> SupplyNetworks,
|
||||
IReadOnlyList<LogisticsCorridorSnapshot> Corridors,
|
||||
IReadOnlyList<RegionalProductionProfileSnapshot> ProductionProfiles,
|
||||
IReadOnlyList<RegionalTradeBalanceSnapshot> TradeBalances,
|
||||
IReadOnlyList<RegionalBottleneckSnapshot> Bottlenecks,
|
||||
IReadOnlyList<RegionalSecurityAssessmentSnapshot> SecurityAssessments,
|
||||
IReadOnlyList<RegionalEconomicAssessmentSnapshot> EconomicAssessments);
|
||||
|
||||
public sealed record GeopoliticalStateSnapshot(
|
||||
int Cycle,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
IReadOnlyList<SystemRouteLinkSnapshot> Routes,
|
||||
DiplomaticStateSnapshot Diplomacy,
|
||||
TerritoryStateSnapshot Territory,
|
||||
EconomyRegionStateSnapshot EconomyRegions);
|
||||
336
apps/backend/Geopolitics/Runtime/GeopoliticalRuntimeModels.cs
Normal file
336
apps/backend/Geopolitics/Runtime/GeopoliticalRuntimeModels.cs
Normal file
@@ -0,0 +1,336 @@
|
||||
namespace SpaceGame.Api.Geopolitics.Runtime;
|
||||
|
||||
public sealed class GeopoliticalStateRuntime
|
||||
{
|
||||
public int Cycle { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<SystemRouteLinkRuntime> Routes { get; } = [];
|
||||
public DiplomaticStateRuntime Diplomacy { get; set; } = new();
|
||||
public TerritoryStateRuntime Territory { get; set; } = new();
|
||||
public EconomyRegionStateRuntime EconomyRegions { get; set; } = new();
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SystemRouteLinkRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SourceSystemId { get; set; }
|
||||
public required string DestinationSystemId { get; set; }
|
||||
public float Distance { get; set; }
|
||||
public bool IsPrimaryLane { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class DiplomaticStateRuntime
|
||||
{
|
||||
public List<DiplomaticRelationRuntime> Relations { get; } = [];
|
||||
public List<TreatyRuntime> Treaties { get; } = [];
|
||||
public List<DiplomaticIncidentRuntime> Incidents { get; } = [];
|
||||
public List<BorderTensionRuntime> BorderTensions { get; } = [];
|
||||
public List<WarStateRuntime> Wars { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class DiplomaticRelationRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string FactionAId { get; set; }
|
||||
public required string FactionBId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Posture { get; set; } = "neutral";
|
||||
public float TrustScore { get; set; }
|
||||
public float TensionScore { get; set; }
|
||||
public float GrievanceScore { get; set; }
|
||||
public string TradeAccessPolicy { get; set; } = "restricted";
|
||||
public string MilitaryAccessPolicy { get; set; } = "restricted";
|
||||
public string? WarStateId { get; set; }
|
||||
public DateTimeOffset? CeasefireUntilUtc { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> ActiveTreatyIds { get; } = [];
|
||||
public List<string> ActiveIncidentIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class TreatyRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string TradeAccessPolicy { get; set; } = "restricted";
|
||||
public string MilitaryAccessPolicy { get; set; } = "restricted";
|
||||
public string? Summary { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> FactionIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class DiplomaticIncidentRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public required string SourceFactionId { get; set; }
|
||||
public required string TargetFactionId { get; set; }
|
||||
public string? SystemId { get; set; }
|
||||
public string? BorderEdgeId { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public float Severity { get; set; }
|
||||
public float EscalationScore { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset LastObservedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class BorderTensionRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string RelationId { get; set; }
|
||||
public required string BorderEdgeId { get; set; }
|
||||
public required string FactionAId { get; set; }
|
||||
public required string FactionBId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public float TensionScore { get; set; }
|
||||
public float IncidentScore { get; set; }
|
||||
public float MilitaryPressure { get; set; }
|
||||
public float AccessFriction { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> SystemIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class WarStateRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string RelationId { get; set; }
|
||||
public required string FactionAId { get; set; }
|
||||
public required string FactionBId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string WarGoal { get; set; } = "territorial-pressure";
|
||||
public float EscalationScore { get; set; }
|
||||
public DateTimeOffset StartedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset? CeasefireUntilUtc { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> ActiveFrontLineIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class TerritoryStateRuntime
|
||||
{
|
||||
public List<TerritoryClaimRuntime> Claims { get; } = [];
|
||||
public List<TerritoryInfluenceRuntime> Influences { get; } = [];
|
||||
public List<TerritoryControlStateRuntime> ControlStates { get; } = [];
|
||||
public List<SectorStrategicProfileRuntime> StrategicProfiles { get; } = [];
|
||||
public List<BorderEdgeRuntime> BorderEdges { get; } = [];
|
||||
public List<FrontLineRuntime> FrontLines { get; } = [];
|
||||
public List<TerritoryZoneRuntime> Zones { get; } = [];
|
||||
public List<TerritoryPressureRuntime> Pressures { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class TerritoryClaimRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public string? SourceClaimId { get; set; }
|
||||
public required string FactionId { get; set; }
|
||||
public required string SystemId { get; set; }
|
||||
public required string CelestialId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string ClaimKind { get; set; } = "infrastructure";
|
||||
public float ClaimStrength { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class TerritoryInfluenceRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public required string FactionId { get; set; }
|
||||
public float ClaimStrength { get; set; }
|
||||
public float AssetStrength { get; set; }
|
||||
public float LogisticsStrength { get; set; }
|
||||
public float TotalInfluence { get; set; }
|
||||
public bool IsContesting { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class TerritoryControlStateRuntime
|
||||
{
|
||||
public required string SystemId { get; init; }
|
||||
public string? ControllerFactionId { get; set; }
|
||||
public string? PrimaryClaimantFactionId { get; set; }
|
||||
public string ControlKind { get; set; } = "unclaimed";
|
||||
public bool IsContested { get; set; }
|
||||
public float ControlScore { get; set; }
|
||||
public float StrategicValue { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> ClaimantFactionIds { get; } = [];
|
||||
public List<string> InfluencingFactionIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class SectorStrategicProfileRuntime
|
||||
{
|
||||
public required string SystemId { get; init; }
|
||||
public string? ControllerFactionId { get; set; }
|
||||
public string ZoneKind { get; set; } = "unclaimed";
|
||||
public bool IsContested { get; set; }
|
||||
public float StrategicValue { get; set; }
|
||||
public float SecurityRating { get; set; }
|
||||
public float TerritorialPressure { get; set; }
|
||||
public float LogisticsValue { get; set; }
|
||||
public string? EconomicRegionId { get; set; }
|
||||
public string? FrontLineId { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class BorderEdgeRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SourceSystemId { get; set; }
|
||||
public required string DestinationSystemId { get; set; }
|
||||
public string? SourceFactionId { get; set; }
|
||||
public string? DestinationFactionId { get; set; }
|
||||
public bool IsContested { get; set; }
|
||||
public string? RelationId { get; set; }
|
||||
public float TensionScore { get; set; }
|
||||
public float CorridorImportance { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class FrontLineRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public string Kind { get; set; } = "border-front";
|
||||
public string Status { get; set; } = "active";
|
||||
public string? AnchorSystemId { get; set; }
|
||||
public float PressureScore { get; set; }
|
||||
public float SupplyRisk { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> FactionIds { get; } = [];
|
||||
public List<string> SystemIds { get; } = [];
|
||||
public List<string> BorderEdgeIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class TerritoryZoneRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public string? FactionId { get; set; }
|
||||
public string Kind { get; set; } = "unclaimed";
|
||||
public string Status { get; set; } = "active";
|
||||
public string? Reason { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class TerritoryPressureRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public string? FactionId { get; set; }
|
||||
public string Kind { get; set; } = "border-pressure";
|
||||
public float PressureScore { get; set; }
|
||||
public float SecurityScore { get; set; }
|
||||
public float HostileInfluence { get; set; }
|
||||
public float CorridorRisk { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class EconomyRegionStateRuntime
|
||||
{
|
||||
public List<EconomicRegionRuntime> Regions { get; } = [];
|
||||
public List<SupplyNetworkRuntime> SupplyNetworks { get; } = [];
|
||||
public List<LogisticsCorridorRuntime> Corridors { get; } = [];
|
||||
public List<RegionalProductionProfileRuntime> ProductionProfiles { get; } = [];
|
||||
public List<RegionalTradeBalanceRuntime> TradeBalances { get; } = [];
|
||||
public List<RegionalBottleneckRuntime> Bottlenecks { get; } = [];
|
||||
public List<RegionalSecurityAssessmentRuntime> SecurityAssessments { get; } = [];
|
||||
public List<RegionalEconomicAssessmentRuntime> EconomicAssessments { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class EconomicRegionRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public string? FactionId { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public string Kind { get; set; } = "balanced-region";
|
||||
public string Status { get; set; } = "active";
|
||||
public required string CoreSystemId { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> SystemIds { get; } = [];
|
||||
public List<string> StationIds { get; } = [];
|
||||
public List<string> FrontLineIds { get; } = [];
|
||||
public List<string> CorridorIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class SupplyNetworkRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string RegionId { get; set; }
|
||||
public float ThroughputScore { get; set; }
|
||||
public float RiskScore { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> StationIds { get; } = [];
|
||||
public List<string> ProducerItemIds { get; } = [];
|
||||
public List<string> ConsumerItemIds { get; } = [];
|
||||
public List<string> ConstructionItemIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class LogisticsCorridorRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public string? FactionId { get; set; }
|
||||
public string Kind { get; set; } = "supply-corridor";
|
||||
public string Status { get; set; } = "active";
|
||||
public float RiskScore { get; set; }
|
||||
public float ThroughputScore { get; set; }
|
||||
public string AccessState { get; set; } = "restricted";
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> SystemPathIds { get; } = [];
|
||||
public List<string> RegionIds { get; } = [];
|
||||
public List<string> BorderEdgeIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class RegionalProductionProfileRuntime
|
||||
{
|
||||
public required string RegionId { get; set; }
|
||||
public string PrimaryIndustry { get; set; } = "mixed";
|
||||
public int ShipyardCount { get; set; }
|
||||
public int StationCount { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public List<string> ProducedItemIds { get; } = [];
|
||||
public List<string> ScarceItemIds { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class RegionalTradeBalanceRuntime
|
||||
{
|
||||
public required string RegionId { get; set; }
|
||||
public int ImportsRequiredCount { get; set; }
|
||||
public int ExportsSurplusCount { get; set; }
|
||||
public int CriticalShortageCount { get; set; }
|
||||
public float NetTradeScore { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class RegionalBottleneckRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string RegionId { get; set; }
|
||||
public required string ItemId { get; set; }
|
||||
public string Cause { get; set; } = "regional-shortage";
|
||||
public string Status { get; set; } = "active";
|
||||
public float Severity { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class RegionalSecurityAssessmentRuntime
|
||||
{
|
||||
public required string RegionId { get; set; }
|
||||
public float SupplyRisk { get; set; }
|
||||
public float BorderPressure { get; set; }
|
||||
public int ActiveWarCount { get; set; }
|
||||
public int HostileRelationCount { get; set; }
|
||||
public float AccessFriction { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class RegionalEconomicAssessmentRuntime
|
||||
{
|
||||
public required string RegionId { get; set; }
|
||||
public float SustainmentScore { get; set; }
|
||||
public float ProductionDepth { get; set; }
|
||||
public float ConstructionPressure { get; set; }
|
||||
public float CorridorDependency { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,923 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace SpaceGame.Api.Geopolitics.Simulation;
|
||||
|
||||
internal sealed class GeopoliticalSimulationService
|
||||
{
|
||||
internal void Update(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var state = EnsureState(world);
|
||||
state.Cycle += 1;
|
||||
state.UpdatedAtUtc = world.GeneratedAtUtc;
|
||||
|
||||
RebuildRoutes(world, state);
|
||||
RebuildTerritory(world, state);
|
||||
RebuildDiplomacy(world, state, events);
|
||||
RebuildEconomyRegions(world, state);
|
||||
}
|
||||
|
||||
internal static GeopoliticalStateRuntime EnsureState(SimulationWorld world)
|
||||
{
|
||||
world.Geopolitics ??= new GeopoliticalStateRuntime();
|
||||
return world.Geopolitics;
|
||||
}
|
||||
|
||||
internal static DiplomaticRelationRuntime? FindRelation(SimulationWorld world, string factionAId, string factionBId)
|
||||
{
|
||||
var state = EnsureState(world);
|
||||
return state.Diplomacy.Relations.FirstOrDefault(relation => string.Equals(relation.Id, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
internal static WarStateRuntime? FindWarState(SimulationWorld world, string factionAId, string factionBId) =>
|
||||
EnsureState(world).Diplomacy.Wars.FirstOrDefault(war => string.Equals(war.RelationId, BuildRelationId(factionAId, factionBId), StringComparison.Ordinal) && war.Status == "active");
|
||||
|
||||
internal static TerritoryControlStateRuntime? GetSystemControlState(SimulationWorld world, string systemId) =>
|
||||
EnsureState(world).Territory.ControlStates.FirstOrDefault(state => string.Equals(state.SystemId, systemId, StringComparison.Ordinal));
|
||||
|
||||
internal static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId) =>
|
||||
string.Equals(GetSystemControlState(world, systemId)?.ControllerFactionId, factionId, StringComparison.Ordinal);
|
||||
|
||||
internal static IReadOnlyList<string> GetControlledSystems(SimulationWorld world, string factionId) =>
|
||||
EnsureState(world).Territory.ControlStates
|
||||
.Where(state => string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal))
|
||||
.OrderBy(state => state.SystemId, StringComparer.Ordinal)
|
||||
.Select(state => state.SystemId)
|
||||
.ToList();
|
||||
|
||||
internal static float GetSystemRouteRisk(SimulationWorld world, string systemId, string? factionId = null)
|
||||
{
|
||||
var pressure = EnsureState(world).Territory.Pressures
|
||||
.Where(entry => string.Equals(entry.SystemId, systemId, StringComparison.Ordinal)
|
||||
&& (factionId is null || string.Equals(entry.FactionId, factionId, StringComparison.Ordinal)))
|
||||
.OrderByDescending(entry => entry.CorridorRisk)
|
||||
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
return pressure?.CorridorRisk
|
||||
?? EnsureState(world).Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == systemId)?.TerritorialPressure
|
||||
?? 0f;
|
||||
}
|
||||
|
||||
internal static bool HasHostileRelation(SimulationWorld world, string factionAId, string factionBId)
|
||||
{
|
||||
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relation = FindRelation(world, factionAId, factionBId);
|
||||
return relation is not null && relation.Posture is "hostile" or "war";
|
||||
}
|
||||
|
||||
internal static bool HasTradeAccess(SimulationWorld world, string factionAId, string factionBId)
|
||||
{
|
||||
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var relation = FindRelation(world, factionAId, factionBId);
|
||||
return relation?.TradeAccessPolicy is "open" or "allied";
|
||||
}
|
||||
|
||||
internal static bool HasMilitaryAccess(SimulationWorld world, string factionAId, string factionBId)
|
||||
{
|
||||
if (string.Equals(factionAId, factionBId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var relation = FindRelation(world, factionAId, factionBId);
|
||||
return relation?.MilitaryAccessPolicy is "open" or "allied";
|
||||
}
|
||||
|
||||
internal static EconomicRegionRuntime? GetPrimaryEconomicRegion(SimulationWorld world, string factionId, string systemId) =>
|
||||
EnsureState(world).EconomyRegions.Regions.FirstOrDefault(region =>
|
||||
string.Equals(region.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& region.SystemIds.Contains(systemId, StringComparer.Ordinal));
|
||||
|
||||
private static void RebuildRoutes(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||
{
|
||||
state.Routes.Clear();
|
||||
if (world.Systems.Count <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var systems = world.Systems
|
||||
.OrderBy(system => system.Definition.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var routeIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var system in systems)
|
||||
{
|
||||
foreach (var neighbor in systems
|
||||
.Where(candidate => candidate.Definition.Id != system.Definition.Id)
|
||||
.Select(candidate => new
|
||||
{
|
||||
candidate.Definition.Id,
|
||||
Distance = system.Position.DistanceTo(candidate.Position),
|
||||
})
|
||||
.OrderBy(candidate => candidate.Distance)
|
||||
.ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
|
||||
.Take(Math.Min(3, systems.Count - 1)))
|
||||
{
|
||||
var routeId = BuildPairId("route", system.Definition.Id, neighbor.Id);
|
||||
if (!routeIds.Add(routeId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Routes.Add(new SystemRouteLinkRuntime
|
||||
{
|
||||
Id = routeId,
|
||||
SourceSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? system.Definition.Id : neighbor.Id,
|
||||
DestinationSystemId = string.Compare(system.Definition.Id, neighbor.Id, StringComparison.Ordinal) <= 0 ? neighbor.Id : system.Definition.Id,
|
||||
Distance = neighbor.Distance,
|
||||
IsPrimaryLane = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebuildTerritory(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||
{
|
||||
state.Territory.Claims.Clear();
|
||||
state.Territory.Influences.Clear();
|
||||
state.Territory.ControlStates.Clear();
|
||||
state.Territory.StrategicProfiles.Clear();
|
||||
state.Territory.BorderEdges.Clear();
|
||||
state.Territory.FrontLines.Clear();
|
||||
state.Territory.Zones.Clear();
|
||||
state.Territory.Pressures.Clear();
|
||||
|
||||
var nowUtc = world.GeneratedAtUtc;
|
||||
foreach (var claim in world.Claims.Where(claim => claim.State != ClaimStateKinds.Destroyed))
|
||||
{
|
||||
state.Territory.Claims.Add(new TerritoryClaimRuntime
|
||||
{
|
||||
Id = $"territory-{claim.Id}",
|
||||
SourceClaimId = claim.Id,
|
||||
FactionId = claim.FactionId,
|
||||
SystemId = claim.SystemId,
|
||||
CelestialId = claim.CelestialId,
|
||||
Status = claim.State,
|
||||
ClaimKind = "infrastructure",
|
||||
ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
|
||||
var influencesBySystem = new Dictionary<string, List<TerritoryInfluenceRuntime>>(StringComparer.Ordinal);
|
||||
foreach (var system in world.Systems)
|
||||
{
|
||||
var claimsByFaction = state.Territory.Claims
|
||||
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal);
|
||||
var stationsByFaction = world.Stations
|
||||
.Where(station => station.SystemId == system.Definition.Id)
|
||||
.GroupBy(station => station.FactionId, StringComparer.Ordinal);
|
||||
var shipsByFaction = world.Ships
|
||||
.Where(ship => ship.SystemId == system.Definition.Id && ship.Health > 0f)
|
||||
.GroupBy(ship => ship.FactionId, StringComparer.Ordinal);
|
||||
var sitesByFaction = world.ConstructionSites
|
||||
.Where(site => site.SystemId == system.Definition.Id && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed)
|
||||
.GroupBy(site => site.FactionId, StringComparer.Ordinal);
|
||||
|
||||
var factionIds = claimsByFaction.Select(group => group.Key)
|
||||
.Concat(stationsByFaction.Select(group => group.Key))
|
||||
.Concat(shipsByFaction.Select(group => group.Key))
|
||||
.Concat(sitesByFaction.Select(group => group.Key))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var influences = new List<TerritoryInfluenceRuntime>();
|
||||
foreach (var factionId in factionIds)
|
||||
{
|
||||
var claimStrength = claimsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(claim => claim.ClaimStrength * 40f) ?? 0f;
|
||||
var stationStrength = (stationsByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 50f;
|
||||
var siteStrength = (sitesByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 18f;
|
||||
var shipStrength = shipsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(ship =>
|
||||
ship.Definition.Kind switch
|
||||
{
|
||||
"military" => 9f,
|
||||
"construction" => 4f,
|
||||
"transport" => 3f,
|
||||
_ when ship.Definition.Kind == "mining" || ship.Definition.Kind == "miner" => 3f,
|
||||
_ => 2f,
|
||||
}) ?? 0f;
|
||||
var logisticsStrength = MathF.Min(30f, stationStrength * 0.18f) + siteStrength;
|
||||
influences.Add(new TerritoryInfluenceRuntime
|
||||
{
|
||||
Id = $"influence-{system.Definition.Id}-{factionId}",
|
||||
SystemId = system.Definition.Id,
|
||||
FactionId = factionId,
|
||||
ClaimStrength = claimStrength,
|
||||
AssetStrength = stationStrength + shipStrength,
|
||||
LogisticsStrength = logisticsStrength,
|
||||
TotalInfluence = claimStrength + stationStrength + shipStrength + logisticsStrength,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
|
||||
influences.Sort((left, right) =>
|
||||
{
|
||||
var total = right.TotalInfluence.CompareTo(left.TotalInfluence);
|
||||
return total != 0 ? total : string.Compare(left.FactionId, right.FactionId, StringComparison.Ordinal);
|
||||
});
|
||||
if (influences.Count > 1)
|
||||
{
|
||||
var lead = influences[0].TotalInfluence;
|
||||
foreach (var influence in influences.Skip(1))
|
||||
{
|
||||
influence.IsContesting = influence.TotalInfluence >= (lead * 0.7f);
|
||||
}
|
||||
|
||||
influences[0].IsContesting = influences[1].TotalInfluence >= (lead * 0.7f);
|
||||
}
|
||||
|
||||
influencesBySystem[system.Definition.Id] = influences;
|
||||
state.Territory.Influences.AddRange(influences);
|
||||
|
||||
var top = influences.FirstOrDefault();
|
||||
var second = influences.Skip(1).FirstOrDefault();
|
||||
var contested = top is not null && second is not null && second.TotalInfluence >= (top.TotalInfluence * 0.7f);
|
||||
var controllerFactionId = top is not null && (!contested || top.TotalInfluence >= second!.TotalInfluence + 20f)
|
||||
? top.FactionId
|
||||
: null;
|
||||
var primaryClaimantFactionId = state.Territory.Claims
|
||||
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||
.GroupBy(claim => claim.FactionId, StringComparer.Ordinal)
|
||||
.OrderByDescending(group => group.Sum(claim => claim.ClaimStrength))
|
||||
.ThenBy(group => group.Key, StringComparer.Ordinal)
|
||||
.Select(group => group.Key)
|
||||
.FirstOrDefault();
|
||||
|
||||
var strategicValue = EstimateSystemStrategicValue(world, system.Definition.Id);
|
||||
var controlState = new TerritoryControlStateRuntime
|
||||
{
|
||||
SystemId = system.Definition.Id,
|
||||
ControllerFactionId = controllerFactionId,
|
||||
PrimaryClaimantFactionId = primaryClaimantFactionId,
|
||||
ControlKind = contested
|
||||
? "contested"
|
||||
: controllerFactionId is not null
|
||||
? "controlled"
|
||||
: primaryClaimantFactionId is not null
|
||||
? "claimed"
|
||||
: "unclaimed",
|
||||
IsContested = contested,
|
||||
ControlScore = top?.TotalInfluence ?? 0f,
|
||||
StrategicValue = strategicValue,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
controlState.ClaimantFactionIds.AddRange(state.Territory.Claims
|
||||
.Where(claim => claim.SystemId == system.Definition.Id)
|
||||
.Select(claim => claim.FactionId)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal));
|
||||
controlState.InfluencingFactionIds.AddRange(influences
|
||||
.Select(influence => influence.FactionId)
|
||||
.OrderBy(id => id, StringComparer.Ordinal));
|
||||
state.Territory.ControlStates.Add(controlState);
|
||||
}
|
||||
|
||||
foreach (var route in state.Routes)
|
||||
{
|
||||
var left = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.SourceSystemId);
|
||||
var right = state.Territory.ControlStates.First(stateItem => stateItem.SystemId == route.DestinationSystemId);
|
||||
var differentControllers = !string.Equals(left.ControllerFactionId, right.ControllerFactionId, StringComparison.Ordinal);
|
||||
var contested = left.IsContested || right.IsContested || differentControllers;
|
||||
if (!contested && left.ControllerFactionId is null && right.ControllerFactionId is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.Territory.BorderEdges.Add(new BorderEdgeRuntime
|
||||
{
|
||||
Id = $"border-{route.Id}",
|
||||
SourceSystemId = route.SourceSystemId,
|
||||
DestinationSystemId = route.DestinationSystemId,
|
||||
SourceFactionId = left.ControllerFactionId ?? left.PrimaryClaimantFactionId,
|
||||
DestinationFactionId = right.ControllerFactionId ?? right.PrimaryClaimantFactionId,
|
||||
IsContested = contested,
|
||||
TensionScore = MathF.Min(1f, MathF.Abs((left.ControlScore - right.ControlScore) / MathF.Max(50f, left.ControlScore + right.ControlScore))),
|
||||
CorridorImportance = route.Distance <= 0.01f ? 0f : Math.Clamp((left.StrategicValue + right.StrategicValue) / MathF.Max(route.Distance, 1f), 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var control in state.Territory.ControlStates)
|
||||
{
|
||||
var adjacentBorders = state.Territory.BorderEdges.Where(edge => edge.SourceSystemId == control.SystemId || edge.DestinationSystemId == control.SystemId).ToList();
|
||||
var hostileBorderCount = adjacentBorders.Count(edge => edge.IsContested);
|
||||
var corridorImportance = adjacentBorders.Sum(edge => edge.CorridorImportance);
|
||||
var zoneKind = control.IsContested
|
||||
? "contested"
|
||||
: control.ControllerFactionId is null && control.PrimaryClaimantFactionId is not null
|
||||
? "buffer"
|
||||
: control.ControllerFactionId is not null && hostileBorderCount == 0
|
||||
? "core"
|
||||
: control.ControllerFactionId is not null && corridorImportance > 1.1f
|
||||
? "corridor"
|
||||
: control.ControllerFactionId is not null
|
||||
? "frontier"
|
||||
: "unclaimed";
|
||||
state.Territory.Zones.Add(new TerritoryZoneRuntime
|
||||
{
|
||||
Id = $"zone-{control.SystemId}",
|
||||
SystemId = control.SystemId,
|
||||
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
|
||||
Kind = zoneKind,
|
||||
Status = "active",
|
||||
Reason = zoneKind == "corridor" ? "high-corridor-importance" : zoneKind == "frontier" ? "hostile-border-contact" : zoneKind,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
state.Territory.StrategicProfiles.Add(new SectorStrategicProfileRuntime
|
||||
{
|
||||
SystemId = control.SystemId,
|
||||
ControllerFactionId = control.ControllerFactionId,
|
||||
ZoneKind = zoneKind,
|
||||
IsContested = control.IsContested,
|
||||
StrategicValue = control.StrategicValue,
|
||||
SecurityRating = Math.Clamp(1f - (hostileBorderCount * 0.22f), 0f, 1f),
|
||||
TerritorialPressure = Math.Clamp(hostileBorderCount * 0.25f, 0f, 1f),
|
||||
LogisticsValue = Math.Clamp(corridorImportance, 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
state.Territory.Pressures.Add(new TerritoryPressureRuntime
|
||||
{
|
||||
Id = $"pressure-{control.SystemId}",
|
||||
SystemId = control.SystemId,
|
||||
FactionId = control.ControllerFactionId ?? control.PrimaryClaimantFactionId,
|
||||
Kind = control.IsContested ? "contested-pressure" : "territorial-pressure",
|
||||
PressureScore = Math.Clamp(hostileBorderCount * 0.28f, 0f, 1f),
|
||||
SecurityScore = Math.Clamp(1f - (hostileBorderCount * 0.2f), 0f, 1f),
|
||||
HostileInfluence = influencesBySystem.GetValueOrDefault(control.SystemId)?.Skip(control.ControllerFactionId is null ? 0 : 1).Sum(entry => entry.TotalInfluence) ?? 0f,
|
||||
CorridorRisk = Math.Clamp(corridorImportance > 0.8f && hostileBorderCount > 0 ? 0.7f : hostileBorderCount * 0.2f, 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebuildDiplomacy(SimulationWorld world, GeopoliticalStateRuntime state, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
state.Diplomacy.Relations.Clear();
|
||||
state.Diplomacy.Treaties.Clear();
|
||||
state.Diplomacy.BorderTensions.Clear();
|
||||
state.Diplomacy.Wars.Clear();
|
||||
|
||||
var nowUtc = world.GeneratedAtUtc;
|
||||
var factionPairs = world.Factions
|
||||
.OrderBy(faction => faction.Id, StringComparer.Ordinal)
|
||||
.SelectMany((left, index) => world.Factions.Skip(index + 1).Select(right => (left, right)));
|
||||
|
||||
foreach (var (leftFaction, rightFaction) in factionPairs)
|
||||
{
|
||||
var borderEdges = state.Territory.BorderEdges
|
||||
.Where(edge =>
|
||||
(string.Equals(edge.SourceFactionId, leftFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, rightFaction.Id, StringComparison.Ordinal))
|
||||
|| (string.Equals(edge.SourceFactionId, rightFaction.Id, StringComparison.Ordinal) && string.Equals(edge.DestinationFactionId, leftFaction.Id, StringComparison.Ordinal)))
|
||||
.OrderBy(edge => edge.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var sharedBorderPressure = borderEdges.Sum(edge => edge.TensionScore + (edge.IsContested ? 0.25f : 0f));
|
||||
var conflictSystems = borderEdges.SelectMany(edge => new[] { edge.SourceSystemId, edge.DestinationSystemId }).Distinct(StringComparer.Ordinal).ToList();
|
||||
var hostilePresence = world.Ships.Count(ship =>
|
||||
ship.Health > 0f
|
||||
&& ((ship.FactionId == leftFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))
|
||||
|| (ship.FactionId == rightFaction.Id && conflictSystems.Contains(ship.SystemId, StringComparer.Ordinal))));
|
||||
var incidentSeverity = Math.Clamp(sharedBorderPressure + (hostilePresence * 0.03f), 0f, 1.6f);
|
||||
var relationId = BuildRelationId(leftFaction.Id, rightFaction.Id);
|
||||
var posture = incidentSeverity switch
|
||||
{
|
||||
>= 1.1f => "war",
|
||||
>= 0.65f => "hostile",
|
||||
>= 0.3f => "wary",
|
||||
_ => "neutral",
|
||||
};
|
||||
|
||||
var relation = new DiplomaticRelationRuntime
|
||||
{
|
||||
Id = relationId,
|
||||
FactionAId = leftFaction.Id,
|
||||
FactionBId = rightFaction.Id,
|
||||
Status = "active",
|
||||
Posture = posture,
|
||||
TrustScore = Math.Clamp(0.7f - incidentSeverity, 0f, 1f),
|
||||
TensionScore = Math.Clamp(incidentSeverity, 0f, 1f),
|
||||
GrievanceScore = Math.Clamp(sharedBorderPressure, 0f, 1f),
|
||||
TradeAccessPolicy = posture is "war" or "hostile" ? "restricted" : "open",
|
||||
MilitaryAccessPolicy = posture == "neutral" ? "transit" : posture == "wary" ? "restricted" : "denied",
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
if (relation.Posture == "neutral")
|
||||
{
|
||||
var treaty = new TreatyRuntime
|
||||
{
|
||||
Id = $"treaty-open-trade-{relationId}",
|
||||
Kind = "trade-understanding",
|
||||
Status = "active",
|
||||
TradeAccessPolicy = "open",
|
||||
MilitaryAccessPolicy = "restricted",
|
||||
Summary = $"Open civilian trade between {leftFaction.Label} and {rightFaction.Label}.",
|
||||
CreatedAtUtc = nowUtc,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
treaty.FactionIds.Add(leftFaction.Id);
|
||||
treaty.FactionIds.Add(rightFaction.Id);
|
||||
state.Diplomacy.Treaties.Add(treaty);
|
||||
relation.ActiveTreatyIds.Add(treaty.Id);
|
||||
relation.TradeAccessPolicy = "open";
|
||||
}
|
||||
|
||||
state.Diplomacy.Relations.Add(relation);
|
||||
|
||||
foreach (var borderEdge in borderEdges)
|
||||
{
|
||||
borderEdge.RelationId = relation.Id;
|
||||
borderEdge.TensionScore = Math.Clamp(borderEdge.TensionScore + (relation.TensionScore * 0.35f), 0f, 1f);
|
||||
var tension = new BorderTensionRuntime
|
||||
{
|
||||
Id = $"tension-{borderEdge.Id}",
|
||||
RelationId = relation.Id,
|
||||
BorderEdgeId = borderEdge.Id,
|
||||
FactionAId = leftFaction.Id,
|
||||
FactionBId = rightFaction.Id,
|
||||
Status = relation.Posture is "war" or "hostile" ? "escalating" : "stable",
|
||||
TensionScore = relation.TensionScore,
|
||||
IncidentScore = incidentSeverity,
|
||||
MilitaryPressure = Math.Clamp(hostilePresence * 0.05f, 0f, 1f),
|
||||
AccessFriction = relation.TradeAccessPolicy == "open" ? 0.15f : 0.75f,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
tension.SystemIds.Add(borderEdge.SourceSystemId);
|
||||
tension.SystemIds.Add(borderEdge.DestinationSystemId);
|
||||
state.Diplomacy.BorderTensions.Add(tension);
|
||||
|
||||
if (tension.TensionScore >= 0.35f)
|
||||
{
|
||||
var incidentId = $"incident-border-{relationId}-{borderEdge.Id}";
|
||||
var incident = new DiplomaticIncidentRuntime
|
||||
{
|
||||
Id = incidentId,
|
||||
Kind = borderEdge.IsContested ? "border-clash" : "border-friction",
|
||||
Status = relation.Posture == "war" ? "escalated" : "active",
|
||||
SourceFactionId = leftFaction.Id,
|
||||
TargetFactionId = rightFaction.Id,
|
||||
SystemId = borderEdge.SourceSystemId,
|
||||
BorderEdgeId = borderEdge.Id,
|
||||
Summary = $"{leftFaction.Label} and {rightFaction.Label} are under pressure on {borderEdge.SourceSystemId}/{borderEdge.DestinationSystemId}.",
|
||||
Severity = tension.TensionScore,
|
||||
EscalationScore = tension.IncidentScore,
|
||||
CreatedAtUtc = nowUtc,
|
||||
LastObservedAtUtc = nowUtc,
|
||||
};
|
||||
state.Diplomacy.Incidents.Add(incident);
|
||||
relation.ActiveIncidentIds.Add(incident.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (relation.Posture == "war")
|
||||
{
|
||||
var warId = $"war-{relationId}";
|
||||
var war = new WarStateRuntime
|
||||
{
|
||||
Id = warId,
|
||||
RelationId = relation.Id,
|
||||
FactionAId = leftFaction.Id,
|
||||
FactionBId = rightFaction.Id,
|
||||
Status = "active",
|
||||
WarGoal = "border-dominance",
|
||||
EscalationScore = relation.TensionScore,
|
||||
StartedAtUtc = nowUtc,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
relation.WarStateId = war.Id;
|
||||
state.Diplomacy.Wars.Add(war);
|
||||
}
|
||||
}
|
||||
|
||||
BuildFrontLines(state, nowUtc, events);
|
||||
}
|
||||
|
||||
private static void BuildFrontLines(GeopoliticalStateRuntime state, DateTimeOffset nowUtc, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
foreach (var group in state.Diplomacy.BorderTensions
|
||||
.Where(tension => tension.TensionScore >= 0.35f)
|
||||
.GroupBy(tension => BuildPairId("front", tension.FactionAId, tension.FactionBId), StringComparer.Ordinal))
|
||||
{
|
||||
var tensions = group.OrderByDescending(tension => tension.TensionScore).ThenBy(tension => tension.Id, StringComparer.Ordinal).ToList();
|
||||
var front = new FrontLineRuntime
|
||||
{
|
||||
Id = group.Key,
|
||||
Kind = state.Diplomacy.Wars.Any(war => war.RelationId == tensions[0].RelationId && war.Status == "active") ? "war-front" : "border-front",
|
||||
Status = "active",
|
||||
AnchorSystemId = tensions.SelectMany(tension => tension.SystemIds).GroupBy(systemId => systemId, StringComparer.Ordinal).OrderByDescending(entry => entry.Count()).ThenBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => entry.Key).FirstOrDefault(),
|
||||
PressureScore = Math.Clamp(tensions.Sum(tension => tension.TensionScore) / tensions.Count, 0f, 1f),
|
||||
SupplyRisk = Math.Clamp(tensions.Sum(tension => tension.AccessFriction) / tensions.Count, 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
front.FactionIds.Add(tensions[0].FactionAId);
|
||||
front.FactionIds.Add(tensions[0].FactionBId);
|
||||
front.SystemIds.AddRange(tensions.SelectMany(tension => tension.SystemIds).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
|
||||
front.BorderEdgeIds.AddRange(tensions.Select(tension => tension.BorderEdgeId).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal));
|
||||
state.Territory.FrontLines.Add(front);
|
||||
|
||||
foreach (var war in state.Diplomacy.Wars.Where(war => string.Equals(war.RelationId, tensions[0].RelationId, StringComparison.Ordinal)))
|
||||
{
|
||||
war.ActiveFrontLineIds.Add(front.Id);
|
||||
}
|
||||
|
||||
events.Add(new SimulationEventRecord("front-line", front.Id, "front-updated", $"Front {front.Id} pressure {front.PressureScore.ToString("0.00", CultureInfo.InvariantCulture)}.", nowUtc, "geopolitics"));
|
||||
}
|
||||
|
||||
foreach (var profile in state.Territory.StrategicProfiles)
|
||||
{
|
||||
profile.FrontLineId = state.Territory.FrontLines.FirstOrDefault(front => front.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebuildEconomyRegions(SimulationWorld world, GeopoliticalStateRuntime state)
|
||||
{
|
||||
state.EconomyRegions.Regions.Clear();
|
||||
state.EconomyRegions.SupplyNetworks.Clear();
|
||||
state.EconomyRegions.Corridors.Clear();
|
||||
state.EconomyRegions.ProductionProfiles.Clear();
|
||||
state.EconomyRegions.TradeBalances.Clear();
|
||||
state.EconomyRegions.Bottlenecks.Clear();
|
||||
state.EconomyRegions.SecurityAssessments.Clear();
|
||||
state.EconomyRegions.EconomicAssessments.Clear();
|
||||
|
||||
var nowUtc = world.GeneratedAtUtc;
|
||||
foreach (var faction in world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal))
|
||||
{
|
||||
var factionSystems = state.Territory.ControlStates
|
||||
.Where(control => string.Equals(control.ControllerFactionId ?? control.PrimaryClaimantFactionId, faction.Id, StringComparison.Ordinal))
|
||||
.Select(control => control.SystemId)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(systemId => systemId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (factionSystems.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var connectedComponents = BuildConnectedComponents(factionSystems, state.Routes);
|
||||
foreach (var component in connectedComponents)
|
||||
{
|
||||
var coreSystemId = component
|
||||
.OrderByDescending(systemId => world.Stations.Count(station => station.FactionId == faction.Id && station.SystemId == systemId))
|
||||
.ThenBy(systemId => systemId, StringComparer.Ordinal)
|
||||
.First();
|
||||
var regionId = $"region-{faction.Id}-{coreSystemId}";
|
||||
var stations = world.Stations
|
||||
.Where(station => station.FactionId == faction.Id && component.Contains(station.SystemId, StringComparer.Ordinal))
|
||||
.OrderBy(station => station.Id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var economy = BuildRegionalEconomy(world, faction.Id, component);
|
||||
var regionKind = ResolveRegionKind(stations, economy);
|
||||
var frontLineIds = state.Territory.FrontLines
|
||||
.Where(front => front.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal)))
|
||||
.Select(front => front.Id)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var region = new EconomicRegionRuntime
|
||||
{
|
||||
Id = regionId,
|
||||
FactionId = faction.Id,
|
||||
Label = $"{faction.Label} {coreSystemId}",
|
||||
Kind = regionKind,
|
||||
Status = "active",
|
||||
CoreSystemId = coreSystemId,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
region.SystemIds.AddRange(component.OrderBy(id => id, StringComparer.Ordinal));
|
||||
region.StationIds.AddRange(stations.Select(station => station.Id));
|
||||
region.FrontLineIds.AddRange(frontLineIds);
|
||||
state.EconomyRegions.Regions.Add(region);
|
||||
|
||||
var producerItems = economy.Commodities
|
||||
.Where(entry => entry.Value.ProductionRatePerSecond > 0.01f)
|
||||
.OrderByDescending(entry => entry.Value.ProductionRatePerSecond)
|
||||
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.Take(8)
|
||||
.Select(entry => entry.Key)
|
||||
.ToList();
|
||||
var scarceItems = economy.Commodities
|
||||
.Where(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low)
|
||||
.OrderByDescending(entry => CommodityOperationalSignal.ComputeNeedScore(entry.Value, 240f))
|
||||
.ThenBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.Take(8)
|
||||
.Select(entry => entry.Key)
|
||||
.ToList();
|
||||
|
||||
var supplyNetwork = new SupplyNetworkRuntime
|
||||
{
|
||||
Id = $"network-{regionId}",
|
||||
RegionId = regionId,
|
||||
ThroughputScore = Math.Clamp(stations.Count * 0.18f, 0f, 1f),
|
||||
RiskScore = Math.Clamp(frontLineIds.Count * 0.24f, 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
supplyNetwork.StationIds.AddRange(stations.Select(station => station.Id));
|
||||
supplyNetwork.ProducerItemIds.AddRange(producerItems);
|
||||
supplyNetwork.ConsumerItemIds.AddRange(scarceItems);
|
||||
supplyNetwork.ConstructionItemIds.AddRange(world.ConstructionSites
|
||||
.Where(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal))
|
||||
.SelectMany(site => site.RequiredItems.Keys)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal));
|
||||
state.EconomyRegions.SupplyNetworks.Add(supplyNetwork);
|
||||
|
||||
var productionProfile = new RegionalProductionProfileRuntime
|
||||
{
|
||||
RegionId = regionId,
|
||||
PrimaryIndustry = regionKind,
|
||||
ShipyardCount = stations.Count(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)),
|
||||
StationCount = stations.Count,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
productionProfile.ProducedItemIds.AddRange(producerItems);
|
||||
productionProfile.ScarceItemIds.AddRange(scarceItems);
|
||||
state.EconomyRegions.ProductionProfiles.Add(productionProfile);
|
||||
|
||||
state.EconomyRegions.TradeBalances.Add(new RegionalTradeBalanceRuntime
|
||||
{
|
||||
RegionId = regionId,
|
||||
ImportsRequiredCount = economy.Commodities.Count(entry => entry.Value.BuyBacklog > 0.01f),
|
||||
ExportsSurplusCount = economy.Commodities.Count(entry => entry.Value.SellBacklog > 0.01f || entry.Value.Level == CommodityLevelKind.Surplus),
|
||||
CriticalShortageCount = scarceItems.Count,
|
||||
NetTradeScore = Math.Clamp((economy.Commodities.Sum(entry => entry.Value.ProjectedNetRatePerSecond) + 5f) / 10f, -1f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
|
||||
if (scarceItems.FirstOrDefault() is { } bottleneckItemId)
|
||||
{
|
||||
state.EconomyRegions.Bottlenecks.Add(new RegionalBottleneckRuntime
|
||||
{
|
||||
Id = $"bottleneck-{regionId}-{bottleneckItemId}",
|
||||
RegionId = regionId,
|
||||
ItemId = bottleneckItemId,
|
||||
Cause = "regional-shortage",
|
||||
Status = "active",
|
||||
Severity = Math.Clamp(CommodityOperationalSignal.ComputeNeedScore(economy.GetCommodity(bottleneckItemId), 240f), 0f, 10f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
|
||||
var supplyRisk = Math.Clamp(frontLineIds.Count * 0.2f, 0f, 1f);
|
||||
state.EconomyRegions.SecurityAssessments.Add(new RegionalSecurityAssessmentRuntime
|
||||
{
|
||||
RegionId = regionId,
|
||||
SupplyRisk = supplyRisk,
|
||||
BorderPressure = Math.Clamp(frontLineIds.Count * 0.22f, 0f, 1f),
|
||||
ActiveWarCount = state.Diplomacy.Wars.Count(war => war.ActiveFrontLineIds.Intersect(frontLineIds, StringComparer.Ordinal).Any()),
|
||||
HostileRelationCount = state.Diplomacy.Relations.Count(relation => relation.Posture is "hostile" or "war"),
|
||||
AccessFriction = Math.Clamp(state.Diplomacy.BorderTensions.Where(tension => tension.SystemIds.Any(systemId => component.Contains(systemId, StringComparer.Ordinal))).DefaultIfEmpty().Average(tension => tension?.AccessFriction ?? 0f), 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
|
||||
state.EconomyRegions.EconomicAssessments.Add(new RegionalEconomicAssessmentRuntime
|
||||
{
|
||||
RegionId = regionId,
|
||||
SustainmentScore = Math.Clamp(1f - (scarceItems.Count * 0.12f) - (supplyRisk * 0.35f), 0f, 1f),
|
||||
ProductionDepth = Math.Clamp(producerItems.Count / 8f, 0f, 1f),
|
||||
ConstructionPressure = Math.Clamp(world.ConstructionSites.Count(site => site.FactionId == faction.Id && component.Contains(site.SystemId, StringComparer.Ordinal)) * 0.22f, 0f, 1f),
|
||||
CorridorDependency = Math.Clamp(frontLineIds.Count * 0.18f, 0f, 1f),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BuildCorridors(world, state, nowUtc);
|
||||
foreach (var profile in state.Territory.StrategicProfiles)
|
||||
{
|
||||
profile.EconomicRegionId = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(profile.SystemId, StringComparer.Ordinal))?.Id;
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildCorridors(SimulationWorld world, GeopoliticalStateRuntime state, DateTimeOffset nowUtc)
|
||||
{
|
||||
foreach (var route in state.Routes)
|
||||
{
|
||||
var sourceRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.SourceSystemId, StringComparer.Ordinal));
|
||||
var destinationRegion = state.EconomyRegions.Regions.FirstOrDefault(region => region.SystemIds.Contains(route.DestinationSystemId, StringComparer.Ordinal));
|
||||
if (sourceRegion is null && destinationRegion is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var borderEdge = state.Territory.BorderEdges.FirstOrDefault(edge =>
|
||||
(edge.SourceSystemId == route.SourceSystemId && edge.DestinationSystemId == route.DestinationSystemId)
|
||||
|| (edge.SourceSystemId == route.DestinationSystemId && edge.DestinationSystemId == route.SourceSystemId));
|
||||
var risk = borderEdge?.TensionScore ?? 0f;
|
||||
var corridor = new LogisticsCorridorRuntime
|
||||
{
|
||||
Id = $"corridor-{route.Id}",
|
||||
FactionId = sourceRegion?.FactionId ?? destinationRegion?.FactionId,
|
||||
Kind = borderEdge?.IsContested == true ? "frontier-corridor" : "supply-corridor",
|
||||
Status = borderEdge?.IsContested == true ? "risky" : "active",
|
||||
RiskScore = Math.Clamp(risk + ((sourceRegion is not null && destinationRegion is not null && sourceRegion.Id != destinationRegion.Id) ? 0.15f : 0f), 0f, 1f),
|
||||
ThroughputScore = Math.Clamp(((sourceRegion?.StationIds.Count ?? 0) + (destinationRegion?.StationIds.Count ?? 0)) / 10f, 0f, 1f),
|
||||
AccessState = ResolveCorridorAccessState(world, borderEdge, sourceRegion, destinationRegion),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
corridor.SystemPathIds.Add(route.SourceSystemId);
|
||||
corridor.SystemPathIds.Add(route.DestinationSystemId);
|
||||
if (sourceRegion is not null)
|
||||
{
|
||||
corridor.RegionIds.Add(sourceRegion.Id);
|
||||
}
|
||||
if (destinationRegion is not null && !corridor.RegionIds.Contains(destinationRegion.Id, StringComparer.Ordinal))
|
||||
{
|
||||
corridor.RegionIds.Add(destinationRegion.Id);
|
||||
}
|
||||
if (borderEdge is not null)
|
||||
{
|
||||
corridor.BorderEdgeIds.Add(borderEdge.Id);
|
||||
}
|
||||
|
||||
state.EconomyRegions.Corridors.Add(corridor);
|
||||
if (sourceRegion is not null && !sourceRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
|
||||
{
|
||||
sourceRegion.CorridorIds.Add(corridor.Id);
|
||||
}
|
||||
if (destinationRegion is not null && !destinationRegion.CorridorIds.Contains(corridor.Id, StringComparer.Ordinal))
|
||||
{
|
||||
destinationRegion.CorridorIds.Add(corridor.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveCorridorAccessState(
|
||||
SimulationWorld world,
|
||||
BorderEdgeRuntime? borderEdge,
|
||||
EconomicRegionRuntime? sourceRegion,
|
||||
EconomicRegionRuntime? destinationRegion)
|
||||
{
|
||||
if (sourceRegion?.FactionId is null || destinationRegion?.FactionId is null)
|
||||
{
|
||||
return borderEdge?.IsContested == true ? "restricted" : "open";
|
||||
}
|
||||
|
||||
var relation = FindRelation(world, sourceRegion.FactionId, destinationRegion.FactionId);
|
||||
if (relation is null)
|
||||
{
|
||||
return "restricted";
|
||||
}
|
||||
|
||||
return relation.Posture switch
|
||||
{
|
||||
"war" => "denied",
|
||||
"hostile" => "restricted",
|
||||
_ => relation.TradeAccessPolicy,
|
||||
};
|
||||
}
|
||||
|
||||
private static FactionEconomySnapshot BuildRegionalEconomy(SimulationWorld world, string factionId, IReadOnlyCollection<string> systemIds)
|
||||
{
|
||||
var snapshot = new FactionEconomySnapshot();
|
||||
foreach (var station in world.Stations.Where(station => station.FactionId == factionId && systemIds.Contains(station.SystemId, StringComparer.Ordinal)))
|
||||
{
|
||||
foreach (var (itemId, amount) in station.Inventory)
|
||||
{
|
||||
snapshot.GetCommodity(itemId).OnHand += amount;
|
||||
}
|
||||
|
||||
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
|
||||
{
|
||||
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
|
||||
if (recipe is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
|
||||
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
|
||||
foreach (var input in recipe.Inputs)
|
||||
{
|
||||
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
|
||||
}
|
||||
foreach (var output in recipe.Outputs)
|
||||
{
|
||||
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var order in world.MarketOrders.Where(order => order.FactionId == factionId))
|
||||
{
|
||||
var relatedSystemId = world.Stations.FirstOrDefault(station => station.Id == order.StationId)?.SystemId
|
||||
?? world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId)?.SystemId;
|
||||
if (relatedSystemId is null || !systemIds.Contains(relatedSystemId, StringComparer.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var commodity = snapshot.GetCommodity(order.ItemId);
|
||||
if (order.Kind == MarketOrderKinds.Buy)
|
||||
{
|
||||
commodity.BuyBacklog += order.RemainingAmount;
|
||||
}
|
||||
else if (order.Kind == MarketOrderKinds.Sell)
|
||||
{
|
||||
commodity.SellBacklog += order.RemainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var site in world.ConstructionSites.Where(site => site.FactionId == factionId && systemIds.Contains(site.SystemId, StringComparer.Ordinal)))
|
||||
{
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var remaining = MathF.Max(0f, required.Value - (site.DeliveredItems.TryGetValue(required.Key, out var delivered) ? delivered : 0f));
|
||||
if (remaining > 0.01f)
|
||||
{
|
||||
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static List<List<string>> BuildConnectedComponents(IReadOnlyCollection<string> systems, IReadOnlyCollection<SystemRouteLinkRuntime> routes)
|
||||
{
|
||||
var remaining = systems.ToHashSet(StringComparer.Ordinal);
|
||||
var adjacency = routes
|
||||
.SelectMany(route => new[]
|
||||
{
|
||||
(route.SourceSystemId, route.DestinationSystemId),
|
||||
(route.DestinationSystemId, route.SourceSystemId),
|
||||
})
|
||||
.GroupBy(entry => entry.Item1, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Select(entry => entry.Item2).ToList(), StringComparer.Ordinal);
|
||||
var components = new List<List<string>>();
|
||||
|
||||
while (remaining.Count > 0)
|
||||
{
|
||||
var start = remaining.OrderBy(id => id, StringComparer.Ordinal).First();
|
||||
var frontier = new Queue<string>();
|
||||
frontier.Enqueue(start);
|
||||
remaining.Remove(start);
|
||||
var component = new List<string>();
|
||||
|
||||
while (frontier.Count > 0)
|
||||
{
|
||||
var current = frontier.Dequeue();
|
||||
component.Add(current);
|
||||
foreach (var neighbor in adjacency.GetValueOrDefault(current, []))
|
||||
{
|
||||
if (remaining.Remove(neighbor))
|
||||
{
|
||||
frontier.Enqueue(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
components.Add(component);
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private static string ResolveRegionKind(IReadOnlyCollection<StationRuntime> stations, FactionEconomySnapshot economy)
|
||||
{
|
||||
if (stations.Any(station => station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)))
|
||||
{
|
||||
return "shipbuilding-region";
|
||||
}
|
||||
|
||||
if (stations.Count(station => StationSimulationService.DetermineStationRole(station) == "refinery") >= 2)
|
||||
{
|
||||
return "industrial-core";
|
||||
}
|
||||
|
||||
if (economy.Commodities.Any(entry => entry.Value.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low))
|
||||
{
|
||||
return "frontier-sustainment";
|
||||
}
|
||||
|
||||
return stations.Count <= 2 ? "extraction-region" : "balanced-region";
|
||||
}
|
||||
|
||||
private static float EstimateSystemStrategicValue(SimulationWorld world, string systemId)
|
||||
{
|
||||
var stationValue = world.Stations.Count(station => station.SystemId == systemId) * 30f;
|
||||
var constructionValue = world.ConstructionSites.Count(site => site.SystemId == systemId && site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed) * 18f;
|
||||
var nodeValue = world.Nodes.Count(node => node.SystemId == systemId) * 8f;
|
||||
return stationValue + constructionValue + nodeValue;
|
||||
}
|
||||
|
||||
private static string BuildRelationId(string factionAId, string factionBId) =>
|
||||
BuildPairId("relation", factionAId, factionBId);
|
||||
|
||||
private static string BuildPairId(string prefix, string leftId, string rightId)
|
||||
{
|
||||
return string.Compare(leftId, rightId, StringComparison.Ordinal) <= 0
|
||||
? $"{prefix}-{leftId}-{rightId}"
|
||||
: $"{prefix}-{rightId}-{leftId}";
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,15 @@ global using SpaceGame.Api.Economy.Runtime;
|
||||
global using SpaceGame.Api.Factions.AI;
|
||||
global using SpaceGame.Api.Factions.Contracts;
|
||||
global using SpaceGame.Api.Factions.Runtime;
|
||||
global using SpaceGame.Api.Geopolitics.Contracts;
|
||||
global using SpaceGame.Api.Geopolitics.Runtime;
|
||||
global using SpaceGame.Api.Geopolitics.Simulation;
|
||||
global using SpaceGame.Api.Industry.Planning;
|
||||
global using SpaceGame.Api.Shared.AI;
|
||||
global using SpaceGame.Api.PlayerFaction.Contracts;
|
||||
global using SpaceGame.Api.PlayerFaction.Runtime;
|
||||
global using SpaceGame.Api.PlayerFaction.Simulation;
|
||||
global using SpaceGame.Api.Shared.Contracts;
|
||||
global using SpaceGame.Api.Shared.Runtime;
|
||||
global using SpaceGame.Api.Ships.AI;
|
||||
global using SpaceGame.Api.Ships.Contracts;
|
||||
global using SpaceGame.Api.Ships.Runtime;
|
||||
global using SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
@@ -219,6 +219,11 @@ internal static class FactionIndustryPlanner
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanEstablishExpansionSite(world, factionId, project))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var claimId = $"claim-{factionId}-{project.CelestialId}";
|
||||
if (world.Claims.All(candidate => candidate.Id != claimId))
|
||||
@@ -303,7 +308,8 @@ internal static class FactionIndustryPlanner
|
||||
.GroupBy(order => order.ItemId, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(order => order.RemainingAmount), StringComparer.Ordinal);
|
||||
|
||||
if (CommanderPlanningService.FactionCommanderHasIssuedTask(world, factionId, FactionIssuedTaskKind.ProduceShips, "military"))
|
||||
var threatAssessment = CommanderPlanningService.FindFactionThreatAssessment(world, factionId);
|
||||
if (threatAssessment is not null && threatAssessment.EnemyFactionCount > 0)
|
||||
{
|
||||
demandByItem["hullparts"] = demandByItem.GetValueOrDefault("hullparts") + 120f;
|
||||
demandByItem["claytronics"] = demandByItem.GetValueOrDefault("claytronics") + 90f;
|
||||
@@ -451,7 +457,8 @@ internal static class FactionIndustryPlanner
|
||||
.Where(celestial =>
|
||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& celestial.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed))
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
@@ -462,7 +469,8 @@ internal static class FactionIndustryPlanner
|
||||
.Where(celestial =>
|
||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& celestial.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed))
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||
.OrderByDescending(celestial => world.Stations.Count(station =>
|
||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)))
|
||||
@@ -482,7 +490,80 @@ internal static class FactionIndustryPlanner
|
||||
var factionPresence = world.Stations.Count(station =>
|
||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal));
|
||||
return resourceScore + (factionPresence * 5_000f);
|
||||
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, celestial.SystemId);
|
||||
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, celestial.SystemId);
|
||||
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == celestial.SystemId);
|
||||
var pressure = world.Geopolitics?.Territory.Pressures
|
||||
.Where(entry => entry.SystemId == celestial.SystemId && entry.FactionId == factionId)
|
||||
.OrderByDescending(entry => entry.HostileInfluence)
|
||||
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
var controlBias = string.Equals(controlState?.ControllerFactionId, factionId, StringComparison.Ordinal)
|
||||
? 12_000f
|
||||
: string.Equals(controlState?.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal)
|
||||
? 4_000f
|
||||
: 0f;
|
||||
var regionBias = region is null
|
||||
? 0f
|
||||
: region.Kind switch
|
||||
{
|
||||
"core-industry" => 4_500f,
|
||||
"shipbuilding" => 3_250f,
|
||||
"trade-hub" => 2_250f,
|
||||
"corridor" => 1_500f,
|
||||
_ => 1_000f,
|
||||
};
|
||||
var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f)
|
||||
+ ((strategicProfile?.TerritorialPressure ?? 0f) * 9f)
|
||||
+ ((world.Geopolitics is null ? 0f : GeopoliticalSimulationService.GetSystemRouteRisk(world, celestial.SystemId, factionId)) * 250f);
|
||||
return resourceScore
|
||||
+ (factionPresence * 5_000f)
|
||||
+ controlBias
|
||||
+ regionBias
|
||||
- securityPenalty;
|
||||
}
|
||||
|
||||
private static bool IsExpansionSystemEligible(SimulationWorld world, string factionId, string systemId)
|
||||
{
|
||||
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId);
|
||||
if (controlState is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var authorityFactionId = controlState.ControllerFactionId ?? controlState.PrimaryClaimantFactionId;
|
||||
if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId);
|
||||
}
|
||||
|
||||
private static bool CanEstablishExpansionSite(SimulationWorld world, string factionId, IndustryExpansionProject project)
|
||||
{
|
||||
if (!IsExpansionSystemEligible(world, factionId, project.SystemId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, project.SystemId);
|
||||
if (controlState?.IsContested == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pressure = world.Geopolitics?.Territory.Pressures
|
||||
.Where(entry => entry.SystemId == project.SystemId && entry.FactionId == factionId)
|
||||
.OrderByDescending(entry => entry.CorridorRisk + entry.HostileInfluence)
|
||||
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
return pressure is null || (pressure.CorridorRisk < 0.8f && pressure.HostileInfluence < 1.2f);
|
||||
}
|
||||
|
||||
private static StationRuntime? SelectSupportStation(SimulationWorld world, string factionId, string moduleId, string targetSystemId)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class CreatePlayerOrganizationHandler(WorldService worldService) : Endpoint<PlayerOrganizationCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/organizations");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = worldService.CreatePlayerOrganization(request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
AddError(ex.Message);
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class DeletePlayerDirectiveRequest
|
||||
{
|
||||
public string DirectiveId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : Endpoint<DeletePlayerDirectiveRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/player-faction/directives/{directiveId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = worldService.DeletePlayerDirective(request.DirectiveId);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class DeletePlayerOrganizationRequest
|
||||
{
|
||||
public string OrganizationId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : Endpoint<DeletePlayerOrganizationRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/player-faction/organizations/{organizationId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = worldService.DeletePlayerOrganization(request.OrganizationId);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
AddError(ex.Message);
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs
Normal file
24
apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class GetPlayerFactionHandler(WorldService worldService) : EndpointWithoutRequest<PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/player-faction");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = worldService.GetPlayerFaction();
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService worldService) : Endpoint<PlayerOrganizationMembershipCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/player-faction/organizations/{organizationId}/membership");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var organizationId = Route<string>("organizationId");
|
||||
if (string.IsNullOrWhiteSpace(organizationId))
|
||||
{
|
||||
AddError("organizationId route parameter is required.");
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = worldService.UpdatePlayerOrganizationMembership(organizationId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
AddError(ex.Message);
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService) : Endpoint<PlayerStrategicIntentCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/player-faction/strategic-intent");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = worldService.UpdatePlayerStrategicIntent(request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : Endpoint<PlayerAssetAssignmentCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/player-faction/assets/{assetId}/assignment");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var assetId = Route<string>("assetId");
|
||||
if (string.IsNullOrWhiteSpace(assetId))
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = worldService.UpsertPlayerAssignment(assetId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldService) : Endpoint<PlayerAutomationPolicyCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/automation-policies");
|
||||
Put("/api/player-faction/automation-policies/{automationPolicyId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var automationPolicyId = Route<string?>("automationPolicyId");
|
||||
var snapshot = worldService.UpsertPlayerAutomationPolicy(string.IsNullOrWhiteSpace(automationPolicyId) ? null : automationPolicyId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : Endpoint<PlayerDirectiveCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/directives");
|
||||
Put("/api/player-faction/directives/{directiveId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var directiveId = Route<string?>("directiveId");
|
||||
var snapshot = worldService.UpsertPlayerDirective(string.IsNullOrWhiteSpace(directiveId) ? null : directiveId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
26
apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs
Normal file
26
apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpoint<PlayerPolicyCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/policies");
|
||||
Put("/api/player-faction/policies/{policyId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var policyId = Route<string?>("policyId");
|
||||
var snapshot = worldService.UpsertPlayerPolicy(string.IsNullOrWhiteSpace(policyId) ? null : policyId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerProductionProgramHandler(WorldService worldService) : Endpoint<PlayerProductionProgramCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/production-programs");
|
||||
Put("/api/player-faction/production-programs/{productionProgramId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var productionProgramId = Route<string?>("productionProgramId");
|
||||
var snapshot = worldService.UpsertPlayerProductionProgram(string.IsNullOrWhiteSpace(productionProgramId) ? null : productionProgramId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.PlayerFaction.Api;
|
||||
|
||||
public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldService) : Endpoint<PlayerReinforcementPolicyCommandRequest, PlayerFactionSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/player-faction/reinforcement-policies");
|
||||
Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var reinforcementPolicyId = Route<string?>("reinforcementPolicyId");
|
||||
var snapshot = worldService.UpsertPlayerReinforcementPolicy(string.IsNullOrWhiteSpace(reinforcementPolicyId) ? null : reinforcementPolicyId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
271
apps/backend/PlayerFaction/Contracts/PlayerFaction.cs
Normal file
271
apps/backend/PlayerFaction/Contracts/PlayerFaction.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
namespace SpaceGame.Api.PlayerFaction.Contracts;
|
||||
|
||||
public sealed record PlayerAssetRegistrySnapshot(
|
||||
IReadOnlyList<string> ShipIds,
|
||||
IReadOnlyList<string> StationIds,
|
||||
IReadOnlyList<string> CommanderIds,
|
||||
IReadOnlyList<string> ClaimIds,
|
||||
IReadOnlyList<string> ConstructionSiteIds,
|
||||
IReadOnlyList<string> PolicySetIds,
|
||||
IReadOnlyList<string> MarketOrderIds,
|
||||
IReadOnlyList<string> FleetIds,
|
||||
IReadOnlyList<string> TaskForceIds,
|
||||
IReadOnlyList<string> StationGroupIds,
|
||||
IReadOnlyList<string> EconomicRegionIds,
|
||||
IReadOnlyList<string> FrontIds,
|
||||
IReadOnlyList<string> ReserveIds);
|
||||
|
||||
public sealed record PlayerStrategicIntentSnapshot(
|
||||
string StrategicPosture,
|
||||
string EconomicPosture,
|
||||
string MilitaryPosture,
|
||||
string LogisticsPosture,
|
||||
float DesiredReserveRatio,
|
||||
bool AllowDelegatedCombatAutomation,
|
||||
bool AllowDelegatedEconomicAutomation,
|
||||
string? Notes);
|
||||
|
||||
public sealed record PlayerFleetSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Role,
|
||||
string? CommanderId,
|
||||
string? FrontId,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string? ReinforcementPolicyId,
|
||||
IReadOnlyList<string> AssetIds,
|
||||
IReadOnlyList<string> TaskForceIds,
|
||||
IReadOnlyList<string> DirectiveIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerTaskForceSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Role,
|
||||
string? FleetId,
|
||||
string? CommanderId,
|
||||
string? FrontId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
IReadOnlyList<string> AssetIds,
|
||||
IReadOnlyList<string> DirectiveIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerStationGroupSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Role,
|
||||
string? EconomicRegionId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
IReadOnlyList<string> StationIds,
|
||||
IReadOnlyList<string> DirectiveIds,
|
||||
IReadOnlyList<string> FocusItemIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerEconomicRegionSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Role,
|
||||
string? SharedEconomicRegionId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
IReadOnlyList<string> SystemIds,
|
||||
IReadOnlyList<string> StationGroupIds,
|
||||
IReadOnlyList<string> DirectiveIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerFrontSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
float Priority,
|
||||
string Posture,
|
||||
string? SharedFrontLineId,
|
||||
string? TargetFactionId,
|
||||
IReadOnlyList<string> SystemIds,
|
||||
IReadOnlyList<string> FleetIds,
|
||||
IReadOnlyList<string> ReserveIds,
|
||||
IReadOnlyList<string> DirectiveIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerReserveGroupSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string ReserveKind,
|
||||
string? HomeSystemId,
|
||||
string? PolicyId,
|
||||
IReadOnlyList<string> AssetIds,
|
||||
IReadOnlyList<string> FrontIds,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerFactionPolicySnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
string? PolicySetId,
|
||||
bool AllowDelegatedCombat,
|
||||
bool AllowDelegatedTrade,
|
||||
float ReserveCreditsRatio,
|
||||
float ReserveMilitaryRatio,
|
||||
string TradeAccessPolicy,
|
||||
string DockingAccessPolicy,
|
||||
string ConstructionAccessPolicy,
|
||||
string OperationalRangePolicy,
|
||||
string CombatEngagementPolicy,
|
||||
bool AvoidHostileSystems,
|
||||
float FleeHullRatio,
|
||||
IReadOnlyList<string> BlacklistedSystemIds,
|
||||
string? Notes,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerAutomationPolicySnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
bool Enabled,
|
||||
string BehaviorKind,
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
int MaxSystemRange,
|
||||
bool KnownStationsOnly,
|
||||
float Radius,
|
||||
float WaitSeconds,
|
||||
string? PreferredItemId,
|
||||
string? Notes,
|
||||
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerReinforcementPolicySnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
string ShipKind,
|
||||
int DesiredAssetCount,
|
||||
int MinimumReserveCount,
|
||||
bool AutoTransferReserves,
|
||||
bool AutoQueueProduction,
|
||||
string? SourceReserveId,
|
||||
string? TargetFrontId,
|
||||
string? Notes,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerProductionProgramSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Kind,
|
||||
string? TargetShipKind,
|
||||
string? TargetModuleId,
|
||||
string? TargetItemId,
|
||||
int TargetCount,
|
||||
int CurrentCount,
|
||||
string? StationGroupId,
|
||||
string? ReinforcementPolicyId,
|
||||
string? Notes,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerDirectiveSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string Status,
|
||||
string Kind,
|
||||
string ScopeKind,
|
||||
string ScopeId,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string BehaviorKind,
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
int Priority,
|
||||
float Radius,
|
||||
float WaitSeconds,
|
||||
int MaxSystemRange,
|
||||
bool KnownStationsOnly,
|
||||
IReadOnlyList<Vector3Dto> PatrolPoints,
|
||||
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string? Notes,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerAssignmentSnapshot(
|
||||
string Id,
|
||||
string AssetKind,
|
||||
string AssetId,
|
||||
string? FleetId,
|
||||
string? TaskForceId,
|
||||
string? StationGroupId,
|
||||
string? EconomicRegionId,
|
||||
string? FrontId,
|
||||
string? ReserveId,
|
||||
string? DirectiveId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string Role,
|
||||
string Status,
|
||||
DateTimeOffset UpdatedAtUtc);
|
||||
|
||||
public sealed record PlayerDecisionLogEntrySnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Summary,
|
||||
string? RelatedEntityKind,
|
||||
string? RelatedEntityId,
|
||||
DateTimeOffset OccurredAtUtc);
|
||||
|
||||
public sealed record PlayerAlertSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Severity,
|
||||
string Summary,
|
||||
string? AssetKind,
|
||||
string? AssetId,
|
||||
string? RelatedDirectiveId,
|
||||
string Status,
|
||||
DateTimeOffset CreatedAtUtc);
|
||||
|
||||
public sealed record PlayerFactionSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
string SovereignFactionId,
|
||||
string Status,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
PlayerAssetRegistrySnapshot AssetRegistry,
|
||||
PlayerStrategicIntentSnapshot StrategicIntent,
|
||||
IReadOnlyList<PlayerFleetSnapshot> Fleets,
|
||||
IReadOnlyList<PlayerTaskForceSnapshot> TaskForces,
|
||||
IReadOnlyList<PlayerStationGroupSnapshot> StationGroups,
|
||||
IReadOnlyList<PlayerEconomicRegionSnapshot> EconomicRegions,
|
||||
IReadOnlyList<PlayerFrontSnapshot> Fronts,
|
||||
IReadOnlyList<PlayerReserveGroupSnapshot> Reserves,
|
||||
IReadOnlyList<PlayerFactionPolicySnapshot> Policies,
|
||||
IReadOnlyList<PlayerAutomationPolicySnapshot> AutomationPolicies,
|
||||
IReadOnlyList<PlayerReinforcementPolicySnapshot> ReinforcementPolicies,
|
||||
IReadOnlyList<PlayerProductionProgramSnapshot> ProductionPrograms,
|
||||
IReadOnlyList<PlayerDirectiveSnapshot> Directives,
|
||||
IReadOnlyList<PlayerAssignmentSnapshot> Assignments,
|
||||
IReadOnlyList<PlayerDecisionLogEntrySnapshot> DecisionLog,
|
||||
IReadOnlyList<PlayerAlertSnapshot> Alerts);
|
||||
140
apps/backend/PlayerFaction/Contracts/PlayerFactionCommands.cs
Normal file
140
apps/backend/PlayerFaction/Contracts/PlayerFactionCommands.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
namespace SpaceGame.Api.PlayerFaction.Contracts;
|
||||
|
||||
public sealed record PlayerOrganizationCommandRequest(
|
||||
string Kind,
|
||||
string Label,
|
||||
string? ParentOrganizationId,
|
||||
string? FrontId,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string? ReinforcementPolicyId,
|
||||
string? TargetFactionId,
|
||||
float? Priority,
|
||||
string? Role,
|
||||
string? ReserveKind,
|
||||
IReadOnlyList<string>? SystemIds,
|
||||
IReadOnlyList<string>? FocusItemIds,
|
||||
string? Notes);
|
||||
|
||||
public sealed record PlayerOrganizationMembershipCommandRequest(
|
||||
IReadOnlyList<string>? AssetIds,
|
||||
IReadOnlyList<string>? ChildOrganizationIds,
|
||||
IReadOnlyList<string>? SystemIds,
|
||||
IReadOnlyList<string>? FrontIds,
|
||||
bool Replace = false);
|
||||
|
||||
public sealed record PlayerDirectiveCommandRequest(
|
||||
string Label,
|
||||
string Kind,
|
||||
string ScopeKind,
|
||||
string ScopeId,
|
||||
string BehaviorKind,
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
int Priority,
|
||||
float? Radius,
|
||||
float? WaitSeconds,
|
||||
int? MaxSystemRange,
|
||||
bool? KnownStationsOnly,
|
||||
IReadOnlyList<Vector3Dto>? PatrolPoints,
|
||||
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string? Notes);
|
||||
|
||||
public sealed record PlayerPolicyCommandRequest(
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
string? PolicySetId,
|
||||
bool AllowDelegatedCombat,
|
||||
bool AllowDelegatedTrade,
|
||||
float ReserveCreditsRatio,
|
||||
float ReserveMilitaryRatio,
|
||||
string? Notes,
|
||||
string? TradeAccessPolicy,
|
||||
string? DockingAccessPolicy,
|
||||
string? ConstructionAccessPolicy,
|
||||
string? OperationalRangePolicy,
|
||||
string? CombatEngagementPolicy,
|
||||
bool? AvoidHostileSystems,
|
||||
float? FleeHullRatio,
|
||||
IReadOnlyList<string>? BlacklistedSystemIds);
|
||||
|
||||
public sealed record PlayerAutomationPolicyCommandRequest(
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
bool Enabled,
|
||||
string BehaviorKind,
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
int MaxSystemRange,
|
||||
bool KnownStationsOnly,
|
||||
float Radius,
|
||||
float WaitSeconds,
|
||||
string? PreferredItemId,
|
||||
string? Notes,
|
||||
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders);
|
||||
|
||||
public sealed record PlayerReinforcementPolicyCommandRequest(
|
||||
string Label,
|
||||
string ScopeKind,
|
||||
string? ScopeId,
|
||||
string ShipKind,
|
||||
int DesiredAssetCount,
|
||||
int MinimumReserveCount,
|
||||
bool AutoTransferReserves,
|
||||
bool AutoQueueProduction,
|
||||
string? SourceReserveId,
|
||||
string? TargetFrontId,
|
||||
string? Notes);
|
||||
|
||||
public sealed record PlayerProductionProgramCommandRequest(
|
||||
string Label,
|
||||
string Kind,
|
||||
string? TargetShipKind,
|
||||
string? TargetModuleId,
|
||||
string? TargetItemId,
|
||||
int TargetCount,
|
||||
string? StationGroupId,
|
||||
string? ReinforcementPolicyId,
|
||||
string? Notes);
|
||||
|
||||
public sealed record PlayerAssetAssignmentCommandRequest(
|
||||
string AssetKind,
|
||||
string AssetId,
|
||||
string? FleetId,
|
||||
string? TaskForceId,
|
||||
string? StationGroupId,
|
||||
string? EconomicRegionId,
|
||||
string? FrontId,
|
||||
string? ReserveId,
|
||||
string? DirectiveId,
|
||||
string? PolicyId,
|
||||
string? AutomationPolicyId,
|
||||
string Role,
|
||||
bool ClearConflicts = true);
|
||||
|
||||
public sealed record PlayerStrategicIntentCommandRequest(
|
||||
string StrategicPosture,
|
||||
string EconomicPosture,
|
||||
string MilitaryPosture,
|
||||
string LogisticsPosture,
|
||||
float DesiredReserveRatio,
|
||||
bool AllowDelegatedCombatAutomation,
|
||||
bool AllowDelegatedEconomicAutomation,
|
||||
string? Notes);
|
||||
306
apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs
Normal file
306
apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
namespace SpaceGame.Api.PlayerFaction.Runtime;
|
||||
|
||||
public sealed class PlayerFactionRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public required string SovereignFactionId { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new();
|
||||
public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new();
|
||||
public List<PlayerFleetRuntime> Fleets { get; } = [];
|
||||
public List<PlayerTaskForceRuntime> TaskForces { get; } = [];
|
||||
public List<PlayerStationGroupRuntime> StationGroups { get; } = [];
|
||||
public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = [];
|
||||
public List<PlayerFrontRuntime> Fronts { get; } = [];
|
||||
public List<PlayerReserveGroupRuntime> Reserves { get; } = [];
|
||||
public List<PlayerFactionPolicyRuntime> Policies { get; } = [];
|
||||
public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = [];
|
||||
public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = [];
|
||||
public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = [];
|
||||
public List<PlayerDirectiveRuntime> Directives { get; } = [];
|
||||
public List<PlayerAssignmentRuntime> Assignments { get; } = [];
|
||||
public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = [];
|
||||
public List<PlayerAlertRuntime> Alerts { get; } = [];
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class PlayerAssetRegistryRuntime
|
||||
{
|
||||
public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class PlayerStrategicIntentRuntime
|
||||
{
|
||||
public string StrategicPosture { get; set; } = "balanced";
|
||||
public string EconomicPosture { get; set; } = "delegated";
|
||||
public string MilitaryPosture { get; set; } = "layered-defense";
|
||||
public string LogisticsPosture { get; set; } = "stable";
|
||||
public float DesiredReserveRatio { get; set; } = 0.2f;
|
||||
public bool AllowDelegatedCombatAutomation { get; set; } = true;
|
||||
public bool AllowDelegatedEconomicAutomation { get; set; } = true;
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PlayerFleetRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Role { get; set; } = "general-purpose";
|
||||
public string? CommanderId { get; set; }
|
||||
public string? FrontId { get; set; }
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? HomeStationId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public string? ReinforcementPolicyId { get; set; }
|
||||
public List<string> AssetIds { get; } = [];
|
||||
public List<string> TaskForceIds { get; } = [];
|
||||
public List<string> DirectiveIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerTaskForceRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Role { get; set; } = "task-force";
|
||||
public string? FleetId { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? FrontId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public List<string> AssetIds { get; } = [];
|
||||
public List<string> DirectiveIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerStationGroupRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Role { get; set; } = "industrial-group";
|
||||
public string? EconomicRegionId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public List<string> StationIds { get; } = [];
|
||||
public List<string> DirectiveIds { get; } = [];
|
||||
public List<string> FocusItemIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerEconomicRegionRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Role { get; set; } = "balanced-region";
|
||||
public string? SharedEconomicRegionId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public List<string> SystemIds { get; } = [];
|
||||
public List<string> StationGroupIds { get; } = [];
|
||||
public List<string> DirectiveIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerFrontRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public float Priority { get; set; } = 50f;
|
||||
public string Posture { get; set; } = "hold";
|
||||
public string? SharedFrontLineId { get; set; }
|
||||
public string? TargetFactionId { get; set; }
|
||||
public List<string> SystemIds { get; } = [];
|
||||
public List<string> FleetIds { get; } = [];
|
||||
public List<string> ReserveIds { get; } = [];
|
||||
public List<string> DirectiveIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerReserveGroupRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "ready";
|
||||
public string ReserveKind { get; set; } = "military";
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public List<string> AssetIds { get; } = [];
|
||||
public List<string> FrontIds { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerFactionPolicyRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string ScopeKind { get; set; } = "player-faction";
|
||||
public string? ScopeId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public bool AllowDelegatedCombat { get; set; } = true;
|
||||
public bool AllowDelegatedTrade { get; set; } = true;
|
||||
public float ReserveCreditsRatio { get; set; } = 0.2f;
|
||||
public float ReserveMilitaryRatio { get; set; } = 0.2f;
|
||||
public string TradeAccessPolicy { get; set; } = "owner-and-allies";
|
||||
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
|
||||
public string ConstructionAccessPolicy { get; set; } = "owner-only";
|
||||
public string OperationalRangePolicy { get; set; } = "unrestricted";
|
||||
public string CombatEngagementPolicy { get; set; } = "defensive";
|
||||
public bool AvoidHostileSystems { get; set; } = true;
|
||||
public float FleeHullRatio { get; set; } = 0.35f;
|
||||
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerAutomationPolicyRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string ScopeKind { get; set; } = "player-faction";
|
||||
public string? ScopeId { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string BehaviorKind { get; set; } = "idle";
|
||||
public bool UseOrders { get; set; }
|
||||
public string? StagingOrderKind { get; set; }
|
||||
public int MaxSystemRange { get; set; }
|
||||
public bool KnownStationsOnly { get; set; }
|
||||
public float Radius { get; set; } = 24f;
|
||||
public float WaitSeconds { get; set; } = 3f;
|
||||
public string? PreferredItemId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerReinforcementPolicyRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string ScopeKind { get; set; } = "player-faction";
|
||||
public string? ScopeId { get; set; }
|
||||
public string ShipKind { get; set; } = "military";
|
||||
public int DesiredAssetCount { get; set; }
|
||||
public int MinimumReserveCount { get; set; }
|
||||
public bool AutoTransferReserves { get; set; } = true;
|
||||
public bool AutoQueueProduction { get; set; } = true;
|
||||
public string? SourceReserveId { get; set; }
|
||||
public string? TargetFrontId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerProductionProgramRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Kind { get; set; } = "ship-production";
|
||||
public string? TargetShipKind { get; set; }
|
||||
public string? TargetModuleId { get; set; }
|
||||
public string? TargetItemId { get; set; }
|
||||
public int TargetCount { get; set; }
|
||||
public int CurrentCount { get; set; }
|
||||
public string? StationGroupId { get; set; }
|
||||
public string? ReinforcementPolicyId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerDirectiveRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Label { get; set; }
|
||||
public string Status { get; set; } = "active";
|
||||
public string Kind { get; set; } = "hold";
|
||||
public string ScopeKind { get; set; } = "asset";
|
||||
public string ScopeId { get; set; } = string.Empty;
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? HomeStationId { get; set; }
|
||||
public string? SourceStationId { get; set; }
|
||||
public string? DestinationStationId { get; set; }
|
||||
public string BehaviorKind { get; set; } = "idle";
|
||||
public bool UseOrders { get; set; }
|
||||
public string? StagingOrderKind { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? PreferredNodeId { get; set; }
|
||||
public string? PreferredConstructionSiteId { get; set; }
|
||||
public string? PreferredModuleId { get; set; }
|
||||
public int Priority { get; set; } = 50;
|
||||
public float Radius { get; set; } = 24f;
|
||||
public float WaitSeconds { get; set; } = 3f;
|
||||
public int MaxSystemRange { get; set; }
|
||||
public bool KnownStationsOnly { get; set; }
|
||||
public List<Vector3> PatrolPoints { get; } = [];
|
||||
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerAssignmentRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string AssetKind { get; set; }
|
||||
public required string AssetId { get; set; }
|
||||
public string? FleetId { get; set; }
|
||||
public string? TaskForceId { get; set; }
|
||||
public string? StationGroupId { get; set; }
|
||||
public string? EconomicRegionId { get; set; }
|
||||
public string? FrontId { get; set; }
|
||||
public string? ReserveId { get; set; }
|
||||
public string? DirectiveId { get; set; }
|
||||
public string? PolicyId { get; set; }
|
||||
public string? AutomationPolicyId { get; set; }
|
||||
public string Role { get; set; } = "line";
|
||||
public string Status { get; set; } = "active";
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerDecisionLogEntryRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public string? RelatedEntityKind { get; set; }
|
||||
public string? RelatedEntityId { get; set; }
|
||||
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public sealed class PlayerAlertRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; set; }
|
||||
public required string Severity { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public string? AssetKind { get; set; }
|
||||
public string? AssetId { get; set; }
|
||||
public string? RelatedDirectiveId { get; set; }
|
||||
public string Status { get; set; } = "open";
|
||||
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
2441
apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs
Normal file
2441
apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
using FastEndpoints;
|
||||
using FastEndpoints.Swagger;
|
||||
using SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.WebHost.UseUrls("http://127.0.0.1:5079");
|
||||
builder.Services.AddCors((options) =>
|
||||
{
|
||||
options.AddDefaultPolicy((policy) =>
|
||||
@@ -17,6 +17,7 @@ builder.Services.AddCors((options) =>
|
||||
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
|
||||
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
||||
builder.Services.AddFastEndpoints();
|
||||
builder.Services.SwaggerDocument();
|
||||
builder.Services.AddSingleton<WorldService>();
|
||||
builder.Services.AddSingleton<TelemetryService>();
|
||||
builder.Services.AddHostedService<SimulationHostedService>();
|
||||
@@ -25,5 +26,6 @@ var app = builder.Build();
|
||||
|
||||
app.UseCors();
|
||||
app.UseFastEndpoints();
|
||||
app.UseSwaggerGen();
|
||||
|
||||
app.Run();
|
||||
|
||||
@@ -5,16 +5,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:0",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:0;http://localhost:0",
|
||||
"applicationUrl": "http://0.0.0.0:5079",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
namespace SpaceGame.Api.Shared.AI;
|
||||
|
||||
public abstract class GoapAction<TState>
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract float Cost { get; }
|
||||
public abstract bool CheckPreconditions(TState state);
|
||||
public abstract TState ApplyEffects(TState state);
|
||||
public abstract void Execute(SimulationEngine engine, SimulationWorld world, CommanderRuntime commander);
|
||||
}
|
||||
|
||||
public abstract class GoapGoal<TState>
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public abstract bool IsSatisfied(TState state);
|
||||
public abstract float ComputePriority(TState state, SimulationWorld world, CommanderRuntime commander);
|
||||
}
|
||||
|
||||
public sealed class GoapPlan<TState>
|
||||
{
|
||||
public static readonly GoapPlan<TState> Empty = new() { Actions = [], TotalCost = 0f };
|
||||
|
||||
public required IReadOnlyList<GoapAction<TState>> Actions { get; init; }
|
||||
public required float TotalCost { get; init; }
|
||||
public int CurrentStep { get; set; }
|
||||
|
||||
public GoapAction<TState>? CurrentAction => CurrentStep < Actions.Count ? Actions[CurrentStep] : null;
|
||||
public bool IsComplete => CurrentStep >= Actions.Count;
|
||||
public void Advance() => CurrentStep++;
|
||||
}
|
||||
|
||||
public sealed class GoapPlanner<TState>
|
||||
{
|
||||
private readonly Func<TState, TState> cloneState;
|
||||
|
||||
public GoapPlanner(Func<TState, TState> cloneState)
|
||||
{
|
||||
this.cloneState = cloneState;
|
||||
}
|
||||
|
||||
public GoapPlan<TState>? Plan(
|
||||
TState initialState,
|
||||
GoapGoal<TState> goal,
|
||||
IReadOnlyList<GoapAction<TState>> availableActions)
|
||||
{
|
||||
if (goal.IsSatisfied(initialState))
|
||||
{
|
||||
return GoapPlan<TState>.Empty;
|
||||
}
|
||||
|
||||
var openSet = new PriorityQueue<PlanNode, float>();
|
||||
openSet.Enqueue(new PlanNode(cloneState(initialState), [], 0f), 0f);
|
||||
|
||||
const int MaxIterations = 256;
|
||||
var iterations = 0;
|
||||
|
||||
while (openSet.Count > 0 && iterations++ < MaxIterations)
|
||||
{
|
||||
var current = openSet.Dequeue();
|
||||
|
||||
if (goal.IsSatisfied(current.State))
|
||||
{
|
||||
return new GoapPlan<TState>
|
||||
{
|
||||
Actions = current.Actions,
|
||||
TotalCost = current.Cost,
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var action in availableActions)
|
||||
{
|
||||
if (!action.CheckPreconditions(current.State))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var newState = action.ApplyEffects(cloneState(current.State));
|
||||
var newCost = current.Cost + action.Cost;
|
||||
var newActions = new List<GoapAction<TState>>(current.Actions) { action };
|
||||
openSet.Enqueue(new PlanNode(newState, newActions, newCost), newCost);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed record PlanNode(
|
||||
TState State,
|
||||
IReadOnlyList<GoapAction<TState>> Actions,
|
||||
float Cost);
|
||||
}
|
||||
@@ -12,14 +12,47 @@ public enum WorkStatus
|
||||
{
|
||||
Pending,
|
||||
Active,
|
||||
Blocked,
|
||||
Completed,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Queued,
|
||||
Accepted,
|
||||
Active,
|
||||
Completed,
|
||||
Cancelled,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
public enum AiPlanStatus
|
||||
{
|
||||
Planned,
|
||||
Running,
|
||||
Blocked,
|
||||
Completed,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
public enum AiPlanStepStatus
|
||||
{
|
||||
Planned,
|
||||
Running,
|
||||
Blocked,
|
||||
Completed,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
public enum AiPlanSourceKind
|
||||
{
|
||||
Rule,
|
||||
Order,
|
||||
DefaultBehavior,
|
||||
}
|
||||
|
||||
public enum ShipState
|
||||
@@ -49,22 +82,8 @@ public enum ShipState
|
||||
Blocked,
|
||||
Undocking,
|
||||
EngagingTarget,
|
||||
}
|
||||
|
||||
public enum ControllerTaskKind
|
||||
{
|
||||
Idle,
|
||||
Travel,
|
||||
Extract,
|
||||
Dock,
|
||||
Load,
|
||||
Unload,
|
||||
DeliverConstruction,
|
||||
BuildConstructionSite,
|
||||
AttackTarget,
|
||||
|
||||
ConstructModule,
|
||||
Undock,
|
||||
HoldingPosition,
|
||||
Fleeing,
|
||||
}
|
||||
|
||||
public static class SpaceLayerKinds
|
||||
@@ -95,37 +114,39 @@ public static class CommanderKind
|
||||
|
||||
public static class ShipTaskKinds
|
||||
{
|
||||
public const string Idle = "idle";
|
||||
public const string LocalMove = "local-move";
|
||||
public const string WarpToNode = "warp-to-node";
|
||||
public const string UseStargate = "use-stargate";
|
||||
public const string UseFtl = "use-ftl";
|
||||
public const string HoldPosition = "hold-position";
|
||||
public const string Travel = "travel";
|
||||
public const string FollowTarget = "follow-target";
|
||||
public const string MineNode = "mine-node";
|
||||
public const string Dock = "dock";
|
||||
public const string Undock = "undock";
|
||||
public const string LoadCargo = "load-cargo";
|
||||
public const string UnloadCargo = "unload-cargo";
|
||||
|
||||
public const string MineNode = "mine-node";
|
||||
public const string HarvestGas = "harvest-gas";
|
||||
public const string DeliverToStation = "deliver-to-station";
|
||||
public const string ClaimLagrangePoint = "claim-lagrange-point";
|
||||
public const string TransferCargoToShip = "transfer-cargo-to-ship";
|
||||
public const string SalvageWreck = "salvage-wreck";
|
||||
public const string DeliverConstruction = "deliver-construction";
|
||||
public const string ConstructModule = "construct-module";
|
||||
public const string BuildConstructionSite = "build-construction-site";
|
||||
public const string EscortTarget = "escort-target";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string DefendCelestial = "defend-celestial";
|
||||
public const string Retreat = "retreat";
|
||||
public const string HoldPosition = "hold-position";
|
||||
public const string Flee = "flee";
|
||||
public const string Wait = "wait";
|
||||
}
|
||||
|
||||
public static class ShipOrderKinds
|
||||
{
|
||||
public const string DirectMove = "direct-move";
|
||||
public const string TravelToNode = "travel-to-node";
|
||||
public const string Move = "move";
|
||||
public const string DockAtStation = "dock-at-station";
|
||||
public const string DeliverCargo = "deliver-cargo";
|
||||
public const string DockAndWait = "dock-and-wait";
|
||||
public const string FlyAndWait = "fly-and-wait";
|
||||
public const string FlyToObject = "fly-to-object";
|
||||
public const string FollowShip = "follow-ship";
|
||||
public const string TradeRoute = "trade-route";
|
||||
public const string MineAndDeliver = "mine-and-deliver";
|
||||
public const string BuildAtSite = "build-at-site";
|
||||
public const string AttackTarget = "attack-target";
|
||||
public const string HoldPosition = "hold-position";
|
||||
public const string RepeatOrders = "repeat-orders";
|
||||
public const string Flee = "flee";
|
||||
}
|
||||
|
||||
public static class ClaimStateKinds
|
||||
@@ -174,18 +195,54 @@ public static class SimulationEnumMappings
|
||||
{
|
||||
WorkStatus.Pending => "pending",
|
||||
WorkStatus.Active => "active",
|
||||
WorkStatus.Blocked => "blocked",
|
||||
WorkStatus.Completed => "completed",
|
||||
WorkStatus.Failed => "failed",
|
||||
WorkStatus.Interrupted => "interrupted",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this OrderStatus status) => status switch
|
||||
{
|
||||
OrderStatus.Queued => "queued",
|
||||
OrderStatus.Accepted => "accepted",
|
||||
OrderStatus.Active => "active",
|
||||
OrderStatus.Completed => "completed",
|
||||
OrderStatus.Cancelled => "cancelled",
|
||||
OrderStatus.Failed => "failed",
|
||||
OrderStatus.Interrupted => "interrupted",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this AiPlanStatus status) => status switch
|
||||
{
|
||||
AiPlanStatus.Planned => "planned",
|
||||
AiPlanStatus.Running => "running",
|
||||
AiPlanStatus.Blocked => "blocked",
|
||||
AiPlanStatus.Completed => "completed",
|
||||
AiPlanStatus.Failed => "failed",
|
||||
AiPlanStatus.Interrupted => "interrupted",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this AiPlanStepStatus status) => status switch
|
||||
{
|
||||
AiPlanStepStatus.Planned => "planned",
|
||||
AiPlanStepStatus.Running => "running",
|
||||
AiPlanStepStatus.Blocked => "blocked",
|
||||
AiPlanStepStatus.Completed => "completed",
|
||||
AiPlanStepStatus.Failed => "failed",
|
||||
AiPlanStepStatus.Interrupted => "interrupted",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this AiPlanSourceKind kind) => kind switch
|
||||
{
|
||||
AiPlanSourceKind.Rule => "rule",
|
||||
AiPlanSourceKind.Order => "order",
|
||||
AiPlanSourceKind.DefaultBehavior => "default-behavior",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this ShipState state) => state switch
|
||||
{
|
||||
ShipState.Idle => "idle",
|
||||
@@ -213,23 +270,8 @@ public static class SimulationEnumMappings
|
||||
ShipState.Blocked => "blocked",
|
||||
ShipState.Undocking => "undocking",
|
||||
ShipState.EngagingTarget => "engaging-target",
|
||||
ShipState.HoldingPosition => "holding-position",
|
||||
ShipState.Fleeing => "fleeing",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
|
||||
};
|
||||
|
||||
public static string ToContractValue(this ControllerTaskKind kind) => kind switch
|
||||
{
|
||||
ControllerTaskKind.Idle => "idle",
|
||||
ControllerTaskKind.Travel => "travel",
|
||||
ControllerTaskKind.Extract => "extract",
|
||||
ControllerTaskKind.Dock => "dock",
|
||||
ControllerTaskKind.Load => "load",
|
||||
ControllerTaskKind.Unload => "unload",
|
||||
ControllerTaskKind.DeliverConstruction => "deliver-construction",
|
||||
ControllerTaskKind.BuildConstructionSite => "build-construction-site",
|
||||
ControllerTaskKind.AttackTarget => "attack-target",
|
||||
|
||||
ControllerTaskKind.ConstructModule => "construct-module",
|
||||
ControllerTaskKind.Undock => "undock",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
39
apps/backend/Ships/Api/EnqueueShipOrderHandler.cs
Normal file
39
apps/backend/Ships/Api/EnqueueShipOrderHandler.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Api;
|
||||
|
||||
public sealed class EnqueueShipOrderHandler(WorldService worldService) : Endpoint<ShipOrderCommandRequest, ShipSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/ships/{shipId}/orders");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var shipId = Route<string>("shipId");
|
||||
if (string.IsNullOrWhiteSpace(shipId))
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = worldService.EnqueueShipOrder(shipId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
AddError(ex.Message);
|
||||
await SendErrorsAsync(cancellation: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
apps/backend/Ships/Api/RemoveShipOrderHandler.cs
Normal file
30
apps/backend/Ships/Api/RemoveShipOrderHandler.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Api;
|
||||
|
||||
public sealed class RemoveShipOrderRequest
|
||||
{
|
||||
public string ShipId { get; set; } = string.Empty;
|
||||
public string OrderId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint<RemoveShipOrderRequest, ShipSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/ships/{shipId}/orders/{orderId}");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(RemoveShipOrderRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = worldService.RemoveShipOrder(request.ShipId, request.OrderId);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
31
apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs
Normal file
31
apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using FastEndpoints;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Api;
|
||||
|
||||
public sealed class UpdateShipDefaultBehaviorHandler(WorldService worldService) : Endpoint<ShipDefaultBehaviorCommandRequest, ShipSnapshot>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/ships/{shipId}/default-behavior");
|
||||
AllowAnonymous();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var shipId = Route<string>("shipId");
|
||||
if (string.IsNullOrWhiteSpace(shipId))
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = worldService.UpdateShipDefaultBehavior(shipId, request);
|
||||
if (snapshot is null)
|
||||
{
|
||||
await SendNotFoundAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendOkAsync(snapshot, cancellationToken);
|
||||
}
|
||||
}
|
||||
55
apps/backend/Ships/Contracts/ShipCommands.cs
Normal file
55
apps/backend/Ships/Contracts/ShipCommands.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace SpaceGame.Api.Ships.Contracts;
|
||||
|
||||
public sealed record ShipOrderCommandRequest(
|
||||
string Kind,
|
||||
int Priority,
|
||||
bool InterruptCurrentPlan,
|
||||
string? Label,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float? WaitSeconds,
|
||||
float? Radius,
|
||||
int? MaxSystemRange,
|
||||
bool? KnownStationsOnly);
|
||||
|
||||
public sealed record ShipOrderTemplateCommandRequest(
|
||||
string Kind,
|
||||
string? Label,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float? WaitSeconds,
|
||||
float? Radius,
|
||||
int? MaxSystemRange,
|
||||
bool? KnownStationsOnly);
|
||||
|
||||
public sealed record ShipDefaultBehaviorCommandRequest(
|
||||
string Kind,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? AreaSystemId,
|
||||
string? TargetEntityId,
|
||||
string? PreferredItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
Vector3Dto? TargetPosition,
|
||||
float? WaitSeconds,
|
||||
float? Radius,
|
||||
int? MaxSystemRange,
|
||||
bool? KnownStationsOnly,
|
||||
IReadOnlyList<Vector3Dto>? PatrolPoints,
|
||||
IReadOnlyList<ShipOrderTemplateCommandRequest>? RepeatOrders);
|
||||
@@ -1,5 +1,132 @@
|
||||
namespace SpaceGame.Api.Ships.Contracts;
|
||||
|
||||
public sealed record ShipSkillProfileSnapshot(
|
||||
int Navigation,
|
||||
int Trade,
|
||||
int Mining,
|
||||
int Combat,
|
||||
int Construction);
|
||||
|
||||
public sealed record ShipOrderSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
int Priority,
|
||||
bool InterruptCurrentPlan,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
string? Label,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float WaitSeconds,
|
||||
float Radius,
|
||||
int? MaxSystemRange,
|
||||
bool KnownStationsOnly,
|
||||
string? FailureReason);
|
||||
|
||||
public sealed record ShipOrderTemplateSnapshot(
|
||||
string Kind,
|
||||
string? Label,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float WaitSeconds,
|
||||
float Radius,
|
||||
int? MaxSystemRange,
|
||||
bool KnownStationsOnly);
|
||||
|
||||
public sealed record DefaultBehaviorSnapshot(
|
||||
string Kind,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? AreaSystemId,
|
||||
string? TargetEntityId,
|
||||
string? PreferredItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
Vector3Dto? TargetPosition,
|
||||
float WaitSeconds,
|
||||
float Radius,
|
||||
int MaxSystemRange,
|
||||
bool KnownStationsOnly,
|
||||
IReadOnlyList<Vector3Dto> PatrolPoints,
|
||||
int PatrolIndex,
|
||||
IReadOnlyList<ShipOrderTemplateSnapshot> RepeatOrders,
|
||||
int RepeatIndex);
|
||||
|
||||
public sealed record ShipAssignmentSnapshot(
|
||||
string CommanderId,
|
||||
string? ParentCommanderId,
|
||||
string Kind,
|
||||
string BehaviorKind,
|
||||
string Status,
|
||||
string? ObjectiveId,
|
||||
string? CampaignId,
|
||||
string? TheaterId,
|
||||
float Priority,
|
||||
string? HomeSystemId,
|
||||
string? HomeStationId,
|
||||
string? TargetSystemId,
|
||||
string? TargetEntityId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? ItemId,
|
||||
string? Notes,
|
||||
DateTimeOffset? UpdatedAtUtc);
|
||||
|
||||
public sealed record ShipSubTaskSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string Summary,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
string? TargetNodeId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? ItemId,
|
||||
string? ModuleId,
|
||||
float Threshold,
|
||||
float Amount,
|
||||
float Progress,
|
||||
float ElapsedSeconds,
|
||||
float TotalSeconds,
|
||||
string? BlockingReason);
|
||||
|
||||
public sealed record ShipPlanStepSnapshot(
|
||||
string Id,
|
||||
string Kind,
|
||||
string Status,
|
||||
string Summary,
|
||||
string? BlockingReason,
|
||||
int CurrentSubTaskIndex,
|
||||
IReadOnlyList<ShipSubTaskSnapshot> SubTasks);
|
||||
|
||||
public sealed record ShipPlanSnapshot(
|
||||
string Id,
|
||||
string SourceKind,
|
||||
string SourceId,
|
||||
string Kind,
|
||||
string Status,
|
||||
string Summary,
|
||||
int CurrentStepIndex,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
DateTimeOffset UpdatedAtUtc,
|
||||
string? InterruptReason,
|
||||
string? FailureReason,
|
||||
IReadOnlyList<ShipPlanStepSnapshot> Steps);
|
||||
|
||||
public sealed record ShipSnapshot(
|
||||
string Id,
|
||||
string Label,
|
||||
@@ -10,24 +137,29 @@ public sealed record ShipSnapshot(
|
||||
Vector3Dto LocalVelocity,
|
||||
Vector3Dto TargetLocalPosition,
|
||||
string State,
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string? BehaviorPhase,
|
||||
string ControllerTaskKind,
|
||||
string? CommanderObjective,
|
||||
IReadOnlyList<ShipOrderSnapshot> OrderQueue,
|
||||
DefaultBehaviorSnapshot DefaultBehavior,
|
||||
ShipAssignmentSnapshot? Assignment,
|
||||
ShipSkillProfileSnapshot Skills,
|
||||
ShipPlanSnapshot? ActivePlan,
|
||||
string? CurrentStepId,
|
||||
IReadOnlyList<ShipSubTaskSnapshot> ActiveSubTasks,
|
||||
string ControlSourceKind,
|
||||
string? ControlSourceId,
|
||||
string? ControlReason,
|
||||
string? LastReplanReason,
|
||||
string? LastAccessFailureReason,
|
||||
string? CelestialId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
|
||||
float TravelSpeed,
|
||||
string TravelSpeedUnit,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
float Health,
|
||||
IReadOnlyList<string> History,
|
||||
ShipActionProgressSnapshot? CurrentAction,
|
||||
ShipSpatialStateSnapshot SpatialState);
|
||||
|
||||
public sealed record ShipDelta(
|
||||
@@ -40,30 +172,31 @@ public sealed record ShipDelta(
|
||||
Vector3Dto LocalVelocity,
|
||||
Vector3Dto TargetLocalPosition,
|
||||
string State,
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string? BehaviorPhase,
|
||||
string ControllerTaskKind,
|
||||
string? CommanderObjective,
|
||||
IReadOnlyList<ShipOrderSnapshot> OrderQueue,
|
||||
DefaultBehaviorSnapshot DefaultBehavior,
|
||||
ShipAssignmentSnapshot? Assignment,
|
||||
ShipSkillProfileSnapshot Skills,
|
||||
ShipPlanSnapshot? ActivePlan,
|
||||
string? CurrentStepId,
|
||||
IReadOnlyList<ShipSubTaskSnapshot> ActiveSubTasks,
|
||||
string ControlSourceKind,
|
||||
string? ControlSourceId,
|
||||
string? ControlReason,
|
||||
string? LastReplanReason,
|
||||
string? LastAccessFailureReason,
|
||||
string? CelestialId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
|
||||
float TravelSpeed,
|
||||
string TravelSpeedUnit,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
float Health,
|
||||
IReadOnlyList<string> History,
|
||||
ShipActionProgressSnapshot? CurrentAction,
|
||||
ShipSpatialStateSnapshot SpatialState);
|
||||
|
||||
public sealed record ShipActionProgressSnapshot(
|
||||
string Label,
|
||||
float Progress);
|
||||
|
||||
public sealed record ShipSpatialStateSnapshot(
|
||||
string SpaceLayer,
|
||||
string CurrentSystemId,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace SpaceGame.Api.Ships.Runtime;
|
||||
|
||||
public sealed class ShipRuntime
|
||||
@@ -12,56 +11,147 @@ public sealed class ShipRuntime
|
||||
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||
public ShipState State { get; set; } = ShipState.Idle;
|
||||
public ShipOrderRuntime? Order { get; set; }
|
||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||
public float ActionTimer { get; set; }
|
||||
public List<ShipOrderRuntime> OrderQueue { get; } = [];
|
||||
public ShipPlanRuntime? ActivePlan { get; set; }
|
||||
public required ShipSkillProfileRuntime Skills { get; set; }
|
||||
public bool NeedsReplan { get; set; } = true;
|
||||
public float ReplanCooldownSeconds { get; set; }
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
public string? DockedStationId { get; set; }
|
||||
public int? AssignedDockingPadIndex { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public string ControlSourceKind { get; set; } = "unassigned";
|
||||
public string? ControlSourceId { get; set; }
|
||||
public string? ControlReason { get; set; }
|
||||
public string? LastReplanReason { get; set; }
|
||||
public string? LastAccessFailureReason { get; set; }
|
||||
public float Health { get; set; }
|
||||
public string? TrackedActionKey { get; set; }
|
||||
public float TrackedActionTotal { get; set; }
|
||||
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShipSkillProfileRuntime
|
||||
{
|
||||
public int Navigation { get; set; }
|
||||
public int Trade { get; set; }
|
||||
public int Mining { get; set; }
|
||||
public int Combat { get; set; }
|
||||
public int Construction { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipOrderRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Accepted;
|
||||
public required string DestinationSystemId { get; init; }
|
||||
public required Vector3 DestinationPosition { get; init; }
|
||||
public OrderStatus Status { get; set; } = OrderStatus.Queued;
|
||||
public int Priority { get; set; }
|
||||
public bool InterruptCurrentPlan { get; set; } = true;
|
||||
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public string? Label { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public string? SourceStationId { get; set; }
|
||||
public string? DestinationStationId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ConstructionSiteId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public float WaitSeconds { get; set; }
|
||||
public float Radius { get; set; }
|
||||
public int? MaxSystemRange { get; set; }
|
||||
public bool KnownStationsOnly { get; set; }
|
||||
public string? FailureReason { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DefaultBehaviorRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public string? HomeSystemId { get; set; }
|
||||
public string? HomeStationId { get; set; }
|
||||
public string? AreaSystemId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? StationId { get; set; }
|
||||
public string? RefineryId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public string? Phase { get; set; }
|
||||
public string? PreferredItemId { get; set; }
|
||||
public string? PreferredNodeId { get; set; }
|
||||
public string? PreferredConstructionSiteId { get; set; }
|
||||
public string? PreferredModuleId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public float WaitSeconds { get; set; } = 3f;
|
||||
public float Radius { get; set; } = 24f;
|
||||
public int MaxSystemRange { get; set; }
|
||||
public bool KnownStationsOnly { get; set; }
|
||||
public List<Vector3> PatrolPoints { get; set; } = [];
|
||||
public int PatrolIndex { get; set; }
|
||||
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
|
||||
public int RepeatIndex { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ControllerTaskRuntime
|
||||
public sealed class ShipOrderTemplateRuntime
|
||||
{
|
||||
public required ControllerTaskKind Kind { get; set; }
|
||||
public required string Kind { get; init; }
|
||||
public string? Label { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public string? SourceStationId { get; set; }
|
||||
public string? DestinationStationId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? ConstructionSiteId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public float WaitSeconds { get; set; }
|
||||
public float Radius { get; set; }
|
||||
public int? MaxSystemRange { get; set; }
|
||||
public bool KnownStationsOnly { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipPlanRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required AiPlanSourceKind SourceKind { get; init; }
|
||||
public required string SourceId { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Summary { get; set; }
|
||||
public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned;
|
||||
public int CurrentStepIndex { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
public string? InterruptReason { get; set; }
|
||||
public string? FailureReason { get; set; }
|
||||
public List<ShipPlanStepRuntime> Steps { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class ShipPlanStepRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Summary { get; set; }
|
||||
public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned;
|
||||
public int CurrentSubTaskIndex { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
public List<ShipSubTaskRuntime> SubTasks { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class ShipSubTaskRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string Summary { get; set; }
|
||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||
public string? CommanderId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? TargetSystemId { get; set; }
|
||||
public string? TargetNodeId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
public float Amount { get; set; }
|
||||
public float ElapsedSeconds { get; set; }
|
||||
public float TotalSeconds { get; set; }
|
||||
public float Progress { get; set; }
|
||||
public string? BlockingReason { get; set; }
|
||||
}
|
||||
|
||||
2708
apps/backend/Ships/Simulation/ShipAiService.cs
Normal file
2708
apps/backend/Ships/Simulation/ShipAiService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,880 +0,0 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
internal sealed class ShipControlService
|
||||
{
|
||||
private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault();
|
||||
|
||||
private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) =>
|
||||
ship.CommanderId is null
|
||||
? null
|
||||
: world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId && candidate.Kind == CommanderKind.Ship);
|
||||
|
||||
private static void SyncCommanderToShip(ShipRuntime ship, CommanderRuntime commander)
|
||||
{
|
||||
if (commander.ActiveBehavior is not null)
|
||||
{
|
||||
ship.DefaultBehavior.Kind = commander.ActiveBehavior.Kind;
|
||||
ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior.AreaSystemId;
|
||||
ship.DefaultBehavior.TargetEntityId = commander.ActiveBehavior.TargetEntityId;
|
||||
ship.DefaultBehavior.ItemId = commander.ActiveBehavior.ItemId;
|
||||
ship.DefaultBehavior.ModuleId = commander.ActiveBehavior.ModuleId;
|
||||
ship.DefaultBehavior.NodeId = commander.ActiveBehavior.NodeId;
|
||||
ship.DefaultBehavior.Phase = commander.ActiveBehavior.Phase;
|
||||
ship.DefaultBehavior.PatrolIndex = commander.ActiveBehavior.PatrolIndex;
|
||||
ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId;
|
||||
}
|
||||
|
||||
if (commander.ActiveOrder is null)
|
||||
{
|
||||
ship.Order = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ship.Order = new ShipOrderRuntime
|
||||
{
|
||||
Kind = commander.ActiveOrder.Kind,
|
||||
Status = commander.ActiveOrder.Status,
|
||||
DestinationSystemId = commander.ActiveOrder.DestinationSystemId,
|
||||
DestinationPosition = commander.ActiveOrder.DestinationPosition,
|
||||
};
|
||||
}
|
||||
|
||||
if (commander.ActiveTask is not null)
|
||||
{
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ParseControllerTaskKind(commander.ActiveTask.Kind),
|
||||
Status = commander.ActiveTask.Status,
|
||||
CommanderId = commander.Id,
|
||||
TargetEntityId = commander.ActiveTask.TargetEntityId,
|
||||
TargetNodeId = commander.ActiveTask.TargetNodeId,
|
||||
TargetPosition = commander.ActiveTask.TargetPosition,
|
||||
TargetSystemId = commander.ActiveTask.TargetSystemId,
|
||||
Threshold = commander.ActiveTask.Threshold,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncShipToCommander(ShipRuntime ship, CommanderRuntime commander)
|
||||
{
|
||||
commander.ActiveBehavior ??= new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind };
|
||||
commander.ActiveBehavior.Kind = ship.DefaultBehavior.Kind;
|
||||
commander.ActiveBehavior.AreaSystemId = ship.DefaultBehavior.AreaSystemId;
|
||||
commander.ActiveBehavior.TargetEntityId = ship.DefaultBehavior.TargetEntityId;
|
||||
commander.ActiveBehavior.ItemId = ship.DefaultBehavior.ItemId;
|
||||
commander.ActiveBehavior.ModuleId = ship.DefaultBehavior.ModuleId;
|
||||
commander.ActiveBehavior.NodeId = ship.DefaultBehavior.NodeId;
|
||||
commander.ActiveBehavior.Phase = ship.DefaultBehavior.Phase;
|
||||
commander.ActiveBehavior.PatrolIndex = ship.DefaultBehavior.PatrolIndex;
|
||||
commander.ActiveBehavior.StationId = ship.DefaultBehavior.StationId;
|
||||
|
||||
if (ship.Order is null)
|
||||
{
|
||||
commander.ActiveOrder = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
commander.ActiveOrder ??= new CommanderOrderRuntime
|
||||
{
|
||||
Kind = ship.Order.Kind,
|
||||
DestinationSystemId = ship.Order.DestinationSystemId,
|
||||
DestinationPosition = ship.Order.DestinationPosition,
|
||||
};
|
||||
commander.ActiveOrder.Status = ship.Order.Status;
|
||||
commander.ActiveOrder.TargetEntityId = ship.ControllerTask.TargetEntityId;
|
||||
commander.ActiveOrder.DestinationNodeId = ship.ControllerTask.TargetNodeId ?? ship.SpatialState.DestinationNodeId;
|
||||
}
|
||||
|
||||
commander.ActiveTask ??= new CommanderTaskRuntime { Kind = ship.ControllerTask.Kind.ToContractValue() };
|
||||
commander.ActiveTask.Kind = ship.ControllerTask.Kind.ToContractValue();
|
||||
commander.ActiveTask.Status = ship.ControllerTask.Status;
|
||||
commander.ActiveTask.TargetEntityId = ship.ControllerTask.TargetEntityId;
|
||||
commander.ActiveTask.TargetNodeId = ship.ControllerTask.TargetNodeId;
|
||||
commander.ActiveTask.TargetPosition = ship.ControllerTask.TargetPosition;
|
||||
commander.ActiveTask.TargetSystemId = ship.ControllerTask.TargetSystemId;
|
||||
commander.ActiveTask.Threshold = ship.ControllerTask.Threshold;
|
||||
}
|
||||
|
||||
internal void RefreshControlLayers(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var commander = GetShipCommander(world, ship);
|
||||
if (commander is not null)
|
||||
{
|
||||
SyncCommanderToShip(ship, commander);
|
||||
}
|
||||
|
||||
if (ship.Order is not null && ship.Order.Status == OrderStatus.Queued)
|
||||
{
|
||||
ship.Order.Status = OrderStatus.Accepted;
|
||||
if (commander?.ActiveOrder is not null)
|
||||
{
|
||||
commander.ActiveOrder.Status = ship.Order.Status;
|
||||
}
|
||||
}
|
||||
|
||||
if (commander is not null)
|
||||
{
|
||||
SyncShipToCommander(ship, commander);
|
||||
}
|
||||
}
|
||||
|
||||
internal void PlanControllerTask(SimulationEngine engine, ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var commander = GetShipCommander(world, ship);
|
||||
if (ship.Order is not null)
|
||||
{
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
Status = WorkStatus.Active,
|
||||
CommanderId = commander?.Id,
|
||||
TargetSystemId = ship.Order.DestinationSystemId,
|
||||
TargetNodeId = ship.SpatialState.DestinationNodeId,
|
||||
TargetPosition = ship.Order.DestinationPosition,
|
||||
Threshold = world.Balance.ArrivalThreshold,
|
||||
};
|
||||
SyncCommanderTask(commander, ship.ControllerTask);
|
||||
return;
|
||||
}
|
||||
|
||||
_shipBehaviorStateMachine.Plan(engine, ship, world);
|
||||
SyncCommanderTask(commander, ship.ControllerTask);
|
||||
}
|
||||
|
||||
internal void PlanAttackTarget(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var target = ResolveAttackTarget(ship, world);
|
||||
if (target is null)
|
||||
{
|
||||
behavior.Kind = "idle";
|
||||
behavior.TargetEntityId = null;
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
behavior.TargetEntityId = target.EntityId;
|
||||
behavior.AreaSystemId = target.SystemId;
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.AttackTarget,
|
||||
TargetEntityId = target.EntityId,
|
||||
TargetSystemId = target.SystemId,
|
||||
TargetPosition = target.Position,
|
||||
Threshold = target.AttackRange,
|
||||
};
|
||||
}
|
||||
|
||||
internal void PlanTransportHaul(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var sourceStation = behavior.StationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId);
|
||||
var destinationStation = behavior.TargetEntityId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.TargetEntityId);
|
||||
if (sourceStation is null || destinationStation is null || string.IsNullOrWhiteSpace(behavior.ItemId))
|
||||
{
|
||||
behavior.Kind = "idle";
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
var carryingCargo = GetShipCargoAmount(ship) > 0.01f;
|
||||
if (carryingCargo)
|
||||
{
|
||||
if (ship.DockedStationId == destinationStation.Id)
|
||||
{
|
||||
behavior.Phase = "unload";
|
||||
}
|
||||
else if (ship.DockedStationId is not null)
|
||||
{
|
||||
behavior.Phase = "undock-from-source";
|
||||
}
|
||||
else if (behavior.Phase is not "travel-to-destination" and not "dock-destination" and not "unload")
|
||||
{
|
||||
behavior.Phase = "travel-to-destination";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ship.DockedStationId == sourceStation.Id)
|
||||
{
|
||||
var available = GetInventoryAmount(sourceStation.Inventory, behavior.ItemId);
|
||||
behavior.Phase = available > 0.01f ? "load" : "wait-source";
|
||||
}
|
||||
else if (ship.DockedStationId == destinationStation.Id)
|
||||
{
|
||||
behavior.Phase = "undock-from-destination";
|
||||
}
|
||||
else if (behavior.Phase is not "travel-to-source" and not "dock-source" and not "load")
|
||||
{
|
||||
behavior.Phase = "travel-to-source";
|
||||
}
|
||||
}
|
||||
|
||||
ship.ControllerTask = behavior.Phase switch
|
||||
{
|
||||
"travel-to-source" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = sourceStation.Id,
|
||||
TargetSystemId = sourceStation.SystemId,
|
||||
TargetPosition = sourceStation.Position,
|
||||
Threshold = sourceStation.Radius + 8f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"dock-source" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Dock,
|
||||
TargetEntityId = sourceStation.Id,
|
||||
TargetSystemId = sourceStation.SystemId,
|
||||
TargetPosition = sourceStation.Position,
|
||||
Threshold = sourceStation.Radius + 4f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"load" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Load,
|
||||
TargetEntityId = sourceStation.Id,
|
||||
TargetSystemId = sourceStation.SystemId,
|
||||
TargetPosition = sourceStation.Position,
|
||||
Threshold = 0f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"undock-from-source" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Undock,
|
||||
TargetEntityId = sourceStation.Id,
|
||||
TargetSystemId = sourceStation.SystemId,
|
||||
TargetPosition = new Vector3(sourceStation.Position.X + world.Balance.UndockDistance, sourceStation.Position.Y, sourceStation.Position.Z),
|
||||
Threshold = 8f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"travel-to-destination" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = destinationStation.Id,
|
||||
TargetSystemId = destinationStation.SystemId,
|
||||
TargetPosition = destinationStation.Position,
|
||||
Threshold = destinationStation.Radius + 8f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"dock-destination" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Dock,
|
||||
TargetEntityId = destinationStation.Id,
|
||||
TargetSystemId = destinationStation.SystemId,
|
||||
TargetPosition = destinationStation.Position,
|
||||
Threshold = destinationStation.Radius + 4f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"unload" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Unload,
|
||||
TargetEntityId = destinationStation.Id,
|
||||
TargetSystemId = destinationStation.SystemId,
|
||||
TargetPosition = destinationStation.Position,
|
||||
Threshold = 0f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
"undock-from-destination" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Undock,
|
||||
TargetEntityId = destinationStation.Id,
|
||||
TargetSystemId = destinationStation.SystemId,
|
||||
TargetPosition = new Vector3(destinationStation.Position.X + world.Balance.UndockDistance, destinationStation.Position.Y, destinationStation.Position.Z),
|
||||
Threshold = 8f,
|
||||
ItemId = behavior.ItemId,
|
||||
},
|
||||
_ => CreateIdleTask(world.Balance.ArrivalThreshold),
|
||||
};
|
||||
}
|
||||
|
||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule)
|
||||
{
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var cargoItemId = ship.Inventory.Keys.FirstOrDefault();
|
||||
var targetResourceItemId = SelectMiningResourceItem(world, ship, cargoItemId ?? behavior.ItemId ?? resourceItemId);
|
||||
if (string.IsNullOrWhiteSpace(targetResourceItemId))
|
||||
{
|
||||
behavior.Phase = null;
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(behavior.ItemId, targetResourceItemId, StringComparison.Ordinal))
|
||||
{
|
||||
behavior.ItemId = targetResourceItemId;
|
||||
behavior.NodeId = null;
|
||||
}
|
||||
|
||||
var refinery = SelectBestBuyStation(world, ship, targetResourceItemId, behavior.StationId);
|
||||
behavior.StationId = refinery?.Id;
|
||||
var node = behavior.NodeId is null
|
||||
? world.Nodes
|
||||
.Where(candidate =>
|
||||
candidate.ItemId == targetResourceItemId &&
|
||||
candidate.OreRemaining > 0.01f &&
|
||||
CanShipMineItem(world, ship, candidate.ItemId))
|
||||
.OrderByDescending(candidate => candidate.SystemId == behavior.AreaSystemId ? 1 : 0)
|
||||
.ThenByDescending(candidate => candidate.OreRemaining)
|
||||
.FirstOrDefault()
|
||||
: world.Nodes.FirstOrDefault(candidate =>
|
||||
candidate.Id == behavior.NodeId &&
|
||||
string.Equals(candidate.ItemId, targetResourceItemId, StringComparison.Ordinal) &&
|
||||
candidate.OreRemaining > 0.01f);
|
||||
|
||||
if (node is not null)
|
||||
{
|
||||
behavior.AreaSystemId = node.SystemId;
|
||||
}
|
||||
|
||||
if (refinery is null || node is null || !HasShipCapabilities(ship.Definition, requiredModule))
|
||||
{
|
||||
if (refinery is null && GetShipCargoAmount(ship) > 0.01f)
|
||||
{
|
||||
ship.Inventory.Clear();
|
||||
}
|
||||
|
||||
behavior.Phase = null;
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
behavior.NodeId ??= node.Id;
|
||||
|
||||
if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|
||||
&& behavior.Phase is "travel-to-node" or "extract")
|
||||
{
|
||||
behavior.Phase = "travel-to-station";
|
||||
}
|
||||
|
||||
if (ship.DockedStationId == refinery.Id)
|
||||
{
|
||||
if (GetShipCargoAmount(ship) > 0.01f)
|
||||
{
|
||||
behavior.Phase = "unload";
|
||||
}
|
||||
else if (behavior.Phase is "dock" or "unload")
|
||||
{
|
||||
behavior.Phase = "undock";
|
||||
}
|
||||
}
|
||||
else if (behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract")
|
||||
{
|
||||
behavior.Phase = "travel-to-station";
|
||||
}
|
||||
|
||||
switch (behavior.Phase)
|
||||
{
|
||||
case "extract":
|
||||
var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f);
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Extract,
|
||||
TargetEntityId = node.Id,
|
||||
TargetSystemId = node.SystemId,
|
||||
TargetPosition = extractionPosition,
|
||||
Threshold = 5f,
|
||||
};
|
||||
break;
|
||||
case "travel-to-station":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = refinery.Id,
|
||||
TargetSystemId = refinery.SystemId,
|
||||
TargetPosition = refinery.Position,
|
||||
Threshold = refinery.Radius + 8f,
|
||||
};
|
||||
break;
|
||||
case "dock":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Dock,
|
||||
TargetEntityId = refinery.Id,
|
||||
TargetSystemId = refinery.SystemId,
|
||||
TargetPosition = refinery.Position,
|
||||
Threshold = refinery.Radius + 4f,
|
||||
};
|
||||
break;
|
||||
case "unload":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Unload,
|
||||
TargetEntityId = refinery.Id,
|
||||
TargetSystemId = refinery.SystemId,
|
||||
TargetPosition = refinery.Position,
|
||||
Threshold = 0f,
|
||||
};
|
||||
break;
|
||||
case "undock":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Undock,
|
||||
TargetEntityId = refinery.Id,
|
||||
TargetSystemId = refinery.SystemId,
|
||||
TargetPosition = new Vector3(refinery.Position.X + world.Balance.UndockDistance, refinery.Position.Y, refinery.Position.Z),
|
||||
Threshold = 8f,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = node.Id,
|
||||
TargetSystemId = node.SystemId,
|
||||
TargetPosition = node.Position,
|
||||
Threshold = 18f,
|
||||
};
|
||||
behavior.Phase = "travel-to-node";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string? fallbackItemId)
|
||||
{
|
||||
var candidateItemId = world.MarketOrders
|
||||
.Where(order =>
|
||||
string.Equals(order.FactionId, ship.FactionId, StringComparison.Ordinal)
|
||||
&& order.Kind == MarketOrderKinds.Buy
|
||||
&& order.ConstructionSiteId is null
|
||||
&& order.State != MarketOrderStateKinds.Cancelled
|
||||
&& order.RemainingAmount > 0.01f)
|
||||
.Select(order => new
|
||||
{
|
||||
ItemId = order.ItemId,
|
||||
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
|
||||
})
|
||||
.Where(entry => CanShipMineItem(world, ship, entry.ItemId))
|
||||
.Where(entry => world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
||||
.GroupBy(entry => entry.ItemId, StringComparer.Ordinal)
|
||||
.Select(group => new
|
||||
{
|
||||
ItemId = group.Key,
|
||||
Score = group.Sum(entry => entry.Score) + (string.Equals(group.Key, ship.DefaultBehavior.ItemId, StringComparison.Ordinal) ? 15f : 0f),
|
||||
})
|
||||
.OrderByDescending(entry => entry.Score)
|
||||
.Select(entry => entry.ItemId)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(candidateItemId))
|
||||
{
|
||||
return candidateItemId;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fallbackItemId)
|
||||
&& CanShipMineItem(world, ship, fallbackItemId)
|
||||
&& world.Nodes.Any(node => string.Equals(node.ItemId, fallbackItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
||||
{
|
||||
return fallbackItemId;
|
||||
}
|
||||
|
||||
return world.Nodes
|
||||
.Where(node => node.OreRemaining > 0.01f && CanShipMineItem(world, ship, node.ItemId))
|
||||
.OrderByDescending(node => node.OreRemaining)
|
||||
.Select(node => node.ItemId)
|
||||
.FirstOrDefault() ?? fallbackItemId;
|
||||
}
|
||||
|
||||
private static bool CanShipMineItem(SimulationWorld world, ShipRuntime ship, string itemId) =>
|
||||
world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)
|
||||
&& string.Equals(itemDefinition.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal)
|
||||
&& HasShipCapabilities(ship.Definition, "mining");
|
||||
|
||||
internal static StationRuntime? SelectBestBuyStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId)
|
||||
{
|
||||
var preferred = preferredStationId is null
|
||||
? null
|
||||
: world.Stations.FirstOrDefault(station => station.Id == preferredStationId);
|
||||
|
||||
var bestOrder = world.MarketOrders
|
||||
.Where(order =>
|
||||
order.Kind == MarketOrderKinds.Buy &&
|
||||
order.ConstructionSiteId is null &&
|
||||
order.State != MarketOrderStateKinds.Cancelled &&
|
||||
order.ItemId == itemId &&
|
||||
order.RemainingAmount > 0.01f)
|
||||
.Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId)))
|
||||
.Where(entry => entry.Station is not null && string.Equals(entry.Station.FactionId, ship.FactionId, StringComparison.Ordinal))
|
||||
.Where(entry => CanStationReceiveItem(world, entry.Station!, itemId))
|
||||
.OrderByDescending(entry =>
|
||||
{
|
||||
var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f;
|
||||
return entry.Order.Valuation - distancePenalty;
|
||||
})
|
||||
.FirstOrDefault();
|
||||
|
||||
return bestOrder.Station ?? (preferred is not null && CanStationReceiveItem(world, preferred, itemId) ? preferred : null);
|
||||
}
|
||||
|
||||
private static bool CanStationReceiveItem(SimulationWorld world, StationRuntime station, string itemId)
|
||||
{
|
||||
if (!world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var requiredModule = GetStorageRequirement(itemDefinition.CargoKind);
|
||||
return requiredModule is null || station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) =>
|
||||
phase switch
|
||||
{
|
||||
"dock" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Dock,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
TargetPosition = station.Position,
|
||||
Threshold = 8f,
|
||||
},
|
||||
"load" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Load,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
TargetPosition = station.Position,
|
||||
Threshold = 8f,
|
||||
},
|
||||
"unload" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Unload,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
TargetPosition = station.Position,
|
||||
Threshold = 8f,
|
||||
},
|
||||
"undock" => new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Undock,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
TargetPosition = new Vector3(station.Position.X + world.Balance.UndockDistance, station.Position.Y, station.Position.Z),
|
||||
Threshold = 8f,
|
||||
},
|
||||
_ => CreateIdleTask(world.Balance.ArrivalThreshold),
|
||||
};
|
||||
|
||||
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
var behavior = ship.DefaultBehavior;
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId);
|
||||
var site = !string.IsNullOrWhiteSpace(behavior.TargetEntityId)
|
||||
? world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == behavior.TargetEntityId)
|
||||
: station is null ? null : GetConstructionSiteForStation(world, station.Id);
|
||||
if (station is null)
|
||||
{
|
||||
behavior.Kind = "idle";
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
if (site is null && !string.IsNullOrWhiteSpace(behavior.TargetEntityId))
|
||||
{
|
||||
behavior.TargetEntityId = null;
|
||||
behavior.ModuleId = null;
|
||||
site = GetConstructionSiteForStation(world, station.Id);
|
||||
}
|
||||
|
||||
var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world);
|
||||
behavior.ModuleId = moduleId;
|
||||
if (moduleId is null)
|
||||
{
|
||||
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ship.DockedStationId is not null)
|
||||
{
|
||||
var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
||||
if (dockedStation is not null)
|
||||
{
|
||||
dockedStation.DockedShipIds.Remove(ship.Id);
|
||||
ReleaseDockingPad(dockedStation, ship.Id);
|
||||
}
|
||||
|
||||
ship.DockedStationId = null;
|
||||
ship.AssignedDockingPadIndex = null;
|
||||
ship.Position = ResolveConstructionHoldPosition(ship, station, site, world);
|
||||
ship.TargetPosition = ship.Position;
|
||||
}
|
||||
|
||||
var constructionHoldPosition = ResolveConstructionHoldPosition(ship, station, site, world);
|
||||
var targetSystemId = site?.SystemId ?? station.SystemId;
|
||||
var targetCelestialId = site?.CelestialId ?? station.CelestialId;
|
||||
var isAtTargetCelestial = !string.IsNullOrWhiteSpace(targetCelestialId)
|
||||
&& string.Equals(ship.SpatialState.CurrentCelestialId, targetCelestialId, StringComparison.Ordinal);
|
||||
var isAtConstructionHold = ship.SystemId == targetSystemId
|
||||
&& (ship.Position.DistanceTo(constructionHoldPosition) <= 10f || isAtTargetCelestial);
|
||||
|
||||
if (isAtConstructionHold)
|
||||
{
|
||||
if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site))
|
||||
{
|
||||
behavior.Phase = "deliver-to-site";
|
||||
}
|
||||
else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site))
|
||||
{
|
||||
behavior.Phase = "build-site";
|
||||
}
|
||||
else if (site is not null)
|
||||
{
|
||||
behavior.Phase = "wait-for-materials";
|
||||
}
|
||||
else if (CanStartModuleConstruction(station, world.ModuleRecipes[moduleId]))
|
||||
{
|
||||
behavior.Phase = "construct-module";
|
||||
}
|
||||
else
|
||||
{
|
||||
behavior.Phase = "wait-for-materials";
|
||||
}
|
||||
}
|
||||
else if (behavior.Phase != "travel-to-station")
|
||||
{
|
||||
behavior.Phase = "travel-to-station";
|
||||
}
|
||||
|
||||
switch (behavior.Phase)
|
||||
{
|
||||
case "construct-module":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.ConstructModule,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = constructionHoldPosition,
|
||||
Threshold = 10f,
|
||||
};
|
||||
break;
|
||||
case "deliver-to-site":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.DeliverConstruction,
|
||||
TargetEntityId = site?.Id,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = constructionHoldPosition,
|
||||
Threshold = 10f,
|
||||
};
|
||||
break;
|
||||
case "build-site":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.BuildConstructionSite,
|
||||
TargetEntityId = site?.Id,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = constructionHoldPosition,
|
||||
Threshold = 10f,
|
||||
};
|
||||
break;
|
||||
case "wait-for-materials":
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
TargetEntityId = site?.Id ?? station.Id,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = constructionHoldPosition,
|
||||
Threshold = 0f,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
ship.ControllerTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = site?.Id ?? station.Id,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = constructionHoldPosition,
|
||||
Threshold = 10f,
|
||||
};
|
||||
behavior.Phase = "travel-to-station";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void AdvanceControlState(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent)
|
||||
{
|
||||
var commander = GetShipCommander(world, ship);
|
||||
if (ship.Order is not null && controllerEvent == "arrived")
|
||||
{
|
||||
ship.Order = null;
|
||||
ship.ControllerTask.Kind = ControllerTaskKind.Idle;
|
||||
if (commander is not null)
|
||||
{
|
||||
commander.ActiveOrder = null;
|
||||
commander.ActiveTask = new CommanderTaskRuntime
|
||||
{
|
||||
Kind = ShipTaskKinds.Idle,
|
||||
Status = WorkStatus.Completed,
|
||||
TargetSystemId = ship.SystemId,
|
||||
Threshold = 0f,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_shipBehaviorStateMachine.ApplyEvent(engine, ship, world, controllerEvent);
|
||||
if (commander is not null)
|
||||
{
|
||||
SyncShipToCommander(ship, commander);
|
||||
if (commander.ActiveTask is not null)
|
||||
{
|
||||
commander.ActiveTask.Status = controllerEvent == "none" ? WorkStatus.Active : WorkStatus.Completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void TrackHistory(ShipRuntime ship, string controllerEvent)
|
||||
{
|
||||
var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}";
|
||||
if (signature == ship.LastSignature)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.LastSignature = signature;
|
||||
var target = ship.ControllerTask.TargetEntityId
|
||||
?? ship.ControllerTask.TargetSystemId
|
||||
?? "none";
|
||||
var eventSummary = controllerEvent == "none" ? string.Empty : $" event={controllerEvent}";
|
||||
ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind.ToContractValue()} target={target} cargo={GetShipCargoAmount(ship):0.#}{eventSummary}");
|
||||
if (ship.History.Count > 18)
|
||||
{
|
||||
ship.History.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
internal void EmitShipStateEvents(
|
||||
ShipRuntime ship,
|
||||
ShipState previousState,
|
||||
string previousBehavior,
|
||||
ControllerTaskKind previousTask,
|
||||
string controllerEvent,
|
||||
ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var occurredAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
if (previousState != ship.State)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (previousBehavior != ship.DefaultBehavior.Kind)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (previousTask != ship.ControllerTask.Kind)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (controllerEvent != "none")
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc));
|
||||
}
|
||||
}
|
||||
|
||||
internal static ControllerTaskRuntime CreateIdleTask(float threshold) =>
|
||||
new()
|
||||
{
|
||||
Kind = ControllerTaskKind.Idle,
|
||||
Threshold = threshold,
|
||||
};
|
||||
|
||||
private static ControllerTaskKind ParseControllerTaskKind(string kind) => kind switch
|
||||
{
|
||||
"travel" => ControllerTaskKind.Travel,
|
||||
"extract" => ControllerTaskKind.Extract,
|
||||
"dock" => ControllerTaskKind.Dock,
|
||||
"load" => ControllerTaskKind.Load,
|
||||
"unload" => ControllerTaskKind.Unload,
|
||||
"deliver-construction" => ControllerTaskKind.DeliverConstruction,
|
||||
"build-construction-site" => ControllerTaskKind.BuildConstructionSite,
|
||||
"attack-target" => ControllerTaskKind.AttackTarget,
|
||||
|
||||
"construct-module" => ControllerTaskKind.ConstructModule,
|
||||
"undock" => ControllerTaskKind.Undock,
|
||||
_ => ControllerTaskKind.Idle,
|
||||
};
|
||||
|
||||
private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task)
|
||||
{
|
||||
if (commander is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
commander.ActiveTask = new CommanderTaskRuntime
|
||||
{
|
||||
Kind = task.Kind.ToContractValue(),
|
||||
Status = task.Status,
|
||||
TargetEntityId = task.TargetEntityId,
|
||||
TargetNodeId = task.TargetNodeId,
|
||||
TargetPosition = task.TargetPosition,
|
||||
TargetSystemId = task.TargetSystemId,
|
||||
Threshold = task.Threshold,
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3 ResolveConstructionHoldPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world)
|
||||
{
|
||||
if (site is null || site.StationId is not null)
|
||||
{
|
||||
return GetConstructionHoldPosition(station, ship.Id);
|
||||
}
|
||||
|
||||
var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
|
||||
var anchorPosition = anchor?.Position ?? station.Position;
|
||||
return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
|
||||
}
|
||||
|
||||
private static AttackTargetCandidate? ResolveAttackTarget(ShipRuntime ship, SimulationWorld world)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ship.DefaultBehavior.TargetEntityId))
|
||||
{
|
||||
var direct = ResolveAttackTargetCandidate(world, ship.DefaultBehavior.TargetEntityId!);
|
||||
if (direct is not null && !string.Equals(direct.FactionId, ship.FactionId, StringComparison.Ordinal))
|
||||
{
|
||||
return direct;
|
||||
}
|
||||
}
|
||||
|
||||
var hostileShips = world.Ships
|
||||
.Where(candidate => candidate.Health > 0f && !string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
|
||||
.Select(candidate => new AttackTargetCandidate(candidate.Id, candidate.FactionId, candidate.SystemId, candidate.Position, 26f))
|
||||
.ToList();
|
||||
|
||||
var hostileStations = world.Stations
|
||||
.Where(candidate => !string.Equals(candidate.FactionId, ship.FactionId, StringComparison.Ordinal))
|
||||
.Select(candidate => new AttackTargetCandidate(candidate.Id, candidate.FactionId, candidate.SystemId, candidate.Position, candidate.Radius + 18f))
|
||||
.ToList();
|
||||
|
||||
var preferredSystemId = ship.DefaultBehavior.AreaSystemId;
|
||||
return hostileShips
|
||||
.Concat(hostileStations)
|
||||
.OrderBy(candidate => preferredSystemId is null || candidate.SystemId == preferredSystemId ? 0 : 1)
|
||||
.ThenBy(candidate => candidate.SystemId == ship.SystemId ? 0 : 1)
|
||||
.ThenBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static AttackTargetCandidate? ResolveAttackTargetCandidate(SimulationWorld world, string entityId)
|
||||
{
|
||||
var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == entityId && candidate.Health > 0f);
|
||||
if (ship is not null)
|
||||
{
|
||||
return new AttackTargetCandidate(ship.Id, ship.FactionId, ship.SystemId, ship.Position, 26f);
|
||||
}
|
||||
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == entityId);
|
||||
return station is null
|
||||
? null
|
||||
: new AttackTargetCandidate(station.Id, station.FactionId, station.SystemId, station.Position, station.Radius + 18f);
|
||||
}
|
||||
|
||||
private sealed record AttackTargetCandidate(string EntityId, string FactionId, string SystemId, Vector3 Position, float AttackRange);
|
||||
}
|
||||
@@ -1,592 +0,0 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
internal sealed partial class ShipTaskExecutionService
|
||||
{
|
||||
private static bool AdvanceTimedAction(ShipRuntime ship, float deltaSeconds, float requiredSeconds)
|
||||
{
|
||||
ship.ActionTimer += deltaSeconds;
|
||||
if (ship.ActionTimer < requiredSeconds)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ship.ActionTimer = 0f;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void BeginTrackedAction(ShipRuntime ship, string actionKey, float total)
|
||||
{
|
||||
if (ship.TrackedActionKey == actionKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.TrackedActionKey = actionKey;
|
||||
ship.TrackedActionTotal = MathF.Max(total, 0.01f);
|
||||
}
|
||||
|
||||
internal static float GetShipCargoAmount(ShipRuntime ship) =>
|
||||
SimulationRuntimeSupport.GetShipCargoAmount(ship);
|
||||
|
||||
private string UpdateExtract(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
var node = world.Nodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (node is null || task.TargetPosition is null || !CanExtractNode(ship, node, world))
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var cargoAmount = GetShipCargoAmount(ship);
|
||||
if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.CargoFull;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "cargo-full";
|
||||
}
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance > task.Threshold)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
|
||||
ship.State = ShipState.MiningApproach;
|
||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Mining;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.MiningCycleSeconds))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount);
|
||||
var mined = MathF.Min(world.Balance.MiningRate, remainingCapacity);
|
||||
mined = MathF.Min(mined, node.OreRemaining);
|
||||
if (mined <= 0.01f)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = node.OreRemaining <= 0.01f ? ShipState.NodeDepleted : ShipState.CargoFull;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return node.OreRemaining <= 0.01f ? "node-depleted" : "cargo-full";
|
||||
}
|
||||
|
||||
AddInventory(ship.Inventory, node.ItemId, mined);
|
||||
|
||||
node.OreRemaining -= mined;
|
||||
node.OreRemaining = MathF.Max(0f, node.OreRemaining);
|
||||
|
||||
return GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity ? "cargo-full" : "none";
|
||||
}
|
||||
|
||||
private string UpdateDock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (station is null || task.TargetPosition is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id);
|
||||
if (padIndex is null)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.AwaitingDock;
|
||||
ship.TargetPosition = GetDockingHoldPosition(station, ship.Id);
|
||||
var waitDistance = ship.Position.DistanceTo(ship.TargetPosition);
|
||||
if (waitDistance > 4f)
|
||||
{
|
||||
ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.AssignedDockingPadIndex = padIndex;
|
||||
var padPosition = GetDockingPadPosition(station, padIndex.Value);
|
||||
ship.TargetPosition = padPosition;
|
||||
var distance = ship.Position.DistanceTo(padPosition);
|
||||
if (distance > 4f)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
|
||||
ship.State = ShipState.DockingApproach;
|
||||
ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Docking;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.DockingDuration))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Docked;
|
||||
ship.DockedStationId = station.Id;
|
||||
station.DockedShipIds.Add(ship.Id);
|
||||
ship.Position = padPosition;
|
||||
ship.TargetPosition = padPosition;
|
||||
return "docked";
|
||||
}
|
||||
|
||||
private string UpdateUnload(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
if (ship.DockedStationId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
||||
if (station is null)
|
||||
{
|
||||
ship.DockedStationId = null;
|
||||
ship.AssignedDockingPadIndex = null;
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = GetShipDockedPosition(ship, station);
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.Transferring;
|
||||
BeginTrackedAction(ship, "transferring", GetShipCargoAmount(ship));
|
||||
|
||||
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId);
|
||||
var transferredAny = false;
|
||||
foreach (var (itemId, amount) in ship.Inventory.ToList())
|
||||
{
|
||||
var moved = MathF.Min(amount, world.Balance.TransferRate * deltaSeconds);
|
||||
var accepted = TryAddStationInventory(world, station, itemId, moved);
|
||||
transferredAny |= accepted > 0.01f;
|
||||
RemoveInventory(ship.Inventory, itemId, accepted);
|
||||
if (faction is not null && string.Equals(itemId, "ore", StringComparison.Ordinal))
|
||||
{
|
||||
faction.OreMined += accepted;
|
||||
faction.Credits += accepted * 0.4f;
|
||||
}
|
||||
}
|
||||
|
||||
if (!transferredAny && GetShipCargoAmount(ship) > 0.01f && HasShipCapabilities(ship.Definition, "mining"))
|
||||
{
|
||||
ship.Inventory.Clear();
|
||||
return "unloaded";
|
||||
}
|
||||
|
||||
return GetShipCargoAmount(ship) <= 0.01f ? "unloaded" : "none";
|
||||
}
|
||||
|
||||
private string UpdateLoadCargo(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
if (ship.DockedStationId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
||||
if (station is null)
|
||||
{
|
||||
ship.DockedStationId = null;
|
||||
ship.AssignedDockingPadIndex = null;
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = GetShipDockedPosition(ship, station);
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.Loading;
|
||||
var itemId = ship.ControllerTask.ItemId;
|
||||
BeginTrackedAction(ship, "loading", MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)));
|
||||
var transfer = MathF.Min(world.Balance.TransferRate * deltaSeconds, ship.Definition.CargoCapacity - GetShipCargoAmount(ship));
|
||||
var moved = itemId is null ? 0f : MathF.Min(transfer, GetInventoryAmount(station.Inventory, itemId));
|
||||
if (itemId is not null && moved > 0.01f)
|
||||
{
|
||||
RemoveInventory(station.Inventory, itemId, moved);
|
||||
AddInventory(ship.Inventory, itemId, moved);
|
||||
}
|
||||
|
||||
return itemId is null
|
||||
|| GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f
|
||||
|| GetInventoryAmount(station.Inventory, itemId) <= 0.01f
|
||||
? "loaded"
|
||||
: "none";
|
||||
}
|
||||
|
||||
private string UpdateConstructModule(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var station = ResolveShipSupportStation(ship, world);
|
||||
if (station is null || ship.DefaultBehavior.ModuleId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!world.ModuleRecipes.TryGetValue(ship.DefaultBehavior.ModuleId, out var recipe))
|
||||
{
|
||||
ship.AssignedDockingPadIndex = null;
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var supportPosition = ResolveShipSupportPosition(ship, station, null, world);
|
||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
||||
{
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!TryEnsureModuleConstructionStarted(station, recipe, ship.Id))
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.WaitingMaterials;
|
||||
ship.TargetPosition = supportPosition;
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (station.ActiveConstruction?.AssignedConstructorShipId != ship.Id)
|
||||
{
|
||||
ship.State = ShipState.ConstructionBlocked;
|
||||
ship.TargetPosition = supportPosition;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.Constructing;
|
||||
station.ActiveConstruction.ProgressSeconds += deltaSeconds;
|
||||
if (station.ActiveConstruction.ProgressSeconds < station.ActiveConstruction.RequiredSeconds)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
AddStationModule(world, station, station.ActiveConstruction.ModuleId);
|
||||
station.ActiveConstruction = null;
|
||||
return "module-constructed";
|
||||
}
|
||||
|
||||
private string UpdateDeliverConstruction(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var station = ResolveShipSupportStation(ship, world);
|
||||
if (station is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
|
||||
if (station is null || site is null || site.State != ConstructionSiteStateKinds.Active)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var supportPosition = ResolveShipSupportPosition(ship, station, site, world);
|
||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
||||
{
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.DeliveringConstruction;
|
||||
BeginTrackedAction(ship, "delivering-construction", GetRemainingConstructionDelivery(world, site));
|
||||
|
||||
if (site.StationId is not null)
|
||||
{
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
|
||||
var remaining = MathF.Max(0f, required.Value - delivered);
|
||||
if (remaining <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
||||
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
||||
moved = MathF.Min(moved, available);
|
||||
if (moved <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoveInventory(station.Inventory, required.Key, moved);
|
||||
AddInventory(site.Inventory, required.Key, moved);
|
||||
AddInventory(site.DeliveredItems, required.Key, moved);
|
||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
||||
}
|
||||
|
||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
||||
}
|
||||
|
||||
foreach (var required in site.RequiredItems)
|
||||
{
|
||||
var delivered = GetInventoryAmount(site.DeliveredItems, required.Key);
|
||||
var remaining = MathF.Max(0f, required.Value - delivered);
|
||||
if (remaining <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
||||
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
||||
moved = MathF.Min(moved, available);
|
||||
if (moved <= 0.01f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RemoveInventory(station.Inventory, required.Key, moved);
|
||||
AddInventory(site.Inventory, required.Key, moved);
|
||||
AddInventory(site.DeliveredItems, required.Key, moved);
|
||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
||||
}
|
||||
|
||||
return IsConstructionSiteReady(world, site) ? "construction-delivered" : "none";
|
||||
}
|
||||
|
||||
private string UpdateBuildConstructionSite(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var station = ResolveShipSupportStation(ship, world);
|
||||
if (station is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == ship.ControllerTask.TargetEntityId);
|
||||
if (station is null || site is null || site.BlueprintId is null || site.State != ConstructionSiteStateKinds.Active)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var supportPosition = ResolveShipSupportPosition(ship, station, site, world);
|
||||
if (!IsShipWithinSupportRange(ship, supportPosition, ship.ControllerTask.Threshold))
|
||||
{
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
|
||||
{
|
||||
ship.State = ShipState.WaitingMaterials;
|
||||
ship.TargetPosition = supportPosition;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = supportPosition;
|
||||
ship.Position = ship.TargetPosition;
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.Constructing;
|
||||
site.AssignedConstructorShipIds.Add(ship.Id);
|
||||
site.Progress += deltaSeconds;
|
||||
if (site.Progress < recipe.Duration)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (site.StationId is null)
|
||||
{
|
||||
CompleteStationFoundation(world, station, site);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddStationModule(world, station, site.BlueprintId);
|
||||
PrepareNextConstructionSiteStep(world, station, site);
|
||||
}
|
||||
|
||||
return "site-constructed";
|
||||
}
|
||||
|
||||
private StationRuntime? ResolveShipSupportStation(ShipRuntime ship, SimulationWorld world) =>
|
||||
ship.DockedStationId is not null
|
||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId)
|
||||
: ship.DefaultBehavior.Kind == "construct-station" && ship.DefaultBehavior.StationId is not null
|
||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DefaultBehavior.StationId)
|
||||
: null;
|
||||
|
||||
private static Vector3 ResolveShipSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world)
|
||||
{
|
||||
if (ship.DockedStationId is not null)
|
||||
{
|
||||
return GetShipDockedPosition(ship, station);
|
||||
}
|
||||
|
||||
if (site?.StationId is null && site is not null)
|
||||
{
|
||||
var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position;
|
||||
return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
|
||||
}
|
||||
|
||||
return GetConstructionHoldPosition(station, ship.Id);
|
||||
}
|
||||
|
||||
private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) =>
|
||||
ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f);
|
||||
|
||||
|
||||
private string UpdateUndock(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
if (ship.DockedStationId is null || task.TargetPosition is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
|
||||
var undockTarget = station is null
|
||||
? task.TargetPosition.Value
|
||||
: GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, world.Balance.UndockDistance);
|
||||
ship.TargetPosition = undockTarget;
|
||||
|
||||
ship.State = ShipState.Undocking;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, world.Balance.UndockingDuration))
|
||||
{
|
||||
if (station is not null)
|
||||
{
|
||||
ship.Position = GetShipDockedPosition(ship, station);
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.Position = ship.Position.MoveToward(undockTarget, world.Balance.UndockDistance);
|
||||
if (ship.Position.DistanceTo(undockTarget) > task.Threshold)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
if (station is not null)
|
||||
{
|
||||
station.DockedShipIds.Remove(ship.Id);
|
||||
ReleaseDockingPad(station, ship.Id);
|
||||
}
|
||||
|
||||
ship.DockedStationId = null;
|
||||
ship.AssignedDockingPadIndex = null;
|
||||
return "undocked";
|
||||
}
|
||||
|
||||
internal static float GetRemainingConstructionDelivery(SimulationWorld world, ConstructionSiteRuntime site) =>
|
||||
site.RequiredItems.Sum(required => MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key)));
|
||||
|
||||
private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site)
|
||||
{
|
||||
var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId);
|
||||
if (anchor is null || site.BlueprintId is null)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
return;
|
||||
}
|
||||
|
||||
var station = new StationRuntime
|
||||
{
|
||||
Id = $"station-{world.Stations.Count + 1}",
|
||||
SystemId = site.SystemId,
|
||||
Label = BuildFoundedStationLabel(site.TargetDefinitionId),
|
||||
Category = "station",
|
||||
Objective = DetermineFoundationObjective(site.TargetDefinitionId),
|
||||
Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color,
|
||||
Position = anchor.Position,
|
||||
FactionId = site.FactionId,
|
||||
CelestialId = site.CelestialId,
|
||||
Health = 600f,
|
||||
MaxHealth = 600f,
|
||||
};
|
||||
|
||||
foreach (var moduleId in GetFoundationModules(world, site.BlueprintId))
|
||||
{
|
||||
AddStationModule(world, station, moduleId);
|
||||
}
|
||||
|
||||
world.Stations.Add(station);
|
||||
StationLifecycleService.EnsureStationCommander(world, station);
|
||||
anchor.OccupyingStructureId = station.Id;
|
||||
site.StationId = station.Id;
|
||||
PrepareNextConstructionSiteStep(world, station, site);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetFoundationModules(SimulationWorld world, string primaryModuleId)
|
||||
{
|
||||
var modules = new List<string> { "module_arg_dock_m_01_lowtech" };
|
||||
foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, []))
|
||||
{
|
||||
if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
|
||||
{
|
||||
var storageModule = GetStorageRequirement(itemDefinition.CargoKind);
|
||||
if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add(storageModule);
|
||||
}
|
||||
else if (storageModule is null && !modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add("module_arg_stor_container_m_01");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal))
|
||||
{
|
||||
modules.Add("module_arg_stor_container_m_01");
|
||||
}
|
||||
|
||||
if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
||||
{
|
||||
modules.Add("module_gen_prod_energycells_01");
|
||||
}
|
||||
|
||||
modules.Add(primaryModuleId);
|
||||
return modules.Distinct(StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
private static string DetermineFoundationObjective(string commodityId) =>
|
||||
commodityId switch
|
||||
{
|
||||
"energycells" => "power",
|
||||
"water" => "water",
|
||||
"refinedmetals" => "refinery",
|
||||
"hullparts" => "hullparts",
|
||||
"claytronics" => "claytronics",
|
||||
"shipyard" => "shipyard",
|
||||
_ => "general",
|
||||
};
|
||||
|
||||
private static string BuildFoundedStationLabel(string commodityId) =>
|
||||
$"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry";
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
namespace SpaceGame.Api.Ships.Simulation;
|
||||
|
||||
internal sealed partial class ShipTaskExecutionService
|
||||
{
|
||||
private const float WarpEngageDistanceKilometers = 250_000f;
|
||||
private const float FrigateDps = 7f;
|
||||
private const float DestroyerDps = 12f;
|
||||
private const float CruiserDps = 18f;
|
||||
private const float CapitalDps = 26f;
|
||||
|
||||
private static float GetLocalTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed);
|
||||
|
||||
private static float GetWarpTravelSpeed(ShipRuntime ship) =>
|
||||
SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed);
|
||||
|
||||
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
|
||||
world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, systemId, StringComparison.Ordinal))?.Position
|
||||
?? Vector3.Zero;
|
||||
|
||||
internal string UpdateControllerTask(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
return task.Kind switch
|
||||
{
|
||||
ControllerTaskKind.Idle => UpdateIdle(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Travel => UpdateTravel(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Extract => UpdateExtract(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Dock => UpdateDock(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Load => UpdateLoadCargo(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Unload => UpdateUnload(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.DeliverConstruction => UpdateDeliverConstruction(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.BuildConstructionSite => UpdateBuildConstructionSite(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.AttackTarget => UpdateAttackTarget(ship, world, deltaSeconds),
|
||||
|
||||
ControllerTaskKind.ConstructModule => UpdateConstructModule(ship, world, deltaSeconds),
|
||||
ControllerTaskKind.Undock => UpdateUndock(ship, world, deltaSeconds),
|
||||
_ => UpdateIdle(ship, world, deltaSeconds),
|
||||
};
|
||||
}
|
||||
|
||||
private static string UpdateIdle(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
return UpdateTravel(ship, world, deltaSeconds, task);
|
||||
}
|
||||
|
||||
private string UpdateTravel(ShipRuntime ship, SimulationWorld world, float deltaSeconds, ControllerTaskRuntime task)
|
||||
{
|
||||
if (task.TargetPosition is null || task.TargetSystemId is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
// Resolve live position each frame — entities like stations orbit celestials and move every tick
|
||||
var targetPosition = ResolveCurrentTargetPosition(world, task);
|
||||
var targetCelestial = ResolveTravelTargetCelestial(world, task, targetPosition);
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
if (ship.SystemId != task.TargetSystemId)
|
||||
{
|
||||
if (!HasShipCapabilities(ship.Definition, "ftl"))
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
return "none";
|
||||
}
|
||||
|
||||
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, task.TargetSystemId);
|
||||
var destinationEntryPosition = destinationEntryCelestial?.Position ?? Vector3.Zero;
|
||||
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryCelestial);
|
||||
}
|
||||
|
||||
var currentCelestial = ResolveCurrentCelestial(world, ship);
|
||||
if (targetCelestial is not null && currentCelestial is not null && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
|
||||
{
|
||||
if (!HasShipCapabilities(ship.Definition, "warp"))
|
||||
{
|
||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
|
||||
}
|
||||
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
||||
}
|
||||
|
||||
if (targetCelestial is not null
|
||||
&& distance > WarpEngageDistanceKilometers
|
||||
&& HasShipCapabilities(ship.Definition, "warp"))
|
||||
{
|
||||
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
|
||||
}
|
||||
|
||||
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
|
||||
}
|
||||
|
||||
private string UpdateAttackTarget(ShipRuntime ship, SimulationWorld world, float deltaSeconds)
|
||||
{
|
||||
var task = ship.ControllerTask;
|
||||
if (string.IsNullOrWhiteSpace(task.TargetEntityId))
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "target-lost";
|
||||
}
|
||||
|
||||
var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId && candidate.Health > 0f);
|
||||
var hostileStation = hostileShip is null
|
||||
? world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId)
|
||||
: null;
|
||||
|
||||
if ((hostileShip is not null && string.Equals(hostileShip.FactionId, ship.FactionId, StringComparison.Ordinal))
|
||||
|| (hostileStation is not null && string.Equals(hostileStation.FactionId, ship.FactionId, StringComparison.Ordinal)))
|
||||
{
|
||||
return "target-lost";
|
||||
}
|
||||
|
||||
if (hostileShip is null && hostileStation is null)
|
||||
{
|
||||
ship.State = ShipState.Idle;
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "target-lost";
|
||||
}
|
||||
|
||||
var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId;
|
||||
var targetPosition = hostileShip?.Position ?? hostileStation!.Position;
|
||||
var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f;
|
||||
var attackTask = new ControllerTaskRuntime
|
||||
{
|
||||
Kind = ControllerTaskKind.Travel,
|
||||
TargetEntityId = task.TargetEntityId,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
Threshold = attackRange,
|
||||
};
|
||||
|
||||
if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange)
|
||||
{
|
||||
return UpdateTravel(ship, world, deltaSeconds, attackTask);
|
||||
}
|
||||
|
||||
ship.State = ShipState.EngagingTarget;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f));
|
||||
var damage = GetShipDamagePerSecond(ship) * deltaSeconds;
|
||||
|
||||
if (hostileShip is not null)
|
||||
{
|
||||
hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage);
|
||||
return hostileShip.Health <= 0f ? "target-destroyed" : "none";
|
||||
}
|
||||
|
||||
hostileStation!.Health = MathF.Max(0f, hostileStation.Health - damage * 0.6f);
|
||||
return hostileStation.Health <= 0f ? "target-destroyed" : "none";
|
||||
}
|
||||
|
||||
private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ControllerTaskRuntime task)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
||||
{
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (station is not null)
|
||||
{
|
||||
return station.Position;
|
||||
}
|
||||
|
||||
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (celestial is not null)
|
||||
{
|
||||
return celestial.Position;
|
||||
}
|
||||
}
|
||||
|
||||
return task.TargetPosition!.Value;
|
||||
}
|
||||
|
||||
private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
|
||||
{
|
||||
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (station?.CelestialId is not null)
|
||||
{
|
||||
return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId);
|
||||
}
|
||||
|
||||
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
|
||||
if (celestial is not null)
|
||||
{
|
||||
return celestial;
|
||||
}
|
||||
}
|
||||
|
||||
return world.Celestials
|
||||
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
if (ship.SpatialState.CurrentCelestialId is not null)
|
||||
{
|
||||
return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId);
|
||||
}
|
||||
|
||||
return world.Celestials
|
||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
|
||||
world.Celestials.FirstOrDefault(candidate =>
|
||||
candidate.SystemId == systemId &&
|
||||
candidate.Kind == SpatialNodeKind.Star);
|
||||
|
||||
private string UpdateLocalTravel(
|
||||
ShipRuntime ship,
|
||||
SimulationWorld world,
|
||||
float deltaSeconds,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
CelestialRuntime? targetCelestial,
|
||||
float threshold)
|
||||
{
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
|
||||
if (distance <= threshold)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = ship.Position;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
|
||||
ship.ActionTimer = 0f;
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
|
||||
return "none";
|
||||
}
|
||||
|
||||
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, CelestialRuntime targetCelestial)
|
||||
{
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id)
|
||||
{
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.Warp,
|
||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
||||
DestinationNodeId = targetCelestial.Id,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
|
||||
ship.SpatialState.CurrentCelestialId = null;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial.Id;
|
||||
|
||||
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
if (ship.State != ShipState.Warping)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingWarp)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
}
|
||||
|
||||
ship.State = ShipState.SpoolingWarp;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, spoolDuration))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Warping;
|
||||
}
|
||||
|
||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
||||
? ship.Position.DistanceTo(targetPosition)
|
||||
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
||||
return ship.Position.DistanceTo(targetPosition) <= 18f
|
||||
? CompleteTransitArrival(ship, targetCelestial.SystemId, targetPosition, targetCelestial)
|
||||
: "none";
|
||||
}
|
||||
|
||||
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
||||
{
|
||||
var destinationNodeId = targetCelestial?.Id;
|
||||
var transit = ship.SpatialState.Transit;
|
||||
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
|
||||
{
|
||||
transit = new ShipTransitRuntime
|
||||
{
|
||||
Regime = MovementRegimeKinds.FtlTransit,
|
||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
||||
DestinationNodeId = destinationNodeId,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
|
||||
ship.SpatialState.CurrentCelestialId = null;
|
||||
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
||||
|
||||
if (ship.State != ShipState.Ftl)
|
||||
{
|
||||
if (ship.State != ShipState.SpoolingFtl)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
}
|
||||
|
||||
ship.State = ShipState.SpoolingFtl;
|
||||
if (!AdvanceTimedAction(ship, deltaSeconds, ship.Definition.SpoolTime))
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.State = ShipState.Ftl;
|
||||
}
|
||||
|
||||
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
|
||||
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
|
||||
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
|
||||
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
|
||||
return transit.Progress >= 0.999f
|
||||
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetCelestial)
|
||||
: "none";
|
||||
}
|
||||
|
||||
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "arrived";
|
||||
}
|
||||
|
||||
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
|
||||
{
|
||||
ship.ActionTimer = 0f;
|
||||
ship.Position = targetPosition;
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
|
||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
ship.State = ShipState.Arriving;
|
||||
return "none";
|
||||
}
|
||||
|
||||
private static float GetShipDamagePerSecond(ShipRuntime ship) =>
|
||||
ship.Definition.Class switch
|
||||
{
|
||||
"frigate" => FrigateDps,
|
||||
"destroyer" => DestroyerDps,
|
||||
"cruiser" => CruiserDps,
|
||||
"capital" => CapitalDps,
|
||||
_ => 4f,
|
||||
};
|
||||
}
|
||||
@@ -6,11 +6,12 @@ public sealed class SimulationEngine
|
||||
private readonly OrbitalSimulationOptions _orbitalSimulation;
|
||||
private readonly OrbitalStateUpdater _orbitalStateUpdater;
|
||||
private readonly InfrastructureSimulationService _infrastructureSimulation;
|
||||
private readonly GeopoliticalSimulationService _geopolitics;
|
||||
private readonly CommanderPlanningService _commanderPlanning;
|
||||
private readonly PlayerFactionService _playerFaction;
|
||||
private readonly StationSimulationService _stationSimulation;
|
||||
private readonly StationLifecycleService _stationLifecycle;
|
||||
private readonly ShipControlService _shipControl;
|
||||
private readonly ShipTaskExecutionService _shipTaskExecution;
|
||||
private readonly ShipAiService _shipAi;
|
||||
private readonly SimulationProjectionService _projection;
|
||||
|
||||
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
|
||||
@@ -18,11 +19,12 @@ public sealed class SimulationEngine
|
||||
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
|
||||
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
|
||||
_infrastructureSimulation = new InfrastructureSimulationService();
|
||||
_geopolitics = new GeopoliticalSimulationService();
|
||||
_commanderPlanning = new CommanderPlanningService();
|
||||
_playerFaction = new PlayerFactionService();
|
||||
_stationSimulation = new StationSimulationService();
|
||||
_stationLifecycle = new StationLifecycleService(_stationSimulation);
|
||||
_shipControl = new ShipControlService();
|
||||
_shipTaskExecution = new ShipTaskExecutionService();
|
||||
_shipAi = new ShipAiService();
|
||||
_projection = new SimulationProjectionService(_orbitalSimulation);
|
||||
}
|
||||
|
||||
@@ -31,13 +33,16 @@ public sealed class SimulationEngine
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var events = new List<SimulationEventRecord>();
|
||||
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
|
||||
world.GeneratedAtUtc = nowUtc;
|
||||
|
||||
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||
|
||||
_orbitalStateUpdater.Update(world);
|
||||
_infrastructureSimulation.UpdateClaims(world, events);
|
||||
_infrastructureSimulation.UpdateConstructionSites(world, events);
|
||||
_commanderPlanning.UpdateCommanders(this, world, simulationDeltaSeconds, events);
|
||||
_geopolitics.Update(world, simulationDeltaSeconds, events);
|
||||
_commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events);
|
||||
_playerFaction.Update(world, simulationDeltaSeconds, events);
|
||||
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
|
||||
|
||||
foreach (var ship in world.Ships.ToList())
|
||||
@@ -48,25 +53,12 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
var previousPosition = ship.Position;
|
||||
var previousState = ship.State;
|
||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
||||
var previousTask = ship.ControllerTask.Kind;
|
||||
|
||||
_shipControl.RefreshControlLayers(ship, world);
|
||||
_shipControl.PlanControllerTask(this, ship, world);
|
||||
|
||||
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, simulationDeltaSeconds);
|
||||
|
||||
_shipControl.AdvanceControlState(this, ship, world, controllerEvent);
|
||||
_shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events);
|
||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
|
||||
_shipControl.TrackHistory(ship, controllerEvent);
|
||||
_shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
||||
}
|
||||
|
||||
_orbitalStateUpdater.SyncSpatialState(world);
|
||||
CleanupDestroyedEntities(world, events);
|
||||
world.GeneratedAtUtc = nowUtc;
|
||||
|
||||
return _projection.BuildDelta(world, sequence, events);
|
||||
}
|
||||
|
||||
@@ -76,18 +68,6 @@ public sealed class SimulationEngine
|
||||
public void PrimeDeltaBaseline(SimulationWorld world) =>
|
||||
_projection.PrimeDeltaBaseline(world);
|
||||
|
||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule) =>
|
||||
_shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
||||
|
||||
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) =>
|
||||
_shipControl.PlanStationConstruction(ship, world);
|
||||
|
||||
internal void PlanAttackTarget(ShipRuntime ship, SimulationWorld world) =>
|
||||
_shipControl.PlanAttackTarget(ship, world);
|
||||
|
||||
internal void PlanTransportHaul(ShipRuntime ship, SimulationWorld world) =>
|
||||
_shipControl.PlanTransportHaul(ship, world);
|
||||
|
||||
internal static float GetShipCargoAmount(ShipRuntime ship) =>
|
||||
SimulationRuntimeSupport.GetShipCargoAmount(ship);
|
||||
|
||||
@@ -95,6 +75,7 @@ public sealed class SimulationEngine
|
||||
{
|
||||
foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
|
||||
{
|
||||
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f));
|
||||
world.Ships.Remove(ship);
|
||||
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
|
||||
{
|
||||
@@ -117,6 +98,7 @@ public sealed class SimulationEngine
|
||||
|
||||
foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())
|
||||
{
|
||||
CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
|
||||
world.Stations.Remove(station);
|
||||
|
||||
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
|
||||
@@ -138,4 +120,29 @@ public sealed class SimulationEngine
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount)
|
||||
{
|
||||
var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
|
||||
? "scrapmetal"
|
||||
: world.ItemDefinitions.ContainsKey("rawscrap")
|
||||
? "rawscrap"
|
||||
: world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
|
||||
if (itemId is null || amount <= 0.01f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
world.Wrecks.Add(new WreckRuntime
|
||||
{
|
||||
Id = $"wreck-{sourceKind}-{sourceEntityId}",
|
||||
SourceKind = sourceKind,
|
||||
SourceEntityId = sourceEntityId,
|
||||
SystemId = systemId,
|
||||
Position = position,
|
||||
ItemId = itemId,
|
||||
RemainingAmount = amount,
|
||||
MaxAmount = amount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FastEndpoints" Version="6.*" />
|
||||
<PackageReference Include="FastEndpoints.Swagger" Version="6.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using static SpaceGame.Api.Ships.Simulation.ShipControlService;
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
|
||||
namespace SpaceGame.Api.Stations.Simulation;
|
||||
@@ -80,7 +79,7 @@ internal sealed class StationLifecycleService
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
|
||||
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
|
||||
ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold),
|
||||
Skills = WorldSeedingService.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
};
|
||||
|
||||
@@ -109,13 +108,22 @@ internal sealed class StationLifecycleService
|
||||
{
|
||||
if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime { Kind = "idle" };
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle",
|
||||
HomeSystemId = station.SystemId,
|
||||
HomeStationId = station.Id,
|
||||
MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
var patrolRadius = station.Radius + 90f;
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
HomeSystemId = station.SystemId,
|
||||
HomeStationId = station.Id,
|
||||
AreaSystemId = station.SystemId,
|
||||
PatrolPoints =
|
||||
[
|
||||
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
|
||||
@@ -150,7 +158,13 @@ internal sealed class StationLifecycleService
|
||||
ParentCommanderId = factionCommander.Id,
|
||||
ControlledEntityId = station.Id,
|
||||
PolicySetId = factionCommander.PolicySetId,
|
||||
Doctrine = "station-default",
|
||||
Doctrine = "station-control",
|
||||
Skills = new CommanderSkillProfileRuntime
|
||||
{
|
||||
Leadership = 3,
|
||||
Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5),
|
||||
Strategy = 3,
|
||||
},
|
||||
};
|
||||
|
||||
station.CommanderId = commander.Id;
|
||||
@@ -179,25 +193,12 @@ internal sealed class StationLifecycleService
|
||||
ParentCommanderId = factionCommander.Id,
|
||||
ControlledEntityId = ship.Id,
|
||||
PolicySetId = factionCommander.PolicySetId,
|
||||
Doctrine = "ship-default",
|
||||
ActiveBehavior = new CommanderBehaviorRuntime
|
||||
Doctrine = "ship-control",
|
||||
Skills = new CommanderSkillProfileRuntime
|
||||
{
|
||||
Kind = ship.DefaultBehavior.Kind,
|
||||
AreaSystemId = ship.DefaultBehavior.AreaSystemId,
|
||||
TargetEntityId = ship.DefaultBehavior.TargetEntityId,
|
||||
ItemId = ship.DefaultBehavior.ItemId,
|
||||
StationId = ship.DefaultBehavior.StationId,
|
||||
ModuleId = ship.DefaultBehavior.ModuleId,
|
||||
NodeId = ship.DefaultBehavior.NodeId,
|
||||
Phase = ship.DefaultBehavior.Phase,
|
||||
PatrolIndex = ship.DefaultBehavior.PatrolIndex,
|
||||
},
|
||||
ActiveTask = new CommanderTaskRuntime
|
||||
{
|
||||
Kind = ShipTaskKinds.Idle,
|
||||
Status = WorkStatus.Pending,
|
||||
TargetSystemId = ship.SystemId,
|
||||
Threshold = 0f,
|
||||
Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5),
|
||||
Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5),
|
||||
Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ internal sealed class StationSimulationService
|
||||
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
|
||||
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
|
||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
||||
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
|
||||
? 90f
|
||||
: 0f;
|
||||
|
||||
@@ -104,6 +104,7 @@ internal sealed class StationSimulationService
|
||||
AddSupplyOrder(desiredOrders, station, "superfluidcoolant", ScaleSupplyTriggerByEconomy(economy, "superfluidcoolant", MathF.Max(superfluidCoolantReserve * 1.35f, superfluidCoolantReserve + 30f)), reserveFloor: superfluidCoolantReserve, valuationBase: ScaleSupplyValuation(economy, "superfluidcoolant", 0.9f));
|
||||
AddSupplyOrder(desiredOrders, station, "quantumtubes", ScaleSupplyTriggerByEconomy(economy, "quantumtubes", MathF.Max(quantumTubesReserve * 1.35f, quantumTubesReserve + 30f)), reserveFloor: quantumTubesReserve, valuationBase: ScaleSupplyValuation(economy, "quantumtubes", 0.9f));
|
||||
|
||||
desiredOrders = ApplyRegionalMarketModifiers(world, station, desiredOrders);
|
||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ internal sealed class StationSimulationService
|
||||
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
||||
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
||||
&& GetShipProductionPressure(world, station.FactionId, "military") > 0.2f
|
||||
? 90f
|
||||
: 0f;
|
||||
|
||||
@@ -257,8 +258,9 @@ internal sealed class StationSimulationService
|
||||
var priority = (float)recipe.Priority;
|
||||
|
||||
var expansionPressure = GetFactionExpansionPressure(world, station.FactionId);
|
||||
var fleetPressure = FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military") ? 1f : 0f;
|
||||
var fleetPressure = GetShipProductionPressure(world, station.FactionId, "military");
|
||||
priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure);
|
||||
priority += GetStrategicRecipeBias(world, station, recipe);
|
||||
|
||||
return priority;
|
||||
}
|
||||
@@ -321,6 +323,52 @@ internal sealed class StationSimulationService
|
||||
};
|
||||
}
|
||||
|
||||
private static float GetStrategicRecipeBias(SimulationWorld world, StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var commander = station.CommanderId is null
|
||||
? null
|
||||
: world.Commanders.FirstOrDefault(candidate => candidate.Id == station.CommanderId);
|
||||
var assignment = commander?.Assignment;
|
||||
if (assignment is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
var outputItemIds = recipe.Outputs
|
||||
.Select(output => output.ItemId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (string.Equals(assignment.Kind, "ship-production-focus", StringComparison.Ordinal)
|
||||
&& recipe.ShipOutputId is not null
|
||||
&& world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)
|
||||
&& string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal))
|
||||
{
|
||||
return 260f;
|
||||
}
|
||||
|
||||
if (string.Equals(assignment.Kind, "commodity-focus", StringComparison.Ordinal)
|
||||
&& assignment.ItemId is not null
|
||||
&& outputItemIds.Contains(assignment.ItemId))
|
||||
{
|
||||
return 220f;
|
||||
}
|
||||
|
||||
if (string.Equals(assignment.Kind, "expansion-support", StringComparison.Ordinal)
|
||||
&& outputItemIds.Overlaps(["energycells", "refinedmetals", "hullparts", "claytronics"]))
|
||||
{
|
||||
return 180f;
|
||||
}
|
||||
|
||||
if (string.Equals(assignment.Kind, "station-oversight", StringComparison.Ordinal)
|
||||
&& assignment.ItemId is not null
|
||||
&& outputItemIds.Contains(assignment.ItemId))
|
||||
{
|
||||
return 90f;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
internal static bool RecipeAppliesToStation(StationRuntime station, RecipeDefinition recipe)
|
||||
{
|
||||
var categoryMatch = string.Equals(recipe.FacilityCategory, "station", StringComparison.Ordinal)
|
||||
@@ -338,7 +386,7 @@ internal sealed class StationSimulationService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, shipDefinition.Kind))
|
||||
if (GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind) <= 0.05f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -559,12 +607,20 @@ internal sealed class StationSimulationService
|
||||
var targetSystems = Math.Max(1, Math.Min(StrategicControlTargetSystems, world.Systems.Count));
|
||||
var controlledSystems = GetFactionControlledSystemsCount(world, factionId);
|
||||
var deficit = Math.Max(0, targetSystems - controlledSystems);
|
||||
return Math.Clamp(deficit / (float)targetSystems, 0f, 1f);
|
||||
var contestedSystems = world.Geopolitics?.Territory.ControlStates.Count(state =>
|
||||
state.IsContested
|
||||
&& (string.Equals(state.ControllerFactionId, factionId, StringComparison.Ordinal)
|
||||
|| string.Equals(state.PrimaryClaimantFactionId, factionId, StringComparison.Ordinal)
|
||||
|| state.ClaimantFactionIds.Contains(factionId, StringComparer.Ordinal))) ?? 0;
|
||||
var frontierSystems = world.Geopolitics?.Territory.Zones.Count(zone =>
|
||||
string.Equals(zone.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& zone.Kind is "frontier" or "corridor" or "contested") ?? 0;
|
||||
return Math.Clamp((deficit / (float)targetSystems) + (contestedSystems * 0.12f) + (frontierSystems * 0.04f), 0f, 1f);
|
||||
}
|
||||
|
||||
internal static int GetFactionControlledSystemsCount(SimulationWorld world, string factionId)
|
||||
{
|
||||
return world.Systems.Count(system => FactionControlsSystem(world, factionId, system.Definition.Id));
|
||||
return GeopoliticalSimulationService.GetControlledSystems(world, factionId).Count;
|
||||
}
|
||||
|
||||
private static float ScaleReserveByEconomy(FactionEconomySnapshot economy, string itemId, float baseReserve)
|
||||
@@ -612,34 +668,66 @@ internal sealed class StationSimulationService
|
||||
: baseValuation;
|
||||
}
|
||||
|
||||
private static List<DesiredMarketOrder> ApplyRegionalMarketModifiers(SimulationWorld world, StationRuntime station, IReadOnlyCollection<DesiredMarketOrder> desiredOrders)
|
||||
{
|
||||
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, station.FactionId, station.SystemId);
|
||||
if (region is null)
|
||||
{
|
||||
return desiredOrders.ToList();
|
||||
}
|
||||
|
||||
var security = world.Geopolitics?.EconomyRegions.SecurityAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal));
|
||||
var economic = world.Geopolitics?.EconomyRegions.EconomicAssessments.FirstOrDefault(assessment => string.Equals(assessment.RegionId, region.Id, StringComparison.Ordinal));
|
||||
var bottlenecks = world.Geopolitics?.EconomyRegions.Bottlenecks
|
||||
.Where(bottleneck => string.Equals(bottleneck.RegionId, region.Id, StringComparison.Ordinal))
|
||||
.ToDictionary(bottleneck => bottleneck.ItemId, StringComparer.Ordinal) ?? new Dictionary<string, RegionalBottleneckRuntime>(StringComparer.Ordinal);
|
||||
var riskMultiplier = 1f + ((security?.SupplyRisk ?? 0f) * 0.3f) + ((security?.AccessFriction ?? 0f) * 0.2f);
|
||||
var sustainmentFloor = 1f + MathF.Max(0f, 0.55f - (economic?.SustainmentScore ?? 1f));
|
||||
|
||||
return desiredOrders
|
||||
.Select(order =>
|
||||
{
|
||||
bottlenecks.TryGetValue(order.ItemId, out var bottleneck);
|
||||
var severity = bottleneck?.Severity ?? 0f;
|
||||
var buyBias = order.Kind == MarketOrderKinds.Buy ? 1f + (severity * 0.08f) : 1f;
|
||||
var sellBias = order.Kind == MarketOrderKinds.Sell && severity > 0f ? MathF.Max(0.35f, 1f - (severity * 0.07f)) : 1f;
|
||||
var amount = order.Amount * (order.Kind == MarketOrderKinds.Buy ? riskMultiplier * buyBias * sustainmentFloor : sellBias);
|
||||
var valuation = order.Valuation * (order.Kind == MarketOrderKinds.Buy
|
||||
? 1f + (severity * 0.06f) + ((security?.SupplyRisk ?? 0f) * 0.18f)
|
||||
: 1f + (severity * 0.04f));
|
||||
float? reserveThreshold = order.ReserveThreshold.HasValue
|
||||
? order.ReserveThreshold.Value * (1f + ((security?.SupplyRisk ?? 0f) * 0.15f))
|
||||
: null;
|
||||
return new DesiredMarketOrder(order.Kind, order.ItemId, amount, valuation, reserveThreshold);
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind)
|
||||
{
|
||||
var factionCommander = FindFactionCommander(world, factionId);
|
||||
var task = GetHighestPriorityIssuedTask(factionCommander, FactionIssuedTaskKind.ProduceShips, shipKind);
|
||||
if (task is null)
|
||||
var economic = FindFactionEconomicAssessment(world, factionId);
|
||||
var threat = FindFactionThreatAssessment(world, factionId);
|
||||
if (economic is null || threat is null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return task.State == FactionIssuedTaskState.Blocked ? 0.4f : 1f;
|
||||
return shipKind switch
|
||||
{
|
||||
"military" => threat.EnemyFactionCount > 0
|
||||
? economic.MilitaryShipCount < Math.Max(4, economic.ControlledSystemCount * 2) ? 1f : 0.25f
|
||||
: 0.1f,
|
||||
"construction" => economic.PrimaryExpansionSiteId is not null
|
||||
? economic.ConstructorShipCount < 1 ? 1f : 0.35f
|
||||
: economic.ConstructorShipCount < 1 ? 0.5f : 0f,
|
||||
"transport" => economic.TransportShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.8f : 0.2f,
|
||||
_ when shipKind == "mining" || shipKind == "miner" => economic.MinerShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.85f : 0.2f,
|
||||
_ => 0.15f,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
|
||||
{
|
||||
var totalLagrangePoints = world.Celestials.Count(node =>
|
||||
node.SystemId == systemId &&
|
||||
node.Kind == SpatialNodeKind.LagrangePoint);
|
||||
if (totalLagrangePoints == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ownedLocations = world.Claims.Count(claim =>
|
||||
claim.SystemId == systemId &&
|
||||
claim.FactionId == factionId &&
|
||||
claim.State is ClaimStateKinds.Activating or ClaimStateKinds.Active);
|
||||
return ownedLocations > (totalLagrangePoints / 2f);
|
||||
}
|
||||
=> GeopoliticalSimulationService.FactionControlsSystem(world, factionId, systemId);
|
||||
|
||||
private sealed record DesiredMarketOrder(string Kind, string ItemId, float Amount, float Valuation, float? ReserveThreshold);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ public sealed record WorldSnapshot(
|
||||
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
|
||||
IReadOnlyList<PolicySetSnapshot> Policies,
|
||||
IReadOnlyList<ShipSnapshot> Ships,
|
||||
IReadOnlyList<FactionSnapshot> Factions);
|
||||
IReadOnlyList<FactionSnapshot> Factions,
|
||||
PlayerFactionSnapshot? PlayerFaction,
|
||||
GeopoliticalStateSnapshot? Geopolitics);
|
||||
|
||||
public sealed record WorldDelta(
|
||||
long Sequence,
|
||||
@@ -36,6 +38,8 @@ public sealed record WorldDelta(
|
||||
IReadOnlyList<PolicySetDelta> Policies,
|
||||
IReadOnlyList<ShipDelta> Ships,
|
||||
IReadOnlyList<FactionDelta> Factions,
|
||||
PlayerFactionSnapshot? PlayerFaction,
|
||||
GeopoliticalStateSnapshot? Geopolitics,
|
||||
ObserverScope? Scope = null);
|
||||
|
||||
public sealed record SimulationEventRecord(
|
||||
|
||||
@@ -9,9 +9,12 @@ public sealed class SimulationWorld
|
||||
public required List<SystemRuntime> Systems { get; init; }
|
||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||
public required List<CelestialRuntime> Celestials { get; init; }
|
||||
public required List<WreckRuntime> Wrecks { get; init; }
|
||||
public required List<StationRuntime> Stations { get; init; }
|
||||
public required List<ShipRuntime> Ships { get; init; }
|
||||
public required List<FactionRuntime> Factions { get; init; }
|
||||
public PlayerFactionRuntime? PlayerFaction { get; set; }
|
||||
public GeopoliticalStateRuntime? Geopolitics { get; set; }
|
||||
public required List<CommanderRuntime> Commanders { get; init; }
|
||||
public required List<ClaimRuntime> Claims { get; init; }
|
||||
public required List<ConstructionSiteRuntime> ConstructionSites { get; init; }
|
||||
|
||||
@@ -36,6 +36,18 @@ public sealed class CelestialRuntime
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class WreckRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SourceKind { get; init; }
|
||||
public required string SourceEntityId { get; init; }
|
||||
public required string SystemId { get; set; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required string ItemId { get; set; }
|
||||
public float RemainingAmount { get; set; }
|
||||
public float MaxAmount { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ShipSpatialStateRuntime
|
||||
{
|
||||
public string SpaceLayer { get; set; } = SpaceLayerKinds.LocalSpace;
|
||||
|
||||
@@ -15,10 +15,16 @@ internal sealed class WorldBuilder(
|
||||
var systems = generationService.ExpandSystems(
|
||||
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
|
||||
worldGeneration.TargetSystemCount);
|
||||
|
||||
Console.WriteLine("TEST");
|
||||
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
|
||||
|
||||
var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
|
||||
catalog.Scenario,
|
||||
systems.Select(system => system.Id).ToList());
|
||||
|
||||
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
|
||||
|
||||
var systemRuntimes = systems
|
||||
.Select(definition => new SystemRuntime
|
||||
{
|
||||
@@ -42,11 +48,29 @@ internal sealed class WorldBuilder(
|
||||
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
|
||||
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
|
||||
|
||||
if (worldGeneration.AiControllerFactionCount < int.MaxValue)
|
||||
{
|
||||
var aiFactionIds = stations
|
||||
.Select(s => s.FactionId)
|
||||
.Concat(ships.Select(s => s.FactionId))
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(id => id, StringComparer.Ordinal)
|
||||
.Take(worldGeneration.AiControllerFactionCount)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
aiFactionIds.Add(DefaultFactionId);
|
||||
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
|
||||
}
|
||||
|
||||
var factions = seedingService.CreateFactions(stations, ships);
|
||||
seedingService.BootstrapFactionEconomy(factions, stations);
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, stations, ships);
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var playerFaction = worldGeneration.GeneratePlayerFaction
|
||||
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
|
||||
: null;
|
||||
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
|
||||
var bootstrapWorld = new SimulationWorld
|
||||
{
|
||||
@@ -56,9 +80,11 @@ internal sealed class WorldBuilder(
|
||||
Systems = systemRuntimes,
|
||||
Celestials = spatialLayout.Celestials,
|
||||
Nodes = spatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = stations,
|
||||
Ships = ships,
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = [],
|
||||
@@ -75,7 +101,7 @@ internal sealed class WorldBuilder(
|
||||
};
|
||||
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
|
||||
|
||||
return new SimulationWorld
|
||||
var world = new SimulationWorld
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = WorldSeed,
|
||||
@@ -83,9 +109,12 @@ internal sealed class WorldBuilder(
|
||||
Systems = systemRuntimes,
|
||||
Celestials = spatialLayout.Celestials,
|
||||
Nodes = spatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
Stations = stations,
|
||||
Ships = ships,
|
||||
Factions = factions,
|
||||
PlayerFaction = playerFaction,
|
||||
Geopolitics = null,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = constructionSites,
|
||||
@@ -100,6 +129,10 @@ internal sealed class WorldBuilder(
|
||||
OrbitalTimeSeconds = WorldSeed * 97d,
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
var geopolitics = new GeopoliticalSimulationService();
|
||||
geopolitics.Update(world, 0f, []);
|
||||
return world;
|
||||
}
|
||||
|
||||
private static List<StationRuntime> CreateStations(
|
||||
@@ -291,7 +324,7 @@ internal sealed class WorldBuilder(
|
||||
patrolRoutes,
|
||||
stations,
|
||||
refinery),
|
||||
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
|
||||
Skills = WorldSeedingService.CreateSkills(definition),
|
||||
Health = definition.MaxHealth,
|
||||
});
|
||||
|
||||
|
||||
@@ -286,16 +286,9 @@ internal sealed class WorldSeedingService
|
||||
FactionId = faction.Id,
|
||||
ControlledEntityId = faction.Id,
|
||||
PolicySetId = faction.DefaultPolicySetId,
|
||||
Doctrine = "strategic-expansionist",
|
||||
Doctrine = "strategic-control",
|
||||
};
|
||||
|
||||
commander.Goals.Add("control-all-systems");
|
||||
commander.Goals.Add("control-five-systems-fast");
|
||||
commander.Goals.Add("expand-industrial-base");
|
||||
commander.Goals.Add("grow-war-fleet");
|
||||
commander.Goals.Add("deter-pirate-harassment");
|
||||
commander.Goals.Add("contest-rival-expansion");
|
||||
|
||||
commanders.Add(commander);
|
||||
factionCommanders[faction.Id] = commander;
|
||||
faction.CommanderIds.Add(commander.Id);
|
||||
@@ -316,7 +309,7 @@ internal sealed class WorldSeedingService
|
||||
ParentCommanderId = parentCommander.Id,
|
||||
ControlledEntityId = station.Id,
|
||||
PolicySetId = parentCommander.PolicySetId,
|
||||
Doctrine = "station-default",
|
||||
Doctrine = "station-control",
|
||||
};
|
||||
|
||||
station.CommanderId = commander.Id;
|
||||
@@ -341,16 +334,9 @@ internal sealed class WorldSeedingService
|
||||
ParentCommanderId = parentCommander.Id,
|
||||
ControlledEntityId = ship.Id,
|
||||
PolicySetId = parentCommander.PolicySetId,
|
||||
Doctrine = "ship-default",
|
||||
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
|
||||
ActiveTask = CopyTask(ship.ControllerTask, null),
|
||||
Doctrine = "ship-control",
|
||||
};
|
||||
|
||||
if (ship.Order is not null)
|
||||
{
|
||||
commander.ActiveOrder = CopyOrder(ship.Order);
|
||||
}
|
||||
|
||||
ship.CommanderId = commander.Id;
|
||||
ship.PolicySetId = parentCommander.PolicySetId;
|
||||
parentCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||
@@ -361,6 +347,93 @@ internal sealed class WorldSeedingService
|
||||
return commanders;
|
||||
}
|
||||
|
||||
internal PlayerFactionRuntime CreatePlayerFaction(
|
||||
IReadOnlyCollection<FactionRuntime> factions,
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
IReadOnlyCollection<ShipRuntime> ships,
|
||||
IReadOnlyCollection<CommanderRuntime> commanders,
|
||||
IReadOnlyCollection<PolicySetRuntime> policies,
|
||||
DateTimeOffset nowUtc)
|
||||
{
|
||||
var sovereignFaction = factions.FirstOrDefault(faction => string.Equals(faction.Id, DefaultFactionId, StringComparison.Ordinal))
|
||||
?? factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).First();
|
||||
|
||||
var player = new PlayerFactionRuntime
|
||||
{
|
||||
Id = "player-faction",
|
||||
Label = $"{sovereignFaction.Label} Command",
|
||||
SovereignFactionId = sovereignFaction.Id,
|
||||
CreatedAtUtc = nowUtc,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
};
|
||||
|
||||
foreach (var shipId in ships.Where(ship => ship.FactionId == sovereignFaction.Id).Select(ship => ship.Id))
|
||||
{
|
||||
player.AssetRegistry.ShipIds.Add(shipId);
|
||||
}
|
||||
|
||||
foreach (var stationId in stations.Where(station => station.FactionId == sovereignFaction.Id).Select(station => station.Id))
|
||||
{
|
||||
player.AssetRegistry.StationIds.Add(stationId);
|
||||
}
|
||||
|
||||
foreach (var commanderId in commanders.Where(commander => commander.FactionId == sovereignFaction.Id).Select(commander => commander.Id))
|
||||
{
|
||||
player.AssetRegistry.CommanderIds.Add(commanderId);
|
||||
}
|
||||
|
||||
foreach (var policy in policies.Where(policy => string.Equals(policy.OwnerId, sovereignFaction.Id, StringComparison.Ordinal)))
|
||||
{
|
||||
player.AssetRegistry.PolicySetIds.Add(policy.Id);
|
||||
}
|
||||
|
||||
player.Policies.Add(new PlayerFactionPolicyRuntime
|
||||
{
|
||||
Id = "player-core-policy",
|
||||
Label = "Core Empire Policy",
|
||||
ScopeKind = "player-faction",
|
||||
ScopeId = player.Id,
|
||||
PolicySetId = sovereignFaction.DefaultPolicySetId,
|
||||
TradeAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.TradeAccessPolicy ?? "owner-and-allies",
|
||||
DockingAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.DockingAccessPolicy ?? "owner-and-allies",
|
||||
ConstructionAccessPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.ConstructionAccessPolicy ?? "owner-only",
|
||||
OperationalRangePolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.OperationalRangePolicy ?? "unrestricted",
|
||||
CombatEngagementPolicy = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.CombatEngagementPolicy ?? "defensive",
|
||||
AvoidHostileSystems = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.AvoidHostileSystems ?? true,
|
||||
FleeHullRatio = policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId)?.FleeHullRatio ?? 0.35f,
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
|
||||
if (policies.FirstOrDefault(policy => policy.Id == sovereignFaction.DefaultPolicySetId) is { } defaultPolicy)
|
||||
{
|
||||
foreach (var systemId in defaultPolicy.BlacklistedSystemIds)
|
||||
{
|
||||
player.Policies[0].BlacklistedSystemIds.Add(systemId);
|
||||
}
|
||||
}
|
||||
|
||||
player.AutomationPolicies.Add(new PlayerAutomationPolicyRuntime
|
||||
{
|
||||
Id = "player-core-automation",
|
||||
Label = "Core Automation",
|
||||
ScopeKind = "player-faction",
|
||||
ScopeId = player.Id,
|
||||
BehaviorKind = "idle",
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
|
||||
player.Reserves.Add(new PlayerReserveGroupRuntime
|
||||
{
|
||||
Id = "player-core-reserve",
|
||||
Label = "Strategic Reserve",
|
||||
ReserveKind = "military",
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
player.AssetRegistry.ReserveIds.Add("player-core-reserve");
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
internal static DefaultBehaviorRuntime CreateBehavior(
|
||||
ShipDefinition definition,
|
||||
string systemId,
|
||||
@@ -381,22 +454,32 @@ internal sealed class WorldSeedingService
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "construct-station",
|
||||
StationId = homeStation.Id,
|
||||
Phase = "travel-to-station",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
PreferredConstructionSiteId = null,
|
||||
};
|
||||
}
|
||||
|
||||
if (HasCapabilities(definition, "mining") && homeStation is not null)
|
||||
{
|
||||
return CreateResourceHarvestBehavior("auto-mine", scenario.MiningDefaults.NodeSystemId, homeStation.Id);
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine",
|
||||
HomeSystemId = homeStation.SystemId,
|
||||
HomeStationId = homeStation.Id,
|
||||
AreaSystemId = scenario.MiningDefaults.NodeSystemId,
|
||||
MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal))
|
||||
{
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "trade-haul",
|
||||
Phase = "travel-to-source",
|
||||
Kind = "advanced-auto-trade",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
MaxSystemRange = 2,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -405,7 +488,9 @@ internal sealed class WorldSeedingService
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "patrol",
|
||||
StationId = homeStation?.Id,
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
AreaSystemId = systemId,
|
||||
PatrolPoints = route,
|
||||
PatrolIndex = 0,
|
||||
};
|
||||
@@ -414,6 +499,20 @@ internal sealed class WorldSeedingService
|
||||
return new DefaultBehaviorRuntime
|
||||
{
|
||||
Kind = "idle",
|
||||
HomeSystemId = homeStation?.SystemId ?? systemId,
|
||||
HomeStationId = homeStation?.Id,
|
||||
};
|
||||
}
|
||||
|
||||
internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition)
|
||||
{
|
||||
return definition.Kind switch
|
||||
{
|
||||
"transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 },
|
||||
"construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 },
|
||||
"military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 },
|
||||
_ when HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 },
|
||||
_ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -471,43 +570,4 @@ internal sealed class WorldSeedingService
|
||||
.Select(segment => char.ToUpperInvariant(segment[0]) + segment[1..]));
|
||||
}
|
||||
|
||||
private static DefaultBehaviorRuntime CreateResourceHarvestBehavior(string kind, string areaSystemId, string stationId) => new()
|
||||
{
|
||||
Kind = kind,
|
||||
AreaSystemId = areaSystemId,
|
||||
StationId = stationId,
|
||||
Phase = "travel-to-node",
|
||||
};
|
||||
|
||||
private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
|
||||
{
|
||||
Kind = behavior.Kind,
|
||||
AreaSystemId = behavior.AreaSystemId,
|
||||
TargetEntityId = behavior.TargetEntityId,
|
||||
ItemId = behavior.ItemId,
|
||||
ModuleId = behavior.ModuleId,
|
||||
NodeId = behavior.NodeId,
|
||||
Phase = behavior.Phase,
|
||||
PatrolIndex = behavior.PatrolIndex,
|
||||
StationId = behavior.StationId,
|
||||
};
|
||||
|
||||
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
|
||||
{
|
||||
Kind = order.Kind,
|
||||
Status = order.Status,
|
||||
DestinationSystemId = order.DestinationSystemId,
|
||||
DestinationPosition = order.DestinationPosition,
|
||||
};
|
||||
|
||||
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
|
||||
{
|
||||
Kind = task.Kind.ToContractValue(),
|
||||
Status = task.Status,
|
||||
TargetEntityId = task.TargetEntityId,
|
||||
TargetNodeId = targetNodeId ?? task.TargetNodeId,
|
||||
TargetPosition = task.TargetPosition,
|
||||
TargetSystemId = task.TargetSystemId,
|
||||
Threshold = task.Threshold,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
public sealed class WorldGenerationOptions
|
||||
{
|
||||
public int TargetSystemCount { get; init; } = 160;
|
||||
|
||||
public int TargetSystemCount { get; init; }
|
||||
public int AiControllerFactionCount { get; init; }
|
||||
public bool GeneratePlayerFaction { get; init; }
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class WorldService(
|
||||
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
|
||||
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
|
||||
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
|
||||
private readonly PlayerFactionService _playerFaction = new();
|
||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||
private readonly Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
||||
@@ -74,6 +75,156 @@ public sealed class WorldService(
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetShipSnapshotUnsafe(ship.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? RemoveShipOrder(string shipId, string orderId)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetShipSnapshotUnsafe(ship.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetShipSnapshotUnsafe(ship.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? GetPlayerFaction()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.EnsureDomain(_world);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.CreateOrganization(_world, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.DeleteOrganization(_world, organizationId);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpdateOrganizationMembership(_world, organizationId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertDirective(_world, directiveId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.DeleteDirective(_world, directiveId);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertPolicy(_world, policyId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertProductionProgram(_world, productionProgramId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpsertAssignment(_world, assetId, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_playerFaction.UpdateStrategicIntent(_world, request);
|
||||
return GetPlayerFactionSnapshotUnsafe();
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||
@@ -158,7 +309,9 @@ public sealed class WorldService(
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]);
|
||||
[],
|
||||
null,
|
||||
null);
|
||||
|
||||
_history.Enqueue(resetDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
@@ -203,6 +356,12 @@ public sealed class WorldService(
|
||||
};
|
||||
}
|
||||
|
||||
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
|
||||
_engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId);
|
||||
|
||||
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
|
||||
_engine.BuildSnapshot(_world, _sequence).PlayerFaction;
|
||||
|
||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||
delta.RequiresSnapshotRefresh
|
||||
|| delta.Events.Count > 0
|
||||
@@ -214,7 +373,9 @@ public sealed class WorldService(
|
||||
|| delta.MarketOrders.Count > 0
|
||||
|| delta.Policies.Count > 0
|
||||
|| delta.Ships.Count > 0
|
||||
|| delta.Factions.Count > 0;
|
||||
|| delta.Factions.Count > 0
|
||||
|| delta.PlayerFaction is not null
|
||||
|| delta.Geopolitics is not null;
|
||||
|
||||
private void Unsubscribe(Guid subscriberId)
|
||||
{
|
||||
@@ -261,6 +422,8 @@ public sealed class WorldService(
|
||||
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
|
||||
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
|
||||
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
|
||||
PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null,
|
||||
Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null,
|
||||
Scope = scope,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
}
|
||||
},
|
||||
"WorldGeneration": {
|
||||
"TargetSystemCount": 10,
|
||||
"IncludeSolSystem": true
|
||||
"TargetSystemCount": 2,
|
||||
"IncludeSolSystem": true,
|
||||
"AiControllerFactionCount": 0,
|
||||
"GeneratePlayerFaction": false
|
||||
},
|
||||
"OrbitalSimulation": {
|
||||
"SimulatedSecondsPerRealSecond": 0
|
||||
|
||||
Reference in New Issue
Block a user