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
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
||||
import type { TelemetrySnapshot } from "./contractsTelemetry";
|
||||
import type { BalanceSettings } from "./contractsBalance";
|
||||
import type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||
import type { ShipSnapshot } from "./contractsShips";
|
||||
import type {
|
||||
PlayerAssetAssignmentCommandRequest,
|
||||
PlayerAutomationPolicyCommandRequest,
|
||||
PlayerDirectiveCommandRequest,
|
||||
PlayerOrganizationCommandRequest,
|
||||
PlayerOrganizationMembershipCommandRequest,
|
||||
PlayerPolicyCommandRequest,
|
||||
PlayerStrategicIntentCommandRequest,
|
||||
} from "./playerFactionCommands";
|
||||
import type {
|
||||
ShipDefaultBehaviorCommandRequest,
|
||||
ShipOrderCommandRequest,
|
||||
} from "./shipCommands";
|
||||
|
||||
export interface WorldStreamScope {
|
||||
scopeKind?: string;
|
||||
@@ -8,12 +23,16 @@ export interface WorldStreamScope {
|
||||
bubbleId?: string | null;
|
||||
}
|
||||
|
||||
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
||||
const response = await fetch("/api/world", { signal });
|
||||
async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(input, init);
|
||||
if (!response.ok) {
|
||||
throw new Error(`World request failed with ${response.status}`);
|
||||
throw new Error(`${init?.method ?? "GET"} ${typeof input === "string" ? input : input.toString()} failed with ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<WorldSnapshot>;
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
||||
return fetchJson<WorldSnapshot>("/api/world", { signal });
|
||||
}
|
||||
|
||||
export function openWorldStream(
|
||||
@@ -52,39 +71,114 @@ export function openWorldStream(
|
||||
}
|
||||
|
||||
export async function fetchTelemetry(signal?: AbortSignal) {
|
||||
const response = await fetch("/api/telemetry", { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Telemetry request failed with ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<TelemetrySnapshot>;
|
||||
return fetchJson<TelemetrySnapshot>("/api/telemetry", { signal });
|
||||
}
|
||||
|
||||
export async function fetchBalance(signal?: AbortSignal) {
|
||||
const response = await fetch("/api/balance", { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Balance request failed with ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<BalanceSettings>;
|
||||
return fetchJson<BalanceSettings>("/api/balance", { signal });
|
||||
}
|
||||
|
||||
export async function updateBalance(settings: BalanceSettings) {
|
||||
const response = await fetch("/api/balance", {
|
||||
return fetchJson<BalanceSettings>("/api/balance", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Balance update failed with ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<BalanceSettings>;
|
||||
}
|
||||
|
||||
export async function resetWorld() {
|
||||
const response = await fetch("/api/world/reset", {
|
||||
return fetchJson<WorldSnapshot>("/api/world/reset", {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Reset request failed with ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<WorldSnapshot>;
|
||||
}
|
||||
|
||||
export async function fetchPlayerFaction(signal?: AbortSignal) {
|
||||
return fetchJson<PlayerFactionSnapshot>("/api/player-faction", { signal });
|
||||
}
|
||||
|
||||
export async function createPlayerOrganization(request: PlayerOrganizationCommandRequest) {
|
||||
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/organizations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePlayerOrganization(organizationId: string) {
|
||||
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePlayerOrganizationMembership(organizationId: string, request: PlayerOrganizationMembershipCommandRequest) {
|
||||
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/organizations/${organizationId}/membership`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertPlayerDirective(request: PlayerDirectiveCommandRequest, directiveId?: string | null) {
|
||||
const path = directiveId ? `/api/player-faction/directives/${directiveId}` : "/api/player-faction/directives";
|
||||
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||
method: directiveId ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePlayerDirective(directiveId: string) {
|
||||
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/directives/${directiveId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertPlayerAssignment(assetId: string, request: PlayerAssetAssignmentCommandRequest) {
|
||||
return fetchJson<PlayerFactionSnapshot>(`/api/player-faction/assignments/${assetId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertPlayerPolicy(request: PlayerPolicyCommandRequest, policyId?: string | null) {
|
||||
const path = policyId ? `/api/player-faction/policies/${policyId}` : "/api/player-faction/policies";
|
||||
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||
method: policyId ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertPlayerAutomationPolicy(request: PlayerAutomationPolicyCommandRequest, automationPolicyId?: string | null) {
|
||||
const path = automationPolicyId ? `/api/player-faction/automation-policies/${automationPolicyId}` : "/api/player-faction/automation-policies";
|
||||
return fetchJson<PlayerFactionSnapshot>(path, {
|
||||
method: automationPolicyId ? "PUT" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePlayerStrategicIntent(request: PlayerStrategicIntentCommandRequest) {
|
||||
return fetchJson<PlayerFactionSnapshot>("/api/player-faction/strategic-intent", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function enqueueShipOrder(shipId: string, request: ShipOrderCommandRequest) {
|
||||
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/orders`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateShipDefaultBehavior(shipId: string, request: ShipDefaultBehaviorCommandRequest) {
|
||||
return fetchJson<ShipSnapshot>(`/api/ships/${shipId}/default-behavior`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
}
|
||||
|
||||
206
apps/viewer/src/components/gm/GmGeopoliticsPanel.vue
Normal file
206
apps/viewer/src/components/gm/GmGeopoliticsPanel.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useGmStore } from "../../ui/stores/gmStore";
|
||||
|
||||
const gmStore = useGmStore();
|
||||
|
||||
const factionLabelById = computed(() =>
|
||||
new Map(gmStore.factions.map((faction) => [faction.id, faction.label])),
|
||||
);
|
||||
|
||||
function factionLabel(factionId?: string | null) {
|
||||
if (!factionId) return "—";
|
||||
return factionLabelById.value.get(factionId) ?? factionId;
|
||||
}
|
||||
|
||||
function titleCase(value?: string | null) {
|
||||
if (!value) return "—";
|
||||
return value
|
||||
.replace(/[-_]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
function percent(value?: number | null) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
return `${Math.round(value * 100)}%`;
|
||||
}
|
||||
|
||||
const relations = computed(() =>
|
||||
[...(gmStore.geopolitics?.diplomacy.relations ?? [])]
|
||||
.sort((left, right) => right.tensionScore - left.tensionScore || left.id.localeCompare(right.id))
|
||||
.slice(0, 12),
|
||||
);
|
||||
|
||||
const incidents = computed(() =>
|
||||
[...(gmStore.geopolitics?.diplomacy.incidents ?? [])]
|
||||
.sort((left, right) => right.lastObservedAtUtc.localeCompare(left.lastObservedAtUtc))
|
||||
.slice(0, 8),
|
||||
);
|
||||
|
||||
const contestedSystems = computed(() =>
|
||||
[...(gmStore.geopolitics?.territory.controlStates ?? [])]
|
||||
.filter((state) => state.isContested)
|
||||
.sort((left, right) => right.strategicValue - left.strategicValue || left.systemId.localeCompare(right.systemId))
|
||||
.slice(0, 12),
|
||||
);
|
||||
|
||||
const frontLines = computed(() =>
|
||||
[...(gmStore.geopolitics?.territory.frontLines ?? [])]
|
||||
.sort((left, right) => right.pressureScore - left.pressureScore || left.id.localeCompare(right.id))
|
||||
.slice(0, 10),
|
||||
);
|
||||
|
||||
const regions = computed(() =>
|
||||
[...(gmStore.geopolitics?.economyRegions.regions ?? [])]
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.slice(0, 16),
|
||||
);
|
||||
|
||||
const bottlenecks = computed(() =>
|
||||
[...(gmStore.geopolitics?.economyRegions.bottlenecks ?? [])]
|
||||
.sort((left, right) => right.severity - left.severity || left.id.localeCompare(right.id))
|
||||
.slice(0, 12),
|
||||
);
|
||||
|
||||
const corridors = computed(() =>
|
||||
[...(gmStore.geopolitics?.economyRegions.corridors ?? [])]
|
||||
.sort((left, right) => right.riskScore - left.riskScore || left.id.localeCompare(right.id))
|
||||
.slice(0, 12),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-5 p-4 text-xs text-white/90">
|
||||
<div v-if="!gmStore.geopolitics" class="rounded border border-white/10 bg-white/5 p-4 text-white/60">
|
||||
No geopolitical state loaded.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="grid gap-3 md:grid-cols-4">
|
||||
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Diplomacy</div>
|
||||
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.diplomacy.relations.length }}</div>
|
||||
<div class="text-white/60">relations</div>
|
||||
</div>
|
||||
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Wars</div>
|
||||
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.diplomacy.wars.length }}</div>
|
||||
<div class="text-white/60">active conflicts</div>
|
||||
</div>
|
||||
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Contested</div>
|
||||
<div class="mt-2 text-lg font-semibold">{{ contestedSystems.length }}</div>
|
||||
<div class="text-white/60">systems</div>
|
||||
</div>
|
||||
<div class="rounded border border-white/10 bg-white/5 p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-white/50">Regions</div>
|
||||
<div class="mt-2 text-lg font-semibold">{{ gmStore.geopolitics.economyRegions.regions.length }}</div>
|
||||
<div class="text-white/60">economic regions</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 xl:grid-cols-2">
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Relations</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="relation in relations" :key="relation.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ factionLabel(relation.factionAId) }} vs {{ factionLabel(relation.factionBId) }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
{{ titleCase(relation.posture) }} · tension {{ percent(relation.tensionScore) }} · grievance {{ percent(relation.grievanceScore) }}
|
||||
</div>
|
||||
<div class="text-white/55">
|
||||
Trade {{ relation.tradeAccessPolicy }} · Military {{ relation.militaryAccessPolicy }} · treaties {{ relation.activeTreatyIds.length }} · incidents {{ relation.activeIncidentIds.length }}<span v-if="relation.warStateId"> · war {{ relation.warStateId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Incidents</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="incident in incidents" :key="incident.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ titleCase(incident.kind) }} · {{ incident.systemId ?? "no-system" }}</div>
|
||||
<div class="mt-1 text-white/70">{{ incident.summary }}</div>
|
||||
<div class="text-white/55">
|
||||
Severity {{ incident.severity.toFixed(2) }} · Escalation {{ incident.escalationScore.toFixed(2) }} · {{ factionLabel(incident.sourceFactionId) }} → {{ factionLabel(incident.targetFactionId) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 xl:grid-cols-2">
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Territory</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="state in contestedSystems" :key="state.systemId" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ state.systemId }} · {{ titleCase(state.controlKind) }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
Control {{ state.controlScore.toFixed(1) }} · strategic {{ state.strategicValue.toFixed(1) }}
|
||||
</div>
|
||||
<div class="text-white/55">
|
||||
Controller {{ factionLabel(state.controllerFactionId) }} · Claimant {{ factionLabel(state.primaryClaimantFactionId) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Front Lines</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="front in frontLines" :key="front.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ front.id }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
{{ titleCase(front.kind) }} · pressure {{ percent(front.pressureScore) }} · supply {{ percent(front.supplyRisk) }}
|
||||
</div>
|
||||
<div class="text-white/55">
|
||||
{{ front.factionIds.map((id) => factionLabel(id)).join(" vs ") }}<span v-if="front.anchorSystemId"> · anchor {{ front.anchorSystemId }}</span><br>
|
||||
{{ front.systemIds.join(", ") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-5 xl:grid-cols-2">
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Economic Regions</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="region in regions" :key="region.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ region.label }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
{{ titleCase(region.kind) }} · core {{ region.coreSystemId }} · systems {{ region.systemIds.length }}
|
||||
</div>
|
||||
<div class="text-white/55">
|
||||
Faction {{ factionLabel(region.factionId) }} · fronts {{ region.frontLineIds.length }} · corridors {{ region.corridorIds.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded border border-white/10 bg-white/5 p-4">
|
||||
<h3 class="text-sm font-semibold">Bottlenecks And Corridors</h3>
|
||||
<div class="mt-3 space-y-2">
|
||||
<div v-for="bottleneck in bottlenecks" :key="bottleneck.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ bottleneck.itemId }} · {{ bottleneck.regionId }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
{{ titleCase(bottleneck.cause) }} · severity {{ bottleneck.severity.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="corridor in corridors" :key="corridor.id" class="rounded border border-white/10 bg-black/20 p-2">
|
||||
<div class="font-medium">{{ corridor.id }}</div>
|
||||
<div class="mt-1 text-white/70">
|
||||
{{ titleCase(corridor.kind) }} · {{ titleCase(corridor.accessState) }} · risk {{ percent(corridor.riskScore) }}
|
||||
</div>
|
||||
<div class="text-white/55">
|
||||
Path {{ corridor.systemPathIds.join(" → ") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
} from "@tanstack/vue-table";
|
||||
import { storeToRefs } from "pinia";
|
||||
import GmWindow from "./GmWindow.vue";
|
||||
import GmPlayerFactionPanel from "./GmPlayerFactionPanel.vue";
|
||||
import GmGeopoliticsPanel from "./GmGeopoliticsPanel.vue";
|
||||
import { useGmStore } from "../../ui/stores/gmStore";
|
||||
import { usePlayerFactionStore } from "../../ui/stores/playerFactionStore";
|
||||
import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
||||
import type { ShipSnapshot } from "../../contractsShips";
|
||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||
@@ -74,10 +77,11 @@ const emit = defineEmits<{
|
||||
focus: [id: string, kind: "ship" | "station"];
|
||||
}>();
|
||||
|
||||
type TabId = "ships" | "stations" | "factions";
|
||||
type TabId = "ships" | "stations" | "factions" | "player" | "geopolitics";
|
||||
const activeTab = ref<TabId>("ships");
|
||||
|
||||
const gmStore = useGmStore();
|
||||
const playerFactionStore = usePlayerFactionStore();
|
||||
const selectionStore = useViewerSelectionStore();
|
||||
const { selectedEntityId, selectedEntityKind } = storeToRefs(selectionStore);
|
||||
|
||||
@@ -128,62 +132,51 @@ function formatCargoAmount(value: number | null | undefined) {
|
||||
return value.toFixed(2).replace(/\.?0+$/, "");
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
return `${Math.round(value * 100)}%`;
|
||||
}
|
||||
|
||||
function getLeadCampaign(faction: FactionSnapshot) {
|
||||
return [...faction.strategicState.campaigns]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
.find((campaign) => campaign.status !== "completed" && campaign.status !== "cancelled")
|
||||
?? faction.strategicState.campaigns[0];
|
||||
}
|
||||
|
||||
function getLeadObjective(faction: FactionSnapshot) {
|
||||
return [...(faction.objectives ?? [])]
|
||||
return [...faction.strategicState.objectives]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
.find((objective) => objective.state !== "Complete" && objective.state !== "Cancelled")
|
||||
?? faction.objectives?.[0];
|
||||
.find((objective) => objective.status !== "completed" && objective.status !== "cancelled")
|
||||
?? faction.strategicState.objectives[0];
|
||||
}
|
||||
|
||||
function getLeadStep(faction: FactionSnapshot) {
|
||||
const objective = getLeadObjective(faction);
|
||||
return [...(objective?.steps ?? [])]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
.find((step) => step.status !== "Complete" && step.status !== "Cancelled")
|
||||
?? objective?.steps?.[0];
|
||||
}
|
||||
|
||||
function getLeadTask(faction: FactionSnapshot) {
|
||||
return [...(faction.issuedTasks ?? [])]
|
||||
.sort((left, right) => right.priority - left.priority)
|
||||
.find((task) => task.state !== "Complete" && task.state !== "Cancelled")
|
||||
?? faction.issuedTasks?.[0];
|
||||
function getLatestDecision(faction: FactionSnapshot) {
|
||||
return [...faction.decisionLog]
|
||||
.sort((left, right) => right.occurredAtUtc.localeCompare(left.occurredAtUtc))[0];
|
||||
}
|
||||
|
||||
function describeCommodityState(faction: FactionSnapshot, itemId: string, shortLabel: string) {
|
||||
const signal = faction.blackboard?.commoditySignals.find((entry) => entry.itemId === itemId);
|
||||
const signal = faction.strategicState.economicAssessment.commoditySignals.find((entry) => entry.itemId === itemId);
|
||||
if (!signal) return `${shortLabel} —`;
|
||||
return `${shortLabel} ${titleCaseToken(signal.level)} ${compactRate(signal.projectedNetRatePerSecond)}`;
|
||||
}
|
||||
|
||||
function describeFactionStrategicState(faction: FactionSnapshot) {
|
||||
const campaign = getLeadCampaign(faction);
|
||||
const objective = getLeadObjective(faction);
|
||||
if (!objective) return "No objectives";
|
||||
return `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.state)}`;
|
||||
}
|
||||
|
||||
function describeFactionLeadStep(faction: FactionSnapshot) {
|
||||
const step = getLeadStep(faction);
|
||||
if (!step) return "No steps";
|
||||
const target = step.commodityId ?? step.moduleId ?? step.targetFactionId ?? step.targetSiteId;
|
||||
return target
|
||||
? `${titleCaseToken(step.kind)} · ${titleCaseToken(step.status)} · ${target}`
|
||||
: `${titleCaseToken(step.kind)} · ${titleCaseToken(step.status)}`;
|
||||
if (!campaign && !objective) return "No campaigns";
|
||||
if (!campaign) return `${titleCaseToken(objective?.kind)} · ${titleCaseToken(objective?.status)}`;
|
||||
return `${titleCaseToken(campaign.kind)} · ${titleCaseToken(campaign.status)}`;
|
||||
}
|
||||
|
||||
function describeFactionLeadTask(faction: FactionSnapshot) {
|
||||
const task = getLeadTask(faction);
|
||||
if (!task) return "No tasks";
|
||||
const target = task.shipRole ?? task.commodityId ?? task.moduleId ?? task.targetFactionId ?? task.targetSiteId;
|
||||
const objective = getLeadObjective(faction);
|
||||
if (!objective) return "No objectives";
|
||||
const target = objective.itemId ?? objective.targetEntityId ?? objective.targetSystemId ?? objective.homeStationId;
|
||||
return target
|
||||
? `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)} · ${target}`
|
||||
: `${titleCaseToken(task.kind)} · ${titleCaseToken(task.state)}`;
|
||||
}
|
||||
|
||||
function describeFactionPriority(faction: FactionSnapshot) {
|
||||
const priority = [...(faction.strategicPriorities ?? [])]
|
||||
.sort((left, right) => right.priority - left.priority)[0];
|
||||
return priority ? `${titleCaseToken(priority.goalName)} · ${compactNumber(priority.priority, 0)}` : "—";
|
||||
? `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)} · ${target}`
|
||||
: `${titleCaseToken(objective.kind)} · ${titleCaseToken(objective.status)}`;
|
||||
}
|
||||
|
||||
function describeFactionEconomy(faction: FactionSnapshot) {
|
||||
@@ -195,9 +188,57 @@ function describeFactionEconomy(faction: FactionSnapshot) {
|
||||
}
|
||||
|
||||
function describeFactionThreat(faction: FactionSnapshot) {
|
||||
const blackboard = faction.blackboard;
|
||||
if (!blackboard) return "—";
|
||||
return `Enemy ships ${blackboard.enemyShipCount} · stations ${blackboard.enemyStationCount}`;
|
||||
const threat = faction.strategicState.threatAssessment;
|
||||
return `Enemy ships ${threat.enemyShipCount} · stations ${threat.enemyStationCount}`;
|
||||
}
|
||||
|
||||
function describeFactionCommitments(faction: FactionSnapshot) {
|
||||
const economic = faction.strategicState.economicAssessment;
|
||||
return `Mil ${economic.militaryShipCount}/${economic.targetMilitaryShipCount} · Min ${economic.minerShipCount}/${economic.targetMinerShipCount} · Tr ${economic.transportShipCount}/${economic.targetTransportShipCount}`;
|
||||
}
|
||||
|
||||
function describeFactionReserves(faction: FactionSnapshot) {
|
||||
const budget = faction.strategicState.budget;
|
||||
return `Assets ${budget.reservedMilitaryAssets}/${budget.reservedLogisticsAssets}/${budget.reservedConstructionAssets} · Credits ${compactNumber(budget.reservedCredits, 0)}`;
|
||||
}
|
||||
|
||||
function describeFactionBottleneck(faction: FactionSnapshot) {
|
||||
const economic = faction.strategicState.economicAssessment;
|
||||
if (!economic.industrialBottleneckItemId) {
|
||||
return `None · sustain ${formatPercent(economic.sustainmentScore)}`;
|
||||
}
|
||||
return `${economic.industrialBottleneckItemId} · sustain ${formatPercent(economic.sustainmentScore)} · replace ${formatPercent(economic.replacementPressure)}`;
|
||||
}
|
||||
|
||||
function describeFactionIntent(faction: FactionSnapshot) {
|
||||
const latestDecision = getLatestDecision(faction);
|
||||
const leadCampaign = getLeadCampaign(faction);
|
||||
if (!leadCampaign) return latestDecision?.summary ?? "—";
|
||||
const pause = leadCampaign.pauseReason ? ` · ${leadCampaign.pauseReason}` : "";
|
||||
return `${titleCaseToken(leadCampaign.kind)} · ${titleCaseToken(leadCampaign.status)}${pause}`;
|
||||
}
|
||||
|
||||
function describeFactionMemory(faction: FactionSnapshot) {
|
||||
const topSystem = [...faction.memory.systems]
|
||||
.sort((left, right) => (right.frontierPressure + right.routeRisk + right.historicalShortagePressure)
|
||||
- (left.frontierPressure + left.routeRisk + left.historicalShortagePressure))[0];
|
||||
const topCommodity = [...faction.memory.commodities]
|
||||
.sort((left, right) => right.historicalShortageScore - left.historicalShortageScore)[0];
|
||||
if (!topSystem && !topCommodity) return "—";
|
||||
return `${topSystem ? `${topSystem.systemId} fp ${compactNumber(topSystem.frontierPressure, 1)}` : "no-front"}${topCommodity ? ` · ${topCommodity.itemId} hs ${compactNumber(topCommodity.historicalShortageScore, 1)}` : ""}`;
|
||||
}
|
||||
|
||||
function describeFactionDecision(faction: FactionSnapshot) {
|
||||
const latestDecision = getLatestDecision(faction);
|
||||
return latestDecision ? `${titleCaseToken(latestDecision.kind)} · ${latestDecision.summary}` : "—";
|
||||
}
|
||||
|
||||
function describeFactionFronts(faction: FactionSnapshot) {
|
||||
const activeTheaters = faction.strategicState.theaters.filter((theater) => theater.status === "active");
|
||||
const defense = activeTheaters.filter((theater) => theater.kind.includes("defense")).length;
|
||||
const offense = activeTheaters.filter((theater) => theater.kind.includes("offense")).length;
|
||||
const economy = activeTheaters.filter((theater) => theater.kind.includes("economic")).length;
|
||||
return `${activeTheaters.length} active · D ${defense} · O ${offense} · E ${economy}`;
|
||||
}
|
||||
|
||||
// ── Ships table ────────────────────────────────────────────────────────────
|
||||
@@ -210,32 +251,40 @@ type ShipRow = {
|
||||
faction: string;
|
||||
system: string;
|
||||
state: string;
|
||||
objective: string;
|
||||
assignment: string;
|
||||
behavior: string;
|
||||
phase: string;
|
||||
action: string;
|
||||
task: string;
|
||||
orders: string;
|
||||
plan: string;
|
||||
step: string;
|
||||
subtask: string;
|
||||
cargo: number;
|
||||
health: number;
|
||||
};
|
||||
|
||||
const shipRows = computed<ShipRow[]>(() =>
|
||||
gmStore.ships.map((s) => ({
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
state: titleCaseToken(s.state),
|
||||
objective: s.commanderObjective ? titleCaseToken(s.commanderObjective) : "—",
|
||||
behavior: titleCaseToken(s.defaultBehaviorKind),
|
||||
phase: s.behaviorPhase ? titleCaseToken(s.behaviorPhase) : "—",
|
||||
action: s.currentAction ? `${s.currentAction.label} ${Math.round(s.currentAction.progress * 100)}%` : "—",
|
||||
task: titleCaseToken(s.controllerTaskKind),
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
health: Math.round(s.health),
|
||||
})),
|
||||
gmStore.ships.map((s) => {
|
||||
const topOrder = [...s.orderQueue]
|
||||
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||
const currentStep = s.activePlan?.steps[s.activePlan.currentStepIndex];
|
||||
const currentSubTask = s.activeSubTasks[0];
|
||||
return {
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
class: s.class,
|
||||
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||
system: s.systemId,
|
||||
state: titleCaseToken(s.state),
|
||||
assignment: s.assignment ? titleCaseToken(s.assignment.kind) : "—",
|
||||
behavior: titleCaseToken(s.defaultBehavior.kind),
|
||||
orders: topOrder ? `${titleCaseToken(topOrder.kind)} · ${s.orderQueue.length}` : "—",
|
||||
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
|
||||
step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—",
|
||||
subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—",
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
health: Math.round(s.health),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const shipColumnHelper = createColumnHelper<ShipRow>();
|
||||
@@ -250,11 +299,12 @@ const shipColumns = [
|
||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||
shipColumnHelper.accessor("system", { header: "System" }),
|
||||
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
||||
shipColumnHelper.accessor("objective", { header: "Commander Objective" }),
|
||||
shipColumnHelper.accessor("assignment", { header: "Assignment" }),
|
||||
shipColumnHelper.accessor("behavior", { header: "Behavior" }),
|
||||
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
||||
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||
shipColumnHelper.accessor("orders", { header: "Orders" }),
|
||||
shipColumnHelper.accessor("plan", { header: "Plan" }),
|
||||
shipColumnHelper.accessor("step", { header: "Current Step" }),
|
||||
shipColumnHelper.accessor("subtask", { header: "SubTask" }),
|
||||
shipColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
@@ -264,7 +314,7 @@ const shipColumns = [
|
||||
|
||||
const shipFilter = ref("");
|
||||
const shipSorting = ref<SortingState>([]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
||||
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask", "cargo", "health"]);
|
||||
|
||||
const shipTable = useVueTable({
|
||||
get data() { return shipRows.value; },
|
||||
@@ -383,14 +433,18 @@ type FactionRow = {
|
||||
label: string;
|
||||
color: string;
|
||||
planCycle: number;
|
||||
priority: string;
|
||||
strategicState: string;
|
||||
leadStep: string;
|
||||
leadTask: string;
|
||||
warReadiness: string;
|
||||
posture: string;
|
||||
fronts: string;
|
||||
leadCampaign: string;
|
||||
leadObjective: string;
|
||||
commitments: string;
|
||||
reserves: string;
|
||||
bottleneck: string;
|
||||
intent: string;
|
||||
decision: string;
|
||||
memory: string;
|
||||
economy: string;
|
||||
threat: string;
|
||||
fleets: string;
|
||||
systems: string;
|
||||
credits: number;
|
||||
population: number;
|
||||
@@ -399,29 +453,29 @@ type FactionRow = {
|
||||
};
|
||||
|
||||
const factionRows = computed<FactionRow[]>(() =>
|
||||
gmStore.factions.map((f) => {
|
||||
const assessment = f.strategicAssessment;
|
||||
const blackboard = f.blackboard;
|
||||
return {
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
color: f.color,
|
||||
planCycle: blackboard?.planCycle ?? 0,
|
||||
priority: describeFactionPriority(f),
|
||||
strategicState: describeFactionStrategicState(f),
|
||||
leadStep: describeFactionLeadStep(f),
|
||||
leadTask: describeFactionLeadTask(f),
|
||||
warReadiness: `Industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"} · Shipyard ${blackboard?.hasShipyard ? "yes" : "no"}${blackboard?.hasActiveExpansionProject ? ` · Expanding ${blackboard.activeExpansionCommodityId ?? blackboard.activeExpansionModuleId ?? "site"}` : ""}`,
|
||||
economy: describeFactionEconomy(f),
|
||||
threat: describeFactionThreat(f),
|
||||
fleets: assessment ? `M ${assessment.militaryShipCount}/${blackboard?.targetWarshipCount ?? 0} · Mn ${assessment.minerShipCount} · Tr ${assessment.transportShipCount} · Cn ${assessment.constructorShipCount}` : "—",
|
||||
systems: assessment ? `${assessment.controlledSystemCount} / ${assessment.targetSystemCount}` : "—",
|
||||
credits: Math.round(f.credits),
|
||||
population: Math.round(f.populationTotal),
|
||||
shipsBuilt: f.shipsBuilt,
|
||||
shipsLost: f.shipsLost,
|
||||
};
|
||||
}),
|
||||
gmStore.factions.map((f) => ({
|
||||
id: f.id,
|
||||
label: f.label,
|
||||
color: f.color,
|
||||
planCycle: f.strategicState.planCycle,
|
||||
posture: `${titleCaseToken(f.doctrine.strategicPosture)} · ${titleCaseToken(f.doctrine.militaryPosture)} · ${titleCaseToken(f.doctrine.economicPosture)}`,
|
||||
fronts: describeFactionFronts(f),
|
||||
leadCampaign: describeFactionStrategicState(f),
|
||||
leadObjective: describeFactionLeadTask(f),
|
||||
commitments: describeFactionCommitments(f),
|
||||
reserves: describeFactionReserves(f),
|
||||
bottleneck: describeFactionBottleneck(f),
|
||||
intent: describeFactionIntent(f),
|
||||
decision: describeFactionDecision(f),
|
||||
memory: describeFactionMemory(f),
|
||||
economy: describeFactionEconomy(f),
|
||||
threat: describeFactionThreat(f),
|
||||
systems: `${f.strategicState.economicAssessment.controlledSystemCount} / ${f.doctrine.desiredControlledSystems}`,
|
||||
credits: Math.round(f.credits),
|
||||
population: Math.round(f.populationTotal),
|
||||
shipsBuilt: f.shipsBuilt,
|
||||
shipsLost: f.shipsLost,
|
||||
})),
|
||||
);
|
||||
|
||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||
@@ -432,14 +486,18 @@ const factionColumns = [
|
||||
cell: (info) => renderColorCell(info.getValue()),
|
||||
}),
|
||||
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
||||
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
|
||||
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
|
||||
factionColumnHelper.accessor("leadStep", { header: "Lead Step" }),
|
||||
factionColumnHelper.accessor("leadTask", { header: "Issued Task" }),
|
||||
factionColumnHelper.accessor("warReadiness", { header: "Campaign State" }),
|
||||
factionColumnHelper.accessor("posture", { header: "Posture" }),
|
||||
factionColumnHelper.accessor("fronts", { header: "Fronts" }),
|
||||
factionColumnHelper.accessor("leadCampaign", { header: "Lead Campaign" }),
|
||||
factionColumnHelper.accessor("leadObjective", { header: "Lead Objective" }),
|
||||
factionColumnHelper.accessor("commitments", { header: "Commitments" }),
|
||||
factionColumnHelper.accessor("reserves", { header: "Reserves" }),
|
||||
factionColumnHelper.accessor("bottleneck", { header: "Bottleneck" }),
|
||||
factionColumnHelper.accessor("intent", { header: "Strategic Intent" }),
|
||||
factionColumnHelper.accessor("decision", { header: "Recent Decision" }),
|
||||
factionColumnHelper.accessor("memory", { header: "Memory" }),
|
||||
factionColumnHelper.accessor("economy", { header: "Economy" }),
|
||||
factionColumnHelper.accessor("threat", { header: "Threat" }),
|
||||
factionColumnHelper.accessor("fleets", { header: "Fleets" }),
|
||||
factionColumnHelper.accessor("systems", { header: "Systems" }),
|
||||
factionColumnHelper.accessor("credits", { header: "Credits" }),
|
||||
factionColumnHelper.accessor("population", { header: "Pop" }),
|
||||
@@ -449,7 +507,7 @@ const factionColumns = [
|
||||
|
||||
const factionFilter = ref("");
|
||||
const factionSorting = ref<SortingState>([]);
|
||||
const factionOrder = useColumnOrder(["label", "color", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||
const factionOrder = useColumnOrder(["label", "color", "planCycle", "posture", "fronts", "leadCampaign", "leadObjective", "commitments", "reserves", "bottleneck", "intent", "decision", "memory", "economy", "threat", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||
|
||||
const factionTable = useVueTable({
|
||||
get data() { return factionRows.value; },
|
||||
@@ -472,6 +530,8 @@ const factionTable = useVueTable({
|
||||
// ── Row counts ─────────────────────────────────────────────────────────────
|
||||
|
||||
const tabs: { id: TabId; label: string }[] = [
|
||||
{ id: "player", label: "Player" },
|
||||
{ id: "geopolitics", label: "Geopolitics" },
|
||||
{ id: "ships", label: "Ships" },
|
||||
{ id: "stations", label: "Stations" },
|
||||
{ id: "factions", label: "Factions" },
|
||||
@@ -479,11 +539,15 @@ const tabs: { id: TabId; label: string }[] = [
|
||||
|
||||
const activeFilter = computed({
|
||||
get: () => {
|
||||
if (activeTab.value === "player") return "";
|
||||
if (activeTab.value === "geopolitics") return "";
|
||||
if (activeTab.value === "ships") return shipFilter.value;
|
||||
if (activeTab.value === "stations") return stationFilter.value;
|
||||
return factionFilter.value;
|
||||
},
|
||||
set: (v: string) => {
|
||||
if (activeTab.value === "player") return;
|
||||
if (activeTab.value === "geopolitics") return;
|
||||
if (activeTab.value === "ships") shipFilter.value = v;
|
||||
else if (activeTab.value === "stations") stationFilter.value = v;
|
||||
else factionFilter.value = v;
|
||||
@@ -491,12 +555,24 @@ const activeFilter = computed({
|
||||
});
|
||||
|
||||
const activeRowCount = computed(() => {
|
||||
if (activeTab.value === "player") {
|
||||
return (playerFactionStore.playerFaction?.assetRegistry.shipIds.length ?? 0)
|
||||
+ (playerFactionStore.playerFaction?.assetRegistry.stationIds.length ?? 0);
|
||||
}
|
||||
if (activeTab.value === "geopolitics") {
|
||||
const geopolitics = gmStore.geopolitics;
|
||||
return (geopolitics?.diplomacy.relations.length ?? 0)
|
||||
+ (geopolitics?.territory.controlStates.length ?? 0)
|
||||
+ (geopolitics?.economyRegions.regions.length ?? 0);
|
||||
}
|
||||
if (activeTab.value === "ships") return shipTable.getFilteredRowModel().rows.length;
|
||||
if (activeTab.value === "stations") return stationTable.getFilteredRowModel().rows.length;
|
||||
return factionTable.getFilteredRowModel().rows.length;
|
||||
});
|
||||
|
||||
const activeTotalCount = computed(() => {
|
||||
if (activeTab.value === "player") return activeRowCount.value;
|
||||
if (activeTab.value === "geopolitics") return activeRowCount.value;
|
||||
if (activeTab.value === "ships") return gmStore.ships.length;
|
||||
if (activeTab.value === "stations") return gmStore.stations.length;
|
||||
return gmStore.factions.length;
|
||||
@@ -558,7 +634,7 @@ function hideOrdersTooltip() {
|
||||
|
||||
<template>
|
||||
<GmWindow
|
||||
title="AI States"
|
||||
title="Empire / AI States"
|
||||
:initial-width="980"
|
||||
:initial-height="560"
|
||||
:initial-x="80"
|
||||
@@ -581,7 +657,7 @@ function hideOrdersTooltip() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1">
|
||||
<div v-if="activeTab !== 'player' && activeTab !== 'geopolitics'" class="relative flex-1">
|
||||
<input
|
||||
v-model="activeFilter"
|
||||
class="gm-search-input w-full rounded border py-1 pl-7 pr-7 text-xs"
|
||||
@@ -600,12 +676,30 @@ function hideOrdersTooltip() {
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex-1 text-xs opacity-60">
|
||||
{{ activeTab === "player" ? "Player empire control, policy, and observability." : "Diplomacy, territory, and regional economy observability." }}
|
||||
</div>
|
||||
|
||||
<span class="gm-row-count shrink-0 font-mono text-xs tabular-nums opacity-60">
|
||||
{{ activeRowCount }} / {{ activeTotalCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Ships table -->
|
||||
<div
|
||||
v-show="activeTab === 'player'"
|
||||
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
<GmPlayerFactionPanel />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="activeTab === 'geopolitics'"
|
||||
class="gm-table-container min-h-0 flex-1 overflow-auto"
|
||||
>
|
||||
<GmGeopoliticsPanel />
|
||||
</div>
|
||||
|
||||
<!-- Ships table -->
|
||||
<div
|
||||
v-show="activeTab === 'ships'"
|
||||
|
||||
1162
apps/viewer/src/components/gm/GmPlayerFactionPanel.vue
Normal file
1162
apps/viewer/src/components/gm/GmPlayerFactionPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -37,3 +37,5 @@ export type {
|
||||
ShipTransitSnapshot,
|
||||
} from "./contractsShips";
|
||||
export type { FactionSnapshot, FactionDelta } from "./contractsFactions";
|
||||
export type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||
export type { GeopoliticalStateSnapshot } from "./contractsGeopolitics";
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface PolicySetSnapshot {
|
||||
dockingAccessPolicy: string;
|
||||
constructionAccessPolicy: string;
|
||||
operationalRangePolicy: string;
|
||||
combatEngagementPolicy: string;
|
||||
avoidHostileSystems: boolean;
|
||||
fleeHullRatio: number;
|
||||
blacklistedSystemIds: string[];
|
||||
}
|
||||
|
||||
export interface PolicySetDelta extends PolicySetSnapshot {}
|
||||
|
||||
@@ -1,41 +1,81 @@
|
||||
export interface FactionPlanningStateSnapshot {
|
||||
militaryShipCount: number;
|
||||
minerShipCount: number;
|
||||
transportShipCount: number;
|
||||
constructorShipCount: number;
|
||||
controlledSystemCount: number;
|
||||
targetSystemCount: number;
|
||||
hasShipFactory: boolean;
|
||||
oreStockpile: number;
|
||||
refinedMetalsAvailableStock: number;
|
||||
refinedMetalsUsageRate: number;
|
||||
refinedMetalsProjectedProductionRate: number;
|
||||
refinedMetalsProjectedNetRate: number;
|
||||
refinedMetalsLevelSeconds: number;
|
||||
refinedMetalsLevel: string;
|
||||
hullpartsAvailableStock: number;
|
||||
hullpartsUsageRate: number;
|
||||
hullpartsProjectedProductionRate: number;
|
||||
hullpartsProjectedNetRate: number;
|
||||
hullpartsLevelSeconds: number;
|
||||
hullpartsLevel: string;
|
||||
claytronicsAvailableStock: number;
|
||||
claytronicsUsageRate: number;
|
||||
claytronicsProjectedProductionRate: number;
|
||||
claytronicsProjectedNetRate: number;
|
||||
claytronicsLevelSeconds: number;
|
||||
claytronicsLevel: string;
|
||||
waterAvailableStock: number;
|
||||
waterUsageRate: number;
|
||||
waterProjectedProductionRate: number;
|
||||
waterProjectedNetRate: number;
|
||||
waterLevelSeconds: number;
|
||||
waterLevel: string;
|
||||
import type { Vector3Dto } from "./contractsCommon";
|
||||
|
||||
export interface FactionDoctrineSnapshot {
|
||||
strategicPosture: string;
|
||||
expansionPosture: string;
|
||||
militaryPosture: string;
|
||||
economicPosture: string;
|
||||
desiredControlledSystems: number;
|
||||
desiredMilitaryPerFront: number;
|
||||
desiredMinersPerSystem: number;
|
||||
desiredTransportsPerSystem: number;
|
||||
desiredConstructors: number;
|
||||
reserveCreditsRatio: number;
|
||||
expansionBudgetRatio: number;
|
||||
warBudgetRatio: number;
|
||||
reserveMilitaryRatio: number;
|
||||
offensiveReadinessThreshold: number;
|
||||
supplySecurityBias: number;
|
||||
failureAversion: number;
|
||||
reinforcementLeadPerFront: number;
|
||||
}
|
||||
|
||||
export interface FactionStrategicPrioritySnapshot {
|
||||
goalName: string;
|
||||
priority: number;
|
||||
export interface FactionSystemMemorySnapshot {
|
||||
systemId: string;
|
||||
lastSeenAtUtc: string;
|
||||
lastEnemyShipCount: number;
|
||||
lastEnemyStationCount: number;
|
||||
controlledByFaction: boolean;
|
||||
lastRole?: string | null;
|
||||
frontierPressure: number;
|
||||
routeRisk: number;
|
||||
historicalShortagePressure: number;
|
||||
offensiveFailures: number;
|
||||
defensiveFailures: number;
|
||||
offensiveSuccesses: number;
|
||||
defensiveSuccesses: number;
|
||||
lastContestedAtUtc?: string | null;
|
||||
lastShortageAtUtc?: string | null;
|
||||
}
|
||||
|
||||
export interface FactionCommodityMemorySnapshot {
|
||||
itemId: string;
|
||||
historicalShortageScore: number;
|
||||
historicalSurplusScore: number;
|
||||
lastObservedBacklog: number;
|
||||
updatedAtUtc: string;
|
||||
lastCriticalAtUtc?: string | null;
|
||||
}
|
||||
|
||||
export interface FactionOutcomeRecordSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
summary: string;
|
||||
relatedCampaignId?: string | null;
|
||||
relatedObjectiveId?: string | null;
|
||||
occurredAtUtc: string;
|
||||
}
|
||||
|
||||
export interface FactionMemorySnapshot {
|
||||
lastPlanCycle: number;
|
||||
updatedAtUtc: string;
|
||||
lastObservedShipsBuilt: number;
|
||||
lastObservedShipsLost: number;
|
||||
lastObservedCredits: number;
|
||||
knownSystemIds: string[];
|
||||
knownEnemyFactionIds: string[];
|
||||
systems: FactionSystemMemorySnapshot[];
|
||||
commodities: FactionCommodityMemorySnapshot[];
|
||||
recentOutcomes: FactionOutcomeRecordSnapshot[];
|
||||
}
|
||||
|
||||
export interface FactionBudgetSnapshot {
|
||||
reservedCredits: number;
|
||||
expansionCredits: number;
|
||||
warCredits: number;
|
||||
reservedMilitaryAssets: number;
|
||||
reservedLogisticsAssets: number;
|
||||
reservedConstructionAssets: number;
|
||||
}
|
||||
|
||||
export interface FactionCommoditySignalSnapshot {
|
||||
@@ -54,96 +94,196 @@ export interface FactionCommoditySignalSnapshot {
|
||||
reservedForConstruction: number;
|
||||
}
|
||||
|
||||
export interface FactionThreatSignalSnapshot {
|
||||
scopeId: string;
|
||||
scopeKind: string;
|
||||
enemyShipCount: number;
|
||||
enemyStationCount: number;
|
||||
}
|
||||
|
||||
export interface FactionBlackboardSnapshot {
|
||||
export interface FactionEconomicAssessmentSnapshot {
|
||||
planCycle: number;
|
||||
updatedAtUtc: string;
|
||||
targetWarshipCount: number;
|
||||
hasWarIndustrySupplyChain: boolean;
|
||||
hasShipyard: boolean;
|
||||
hasActiveExpansionProject: boolean;
|
||||
activeExpansionCommodityId?: string | null;
|
||||
activeExpansionModuleId?: string | null;
|
||||
activeExpansionSiteId?: string | null;
|
||||
activeExpansionSystemId?: string | null;
|
||||
enemyFactionCount: number;
|
||||
enemyShipCount: number;
|
||||
enemyStationCount: number;
|
||||
militaryShipCount: number;
|
||||
minerShipCount: number;
|
||||
transportShipCount: number;
|
||||
constructorShipCount: number;
|
||||
controlledSystemCount: number;
|
||||
targetMilitaryShipCount: number;
|
||||
targetMinerShipCount: number;
|
||||
targetTransportShipCount: number;
|
||||
targetConstructorShipCount: number;
|
||||
hasShipyard: boolean;
|
||||
hasWarIndustrySupplyChain: boolean;
|
||||
primaryExpansionSiteId?: string | null;
|
||||
primaryExpansionSystemId?: string | null;
|
||||
replacementPressure: number;
|
||||
sustainmentScore: number;
|
||||
logisticsSecurityScore: number;
|
||||
criticalShortageCount: number;
|
||||
industrialBottleneckItemId?: string | null;
|
||||
commoditySignals: FactionCommoditySignalSnapshot[];
|
||||
}
|
||||
|
||||
export interface FactionThreatSignalSnapshot {
|
||||
scopeId: string;
|
||||
scopeKind: string;
|
||||
enemyShipCount: number;
|
||||
enemyStationCount: number;
|
||||
enemyFactionId?: string | null;
|
||||
}
|
||||
|
||||
export interface FactionThreatAssessmentSnapshot {
|
||||
planCycle: number;
|
||||
updatedAtUtc: string;
|
||||
enemyFactionCount: number;
|
||||
enemyShipCount: number;
|
||||
enemyStationCount: number;
|
||||
primaryThreatFactionId?: string | null;
|
||||
primaryThreatSystemId?: string | null;
|
||||
threatSignals: FactionThreatSignalSnapshot[];
|
||||
}
|
||||
|
||||
export interface FactionTheaterSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
systemId: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
supplyRisk: number;
|
||||
friendlyAssetValue: number;
|
||||
targetFactionId?: string | null;
|
||||
anchorEntityId?: string | null;
|
||||
anchorPosition?: Vector3Dto | null;
|
||||
updatedAtUtc: string;
|
||||
campaignIds: string[];
|
||||
}
|
||||
|
||||
export interface FactionPlanStepSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
commodityId?: string | null;
|
||||
moduleId?: string | null;
|
||||
targetFactionId?: string | null;
|
||||
targetSiteId?: string | null;
|
||||
summary?: string | null;
|
||||
blockingReason?: string | null;
|
||||
notes?: string | null;
|
||||
lastEvaluatedCycle: number;
|
||||
dependencyStepIds: string[];
|
||||
requiredFacts: string[];
|
||||
producedFacts: string[];
|
||||
assignedAssets: string[];
|
||||
issuedTaskIds: string[];
|
||||
}
|
||||
|
||||
export interface FactionIssuedTaskSnapshot {
|
||||
export interface FactionCampaignSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
state: string;
|
||||
objectiveId: string;
|
||||
stepId: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
shipRole?: string | null;
|
||||
commodityId?: string | null;
|
||||
moduleId?: string | null;
|
||||
theaterId?: string | null;
|
||||
targetFactionId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetSiteId?: string | null;
|
||||
createdAtCycle: number;
|
||||
updatedAtCycle: number;
|
||||
blockingReason?: string | null;
|
||||
notes?: string | null;
|
||||
assignedAssets: string[];
|
||||
targetEntityId?: string | null;
|
||||
commodityId?: string | null;
|
||||
supportStationId?: string | null;
|
||||
currentStepIndex: number;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
summary?: string | null;
|
||||
pauseReason?: string | null;
|
||||
continuationScore: number;
|
||||
supplyAdequacy: number;
|
||||
replacementPressure: number;
|
||||
failureCount: number;
|
||||
successCount: number;
|
||||
fleetCommanderId?: string | null;
|
||||
requiresReinforcement: boolean;
|
||||
steps: FactionPlanStepSnapshot[];
|
||||
objectiveIds: string[];
|
||||
}
|
||||
|
||||
export interface FactionObjectiveSnapshot {
|
||||
id: string;
|
||||
campaignId: string;
|
||||
theaterId?: string | null;
|
||||
kind: string;
|
||||
state: string;
|
||||
delegationKind: string;
|
||||
behaviorKind: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
parentObjectiveId?: string | null;
|
||||
targetFactionId?: string | null;
|
||||
commanderId?: string | null;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetSiteId?: string | null;
|
||||
targetRegionId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
notes?: string | null;
|
||||
currentStepIndex: number;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
reinforcementLevel: number;
|
||||
steps: FactionPlanStepSnapshot[];
|
||||
reservedAssetIds: string[];
|
||||
}
|
||||
|
||||
export interface FactionReservationSnapshot {
|
||||
id: string;
|
||||
objectiveId: string;
|
||||
campaignId?: string | null;
|
||||
assetKind: string;
|
||||
assetId: string;
|
||||
priority: number;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface FactionProductionProgramSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
campaignId?: string | null;
|
||||
commodityId?: string | null;
|
||||
moduleId?: string | null;
|
||||
budgetWeight: number;
|
||||
slotCost: number;
|
||||
createdAtCycle: number;
|
||||
updatedAtCycle: number;
|
||||
invalidationReason?: string | null;
|
||||
blockingReason?: string | null;
|
||||
prerequisiteObjectiveIds: string[];
|
||||
assignedAssets: string[];
|
||||
steps: FactionPlanStepSnapshot[];
|
||||
shipKind?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetCount: number;
|
||||
currentCount: number;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface FactionDecisionLogEntrySnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
summary: string;
|
||||
relatedEntityId?: string | null;
|
||||
planCycle: number;
|
||||
occurredAtUtc: string;
|
||||
}
|
||||
|
||||
export interface FactionStrategicStateSnapshot {
|
||||
planCycle: number;
|
||||
updatedAtUtc: string;
|
||||
status: string;
|
||||
budget: FactionBudgetSnapshot;
|
||||
economicAssessment: FactionEconomicAssessmentSnapshot;
|
||||
threatAssessment: FactionThreatAssessmentSnapshot;
|
||||
theaters: FactionTheaterSnapshot[];
|
||||
campaigns: FactionCampaignSnapshot[];
|
||||
objectives: FactionObjectiveSnapshot[];
|
||||
reservations: FactionReservationSnapshot[];
|
||||
productionPrograms: FactionProductionProgramSnapshot[];
|
||||
}
|
||||
|
||||
export interface CommanderAssignmentSnapshot {
|
||||
commanderId: string;
|
||||
kind: string;
|
||||
behaviorKind: string;
|
||||
status: string;
|
||||
objectiveId?: string | null;
|
||||
campaignId?: string | null;
|
||||
theaterId?: string | null;
|
||||
parentCommanderId?: string | null;
|
||||
controlledEntityId?: string | null;
|
||||
priority: number;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
notes?: string | null;
|
||||
updatedAtUtc?: string | null;
|
||||
activeObjectiveIds: string[];
|
||||
subordinateCommanderIds: string[];
|
||||
}
|
||||
|
||||
export interface FactionSnapshot {
|
||||
@@ -157,11 +297,11 @@ export interface FactionSnapshot {
|
||||
shipsBuilt: number;
|
||||
shipsLost: number;
|
||||
defaultPolicySetId?: string | null;
|
||||
strategicAssessment?: FactionPlanningStateSnapshot | null;
|
||||
strategicPriorities?: FactionStrategicPrioritySnapshot[] | null;
|
||||
blackboard?: FactionBlackboardSnapshot | null;
|
||||
objectives?: FactionObjectiveSnapshot[] | null;
|
||||
issuedTasks?: FactionIssuedTaskSnapshot[] | null;
|
||||
doctrine: FactionDoctrineSnapshot;
|
||||
memory: FactionMemorySnapshot;
|
||||
strategicState: FactionStrategicStateSnapshot;
|
||||
decisionLog: FactionDecisionLogEntrySnapshot[];
|
||||
commanders: CommanderAssignmentSnapshot[];
|
||||
}
|
||||
|
||||
export interface FactionDelta extends FactionSnapshot {}
|
||||
|
||||
307
apps/viewer/src/contractsGeopolitics.ts
Normal file
307
apps/viewer/src/contractsGeopolitics.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
export interface SystemRouteLinkSnapshot {
|
||||
id: string;
|
||||
sourceSystemId: string;
|
||||
destinationSystemId: string;
|
||||
distance: number;
|
||||
isPrimaryLane: boolean;
|
||||
}
|
||||
|
||||
export interface DiplomaticRelationSnapshot {
|
||||
id: string;
|
||||
factionAId: string;
|
||||
factionBId: string;
|
||||
status: string;
|
||||
posture: string;
|
||||
trustScore: number;
|
||||
tensionScore: number;
|
||||
grievanceScore: number;
|
||||
tradeAccessPolicy: string;
|
||||
militaryAccessPolicy: string;
|
||||
warStateId?: string | null;
|
||||
ceasefireUntilUtc?: string | null;
|
||||
updatedAtUtc: string;
|
||||
activeTreatyIds: string[];
|
||||
activeIncidentIds: string[];
|
||||
}
|
||||
|
||||
export interface TreatySnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
tradeAccessPolicy: string;
|
||||
militaryAccessPolicy: string;
|
||||
summary?: string | null;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
factionIds: string[];
|
||||
}
|
||||
|
||||
export interface DiplomaticIncidentSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
sourceFactionId: string;
|
||||
targetFactionId: string;
|
||||
systemId?: string | null;
|
||||
borderEdgeId?: string | null;
|
||||
summary: string;
|
||||
severity: number;
|
||||
escalationScore: number;
|
||||
createdAtUtc: string;
|
||||
lastObservedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface BorderTensionSnapshot {
|
||||
id: string;
|
||||
relationId: string;
|
||||
borderEdgeId: string;
|
||||
factionAId: string;
|
||||
factionBId: string;
|
||||
status: string;
|
||||
tensionScore: number;
|
||||
incidentScore: number;
|
||||
militaryPressure: number;
|
||||
accessFriction: number;
|
||||
updatedAtUtc: string;
|
||||
systemIds: string[];
|
||||
}
|
||||
|
||||
export interface WarStateSnapshot {
|
||||
id: string;
|
||||
relationId: string;
|
||||
factionAId: string;
|
||||
factionBId: string;
|
||||
status: string;
|
||||
warGoal: string;
|
||||
escalationScore: number;
|
||||
startedAtUtc: string;
|
||||
ceasefireUntilUtc?: string | null;
|
||||
updatedAtUtc: string;
|
||||
activeFrontLineIds: string[];
|
||||
}
|
||||
|
||||
export interface DiplomaticStateSnapshot {
|
||||
relations: DiplomaticRelationSnapshot[];
|
||||
treaties: TreatySnapshot[];
|
||||
incidents: DiplomaticIncidentSnapshot[];
|
||||
borderTensions: BorderTensionSnapshot[];
|
||||
wars: WarStateSnapshot[];
|
||||
}
|
||||
|
||||
export interface TerritoryClaimSnapshot {
|
||||
id: string;
|
||||
sourceClaimId?: string | null;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId?: string | null;
|
||||
status: string;
|
||||
claimKind: string;
|
||||
claimStrength: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface TerritoryInfluenceSnapshot {
|
||||
id: string;
|
||||
systemId: string;
|
||||
factionId: string;
|
||||
claimStrength: number;
|
||||
assetStrength: number;
|
||||
logisticsStrength: number;
|
||||
totalInfluence: number;
|
||||
isContesting: boolean;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface TerritoryControlStateSnapshot {
|
||||
systemId: string;
|
||||
controllerFactionId?: string | null;
|
||||
primaryClaimantFactionId?: string | null;
|
||||
controlKind: string;
|
||||
isContested: boolean;
|
||||
controlScore: number;
|
||||
strategicValue: number;
|
||||
updatedAtUtc: string;
|
||||
claimantFactionIds: string[];
|
||||
influencingFactionIds: string[];
|
||||
}
|
||||
|
||||
export interface SectorStrategicProfileSnapshot {
|
||||
systemId: string;
|
||||
controllerFactionId?: string | null;
|
||||
zoneKind: string;
|
||||
isContested: boolean;
|
||||
strategicValue: number;
|
||||
securityRating: number;
|
||||
territorialPressure: number;
|
||||
logisticsValue: number;
|
||||
updatedAtUtc: string;
|
||||
economicRegionId?: string | null;
|
||||
frontLineId?: string | null;
|
||||
}
|
||||
|
||||
export interface BorderEdgeSnapshot {
|
||||
id: string;
|
||||
sourceSystemId: string;
|
||||
destinationSystemId: string;
|
||||
sourceFactionId?: string | null;
|
||||
destinationFactionId?: string | null;
|
||||
isContested: boolean;
|
||||
relationId?: string | null;
|
||||
tensionScore: number;
|
||||
corridorImportance: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface FrontLineSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
anchorSystemId?: string | null;
|
||||
pressureScore: number;
|
||||
supplyRisk: number;
|
||||
updatedAtUtc: string;
|
||||
factionIds: string[];
|
||||
systemIds: string[];
|
||||
borderEdgeIds: string[];
|
||||
}
|
||||
|
||||
export interface TerritoryZoneSnapshot {
|
||||
id: string;
|
||||
systemId: string;
|
||||
factionId?: string | null;
|
||||
kind: string;
|
||||
status: string;
|
||||
reason?: string | null;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface TerritoryPressureSnapshot {
|
||||
id: string;
|
||||
systemId: string;
|
||||
factionId?: string | null;
|
||||
kind: string;
|
||||
pressureScore: number;
|
||||
securityScore: number;
|
||||
hostileInfluence: number;
|
||||
corridorRisk: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface TerritoryStateSnapshot {
|
||||
claims: TerritoryClaimSnapshot[];
|
||||
influences: TerritoryInfluenceSnapshot[];
|
||||
controlStates: TerritoryControlStateSnapshot[];
|
||||
strategicProfiles: SectorStrategicProfileSnapshot[];
|
||||
borderEdges: BorderEdgeSnapshot[];
|
||||
frontLines: FrontLineSnapshot[];
|
||||
zones: TerritoryZoneSnapshot[];
|
||||
pressures: TerritoryPressureSnapshot[];
|
||||
}
|
||||
|
||||
export interface EconomicRegionSnapshot {
|
||||
id: string;
|
||||
factionId?: string | null;
|
||||
label: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
coreSystemId: string;
|
||||
updatedAtUtc: string;
|
||||
systemIds: string[];
|
||||
stationIds: string[];
|
||||
frontLineIds: string[];
|
||||
corridorIds: string[];
|
||||
}
|
||||
|
||||
export interface SupplyNetworkSnapshot {
|
||||
id: string;
|
||||
regionId: string;
|
||||
throughputScore: number;
|
||||
riskScore: number;
|
||||
updatedAtUtc: string;
|
||||
stationIds: string[];
|
||||
producerItemIds: string[];
|
||||
consumerItemIds: string[];
|
||||
constructionItemIds: string[];
|
||||
}
|
||||
|
||||
export interface LogisticsCorridorSnapshot {
|
||||
id: string;
|
||||
factionId?: string | null;
|
||||
kind: string;
|
||||
status: string;
|
||||
riskScore: number;
|
||||
throughputScore: number;
|
||||
accessState: string;
|
||||
updatedAtUtc: string;
|
||||
systemPathIds: string[];
|
||||
regionIds: string[];
|
||||
borderEdgeIds: string[];
|
||||
}
|
||||
|
||||
export interface RegionalProductionProfileSnapshot {
|
||||
regionId: string;
|
||||
primaryIndustry: string;
|
||||
shipyardCount: number;
|
||||
stationCount: number;
|
||||
updatedAtUtc: string;
|
||||
producedItemIds: string[];
|
||||
scarceItemIds: string[];
|
||||
}
|
||||
|
||||
export interface RegionalTradeBalanceSnapshot {
|
||||
regionId: string;
|
||||
importsRequiredCount: number;
|
||||
exportsSurplusCount: number;
|
||||
criticalShortageCount: number;
|
||||
netTradeScore: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface RegionalBottleneckSnapshot {
|
||||
id: string;
|
||||
regionId: string;
|
||||
itemId: string;
|
||||
cause: string;
|
||||
status: string;
|
||||
severity: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface RegionalSecurityAssessmentSnapshot {
|
||||
regionId: string;
|
||||
supplyRisk: number;
|
||||
borderPressure: number;
|
||||
activeWarCount: number;
|
||||
hostileRelationCount: number;
|
||||
accessFriction: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface RegionalEconomicAssessmentSnapshot {
|
||||
regionId: string;
|
||||
sustainmentScore: number;
|
||||
productionDepth: number;
|
||||
constructionPressure: number;
|
||||
corridorDependency: number;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface EconomyRegionStateSnapshot {
|
||||
regions: EconomicRegionSnapshot[];
|
||||
supplyNetworks: SupplyNetworkSnapshot[];
|
||||
corridors: LogisticsCorridorSnapshot[];
|
||||
productionProfiles: RegionalProductionProfileSnapshot[];
|
||||
tradeBalances: RegionalTradeBalanceSnapshot[];
|
||||
bottlenecks: RegionalBottleneckSnapshot[];
|
||||
securityAssessments: RegionalSecurityAssessmentSnapshot[];
|
||||
economicAssessments: RegionalEconomicAssessmentSnapshot[];
|
||||
}
|
||||
|
||||
export interface GeopoliticalStateSnapshot {
|
||||
cycle: number;
|
||||
updatedAtUtc: string;
|
||||
routes: SystemRouteLinkSnapshot[];
|
||||
diplomacy: DiplomaticStateSnapshot;
|
||||
territory: TerritoryStateSnapshot;
|
||||
economyRegions: EconomyRegionStateSnapshot;
|
||||
}
|
||||
289
apps/viewer/src/contractsPlayerFaction.ts
Normal file
289
apps/viewer/src/contractsPlayerFaction.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { Vector3Dto } from "./contractsCommon";
|
||||
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||
|
||||
export interface PlayerAssetRegistrySnapshot {
|
||||
shipIds: string[];
|
||||
stationIds: string[];
|
||||
commanderIds: string[];
|
||||
claimIds: string[];
|
||||
constructionSiteIds: string[];
|
||||
policySetIds: string[];
|
||||
marketOrderIds: string[];
|
||||
fleetIds: string[];
|
||||
taskForceIds: string[];
|
||||
stationGroupIds: string[];
|
||||
economicRegionIds: string[];
|
||||
frontIds: string[];
|
||||
reserveIds: string[];
|
||||
}
|
||||
|
||||
export interface PlayerStrategicIntentSnapshot {
|
||||
strategicPosture: string;
|
||||
economicPosture: string;
|
||||
militaryPosture: string;
|
||||
logisticsPosture: string;
|
||||
desiredReserveRatio: number;
|
||||
allowDelegatedCombatAutomation: boolean;
|
||||
allowDelegatedEconomicAutomation: boolean;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PlayerFleetSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
role: string;
|
||||
commanderId?: string | null;
|
||||
frontId?: string | null;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
reinforcementPolicyId?: string | null;
|
||||
assetIds: string[];
|
||||
taskForceIds: string[];
|
||||
directiveIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerTaskForceSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
role: string;
|
||||
fleetId?: string | null;
|
||||
commanderId?: string | null;
|
||||
frontId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
assetIds: string[];
|
||||
directiveIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerStationGroupSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
role: string;
|
||||
economicRegionId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
stationIds: string[];
|
||||
directiveIds: string[];
|
||||
focusItemIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerEconomicRegionSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
role: string;
|
||||
sharedEconomicRegionId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
systemIds: string[];
|
||||
stationGroupIds: string[];
|
||||
directiveIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerFrontSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
posture: string;
|
||||
sharedFrontLineId?: string | null;
|
||||
targetFactionId?: string | null;
|
||||
systemIds: string[];
|
||||
fleetIds: string[];
|
||||
reserveIds: string[];
|
||||
directiveIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerReserveGroupSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
reserveKind: string;
|
||||
homeSystemId?: string | null;
|
||||
policyId?: string | null;
|
||||
assetIds: string[];
|
||||
frontIds: string[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerFactionPolicySnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
scopeKind: string;
|
||||
scopeId?: string | null;
|
||||
policySetId?: string | null;
|
||||
allowDelegatedCombat: boolean;
|
||||
allowDelegatedTrade: boolean;
|
||||
reserveCreditsRatio: number;
|
||||
reserveMilitaryRatio: number;
|
||||
tradeAccessPolicy: string;
|
||||
dockingAccessPolicy: string;
|
||||
constructionAccessPolicy: string;
|
||||
operationalRangePolicy: string;
|
||||
combatEngagementPolicy: string;
|
||||
avoidHostileSystems: boolean;
|
||||
fleeHullRatio: number;
|
||||
blacklistedSystemIds: string[];
|
||||
notes?: string | null;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerAutomationPolicySnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
scopeKind: string;
|
||||
scopeId?: string | null;
|
||||
enabled: boolean;
|
||||
behaviorKind: string;
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
maxSystemRange: number;
|
||||
knownStationsOnly: boolean;
|
||||
radius: number;
|
||||
waitSeconds: number;
|
||||
preferredItemId?: string | null;
|
||||
notes?: string | null;
|
||||
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerReinforcementPolicySnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
scopeKind: string;
|
||||
scopeId?: string | null;
|
||||
shipKind: string;
|
||||
desiredAssetCount: number;
|
||||
minimumReserveCount: number;
|
||||
autoTransferReserves: boolean;
|
||||
autoQueueProduction: boolean;
|
||||
sourceReserveId?: string | null;
|
||||
targetFrontId?: string | null;
|
||||
notes?: string | null;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerProductionProgramSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
kind: string;
|
||||
targetShipKind?: string | null;
|
||||
targetModuleId?: string | null;
|
||||
targetItemId?: string | null;
|
||||
targetCount: number;
|
||||
currentCount: number;
|
||||
stationGroupId?: string | null;
|
||||
reinforcementPolicyId?: string | null;
|
||||
notes?: string | null;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerDirectiveSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
status: string;
|
||||
kind: string;
|
||||
scopeKind: string;
|
||||
scopeId: string;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
behaviorKind: string;
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
radius: number;
|
||||
waitSeconds: number;
|
||||
maxSystemRange: number;
|
||||
knownStationsOnly: boolean;
|
||||
patrolPoints: Vector3Dto[];
|
||||
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
notes?: string | null;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerAssignmentSnapshot {
|
||||
id: string;
|
||||
assetKind: string;
|
||||
assetId: string;
|
||||
fleetId?: string | null;
|
||||
taskForceId?: string | null;
|
||||
stationGroupId?: string | null;
|
||||
economicRegionId?: string | null;
|
||||
frontId?: string | null;
|
||||
reserveId?: string | null;
|
||||
directiveId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
role: string;
|
||||
status: string;
|
||||
updatedAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerDecisionLogEntrySnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
summary: string;
|
||||
relatedEntityKind?: string | null;
|
||||
relatedEntityId?: string | null;
|
||||
occurredAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerAlertSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
severity: string;
|
||||
summary: string;
|
||||
assetKind?: string | null;
|
||||
assetId?: string | null;
|
||||
relatedDirectiveId?: string | null;
|
||||
status: string;
|
||||
createdAtUtc: string;
|
||||
}
|
||||
|
||||
export interface PlayerFactionSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
sovereignFactionId: string;
|
||||
status: string;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
assetRegistry: PlayerAssetRegistrySnapshot;
|
||||
strategicIntent: PlayerStrategicIntentSnapshot;
|
||||
fleets: PlayerFleetSnapshot[];
|
||||
taskForces: PlayerTaskForceSnapshot[];
|
||||
stationGroups: PlayerStationGroupSnapshot[];
|
||||
economicRegions: PlayerEconomicRegionSnapshot[];
|
||||
fronts: PlayerFrontSnapshot[];
|
||||
reserves: PlayerReserveGroupSnapshot[];
|
||||
policies: PlayerFactionPolicySnapshot[];
|
||||
automationPolicies: PlayerAutomationPolicySnapshot[];
|
||||
reinforcementPolicies: PlayerReinforcementPolicySnapshot[];
|
||||
productionPrograms: PlayerProductionProgramSnapshot[];
|
||||
directives: PlayerDirectiveSnapshot[];
|
||||
assignments: PlayerAssignmentSnapshot[];
|
||||
decisionLog: PlayerDecisionLogEntrySnapshot[];
|
||||
alerts: PlayerAlertSnapshot[];
|
||||
}
|
||||
@@ -1,5 +1,140 @@
|
||||
import type { InventoryEntry, Vector3Dto } from "./contractsCommon";
|
||||
|
||||
export interface ShipSkillProfileSnapshot {
|
||||
navigation: number;
|
||||
trade: number;
|
||||
mining: number;
|
||||
combat: number;
|
||||
construction: number;
|
||||
}
|
||||
|
||||
export interface ShipOrderSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
interruptCurrentPlan: boolean;
|
||||
createdAtUtc: string;
|
||||
label?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
radius: number;
|
||||
maxSystemRange?: number | null;
|
||||
knownStationsOnly: boolean;
|
||||
failureReason?: string | null;
|
||||
}
|
||||
|
||||
export interface ShipOrderTemplateSnapshot {
|
||||
kind: string;
|
||||
label?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
radius: number;
|
||||
maxSystemRange?: number | null;
|
||||
knownStationsOnly: boolean;
|
||||
}
|
||||
|
||||
export interface DefaultBehaviorSnapshot {
|
||||
kind: string;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
preferredItemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
waitSeconds: number;
|
||||
radius: number;
|
||||
maxSystemRange: number;
|
||||
knownStationsOnly: boolean;
|
||||
patrolPoints: Vector3Dto[];
|
||||
patrolIndex: number;
|
||||
repeatOrders: ShipOrderTemplateSnapshot[];
|
||||
repeatIndex: number;
|
||||
}
|
||||
|
||||
export interface ShipAssignmentSnapshot {
|
||||
commanderId: string;
|
||||
parentCommanderId?: string | null;
|
||||
kind: string;
|
||||
behaviorKind: string;
|
||||
status: string;
|
||||
objectiveId?: string | null;
|
||||
campaignId?: string | null;
|
||||
theaterId?: string | null;
|
||||
priority: number;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
notes?: string | null;
|
||||
updatedAtUtc?: string | null;
|
||||
}
|
||||
|
||||
export interface ShipSubTaskSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
summary: string;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetNodeId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
moduleId?: string | null;
|
||||
threshold: number;
|
||||
amount: number;
|
||||
progress: number;
|
||||
elapsedSeconds: number;
|
||||
totalSeconds: number;
|
||||
blockingReason?: string | null;
|
||||
}
|
||||
|
||||
export interface ShipPlanStepSnapshot {
|
||||
id: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
summary: string;
|
||||
blockingReason?: string | null;
|
||||
currentSubTaskIndex: number;
|
||||
subTasks: ShipSubTaskSnapshot[];
|
||||
}
|
||||
|
||||
export interface ShipPlanSnapshot {
|
||||
id: string;
|
||||
sourceKind: string;
|
||||
sourceId: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
summary: string;
|
||||
currentStepIndex: number;
|
||||
createdAtUtc: string;
|
||||
updatedAtUtc: string;
|
||||
interruptReason?: string | null;
|
||||
failureReason?: string | null;
|
||||
steps: ShipPlanStepSnapshot[];
|
||||
}
|
||||
|
||||
export interface ShipSnapshot {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -10,33 +145,33 @@ export interface ShipSnapshot {
|
||||
localVelocity: Vector3Dto;
|
||||
targetLocalPosition: Vector3Dto;
|
||||
state: string;
|
||||
orderKind: string | null;
|
||||
defaultBehaviorKind: string;
|
||||
behaviorPhase: string | null;
|
||||
controllerTaskKind: string;
|
||||
commanderObjective: string | null;
|
||||
orderQueue: ShipOrderSnapshot[];
|
||||
defaultBehavior: DefaultBehaviorSnapshot;
|
||||
assignment?: ShipAssignmentSnapshot | null;
|
||||
skills: ShipSkillProfileSnapshot;
|
||||
activePlan?: ShipPlanSnapshot | null;
|
||||
currentStepId?: string | null;
|
||||
activeSubTasks: ShipSubTaskSnapshot[];
|
||||
controlSourceKind: string;
|
||||
controlSourceId?: string | null;
|
||||
controlReason?: string | null;
|
||||
lastReplanReason?: string | null;
|
||||
lastAccessFailureReason?: string | null;
|
||||
celestialId?: string | null;
|
||||
dockedStationId?: string | null;
|
||||
commanderId?: string | null;
|
||||
policySetId?: string | null;
|
||||
cargoCapacity: number;
|
||||
|
||||
travelSpeed: number;
|
||||
travelSpeedUnit: string;
|
||||
inventory: InventoryEntry[];
|
||||
factionId: string;
|
||||
health: number;
|
||||
history: string[];
|
||||
currentAction?: ShipActionProgressSnapshot | null;
|
||||
spatialState: ShipSpatialStateSnapshot;
|
||||
}
|
||||
|
||||
export interface ShipDelta extends ShipSnapshot { }
|
||||
|
||||
export interface ShipActionProgressSnapshot {
|
||||
label: string;
|
||||
progress: number;
|
||||
}
|
||||
export interface ShipDelta extends ShipSnapshot {}
|
||||
|
||||
export interface ShipSpatialStateSnapshot {
|
||||
spaceLayer: string;
|
||||
|
||||
@@ -21,6 +21,8 @@ import type {
|
||||
PolicySetSnapshot,
|
||||
MarketOrderSnapshot,
|
||||
} from "./contractsEconomy";
|
||||
import type { PlayerFactionSnapshot } from "./contractsPlayerFaction";
|
||||
import type { GeopoliticalStateSnapshot } from "./contractsGeopolitics";
|
||||
import type {
|
||||
ShipDelta,
|
||||
ShipSnapshot,
|
||||
@@ -44,6 +46,8 @@ export interface WorldSnapshot {
|
||||
policies: PolicySetSnapshot[];
|
||||
ships: ShipSnapshot[];
|
||||
factions: FactionSnapshot[];
|
||||
playerFaction?: PlayerFactionSnapshot | null;
|
||||
geopolitics?: GeopoliticalStateSnapshot | null;
|
||||
}
|
||||
|
||||
export interface WorldDelta {
|
||||
@@ -63,6 +67,8 @@ export interface WorldDelta {
|
||||
policies: PolicySetDelta[];
|
||||
ships: ShipDelta[];
|
||||
factions: FactionDelta[];
|
||||
playerFaction?: PlayerFactionSnapshot | null;
|
||||
geopolitics?: GeopoliticalStateSnapshot | null;
|
||||
scope?: ObserverScope | null;
|
||||
}
|
||||
|
||||
|
||||
124
apps/viewer/src/playerFactionCommands.ts
Normal file
124
apps/viewer/src/playerFactionCommands.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Vector3Dto } from "./contractsCommon";
|
||||
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||
|
||||
export interface PlayerOrganizationCommandRequest {
|
||||
kind: string;
|
||||
label: string;
|
||||
parentOrganizationId?: string | null;
|
||||
frontId?: string | null;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
reinforcementPolicyId?: string | null;
|
||||
targetFactionId?: string | null;
|
||||
priority?: number | null;
|
||||
role?: string | null;
|
||||
reserveKind?: string | null;
|
||||
systemIds?: string[] | null;
|
||||
focusItemIds?: string[] | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PlayerOrganizationMembershipCommandRequest {
|
||||
assetIds?: string[] | null;
|
||||
childOrganizationIds?: string[] | null;
|
||||
systemIds?: string[] | null;
|
||||
frontIds?: string[] | null;
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface PlayerDirectiveCommandRequest {
|
||||
label: string;
|
||||
kind: string;
|
||||
scopeKind: string;
|
||||
scopeId: string;
|
||||
behaviorKind: string;
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
radius?: number | null;
|
||||
waitSeconds?: number | null;
|
||||
maxSystemRange?: number | null;
|
||||
knownStationsOnly?: boolean | null;
|
||||
patrolPoints?: Vector3Dto[] | null;
|
||||
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PlayerPolicyCommandRequest {
|
||||
label: string;
|
||||
scopeKind: string;
|
||||
scopeId?: string | null;
|
||||
policySetId?: string | null;
|
||||
allowDelegatedCombat: boolean;
|
||||
allowDelegatedTrade: boolean;
|
||||
reserveCreditsRatio: number;
|
||||
reserveMilitaryRatio: number;
|
||||
notes?: string | null;
|
||||
tradeAccessPolicy?: string | null;
|
||||
dockingAccessPolicy?: string | null;
|
||||
constructionAccessPolicy?: string | null;
|
||||
operationalRangePolicy?: string | null;
|
||||
combatEngagementPolicy?: string | null;
|
||||
avoidHostileSystems?: boolean | null;
|
||||
fleeHullRatio?: number | null;
|
||||
blacklistedSystemIds?: string[] | null;
|
||||
}
|
||||
|
||||
export interface PlayerAutomationPolicyCommandRequest {
|
||||
label: string;
|
||||
scopeKind: string;
|
||||
scopeId?: string | null;
|
||||
enabled: boolean;
|
||||
behaviorKind: string;
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
maxSystemRange: number;
|
||||
knownStationsOnly: boolean;
|
||||
radius: number;
|
||||
waitSeconds: number;
|
||||
preferredItemId?: string | null;
|
||||
notes?: string | null;
|
||||
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||
}
|
||||
|
||||
export interface PlayerAssetAssignmentCommandRequest {
|
||||
assetKind: string;
|
||||
assetId: string;
|
||||
fleetId?: string | null;
|
||||
taskForceId?: string | null;
|
||||
stationGroupId?: string | null;
|
||||
economicRegionId?: string | null;
|
||||
frontId?: string | null;
|
||||
reserveId?: string | null;
|
||||
directiveId?: string | null;
|
||||
policyId?: string | null;
|
||||
automationPolicyId?: string | null;
|
||||
role: string;
|
||||
clearConflicts?: boolean;
|
||||
}
|
||||
|
||||
export interface PlayerStrategicIntentCommandRequest {
|
||||
strategicPosture: string;
|
||||
economicPosture: string;
|
||||
militaryPosture: string;
|
||||
logisticsPosture: string;
|
||||
desiredReserveRatio: number;
|
||||
allowDelegatedCombatAutomation: boolean;
|
||||
allowDelegatedEconomicAutomation: boolean;
|
||||
notes?: string | null;
|
||||
}
|
||||
41
apps/viewer/src/shipCommands.ts
Normal file
41
apps/viewer/src/shipCommands.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Vector3Dto } from "./contractsCommon";
|
||||
import type { ShipOrderTemplateSnapshot } from "./contractsShips";
|
||||
|
||||
export interface ShipOrderCommandRequest {
|
||||
kind: string;
|
||||
priority: number;
|
||||
interruptCurrentPlan: boolean;
|
||||
label?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds?: number | null;
|
||||
radius?: number | null;
|
||||
maxSystemRange?: number | null;
|
||||
knownStationsOnly?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ShipDefaultBehaviorCommandRequest {
|
||||
kind: string;
|
||||
homeSystemId?: string | null;
|
||||
homeStationId?: string | null;
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
preferredItemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
waitSeconds?: number | null;
|
||||
radius?: number | null;
|
||||
maxSystemRange?: number | null;
|
||||
knownStationsOnly?: boolean | null;
|
||||
patrolPoints?: Vector3Dto[] | null;
|
||||
repeatOrders?: ShipOrderTemplateSnapshot[] | null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { ShipSnapshot } from "../../contractsShips";
|
||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||
import type { FactionSnapshot } from "../../contractsFactions";
|
||||
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
||||
import type { GeopoliticalStateSnapshot } from "../../contractsGeopolitics";
|
||||
|
||||
export const useGmStore = defineStore("gm", {
|
||||
state: () => ({
|
||||
@@ -10,6 +11,7 @@ export const useGmStore = defineStore("gm", {
|
||||
stations: [] as StationSnapshot[],
|
||||
factions: [] as FactionSnapshot[],
|
||||
marketOrders: [] as MarketOrderSnapshot[],
|
||||
geopolitics: null as GeopoliticalStateSnapshot | null,
|
||||
}),
|
||||
actions: {
|
||||
updateWorld(
|
||||
@@ -17,11 +19,21 @@ export const useGmStore = defineStore("gm", {
|
||||
stations: StationSnapshot[],
|
||||
factions: FactionSnapshot[],
|
||||
marketOrders: MarketOrderSnapshot[],
|
||||
geopolitics: GeopoliticalStateSnapshot | null,
|
||||
) {
|
||||
this.ships = ships;
|
||||
this.stations = stations;
|
||||
this.factions = factions;
|
||||
this.marketOrders = marketOrders;
|
||||
this.geopolitics = geopolitics;
|
||||
},
|
||||
upsertShip(ship: ShipSnapshot) {
|
||||
const index = this.ships.findIndex((candidate) => candidate.id === ship.id);
|
||||
if (index >= 0) {
|
||||
this.ships.splice(index, 1, ship);
|
||||
return;
|
||||
}
|
||||
this.ships.push(ship);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
43
apps/viewer/src/ui/stores/playerFactionStore.ts
Normal file
43
apps/viewer/src/ui/stores/playerFactionStore.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { PlayerFactionSnapshot } from "../../contractsPlayerFaction";
|
||||
|
||||
export const usePlayerFactionStore = defineStore("playerFaction", {
|
||||
state: () => ({
|
||||
playerFaction: null as PlayerFactionSnapshot | null,
|
||||
selectedOrganizationId: null as string | null,
|
||||
selectedDirectiveId: null as string | null,
|
||||
}),
|
||||
actions: {
|
||||
setPlayerFaction(snapshot: PlayerFactionSnapshot | null) {
|
||||
this.playerFaction = snapshot;
|
||||
if (snapshot == null) {
|
||||
this.selectedOrganizationId = null;
|
||||
this.selectedDirectiveId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const knownOrganizationIds = new Set<string>([
|
||||
...snapshot.fleets.map((entry) => entry.id),
|
||||
...snapshot.taskForces.map((entry) => entry.id),
|
||||
...snapshot.stationGroups.map((entry) => entry.id),
|
||||
...snapshot.economicRegions.map((entry) => entry.id),
|
||||
...snapshot.fronts.map((entry) => entry.id),
|
||||
...snapshot.reserves.map((entry) => entry.id),
|
||||
]);
|
||||
if (this.selectedOrganizationId && !knownOrganizationIds.has(this.selectedOrganizationId)) {
|
||||
this.selectedOrganizationId = null;
|
||||
}
|
||||
|
||||
const knownDirectiveIds = new Set(snapshot.directives.map((entry) => entry.id));
|
||||
if (this.selectedDirectiveId && !knownDirectiveIds.has(this.selectedDirectiveId)) {
|
||||
this.selectedDirectiveId = null;
|
||||
}
|
||||
},
|
||||
selectOrganization(organizationId: string | null) {
|
||||
this.selectedOrganizationId = organizationId;
|
||||
},
|
||||
selectDirective(directiveId: string | null) {
|
||||
this.selectedDirectiveId = directiveId;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -7,29 +7,59 @@ import type {
|
||||
OpsStationCardState,
|
||||
OpsStripState,
|
||||
} from "./viewerHudState";
|
||||
import { describeShipCurrentAction, describeShipLocation, describeShipObjective, describeShipState } from "./viewerSelection";
|
||||
import { describeShipCurrentAction, describeShipLocation, describeShipState } from "./viewerSelection";
|
||||
import type { CameraMode, Selectable, WorldState, PovLevel } from "./viewerTypes";
|
||||
|
||||
function buildFactionCard(faction: FactionSnapshot): OpsFactionCardState {
|
||||
const state = faction.strategicAssessment;
|
||||
const blackboard = faction.blackboard;
|
||||
const leadTask = [...(faction.issuedTasks ?? [])]
|
||||
function buildFactionCard(world: WorldState, faction: FactionSnapshot): OpsFactionCardState {
|
||||
const playerFaction = world.playerFaction;
|
||||
if (playerFaction && playerFaction.sovereignFactionId === faction.id) {
|
||||
const selectedDirective = playerFaction.directives[0];
|
||||
return {
|
||||
kind: "faction",
|
||||
id: faction.id,
|
||||
label: `${faction.label} Command`,
|
||||
stateLines: [
|
||||
`Player ${playerFaction.assetRegistry.shipIds.length} ships · ${playerFaction.assetRegistry.stationIds.length} stations`,
|
||||
`Groups ${playerFaction.fleets.length + playerFaction.taskForces.length + playerFaction.stationGroups.length + playerFaction.economicRegions.length + playerFaction.fronts.length + playerFaction.reserves.length}`,
|
||||
`Intent ${playerFaction.strategicIntent.strategicPosture} · ${playerFaction.strategicIntent.economicPosture}`,
|
||||
`Alerts ${playerFaction.alerts.length} · Decisions ${playerFaction.decisionLog.length}`,
|
||||
`Lead ${selectedDirective ? `${selectedDirective.behaviorKind} · ${selectedDirective.scopeKind}` : "no active directives"}`,
|
||||
],
|
||||
priorities: [
|
||||
{ label: "Reserve", value: `${Math.round(playerFaction.strategicIntent.desiredReserveRatio * 100)}%` },
|
||||
{ label: "Auto", value: `${Number(playerFaction.strategicIntent.allowDelegatedEconomicAutomation)}/${Number(playerFaction.strategicIntent.allowDelegatedCombatAutomation)}` },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const strategicState = faction.strategicState;
|
||||
const economic = strategicState.economicAssessment;
|
||||
const activeCampaigns = strategicState.campaigns.filter((campaign) => campaign.status === "active");
|
||||
const activeTheaters = strategicState.theaters.filter((theater) => theater.status === "active");
|
||||
const activeWars = world.geopolitics?.diplomacy.wars.filter((war) => war.factionAId === faction.id || war.factionBId === faction.id).length ?? 0;
|
||||
const contestedSystems = world.geopolitics?.territory.controlStates.filter((state) =>
|
||||
state.isContested && (state.controllerFactionId === faction.id || state.primaryClaimantFactionId === faction.id || state.claimantFactionIds.includes(faction.id))).length ?? 0;
|
||||
const leadCampaign = [...strategicState.campaigns]
|
||||
.sort((left, right) => right.priority - left.priority)[0];
|
||||
const leadTheater = [...strategicState.theaters]
|
||||
.sort((left, right) => right.priority - left.priority)[0];
|
||||
const latestDecision = [...faction.decisionLog]
|
||||
.sort((left, right) => right.occurredAtUtc.localeCompare(left.occurredAtUtc))[0];
|
||||
return {
|
||||
kind: "faction",
|
||||
id: faction.id,
|
||||
label: faction.label,
|
||||
stateLines: state ? [
|
||||
`Military ${state.militaryShipCount} · Miners ${state.minerShipCount}`,
|
||||
`Transport ${state.transportShipCount} · Constructors ${state.constructorShipCount}`,
|
||||
`Systems ${state.controlledSystemCount} / ${state.targetSystemCount}`,
|
||||
`Shipyard ${blackboard?.hasShipyard ? "yes" : "no"} · War industry ${blackboard?.hasWarIndustrySupplyChain ? "yes" : "no"}`,
|
||||
leadTask ? `Task ${leadTask.kind}${leadTask.shipRole ? ` · ${leadTask.shipRole}` : ""}` : `Ore ${state.oreStockpile.toFixed(0)}`,
|
||||
] : [],
|
||||
priorities: (faction.strategicPriorities ?? []).map((entry) => ({
|
||||
label: entry.goalName,
|
||||
value: entry.priority.toFixed(0),
|
||||
})),
|
||||
stateLines: [
|
||||
`Posture ${faction.doctrine.strategicPosture} · ${faction.doctrine.militaryPosture}`,
|
||||
`Campaigns ${activeCampaigns.length} · Fronts ${activeTheaters.length} · Wars ${activeWars}`,
|
||||
`Commit ${economic.militaryShipCount}/${economic.targetMilitaryShipCount} mil · ${economic.minerShipCount}/${economic.targetMinerShipCount} min`,
|
||||
`Reserve ${strategicState.budget.reservedMilitaryAssets} mil · ${strategicState.budget.reservedLogisticsAssets} log`,
|
||||
`Bottleneck ${economic.industrialBottleneckItemId ?? "none"} · Contested ${contestedSystems}${latestDecision ? ` · ${latestDecision.kind}` : ""}`,
|
||||
],
|
||||
priorities: [
|
||||
...(leadCampaign ? [{ label: leadCampaign.kind, value: leadCampaign.priority.toFixed(0) }] : []),
|
||||
...(leadTheater ? [{ label: leadTheater.kind, value: leadTheater.priority.toFixed(0) }] : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +99,9 @@ function buildShipCard(
|
||||
const shipLocation = describeShipLocation(world, ship);
|
||||
const shipState = describeShipState(world, ship);
|
||||
const shipAction = describeShipCurrentAction(ship);
|
||||
const currentStep = ship.activePlan?.steps[ship.activePlan.currentStepIndex];
|
||||
const topOrder = [...ship.orderQueue]
|
||||
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||
|
||||
return {
|
||||
kind: "ship",
|
||||
@@ -84,9 +117,10 @@ function buildShipCard(
|
||||
],
|
||||
action: shipAction ? buildProgressBar(shipAction.label, shipAction.progress) : undefined,
|
||||
aiLines: [
|
||||
...(ship.commanderObjective ? [`Objective ${describeShipObjective(ship.commanderObjective)}`] : []),
|
||||
`Behavior ${ship.defaultBehaviorKind}${ship.behaviorPhase ? ` · ${ship.behaviorPhase}` : ""}`,
|
||||
`Task ${ship.controllerTaskKind}`,
|
||||
`Assignment ${ship.assignment?.kind ?? "unassigned"}`,
|
||||
`Behavior ${ship.defaultBehavior.kind}`,
|
||||
`Plan ${ship.activePlan ? `${ship.activePlan.kind}${currentStep ? ` · ${currentStep.kind}` : ""}` : "none"}`,
|
||||
`Orders ${topOrder ? `${topOrder.kind} +${Math.max(0, ship.orderQueue.length - 1)}` : "none"}`,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -111,7 +145,7 @@ export function buildOpsStripState(
|
||||
|
||||
const factions = [...world.factions.values()]
|
||||
.sort((left, right) => left.label.localeCompare(right.label))
|
||||
.map(buildFactionCard);
|
||||
.map((faction) => buildFactionCard(world, faction));
|
||||
|
||||
const stations = [...world.stations.values()]
|
||||
.filter((station) => !isSystemFiltered || station.systemId === activeSystemId)
|
||||
|
||||
@@ -212,26 +212,37 @@ function formatStorageWithInventory(
|
||||
}
|
||||
|
||||
function renderSystemOwnership(world: WorldState, systemId: string): string {
|
||||
const claims = [...world.claims.values()].filter((claim) =>
|
||||
claim.systemId === systemId && claim.state !== "destroyed");
|
||||
if (claims.length === 0) {
|
||||
return "Ownership none";
|
||||
const control = world.geopolitics?.territory.controlStates.find((state) => state.systemId === systemId);
|
||||
const zone = world.geopolitics?.territory.zones.find((entry) => entry.systemId === systemId);
|
||||
const profile = world.geopolitics?.territory.strategicProfiles.find((entry) => entry.systemId === systemId);
|
||||
const pressure = world.geopolitics?.territory.pressures.find((entry) => entry.systemId === systemId);
|
||||
const region = world.geopolitics?.economyRegions.regions.find((entry) => entry.systemIds.includes(systemId));
|
||||
const front = world.geopolitics?.territory.frontLines.find((entry) => entry.systemIds.includes(systemId));
|
||||
|
||||
if (!control) {
|
||||
const claims = [...world.claims.values()].filter((claim) =>
|
||||
claim.systemId === systemId && claim.state !== "destroyed");
|
||||
return claims.length === 0 ? "Territory unknown" : `Claims ${claims.length}`;
|
||||
}
|
||||
|
||||
const ownershipByFaction = new Map<string, number>();
|
||||
for (const claim of claims) {
|
||||
ownershipByFaction.set(claim.factionId, (ownershipByFaction.get(claim.factionId) ?? 0) + 1);
|
||||
const controllerLabel = control.controllerFactionId
|
||||
? (world.factions.get(control.controllerFactionId)?.label ?? control.controllerFactionId)
|
||||
: "none";
|
||||
const claimantLabel = control.primaryClaimantFactionId
|
||||
? (world.factions.get(control.primaryClaimantFactionId)?.label ?? control.primaryClaimantFactionId)
|
||||
: "none";
|
||||
const lines = [
|
||||
`Control ${control.controlKind} · Controller ${controllerLabel}`,
|
||||
`Claimant ${claimantLabel} · Zone ${zone?.kind ?? profile?.zoneKind ?? "unknown"}`,
|
||||
`Pressure ${Math.round((pressure?.pressureScore ?? profile?.territorialPressure ?? 0) * 100)}% · Security ${Math.round((pressure?.securityScore ?? profile?.securityRating ?? 0) * 100)}%`,
|
||||
];
|
||||
if (front) {
|
||||
lines.push(`Front ${front.id} · ${front.factionIds.join(" vs ")}`);
|
||||
}
|
||||
|
||||
return [...ownershipByFaction.entries()]
|
||||
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
||||
.map(([factionId, count]) => {
|
||||
const faction = world.factions.get(factionId);
|
||||
const label = faction?.label ?? factionId;
|
||||
const share = Math.round((count / claims.length) * 100);
|
||||
return `${label} ${count}/${claims.length} (${share}%)`;
|
||||
})
|
||||
.join("<br>");
|
||||
if (region) {
|
||||
lines.push(`Region ${region.label} · ${region.kind}`);
|
||||
}
|
||||
return lines.join("<br>");
|
||||
}
|
||||
|
||||
export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
@@ -284,14 +295,32 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
const shipBehavior = describeShipBehavior(ship);
|
||||
const shipOrder = describeShipOrder(ship);
|
||||
const shipAction = describeShipCurrentAction(ship);
|
||||
const currentStep = ship.activePlan?.steps[ship.activePlan.currentStepIndex];
|
||||
const orderQueue = ship.orderQueue.length > 0
|
||||
? ship.orderQueue.slice(0, 4).map((order) => `${order.kind} [${order.status}]`).join("<br>")
|
||||
: "none";
|
||||
const subTaskList = ship.activeSubTasks.length > 0
|
||||
? ship.activeSubTasks.slice(0, 4).map((subTask) => `${subTask.summary || subTask.kind} · ${subTask.status}`).join("<br>")
|
||||
: "none";
|
||||
const playerAssignment = world.playerFaction?.assignments.find((assignment) => assignment.assetKind === "ship" && assignment.assetId === ship.id);
|
||||
const playerDirective = playerAssignment?.directiveId
|
||||
? world.playerFaction?.directives.find((directive) => directive.id === playerAssignment.directiveId)
|
||||
: undefined;
|
||||
return {
|
||||
title: ship.label,
|
||||
bodyHtml: `
|
||||
<p>Parent ${parent}</p>
|
||||
<p>Behavior ${shipBehavior}</p>
|
||||
<p>Command ${ship.controlSourceKind}${ship.controlSourceId ? `<br>ID ${ship.controlSourceId}` : ""}${ship.controlReason ? `<br>${ship.controlReason}` : ""}</p>
|
||||
<p>Assignment ${ship.assignment?.kind ?? "unassigned"}${ship.assignment?.campaignId ? `<br>Campaign ${ship.assignment.campaignId}` : ""}</p>
|
||||
${playerAssignment ? `<p>Player ${playerAssignment.role}${playerDirective ? `<br>Directive ${playerDirective.label}` : ""}${playerAssignment.fleetId ? `<br>Fleet ${playerAssignment.fleetId}` : ""}${playerAssignment.taskForceId ? `<br>Task Force ${playerAssignment.taskForceId}` : ""}${playerAssignment.frontId ? `<br>Front ${playerAssignment.frontId}` : ""}</p>` : ""}
|
||||
<p>State ${shipState}</p>
|
||||
<p>Order ${shipOrder}</p>
|
||||
<p>Task ${ship.controllerTaskKind}</p>
|
||||
<p>Queue ${orderQueue}</p>
|
||||
<p>Plan ${ship.activePlan ? `${ship.activePlan.kind} · ${ship.activePlan.status}` : "none"}${currentStep ? `<br>Step ${currentStep.kind} · ${currentStep.status}` : ""}</p>
|
||||
<p>Subtasks ${subTaskList}</p>
|
||||
${ship.lastReplanReason ? `<p>Last replan ${ship.lastReplanReason}</p>` : ""}
|
||||
${ship.lastAccessFailureReason ? `<p>Access ${ship.lastAccessFailureReason}</p>` : ""}
|
||||
${shipAction ? `
|
||||
<div class="detail-progress">
|
||||
<div class="detail-progress-label">
|
||||
@@ -322,11 +351,16 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
? station.dockedShipIds.map((shipId) => world.ships.get(shipId)?.label ?? shipId).join("<br>")
|
||||
: "none";
|
||||
const stationStorage = formatStorageWithInventory(station.storageUsage, station.inventory);
|
||||
const playerAssignment = world.playerFaction?.assignments.find((assignment) => assignment.assetKind === "station" && assignment.assetId === station.id);
|
||||
const playerDirective = playerAssignment?.directiveId
|
||||
? world.playerFaction?.directives.find((directive) => directive.id === playerAssignment.directiveId)
|
||||
: undefined;
|
||||
return {
|
||||
title: station.label,
|
||||
bodyHtml: `
|
||||
<p>${station.category} · ${station.systemId}</p>
|
||||
<p>Parent ${parent}</p>
|
||||
${playerAssignment ? `<p>Player ${playerAssignment.role}${playerDirective ? `<br>Directive ${playerDirective.label}` : ""}${playerAssignment.stationGroupId ? `<br>Group ${playerAssignment.stationGroupId}` : ""}${playerAssignment.economicRegionId ? `<br>Region ${playerAssignment.economicRegionId}` : ""}</p>` : ""}
|
||||
<p>Docked ${station.dockedShips} / ${station.dockingPads}
|
||||
<br>
|
||||
${dockedShipLabels}</p>
|
||||
|
||||
@@ -320,8 +320,9 @@ export function renderSystemDetails(
|
||||
|
||||
export function describeShipState(world: WorldState | undefined, ship: ShipSnapshot): string {
|
||||
const baseState = ship.state;
|
||||
const currentSubTask = ship.activeSubTasks[0];
|
||||
if (baseState === "capacitor-starved") {
|
||||
return `${baseState} while ${describeControllerTask(ship.controllerTaskKind)}`;
|
||||
return currentSubTask ? `${baseState} while ${titleTask(currentSubTask.kind)}` : baseState;
|
||||
}
|
||||
|
||||
if (!world || (baseState !== "ftl" && baseState !== "spooling-ftl" && baseState !== "warping" && baseState !== "spooling-warp")) {
|
||||
@@ -347,75 +348,52 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
|
||||
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
||||
}
|
||||
|
||||
function describeControllerTask(taskKind: string): string {
|
||||
switch (taskKind) {
|
||||
case "travel":
|
||||
return "travel";
|
||||
case "extract":
|
||||
return "mining";
|
||||
case "dock":
|
||||
return "docking";
|
||||
case "unload":
|
||||
return "transfer";
|
||||
case "deliver-construction":
|
||||
return "material delivery";
|
||||
case "build-construction-site":
|
||||
return "site construction";
|
||||
case "construct-module":
|
||||
return "module construction";
|
||||
case "undock":
|
||||
return "undocking";
|
||||
case "load-workers":
|
||||
return "worker loading";
|
||||
case "unload-workers":
|
||||
return "worker unloading";
|
||||
default:
|
||||
return taskKind;
|
||||
}
|
||||
}
|
||||
|
||||
export function describeShipObjective(objective: string): string {
|
||||
switch (objective) {
|
||||
case "set-mining-objective": return "mine resources";
|
||||
case "set-patrol-objective": return "patrol";
|
||||
case "set-construction-objective": return "build station";
|
||||
case "set-idle-objective": return "idle";
|
||||
default: return objective;
|
||||
}
|
||||
return objective.replace(/[-_]+/g, " ");
|
||||
}
|
||||
|
||||
export function describeShipBehavior(ship: ShipSnapshot): string {
|
||||
return ship.behaviorPhase
|
||||
? `${ship.defaultBehaviorKind} · ${ship.behaviorPhase}`
|
||||
: ship.defaultBehaviorKind;
|
||||
const parts = [ship.defaultBehavior.kind];
|
||||
if (ship.assignment?.kind) {
|
||||
parts.push(ship.assignment.kind);
|
||||
}
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
export function describeShipOrder(ship: ShipSnapshot): string {
|
||||
const orderParts: string[] = [];
|
||||
if (ship.orderKind) {
|
||||
orderParts.push(ship.orderKind);
|
||||
}
|
||||
if (ship.commanderObjective) {
|
||||
orderParts.push(describeShipObjective(ship.commanderObjective));
|
||||
}
|
||||
if (orderParts.length > 0) {
|
||||
return orderParts.join(" · ");
|
||||
const activeOrder = [...ship.orderQueue]
|
||||
.sort((left, right) => right.priority - left.priority || left.createdAtUtc.localeCompare(right.createdAtUtc))[0];
|
||||
if (activeOrder) {
|
||||
return activeOrder.label ?? activeOrder.kind;
|
||||
}
|
||||
|
||||
return describeControllerTask(ship.controllerTaskKind);
|
||||
if (ship.assignment?.kind) {
|
||||
return describeShipObjective(ship.assignment.kind);
|
||||
}
|
||||
|
||||
if (ship.activePlan) {
|
||||
return ship.activePlan.summary || ship.activePlan.kind;
|
||||
}
|
||||
|
||||
return ship.defaultBehavior.kind;
|
||||
}
|
||||
|
||||
export function describeShipCurrentAction(ship: ShipSnapshot): { label: string; progress: number } | undefined {
|
||||
if (!ship.currentAction) {
|
||||
const subTask = ship.activeSubTasks[0];
|
||||
if (!subTask) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
label: ship.currentAction.label,
|
||||
progress: Math.max(0, Math.min(ship.currentAction.progress, 1)),
|
||||
label: subTask.summary || titleTask(subTask.kind),
|
||||
progress: Math.max(0, Math.min(subTask.progress, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
function titleTask(value: string): string {
|
||||
return value.replace(/[-_]+/g, " ");
|
||||
}
|
||||
|
||||
export function describeShipLocation(world: WorldState | undefined, ship: ShipSnapshot): { system: string; local?: string } {
|
||||
const systemId = ship.spatialState.currentSystemId || ship.systemId;
|
||||
const system = world?.systems.get(systemId);
|
||||
|
||||
@@ -49,6 +49,8 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
|
||||
policies: new Map(snapshot.policies.map((policy) => [policy.id, policy])),
|
||||
ships: new Map(snapshot.ships.map((ship) => [ship.id, ship])),
|
||||
factions: new Map(snapshot.factions.map((faction) => [faction.id, faction])),
|
||||
playerFaction: snapshot.playerFaction ?? null,
|
||||
geopolitics: snapshot.geopolitics ?? null,
|
||||
recentEvents: [],
|
||||
};
|
||||
}
|
||||
@@ -88,8 +90,14 @@ export function applyDeltaToWorld(world: WorldState, delta: WorldDelta): boolean
|
||||
for (const faction of delta.factions) {
|
||||
world.factions.set(faction.id, faction);
|
||||
}
|
||||
if (delta.playerFaction !== undefined) {
|
||||
world.playerFaction = delta.playerFaction ?? null;
|
||||
}
|
||||
if (delta.geopolitics !== undefined) {
|
||||
world.geopolitics = delta.geopolitics ?? null;
|
||||
}
|
||||
|
||||
return delta.factions.length > 0;
|
||||
return delta.factions.length > 0 || delta.playerFaction !== undefined || delta.geopolitics !== undefined;
|
||||
}
|
||||
|
||||
export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta, rawBytes: number): void {
|
||||
@@ -102,10 +110,11 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
|
||||
+ delta.marketOrders.length
|
||||
+ delta.policies.length
|
||||
+ delta.factions.length;
|
||||
const changedEntitiesWithPlayer = changedEntities + (delta.playerFaction ? 1 : 0) + (delta.geopolitics ? 1 : 0);
|
||||
networkStats.deltasReceived += 1;
|
||||
networkStats.deltaBytes += rawBytes;
|
||||
networkStats.lastDeltaBytes = rawBytes;
|
||||
networkStats.lastEntityChanges = changedEntities;
|
||||
networkStats.lastEntityChanges = changedEntitiesWithPlayer;
|
||||
networkStats.eventsReceived += delta.events.length;
|
||||
networkStats.lastDeltaAtMs = performance.now();
|
||||
networkStats.throughputSamples.push({ atMs: performance.now(), bytes: rawBytes });
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
StationSnapshot,
|
||||
SystemSnapshot,
|
||||
OrbitalSimulationSnapshot,
|
||||
PlayerFactionSnapshot,
|
||||
GeopoliticalStateSnapshot,
|
||||
} from "./contracts";
|
||||
|
||||
export type PovLevel = "local" | "system" | "galaxy";
|
||||
@@ -152,6 +154,8 @@ export interface WorldState {
|
||||
policies: Map<string, PolicySetSnapshot>;
|
||||
ships: Map<string, ShipSnapshot>;
|
||||
factions: Map<string, FactionSnapshot>;
|
||||
playerFaction: PlayerFactionSnapshot | null;
|
||||
geopolitics: GeopoliticalStateSnapshot | null;
|
||||
recentEvents: SimulationEventRecord[];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fetchWorldSnapshot, openWorldStream } from "./api";
|
||||
import type { ViewerHudState } from "./viewerHudState";
|
||||
import { buildOpsStripState } from "./viewerOpsStrip";
|
||||
import { useGmStore } from "./ui/stores/gmStore";
|
||||
import { usePlayerFactionStore } from "./ui/stores/playerFactionStore";
|
||||
import { viewerPinia } from "./ui/stores/pinia";
|
||||
import { buildDetailPanelState } from "./viewerPanels";
|
||||
import { applyDeltaToWorld, cloneFactions, createWorldState, recordDeltaStats } from "./viewerState";
|
||||
@@ -205,7 +206,9 @@ export class ViewerWorldLifecycle {
|
||||
[...world.stations.values()],
|
||||
[...world.factions.values()],
|
||||
[...world.marketOrders.values()],
|
||||
world.geopolitics,
|
||||
);
|
||||
usePlayerFactionStore(viewerPinia).setPlayerFaction(world.playerFaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user