improvement on gm windows, ai
This commit is contained in:
@@ -40,6 +40,7 @@ public sealed class ItemProductionDefinition
|
|||||||
|
|
||||||
public sealed class BalanceDefinition
|
public sealed class BalanceDefinition
|
||||||
{
|
{
|
||||||
|
public float SimulationSpeedMultiplier { get; set; } = 1f;
|
||||||
public float YPlane { get; set; }
|
public float YPlane { get; set; }
|
||||||
public float ArrivalThreshold { get; set; }
|
public float ArrivalThreshold { get; set; }
|
||||||
public float MiningRate { get; set; }
|
public float MiningRate { get; set; }
|
||||||
@@ -94,7 +95,8 @@ public sealed class AsteroidFieldDefinition
|
|||||||
|
|
||||||
public sealed class ResourceNodeDefinition
|
public sealed class ResourceNodeDefinition
|
||||||
{
|
{
|
||||||
public string SourceKind { get; set; } = "asteroid-belt";
|
public string SourceKind { get; set; } = "local-space";
|
||||||
|
public string? AnchorReference { get; set; }
|
||||||
public float Angle { get; set; }
|
public float Angle { get; set; }
|
||||||
public float RadiusOffset { get; set; }
|
public float RadiusOffset { get; set; }
|
||||||
public float InclinationDegrees { get; set; }
|
public float InclinationDegrees { get; set; }
|
||||||
|
|||||||
@@ -457,6 +457,18 @@ internal sealed class ObjectiveStepFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus Status,
|
||||||
|
string StatusReason,
|
||||||
|
string? BlockingReason = null,
|
||||||
|
StepExecutionBinding? Binding = null,
|
||||||
|
IndustryExpansionProject? ExpectedProject = null);
|
||||||
|
|
||||||
|
internal sealed record StepExecutionBinding(
|
||||||
|
string Kind,
|
||||||
|
string? TargetId,
|
||||||
|
string Summary);
|
||||||
|
|
||||||
internal sealed class FactionObjectiveExecutor
|
internal sealed class FactionObjectiveExecutor
|
||||||
{
|
{
|
||||||
internal void Execute(
|
internal void Execute(
|
||||||
@@ -466,6 +478,7 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
FactionPlanningState state)
|
FactionPlanningState state)
|
||||||
{
|
{
|
||||||
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are executed.");
|
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are executed.");
|
||||||
|
var activeProject = FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId);
|
||||||
|
|
||||||
commander.ActiveGoalName = null;
|
commander.ActiveGoalName = null;
|
||||||
commander.ActiveActionName = null;
|
commander.ActiveActionName = null;
|
||||||
@@ -480,8 +493,8 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
EvaluateObjective(world, commander, objective);
|
EvaluateObjective(world, commander, objective);
|
||||||
foreach (var step in objective.Steps.OrderByDescending(step => step.Priority))
|
foreach (var step in objective.Steps.OrderByDescending(step => step.Priority))
|
||||||
{
|
{
|
||||||
EvaluateStep(world, commander, objective, step, blackboard, state);
|
var assessment = EvaluateStep(world, commander, objective, step, blackboard, state, activeProject);
|
||||||
EmitTasks(engine, world, commander, objective, step, blackboard, state, touchedTaskIds, assignedAssetIds);
|
EmitTasks(engine, world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,102 +544,300 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
objective.State = FactionObjectiveState.Active;
|
objective.State = FactionObjectiveState.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EvaluateStep(
|
private static StepExecutionAssessment EvaluateStep(
|
||||||
SimulationWorld world,
|
SimulationWorld world,
|
||||||
CommanderRuntime commander,
|
CommanderRuntime commander,
|
||||||
FactionObjectiveRuntime objective,
|
FactionObjectiveRuntime objective,
|
||||||
FactionPlanStepRuntime step,
|
FactionPlanStepRuntime step,
|
||||||
FactionBlackboardRuntime blackboard,
|
FactionBlackboardRuntime blackboard,
|
||||||
FactionPlanningState state)
|
FactionPlanningState state,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
{
|
{
|
||||||
step.LastEvaluatedCycle = commander.PlanningCycle;
|
step.LastEvaluatedCycle = commander.PlanningCycle;
|
||||||
step.BlockingReason = null;
|
step.BlockingReason = null;
|
||||||
|
step.StatusReason = null;
|
||||||
|
step.ExecutionBindingKind = null;
|
||||||
|
step.ExecutionBindingTargetId = null;
|
||||||
|
step.ExecutionBindingSummary = null;
|
||||||
|
|
||||||
|
StepExecutionAssessment assessment;
|
||||||
if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step))
|
if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step))
|
||||||
{
|
{
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
assessment = new StepExecutionAssessment(
|
||||||
step.BlockingReason = "Waiting for prerequisite objective steps to complete.";
|
FactionPlanStepStatus.Blocked,
|
||||||
return;
|
"Blocked on prerequisite objective steps.",
|
||||||
|
BlockingReason: "Waiting for prerequisite objective steps to complete.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assessment = step.Kind switch
|
||||||
|
{
|
||||||
|
FactionPlanStepKind.EnsureCommodityProduction => EvaluateCommodityStep(world, commander, step, blackboard, activeProject),
|
||||||
|
FactionPlanStepKind.EnsureShipyardSite => EvaluateShipyardStep(world, commander, step, state, activeProject),
|
||||||
|
FactionPlanStepKind.ProduceFleet => EvaluateFleetProductionStep(commander, step, blackboard, state),
|
||||||
|
FactionPlanStepKind.AttackFactionAssets => EvaluateAttackStep(commander, step, blackboard, state),
|
||||||
|
FactionPlanStepKind.EnsureWaterSupply => EvaluateWaterStep(world, commander, step, blackboard, activeProject),
|
||||||
|
FactionPlanStepKind.EnsureMiningCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.MinerShipCount, 2, "mining"),
|
||||||
|
FactionPlanStepKind.EnsureConstructionCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.ConstructorShipCount, 1, "construction"),
|
||||||
|
FactionPlanStepKind.EnsureTransportCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.TransportShipCount, 1, "transport"),
|
||||||
|
FactionPlanStepKind.MonitorExpansionProject => EvaluateWarIndustryMonitorStep(world, commander, step, blackboard, activeProject),
|
||||||
|
_ => new StepExecutionAssessment(FactionPlanStepStatus.Failed, "Unknown step kind."),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (step.Kind)
|
ApplyAssessment(step, assessment);
|
||||||
{
|
return assessment;
|
||||||
case FactionPlanStepKind.EnsureCommodityProduction:
|
|
||||||
EvaluateCommodityStep(step, blackboard);
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.EnsureShipyardSite:
|
|
||||||
if (state.HasShipFactory)
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Complete;
|
|
||||||
step.ProducedFacts.Add("shipyard-online");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
step.Status = blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionModuleId, step.ModuleId, StringComparison.Ordinal)
|
|
||||||
? FactionPlanStepStatus.Running
|
|
||||||
: FactionPlanStepStatus.Ready;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.ProduceFleet:
|
|
||||||
step.Status = state.MilitaryShipCount >= blackboard.TargetWarshipCount
|
|
||||||
? FactionPlanStepStatus.Complete
|
|
||||||
: FactionPlanStepStatus.Running;
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.AttackFactionAssets:
|
|
||||||
if (blackboard.EnemyFactionCount <= 0)
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Complete;
|
|
||||||
}
|
|
||||||
else if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
|
||||||
step.BlockingReason = "Insufficient military strength to commit to a faction attack objective.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.EnsureWaterSupply:
|
|
||||||
step.Status = IsCommodityOperational(blackboard, "water", 300f)
|
|
||||||
? FactionPlanStepStatus.Complete
|
|
||||||
: blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionCommodityId, "water", StringComparison.Ordinal)
|
|
||||||
? FactionPlanStepStatus.Running
|
|
||||||
: FactionPlanStepStatus.Ready;
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.EnsureMiningCapacity:
|
|
||||||
step.Status = state.MinerShipCount >= 2 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.EnsureConstructionCapacity:
|
|
||||||
step.Status = state.ConstructorShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.EnsureTransportCapacity:
|
|
||||||
step.Status = state.TransportShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
||||||
break;
|
|
||||||
case FactionPlanStepKind.MonitorExpansionProject:
|
|
||||||
step.Status = blackboard.HasWarIndustrySupplyChain ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EvaluateCommodityStep(
|
private static StepExecutionAssessment EvaluateCommodityStep(
|
||||||
|
SimulationWorld world,
|
||||||
|
CommanderRuntime commander,
|
||||||
FactionPlanStepRuntime step,
|
FactionPlanStepRuntime step,
|
||||||
FactionBlackboardRuntime blackboard)
|
FactionBlackboardRuntime blackboard,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(step.CommodityId))
|
if (string.IsNullOrWhiteSpace(step.CommodityId))
|
||||||
{
|
{
|
||||||
step.Status = FactionPlanStepStatus.Failed;
|
return new StepExecutionAssessment(
|
||||||
step.BlockingReason = "Commodity planning step is missing a target commodity.";
|
FactionPlanStepStatus.Failed,
|
||||||
return;
|
"Commodity step is missing a required commodity.",
|
||||||
|
BlockingReason: "Commodity planning step is missing a target commodity.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var completed = IsCommodityOperational(blackboard, step.CommodityId, 240f);
|
if (IsCommodityOperational(blackboard, step.CommodityId, 240f))
|
||||||
|
|
||||||
step.Status = completed ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Ready;
|
|
||||||
if (completed)
|
|
||||||
{
|
{
|
||||||
step.ProducedFacts.Add($"commodity-online:{step.CommodityId}");
|
step.ProducedFacts.Add($"commodity-online:{step.CommodityId}");
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
$"Commodity {step.CommodityId} is operational in the faction economy.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId, ignoreActiveExpansionProject: true);
|
||||||
|
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateShipyardStep(
|
||||||
|
SimulationWorld world,
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
FactionPlanningState state,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
|
{
|
||||||
|
if (state.HasShipFactory)
|
||||||
|
{
|
||||||
|
step.ProducedFacts.Add("shipyard-online");
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
"Faction already has an online shipyard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId, ignoreActiveExpansionProject: true);
|
||||||
|
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateWaterStep(
|
||||||
|
SimulationWorld world,
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
FactionBlackboardRuntime blackboard,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
|
{
|
||||||
|
if (IsCommodityOperational(blackboard, "water", 300f))
|
||||||
|
{
|
||||||
|
step.ProducedFacts.Add("commodity-online:water");
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
"Water supply is operational.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water", ignoreActiveExpansionProject: true);
|
||||||
|
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateFleetProductionStep(
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
FactionBlackboardRuntime blackboard,
|
||||||
|
FactionPlanningState state)
|
||||||
|
{
|
||||||
|
if (state.MilitaryShipCount >= blackboard.TargetWarshipCount)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
"Target war fleet size has been reached.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blackboard.HasShipyard)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Blocked,
|
||||||
|
"Fleet production requires an online shipyard.",
|
||||||
|
BlockingReason: "Fleet production requires an online shipyard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFindIssuedTaskBinding(commander, step, out var binding))
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Running,
|
||||||
|
"Military fleet production is already bound to an issued ship-production task.",
|
||||||
|
Binding: binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Ready,
|
||||||
|
"Shipyard is available; military fleet production can begin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateAttackStep(
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
FactionBlackboardRuntime blackboard,
|
||||||
|
FactionPlanningState state)
|
||||||
|
{
|
||||||
|
if (blackboard.EnemyFactionCount <= 0)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
"No hostile faction remains to attack.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Blocked,
|
||||||
|
"Insufficient military strength to begin the attack objective.",
|
||||||
|
BlockingReason: "Insufficient military strength to commit to a faction attack objective.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFindIssuedTaskBinding(commander, step, out var binding))
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Running,
|
||||||
|
"Attack objective is already bound to a matching combat task.",
|
||||||
|
Binding: binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Ready,
|
||||||
|
"Combat strength is available; attack execution can begin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateCapacityStep(
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
bool hasShipyard,
|
||||||
|
int currentCount,
|
||||||
|
int requiredCount,
|
||||||
|
string shipRole)
|
||||||
|
{
|
||||||
|
if (currentCount >= requiredCount)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
$"Faction already meets the required {shipRole} ship capacity.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasShipyard)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Blocked,
|
||||||
|
$"No shipyard is currently assigned to produce {shipRole} ships.",
|
||||||
|
BlockingReason: $"Ship capacity expansion for {shipRole} requires an online shipyard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFindIssuedTaskBinding(commander, step, out var binding))
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Running,
|
||||||
|
$"{shipRole} ship production is already bound to a matching issued task.",
|
||||||
|
Binding: binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Ready,
|
||||||
|
$"Shipyard capacity is available; {shipRole} ship production can begin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateWarIndustryMonitorStep(
|
||||||
|
SimulationWorld world,
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
FactionBlackboardRuntime blackboard,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
|
{
|
||||||
|
if (blackboard.HasWarIndustrySupplyChain)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Complete,
|
||||||
|
"War-industry supply chain is operational.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var commodityId in new[] { "refinedmetals", "hullparts", "claytronics" })
|
||||||
|
{
|
||||||
|
if (IsCommodityOperational(blackboard, commodityId, 240f))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId, ignoreActiveExpansionProject: true);
|
||||||
|
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Ready,
|
||||||
|
"War-industry prerequisites are unresolved but no matching active project is bound yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StepExecutionAssessment EvaluateExpansionRequirement(
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
IndustryExpansionProject? expectedProject,
|
||||||
|
IndustryExpansionProject? activeProject)
|
||||||
|
{
|
||||||
|
if (expectedProject is null)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Blocked,
|
||||||
|
"Unable to derive a valid expansion plan for the step outcome.",
|
||||||
|
BlockingReason: BuildMissingPlanReason(step));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeProject is not null && ProjectsSemanticallyMatch(expectedProject, activeProject))
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Running,
|
||||||
|
$"Running on matching active expansion project {DescribeProject(activeProject)}.",
|
||||||
|
Binding: new StepExecutionBinding(
|
||||||
|
"expansion-project",
|
||||||
|
activeProject.SiteId,
|
||||||
|
$"Matched active project {DescribeProject(activeProject)}."),
|
||||||
|
ExpectedProject: activeProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeProject is not null)
|
||||||
|
{
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Blocked,
|
||||||
|
$"Blocked by unrelated active expansion {DescribeProject(activeProject)}; step requires {DescribeProject(expectedProject)}.",
|
||||||
|
BlockingReason: $"Active expansion {DescribeProject(activeProject)} does not satisfy required outcome {DescribeProject(expectedProject)}.",
|
||||||
|
ExpectedProject: expectedProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepExecutionAssessment(
|
||||||
|
FactionPlanStepStatus.Ready,
|
||||||
|
$"Ready to start required expansion {DescribeProject(expectedProject)}.",
|
||||||
|
ExpectedProject: expectedProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyAssessment(
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
StepExecutionAssessment assessment)
|
||||||
|
{
|
||||||
|
step.Status = assessment.Status;
|
||||||
|
step.StatusReason = assessment.StatusReason;
|
||||||
|
step.BlockingReason = assessment.BlockingReason;
|
||||||
|
step.ExecutionBindingKind = assessment.Binding?.Kind;
|
||||||
|
step.ExecutionBindingTargetId = assessment.Binding?.TargetId;
|
||||||
|
step.ExecutionBindingSummary = assessment.Binding?.Summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCommodityOperational(
|
private static bool IsCommodityOperational(
|
||||||
@@ -653,8 +864,7 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
CommanderRuntime commander,
|
CommanderRuntime commander,
|
||||||
FactionObjectiveRuntime objective,
|
FactionObjectiveRuntime objective,
|
||||||
FactionPlanStepRuntime step,
|
FactionPlanStepRuntime step,
|
||||||
FactionBlackboardRuntime blackboard,
|
StepExecutionAssessment assessment,
|
||||||
FactionPlanningState state,
|
|
||||||
ISet<string> touchedTaskIds,
|
ISet<string> touchedTaskIds,
|
||||||
ISet<string> assignedAssetIds)
|
ISet<string> assignedAssetIds)
|
||||||
{
|
{
|
||||||
@@ -670,128 +880,12 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
switch (step.Kind)
|
switch (step.Kind)
|
||||||
{
|
{
|
||||||
case FactionPlanStepKind.EnsureCommodityProduction:
|
case FactionPlanStepKind.EnsureCommodityProduction:
|
||||||
if (blackboard.HasActiveExpansionProject)
|
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
|
||||||
{
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: blackboard.ActiveExpansionCommodityId ?? step.CommodityId,
|
|
||||||
moduleId: blackboard.ActiveExpansionModuleId,
|
|
||||||
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
||||||
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes ?? "Expansion project already active for faction.");
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step.CommodityId is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var project = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId);
|
|
||||||
if (project is not null)
|
|
||||||
{
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
|
||||||
step.TargetSiteId = project.SiteId;
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
step.Notes = $"Queued expansion project for {project.CommodityId}.";
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: project.CommodityId,
|
|
||||||
moduleId: project.ModuleId,
|
|
||||||
targetSystemId: project.SystemId,
|
|
||||||
targetSiteId: project.SiteId,
|
|
||||||
blockingReason: null,
|
|
||||||
notes: step.Notes);
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
|
||||||
step.BlockingReason = $"Unable to derive an expansion project for {step.CommodityId}.";
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: step.CommodityId,
|
|
||||||
moduleId: step.ModuleId,
|
|
||||||
targetSystemId: null,
|
|
||||||
targetSiteId: null,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.EnsureShipyardSite:
|
case FactionPlanStepKind.EnsureShipyardSite:
|
||||||
if (blackboard.HasActiveExpansionProject)
|
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
|
||||||
{
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: blackboard.ActiveExpansionCommodityId,
|
|
||||||
moduleId: blackboard.ActiveExpansionModuleId ?? step.ModuleId,
|
|
||||||
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
||||||
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes ?? "Shipyard support project waiting on current expansion site.");
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var shipyardProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId);
|
|
||||||
if (shipyardProject is not null)
|
|
||||||
{
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, shipyardProject);
|
|
||||||
step.TargetSiteId = shipyardProject.SiteId;
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
step.Notes = "Queued shipyard foundation project.";
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: shipyardProject.CommodityId,
|
|
||||||
moduleId: shipyardProject.ModuleId,
|
|
||||||
targetSystemId: shipyardProject.SystemId,
|
|
||||||
targetSiteId: shipyardProject.SiteId,
|
|
||||||
blockingReason: null,
|
|
||||||
notes: step.Notes);
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
|
||||||
step.BlockingReason = "Unable to identify a viable shipyard foundation project.";
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: step.CommodityId,
|
|
||||||
moduleId: step.ModuleId,
|
|
||||||
targetSystemId: null,
|
|
||||||
targetSiteId: null,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.ProduceFleet:
|
case FactionPlanStepKind.ProduceFleet:
|
||||||
if (!blackboard.HasShipyard)
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
|
||||||
step.BlockingReason = "Fleet production requires an online shipyard.";
|
|
||||||
}
|
|
||||||
UpsertShipProductionTask(
|
UpsertShipProductionTask(
|
||||||
commander,
|
commander,
|
||||||
objective,
|
objective,
|
||||||
@@ -801,78 +895,30 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
blockingReason: step.BlockingReason,
|
blockingReason: step.BlockingReason,
|
||||||
notes: step.Notes ?? "Maintain military ship production until war fleet target is satisfied.");
|
notes: step.Notes ?? "Maintain military ship production until war fleet target is satisfied.");
|
||||||
AssignShipyardAssets(world, commander, objective, step);
|
AssignShipyardAssets(world, commander, objective, step);
|
||||||
|
PromoteShipProductionStepToRunning(step, "military");
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.AttackFactionAssets:
|
case FactionPlanStepKind.AttackFactionAssets:
|
||||||
UpsertAttackTask(commander, objective, step, touchedTaskIds);
|
UpsertAttackTask(commander, objective, step, touchedTaskIds);
|
||||||
AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
||||||
|
PromoteCombatStepToRunning(step);
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.EnsureWaterSupply:
|
case FactionPlanStepKind.EnsureWaterSupply:
|
||||||
if (blackboard.HasActiveExpansionProject)
|
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
|
||||||
{
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: blackboard.ActiveExpansionCommodityId,
|
|
||||||
moduleId: blackboard.ActiveExpansionModuleId,
|
|
||||||
targetSystemId: blackboard.ActiveExpansionSystemId,
|
|
||||||
targetSiteId: blackboard.ActiveExpansionSiteId,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes ?? "Water support project waiting on current expansion site.");
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var waterProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water");
|
|
||||||
if (waterProject is not null)
|
|
||||||
{
|
|
||||||
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, waterProject);
|
|
||||||
step.Status = FactionPlanStepStatus.Running;
|
|
||||||
step.Notes = "Queued water expansion project.";
|
|
||||||
step.TargetSiteId = waterProject.SiteId;
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: waterProject.CommodityId,
|
|
||||||
moduleId: waterProject.ModuleId,
|
|
||||||
targetSystemId: waterProject.SystemId,
|
|
||||||
targetSiteId: waterProject.SiteId,
|
|
||||||
blockingReason: null,
|
|
||||||
notes: step.Notes);
|
|
||||||
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
step.Status = FactionPlanStepStatus.Blocked;
|
|
||||||
step.BlockingReason = "Unable to derive an expansion project for water.";
|
|
||||||
UpsertExpansionTask(
|
|
||||||
commander,
|
|
||||||
objective,
|
|
||||||
step,
|
|
||||||
touchedTaskIds,
|
|
||||||
commodityId: "water",
|
|
||||||
moduleId: step.ModuleId,
|
|
||||||
targetSystemId: null,
|
|
||||||
targetSiteId: null,
|
|
||||||
blockingReason: step.BlockingReason,
|
|
||||||
notes: step.Notes);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.EnsureMiningCapacity:
|
case FactionPlanStepKind.EnsureMiningCapacity:
|
||||||
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "mining", step.BlockingReason, "Maintain mining ship production until logistical capacity is healthy.");
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "mining", step.BlockingReason, "Maintain mining ship production until logistical capacity is healthy.");
|
||||||
AssignShipyardAssets(world, commander, objective, step);
|
AssignShipyardAssets(world, commander, objective, step);
|
||||||
|
PromoteShipProductionStepToRunning(step, "mining");
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.EnsureConstructionCapacity:
|
case FactionPlanStepKind.EnsureConstructionCapacity:
|
||||||
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "construction", step.BlockingReason, "Maintain construction ship production until expansion support is healthy.");
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "construction", step.BlockingReason, "Maintain construction ship production until expansion support is healthy.");
|
||||||
AssignShipyardAssets(world, commander, objective, step);
|
AssignShipyardAssets(world, commander, objective, step);
|
||||||
|
PromoteShipProductionStepToRunning(step, "construction");
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.EnsureTransportCapacity:
|
case FactionPlanStepKind.EnsureTransportCapacity:
|
||||||
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "transport", step.BlockingReason, "Maintain transport ship production until logistical throughput is healthy.");
|
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "transport", step.BlockingReason, "Maintain transport ship production until logistical throughput is healthy.");
|
||||||
AssignShipyardAssets(world, commander, objective, step);
|
AssignShipyardAssets(world, commander, objective, step);
|
||||||
|
PromoteShipProductionStepToRunning(step, "transport");
|
||||||
break;
|
break;
|
||||||
case FactionPlanStepKind.MonitorExpansionProject:
|
case FactionPlanStepKind.MonitorExpansionProject:
|
||||||
UpsertWarIndustryTask(commander, objective, step, touchedTaskIds);
|
UpsertWarIndustryTask(commander, objective, step, touchedTaskIds);
|
||||||
@@ -880,6 +926,111 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EmitExpansionExecution(
|
||||||
|
SimulationWorld world,
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionObjectiveRuntime objective,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
StepExecutionAssessment assessment,
|
||||||
|
ISet<string> touchedTaskIds,
|
||||||
|
ISet<string> assignedAssetIds)
|
||||||
|
{
|
||||||
|
var project = assessment.ExpectedProject;
|
||||||
|
if (project is null)
|
||||||
|
{
|
||||||
|
UpsertExpansionTask(
|
||||||
|
commander,
|
||||||
|
objective,
|
||||||
|
step,
|
||||||
|
touchedTaskIds,
|
||||||
|
commodityId: step.CommodityId,
|
||||||
|
moduleId: step.ModuleId,
|
||||||
|
targetSystemId: null,
|
||||||
|
targetSiteId: null,
|
||||||
|
blockingReason: step.BlockingReason,
|
||||||
|
notes: step.StatusReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.Status == FactionPlanStepStatus.Ready)
|
||||||
|
{
|
||||||
|
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
|
||||||
|
project = project with { SiteId = project.SiteId ?? FindMatchingSiteId(world, commander.FactionId, project) };
|
||||||
|
step.TargetSiteId = project.SiteId;
|
||||||
|
step.Status = FactionPlanStepStatus.Running;
|
||||||
|
step.StatusReason = $"Started required expansion {DescribeProject(project)}.";
|
||||||
|
step.ExecutionBindingKind = "expansion-project";
|
||||||
|
step.ExecutionBindingTargetId = project.SiteId;
|
||||||
|
step.ExecutionBindingSummary = $"Started site {project.SiteId ?? "pending"} for {DescribeProject(project)}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.Status == FactionPlanStepStatus.Running)
|
||||||
|
{
|
||||||
|
step.TargetSiteId ??= project.SiteId;
|
||||||
|
UpsertExpansionTask(
|
||||||
|
commander,
|
||||||
|
objective,
|
||||||
|
step,
|
||||||
|
touchedTaskIds,
|
||||||
|
commodityId: project.CommodityId,
|
||||||
|
moduleId: project.ModuleId,
|
||||||
|
targetSystemId: project.SystemId,
|
||||||
|
targetSiteId: project.SiteId,
|
||||||
|
blockingReason: step.BlockingReason,
|
||||||
|
notes: step.StatusReason);
|
||||||
|
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpsertExpansionTask(
|
||||||
|
commander,
|
||||||
|
objective,
|
||||||
|
step,
|
||||||
|
touchedTaskIds,
|
||||||
|
commodityId: project.CommodityId,
|
||||||
|
moduleId: project.ModuleId,
|
||||||
|
targetSystemId: project.SystemId,
|
||||||
|
targetSiteId: project.SiteId,
|
||||||
|
blockingReason: step.BlockingReason,
|
||||||
|
notes: step.StatusReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PromoteShipProductionStepToRunning(FactionPlanStepRuntime step, string shipRole)
|
||||||
|
{
|
||||||
|
if (step.Status != FactionPlanStepStatus.Ready || step.AssignedAssetIds.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
step.Status = FactionPlanStepStatus.Running;
|
||||||
|
step.StatusReason = $"{titleCase(shipRole)} ship production is bound to shipyard assets.";
|
||||||
|
step.ExecutionBindingKind = "shipyard-production";
|
||||||
|
step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault();
|
||||||
|
step.ExecutionBindingSummary = $"Using shipyard assets {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PromoteCombatStepToRunning(FactionPlanStepRuntime step)
|
||||||
|
{
|
||||||
|
if (step.Status != FactionPlanStepStatus.Ready)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step.AssignedAssetIds.Count <= 0)
|
||||||
|
{
|
||||||
|
step.Status = FactionPlanStepStatus.Blocked;
|
||||||
|
step.BlockingReason = "No combat ships were available for the attack step.";
|
||||||
|
step.StatusReason = step.BlockingReason;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
step.Status = FactionPlanStepStatus.Running;
|
||||||
|
step.StatusReason = "Attack step is bound to assigned combat ships.";
|
||||||
|
step.ExecutionBindingKind = "combat-assets";
|
||||||
|
step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault();
|
||||||
|
step.ExecutionBindingSummary = $"Using combat ships {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}.";
|
||||||
|
}
|
||||||
|
|
||||||
private static void ReconcileStaleTasks(CommanderRuntime commander, ISet<string> touchedTaskIds)
|
private static void ReconcileStaleTasks(CommanderRuntime commander, ISet<string> touchedTaskIds)
|
||||||
{
|
{
|
||||||
foreach (var task in commander.IssuedTasks)
|
foreach (var task in commander.IssuedTasks)
|
||||||
@@ -1056,6 +1207,71 @@ internal sealed class FactionObjectiveExecutor
|
|||||||
_ => FactionIssuedTaskState.Cancelled,
|
_ => FactionIssuedTaskState.Cancelled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static bool ProjectsSemanticallyMatch(
|
||||||
|
IndustryExpansionProject expectedProject,
|
||||||
|
IndustryExpansionProject activeProject) =>
|
||||||
|
string.Equals(expectedProject.CommodityId, activeProject.CommodityId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(expectedProject.ModuleId, activeProject.ModuleId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(expectedProject.SystemId, activeProject.SystemId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(expectedProject.CelestialId, activeProject.CelestialId, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string DescribeProject(IndustryExpansionProject project) =>
|
||||||
|
$"{project.CommodityId}/{project.ModuleId} @ {project.SystemId}:{project.CelestialId}";
|
||||||
|
|
||||||
|
private static string BuildMissingPlanReason(FactionPlanStepRuntime step) =>
|
||||||
|
step.Kind switch
|
||||||
|
{
|
||||||
|
FactionPlanStepKind.EnsureCommodityProduction => $"Unable to derive an expansion project for required commodity {step.CommodityId}.",
|
||||||
|
FactionPlanStepKind.EnsureWaterSupply => "Unable to derive an expansion project for water supply.",
|
||||||
|
FactionPlanStepKind.EnsureShipyardSite => "Unable to identify a viable shipyard foundation project.",
|
||||||
|
_ => "Unable to derive the required execution plan for this step.",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool TryFindIssuedTaskBinding(
|
||||||
|
CommanderRuntime commander,
|
||||||
|
FactionPlanStepRuntime step,
|
||||||
|
out StepExecutionBinding binding)
|
||||||
|
{
|
||||||
|
var task = commander.IssuedTasks.FirstOrDefault(candidate =>
|
||||||
|
string.Equals(candidate.StepId, step.Id, StringComparison.Ordinal)
|
||||||
|
&& candidate.State == FactionIssuedTaskState.Active);
|
||||||
|
if (task is null)
|
||||||
|
{
|
||||||
|
binding = default!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetId = task.TargetSiteId
|
||||||
|
?? task.TargetFactionId
|
||||||
|
?? task.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
|
||||||
|
binding = new StepExecutionBinding(
|
||||||
|
"issued-task",
|
||||||
|
targetId,
|
||||||
|
$"Reusing active issued task {task.Kind} ({task.Id}).");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindMatchingSiteId(
|
||||||
|
SimulationWorld world,
|
||||||
|
string factionId,
|
||||||
|
IndustryExpansionProject project) =>
|
||||||
|
world.ConstructionSites
|
||||||
|
.Where(site =>
|
||||||
|
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
|
||||||
|
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed
|
||||||
|
&& string.Equals(site.TargetDefinitionId, project.CommodityId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(site.BlueprintId, project.ModuleId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(site.SystemId, project.SystemId, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(site.CelestialId, project.CelestialId, StringComparison.Ordinal))
|
||||||
|
.Select(site => site.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
private static string titleCase(string value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value)
|
||||||
|
? value
|
||||||
|
: char.ToUpperInvariant(value[0]) + value[1..];
|
||||||
|
|
||||||
private static void AssignCombatAssets(
|
private static void AssignCombatAssets(
|
||||||
SimulationWorld world,
|
SimulationWorld world,
|
||||||
CommanderRuntime commander,
|
CommanderRuntime commander,
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ public sealed record FactionPlanStepSnapshot(
|
|||||||
string? ModuleId,
|
string? ModuleId,
|
||||||
string? TargetFactionId,
|
string? TargetFactionId,
|
||||||
string? TargetSiteId,
|
string? TargetSiteId,
|
||||||
|
string? StatusReason,
|
||||||
|
string? ExecutionBindingKind,
|
||||||
|
string? ExecutionBindingTargetId,
|
||||||
|
string? ExecutionBindingSummary,
|
||||||
string? BlockingReason,
|
string? BlockingReason,
|
||||||
string? Notes,
|
string? Notes,
|
||||||
int LastEvaluatedCycle,
|
int LastEvaluatedCycle,
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ public sealed class FactionPlanStepRuntime
|
|||||||
public string? ModuleId { get; set; }
|
public string? ModuleId { get; set; }
|
||||||
public string? TargetFactionId { get; set; }
|
public string? TargetFactionId { get; set; }
|
||||||
public string? TargetSiteId { 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? BlockingReason { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public int LastEvaluatedCycle { get; set; }
|
public int LastEvaluatedCycle { get; set; }
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ internal static class FactionIndustryPlanner
|
|||||||
private const float CommodityTargetLevelSeconds = 240f;
|
private const float CommodityTargetLevelSeconds = 240f;
|
||||||
private const float WaterTargetLevelSeconds = 300f;
|
private const float WaterTargetLevelSeconds = 300f;
|
||||||
|
|
||||||
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId)
|
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId, bool ignoreActiveExpansionProject = false)
|
||||||
{
|
{
|
||||||
if (HasActiveExpansionProject(world, factionId))
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,9 @@ internal static class FactionIndustryPlanner
|
|||||||
supportStation.Id);
|
supportStation.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IndustryExpansionProject? AnalyzeShipyardNeed(SimulationWorld world, string factionId)
|
internal static IndustryExpansionProject? AnalyzeShipyardNeed(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
|
||||||
{
|
{
|
||||||
if (HasActiveExpansionProject(world, factionId))
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -79,16 +79,16 @@ internal static class FactionIndustryPlanner
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(bottleneckCommodity))
|
if (!string.IsNullOrWhiteSpace(bottleneckCommodity))
|
||||||
{
|
{
|
||||||
return AnalyzeCommodityNeed(world, factionId, bottleneckCommodity);
|
return AnalyzeCommodityNeed(world, factionId, bottleneckCommodity, ignoreActiveExpansionProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateShipyardFoundationProject(world, factionId);
|
return CreateShipyardFoundationProject(world, factionId, ignoreActiveExpansionProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IndustryExpansionProject? CreateShipyardFoundationProject(SimulationWorld world, string factionId)
|
internal static IndustryExpansionProject? CreateShipyardFoundationProject(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
|
||||||
{
|
{
|
||||||
const string shipyardModuleId = "module_gen_build_l_01";
|
const string shipyardModuleId = "module_gen_build_l_01";
|
||||||
if (HasActiveExpansionProject(world, factionId))
|
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSect
|
|||||||
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
|
||||||
builder.Services.AddFastEndpoints();
|
builder.Services.AddFastEndpoints();
|
||||||
builder.Services.AddSingleton<WorldService>();
|
builder.Services.AddSingleton<WorldService>();
|
||||||
|
builder.Services.AddSingleton<TelemetryService>();
|
||||||
builder.Services.AddHostedService<SimulationHostedService>();
|
builder.Services.AddHostedService<SimulationHostedService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ internal sealed class ShipBehaviorStateMachine
|
|||||||
new PatrolShipBehaviorState(),
|
new PatrolShipBehaviorState(),
|
||||||
new AttackTargetShipBehaviorState(),
|
new AttackTargetShipBehaviorState(),
|
||||||
new TradeHaulShipBehaviorState(),
|
new TradeHaulShipBehaviorState(),
|
||||||
new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining"),
|
new ResourceHarvestShipBehaviorState("auto-mine", null, "mining"),
|
||||||
new ConstructStationShipBehaviorState(),
|
new ConstructStationShipBehaviorState(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ internal sealed class PatrolShipBehaviorState : IShipBehaviorState
|
|||||||
|
|
||||||
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
|
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
|
||||||
{
|
{
|
||||||
private readonly string resourceItemId;
|
private readonly string? resourceItemId;
|
||||||
private readonly string requiredModule;
|
private readonly string requiredModule;
|
||||||
|
|
||||||
public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule)
|
public ResourceHarvestShipBehaviorState(string kind, string? resourceItemId, string requiredModule)
|
||||||
{
|
{
|
||||||
Kind = kind;
|
Kind = kind;
|
||||||
this.resourceItemId = resourceItemId;
|
this.resourceItemId = resourceItemId;
|
||||||
|
|||||||
@@ -291,11 +291,18 @@ internal sealed class ShipControlService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule)
|
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule)
|
||||||
{
|
{
|
||||||
var behavior = ship.DefaultBehavior;
|
var behavior = ship.DefaultBehavior;
|
||||||
var cargoItemId = ship.Inventory.Keys.FirstOrDefault();
|
var cargoItemId = ship.Inventory.Keys.FirstOrDefault();
|
||||||
var targetResourceItemId = SelectMiningResourceItem(world, ship, cargoItemId ?? behavior.ItemId ?? resourceItemId);
|
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))
|
if (!string.Equals(behavior.ItemId, targetResourceItemId, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
behavior.ItemId = targetResourceItemId;
|
behavior.ItemId = targetResourceItemId;
|
||||||
@@ -426,22 +433,22 @@ internal sealed class ShipControlService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string fallbackItemId)
|
private static string? SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string? fallbackItemId)
|
||||||
{
|
{
|
||||||
var candidateItemId = world.MarketOrders
|
var candidateItemId = world.MarketOrders
|
||||||
.Where(order =>
|
.Where(order =>
|
||||||
string.Equals(order.FactionId, ship.FactionId, StringComparison.Ordinal)
|
string.Equals(order.FactionId, ship.FactionId, StringComparison.Ordinal)
|
||||||
&& order.Kind == MarketOrderKinds.Buy
|
&& order.Kind == MarketOrderKinds.Buy
|
||||||
|
&& order.ConstructionSiteId is null
|
||||||
|
&& order.State != MarketOrderStateKinds.Cancelled
|
||||||
&& order.RemainingAmount > 0.01f)
|
&& order.RemainingAmount > 0.01f)
|
||||||
.SelectMany(order => FactionIndustryPlanner.ResolveRootResourceItems(world, order.ItemId)
|
.Select(order => new
|
||||||
.Select(itemId => new
|
{
|
||||||
{
|
ItemId = order.ItemId,
|
||||||
ItemId = itemId,
|
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
|
||||||
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
|
})
|
||||||
}))
|
.Where(entry => CanShipMineItem(world, ship, entry.ItemId))
|
||||||
.Where(entry =>
|
.Where(entry => world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
||||||
CanShipMineItem(world, ship, entry.ItemId)
|
|
||||||
&& world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
|
||||||
.GroupBy(entry => entry.ItemId, StringComparer.Ordinal)
|
.GroupBy(entry => entry.ItemId, StringComparer.Ordinal)
|
||||||
.Select(group => new
|
.Select(group => new
|
||||||
{
|
{
|
||||||
@@ -457,7 +464,8 @@ internal sealed class ShipControlService
|
|||||||
return candidateItemId;
|
return candidateItemId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CanShipMineItem(world, ship, fallbackItemId)
|
if (!string.IsNullOrWhiteSpace(fallbackItemId)
|
||||||
|
&& CanShipMineItem(world, ship, fallbackItemId)
|
||||||
&& world.Nodes.Any(node => string.Equals(node.ItemId, fallbackItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
&& world.Nodes.Any(node => string.Equals(node.ItemId, fallbackItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
|
||||||
{
|
{
|
||||||
return fallbackItemId;
|
return fallbackItemId;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||||
|
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
|
||||||
|
|
||||||
namespace SpaceGame.Api.Ships.Simulation;
|
namespace SpaceGame.Api.Ships.Simulation;
|
||||||
|
|
||||||
@@ -331,7 +332,8 @@ internal sealed partial class ShipTaskExecutionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
||||||
moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key));
|
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
||||||
|
moved = MathF.Min(moved, available);
|
||||||
if (moved <= 0.01f)
|
if (moved <= 0.01f)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -356,7 +358,8 @@ internal sealed partial class ShipTaskExecutionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
|
||||||
moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key));
|
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
|
||||||
|
moved = MathF.Min(moved, available);
|
||||||
if (moved <= 0.01f)
|
if (moved <= 0.01f)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ public sealed class SimulationEngine
|
|||||||
{
|
{
|
||||||
var nowUtc = DateTimeOffset.UtcNow;
|
var nowUtc = DateTimeOffset.UtcNow;
|
||||||
var events = new List<SimulationEventRecord>();
|
var events = new List<SimulationEventRecord>();
|
||||||
|
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
|
||||||
|
|
||||||
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
|
||||||
|
|
||||||
_orbitalStateUpdater.Update(world);
|
_orbitalStateUpdater.Update(world);
|
||||||
_infrastructureSimulation.UpdateClaims(world, events);
|
_infrastructureSimulation.UpdateClaims(world, events);
|
||||||
_infrastructureSimulation.UpdateConstructionSites(world, events);
|
_infrastructureSimulation.UpdateConstructionSites(world, events);
|
||||||
_commanderPlanning.UpdateCommanders(this, world, deltaSeconds, events);
|
_commanderPlanning.UpdateCommanders(this, world, simulationDeltaSeconds, events);
|
||||||
_stationLifecycle.UpdateStations(world, deltaSeconds, events);
|
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
|
||||||
|
|
||||||
foreach (var ship in world.Ships.ToList())
|
foreach (var ship in world.Ships.ToList())
|
||||||
{
|
{
|
||||||
@@ -54,10 +55,10 @@ public sealed class SimulationEngine
|
|||||||
_shipControl.RefreshControlLayers(ship, world);
|
_shipControl.RefreshControlLayers(ship, world);
|
||||||
_shipControl.PlanControllerTask(this, ship, world);
|
_shipControl.PlanControllerTask(this, ship, world);
|
||||||
|
|
||||||
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, deltaSeconds);
|
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, simulationDeltaSeconds);
|
||||||
|
|
||||||
_shipControl.AdvanceControlState(this, ship, world, controllerEvent);
|
_shipControl.AdvanceControlState(this, ship, world, controllerEvent);
|
||||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
|
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
|
||||||
_shipControl.TrackHistory(ship, controllerEvent);
|
_shipControl.TrackHistory(ship, controllerEvent);
|
||||||
_shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
_shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ public sealed class SimulationEngine
|
|||||||
public void PrimeDeltaBaseline(SimulationWorld world) =>
|
public void PrimeDeltaBaseline(SimulationWorld world) =>
|
||||||
_projection.PrimeDeltaBaseline(world);
|
_projection.PrimeDeltaBaseline(world);
|
||||||
|
|
||||||
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) =>
|
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule) =>
|
||||||
_shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
_shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
|
||||||
|
|
||||||
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) =>
|
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) =>
|
||||||
|
|||||||
@@ -895,6 +895,10 @@ internal sealed class SimulationProjectionService
|
|||||||
step.ModuleId,
|
step.ModuleId,
|
||||||
step.TargetFactionId,
|
step.TargetFactionId,
|
||||||
step.TargetSiteId,
|
step.TargetSiteId,
|
||||||
|
step.StatusReason,
|
||||||
|
step.ExecutionBindingKind,
|
||||||
|
step.ExecutionBindingTargetId,
|
||||||
|
step.ExecutionBindingSummary,
|
||||||
step.BlockingReason,
|
step.BlockingReason,
|
||||||
step.Notes,
|
step.Notes,
|
||||||
step.LastEvaluatedCycle,
|
step.LastEvaluatedCycle,
|
||||||
|
|||||||
@@ -232,8 +232,13 @@ internal sealed class InfrastructureSimulationService
|
|||||||
"power" => "energycells",
|
"power" => "energycells",
|
||||||
"refinery" => "refinedmetals",
|
"refinery" => "refinedmetals",
|
||||||
"water" => "water",
|
"water" => "water",
|
||||||
|
"graphene" => "graphene",
|
||||||
|
"siliconwafers" => "siliconwafers",
|
||||||
"hullparts" => "hullparts",
|
"hullparts" => "hullparts",
|
||||||
"claytronics" => "claytronics",
|
"claytronics" => "claytronics",
|
||||||
|
"quantumtubes" => "quantumtubes",
|
||||||
|
"antimattercells" => "antimattercells",
|
||||||
|
"superfluidcoolant" => "superfluidcoolant",
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -724,6 +729,11 @@ internal sealed class InfrastructureSimulationService
|
|||||||
private static float GetTargetLevelSeconds(string commodityId) =>
|
private static float GetTargetLevelSeconds(string commodityId) =>
|
||||||
string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
|
string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
|
||||||
string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
|
string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
|
||||||
|
string.Equals(commodityId, "graphene", StringComparison.Ordinal) ? 240f :
|
||||||
|
string.Equals(commodityId, "siliconwafers", StringComparison.Ordinal) ? 240f :
|
||||||
|
string.Equals(commodityId, "quantumtubes", StringComparison.Ordinal) ? 240f :
|
||||||
|
string.Equals(commodityId, "antimattercells", StringComparison.Ordinal) ? 240f :
|
||||||
|
string.Equals(commodityId, "superfluidcoolant", StringComparison.Ordinal) ? 240f :
|
||||||
CommodityTargetLevelSeconds;
|
CommodityTargetLevelSeconds;
|
||||||
|
|
||||||
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)
|
||||||
|
|||||||
@@ -24,12 +24,25 @@ internal sealed class StationSimulationService
|
|||||||
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
|
||||||
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
|
||||||
var iceReserve = role == "water" ? 260f : 0f;
|
var iceReserve = role == "water" ? 260f : 0f;
|
||||||
|
var methaneReserve = role == "graphene" ? 320f : 0f;
|
||||||
|
var hydrogenReserve = role == "antimattercells" ? 320f : 0f;
|
||||||
|
var heliumReserve = role == "superfluidcoolant" ? 320f : 0f;
|
||||||
|
var siliconReserve = role == "siliconwafers" ? 240f : 0f;
|
||||||
|
var grapheneInputReserve = role == "quantumtubes" ? 160f : 0f;
|
||||||
|
var superfluidCoolantInputReserve = role == "quantumtubes" ? 120f : 0f;
|
||||||
|
var antimatterCellsInputReserve = role == "claytronics" ? 120f : 0f;
|
||||||
|
var quantumTubesInputReserve = role == "claytronics" ? 120f : 0f;
|
||||||
var energyReserve = role switch
|
var energyReserve = role switch
|
||||||
{
|
{
|
||||||
"power" => 120f,
|
"power" => 120f,
|
||||||
"refinery" => 160f,
|
"refinery" => 160f,
|
||||||
"hullparts" => 180f,
|
"hullparts" => 180f,
|
||||||
"claytronics" => 220f,
|
"claytronics" => 220f,
|
||||||
|
"graphene" => 160f,
|
||||||
|
"siliconwafers" => 160f,
|
||||||
|
"antimattercells" => 160f,
|
||||||
|
"superfluidcoolant" => 160f,
|
||||||
|
"quantumtubes" => 160f,
|
||||||
"water" => 140f,
|
"water" => 140f,
|
||||||
_ => 60f,
|
_ => 60f,
|
||||||
} + constructionEnergyReserve;
|
} + constructionEnergyReserve;
|
||||||
@@ -43,6 +56,11 @@ internal sealed class StationSimulationService
|
|||||||
var oreReserve = role == "refinery" ? 260f : 0f;
|
var oreReserve = role == "refinery" ? 260f : 0f;
|
||||||
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||||
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
|
||||||
|
var grapheneReserve = role == "graphene" ? 120f : 0f;
|
||||||
|
var siliconWafersReserve = role == "siliconwafers" ? 120f : 0f;
|
||||||
|
var antimatterCellsReserve = role == "antimattercells" ? 120f : 0f;
|
||||||
|
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
|
||||||
|
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
|
||||||
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
|
||||||
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
|
||||||
? 90f
|
? 90f
|
||||||
@@ -51,22 +69,98 @@ internal sealed class StationSimulationService
|
|||||||
AddDemandOrder(desiredOrders, station, "water", ScaleReserveByEconomy(economy, "water", waterReserve), valuationBase: ScaleDemandValuation(economy, "water", 1.1f));
|
AddDemandOrder(desiredOrders, station, "water", ScaleReserveByEconomy(economy, "water", waterReserve), valuationBase: ScaleDemandValuation(economy, "water", 1.1f));
|
||||||
AddDemandOrder(desiredOrders, station, "energycells", ScaleReserveByEconomy(economy, "energycells", energyReserve), valuationBase: ScaleDemandValuation(economy, "energycells", 1.0f));
|
AddDemandOrder(desiredOrders, station, "energycells", ScaleReserveByEconomy(economy, "energycells", energyReserve), valuationBase: ScaleDemandValuation(economy, "energycells", 1.0f));
|
||||||
AddDemandOrder(desiredOrders, station, "ice", ScaleReserveByEconomy(economy, "ice", iceReserve), valuationBase: ScaleDemandValuation(economy, "ice", 1.0f));
|
AddDemandOrder(desiredOrders, station, "ice", ScaleReserveByEconomy(economy, "ice", iceReserve), valuationBase: ScaleDemandValuation(economy, "ice", 1.0f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "methane", ScaleReserveByEconomy(economy, "methane", methaneReserve), valuationBase: ScaleDemandValuation(economy, "methane", 1.0f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "hydrogen", ScaleReserveByEconomy(economy, "hydrogen", hydrogenReserve), valuationBase: ScaleDemandValuation(economy, "hydrogen", 1.0f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "helium", ScaleReserveByEconomy(economy, "helium", heliumReserve), valuationBase: ScaleDemandValuation(economy, "helium", 1.0f));
|
||||||
AddDemandOrder(desiredOrders, station, "ore", ScaleReserveByEconomy(economy, "ore", oreReserve), valuationBase: ScaleDemandValuation(economy, "ore", 1.0f));
|
AddDemandOrder(desiredOrders, station, "ore", ScaleReserveByEconomy(economy, "ore", oreReserve), valuationBase: ScaleDemandValuation(economy, "ore", 1.0f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "silicon", ScaleReserveByEconomy(economy, "silicon", siliconReserve), valuationBase: ScaleDemandValuation(economy, "silicon", 1.0f));
|
||||||
AddDemandOrder(desiredOrders, station, "refinedmetals", ScaleReserveByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve)), valuationBase: ScaleDemandValuation(economy, "refinedmetals", 1.15f));
|
AddDemandOrder(desiredOrders, station, "refinedmetals", ScaleReserveByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve)), valuationBase: ScaleDemandValuation(economy, "refinedmetals", 1.15f));
|
||||||
AddDemandOrder(desiredOrders, station, "hullparts", ScaleReserveByEconomy(economy, "hullparts", hullpartsReserve + shipPartsReserve), valuationBase: ScaleDemandValuation(economy, "hullparts", 1.3f));
|
AddDemandOrder(desiredOrders, station, "hullparts", ScaleReserveByEconomy(economy, "hullparts", hullpartsReserve + shipPartsReserve), valuationBase: ScaleDemandValuation(economy, "hullparts", 1.3f));
|
||||||
AddDemandOrder(desiredOrders, station, "claytronics", ScaleReserveByEconomy(economy, "claytronics", claytronicsReserve), valuationBase: ScaleDemandValuation(economy, "claytronics", 1.35f));
|
AddDemandOrder(desiredOrders, station, "claytronics", ScaleReserveByEconomy(economy, "claytronics", claytronicsReserve), valuationBase: ScaleDemandValuation(economy, "claytronics", 1.35f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.05f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "siliconwafers", ScaleReserveByEconomy(economy, "siliconwafers", siliconWafersReserve), valuationBase: ScaleDemandValuation(economy, "siliconwafers", 1.05f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.05f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.05f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneInputReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.1f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantInputReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.1f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsInputReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.1f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesInputReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.1f));
|
||||||
|
AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.05f));
|
||||||
|
|
||||||
AddSupplyOrder(desiredOrders, station, "water", ScaleSupplyTriggerByEconomy(economy, "water", waterReserve * 1.5f), reserveFloor: waterReserve, valuationBase: ScaleSupplyValuation(economy, "water", 0.65f));
|
AddSupplyOrder(desiredOrders, station, "water", ScaleSupplyTriggerByEconomy(economy, "water", waterReserve * 1.5f), reserveFloor: waterReserve, valuationBase: ScaleSupplyValuation(economy, "water", 0.65f));
|
||||||
AddSupplyOrder(desiredOrders, station, "energycells", ScaleSupplyTriggerByEconomy(economy, "energycells", energyReserve * 1.4f), reserveFloor: energyReserve, valuationBase: ScaleSupplyValuation(economy, "energycells", 0.7f));
|
AddSupplyOrder(desiredOrders, station, "energycells", ScaleSupplyTriggerByEconomy(economy, "energycells", energyReserve * 1.4f), reserveFloor: energyReserve, valuationBase: ScaleSupplyValuation(economy, "energycells", 0.7f));
|
||||||
AddSupplyOrder(desiredOrders, station, "ice", ScaleSupplyTriggerByEconomy(economy, "ice", iceReserve * 1.4f), reserveFloor: iceReserve, valuationBase: ScaleSupplyValuation(economy, "ice", 0.5f));
|
AddSupplyOrder(desiredOrders, station, "ice", ScaleSupplyTriggerByEconomy(economy, "ice", iceReserve * 1.4f), reserveFloor: iceReserve, valuationBase: ScaleSupplyValuation(economy, "ice", 0.5f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "methane", ScaleSupplyTriggerByEconomy(economy, "methane", methaneReserve * 1.4f), reserveFloor: methaneReserve, valuationBase: ScaleSupplyValuation(economy, "methane", 0.7f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "hydrogen", ScaleSupplyTriggerByEconomy(economy, "hydrogen", hydrogenReserve * 1.4f), reserveFloor: hydrogenReserve, valuationBase: ScaleSupplyValuation(economy, "hydrogen", 0.7f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "helium", ScaleSupplyTriggerByEconomy(economy, "helium", heliumReserve * 1.4f), reserveFloor: heliumReserve, valuationBase: ScaleSupplyValuation(economy, "helium", 0.7f));
|
||||||
AddSupplyOrder(desiredOrders, station, "ore", ScaleSupplyTriggerByEconomy(economy, "ore", oreReserve * 1.4f), reserveFloor: oreReserve, valuationBase: ScaleSupplyValuation(economy, "ore", 0.7f));
|
AddSupplyOrder(desiredOrders, station, "ore", ScaleSupplyTriggerByEconomy(economy, "ore", oreReserve * 1.4f), reserveFloor: oreReserve, valuationBase: ScaleSupplyValuation(economy, "ore", 0.7f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "silicon", ScaleSupplyTriggerByEconomy(economy, "silicon", siliconReserve * 1.4f), reserveFloor: siliconReserve, valuationBase: ScaleSupplyValuation(economy, "silicon", 0.7f));
|
||||||
AddSupplyOrder(desiredOrders, station, "refinedmetals", ScaleSupplyTriggerByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve) * 1.4f), reserveFloor: MathF.Max(refinedReserve, constructionRefinedReserve), valuationBase: ScaleSupplyValuation(economy, "refinedmetals", 0.95f));
|
AddSupplyOrder(desiredOrders, station, "refinedmetals", ScaleSupplyTriggerByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve) * 1.4f), reserveFloor: MathF.Max(refinedReserve, constructionRefinedReserve), valuationBase: ScaleSupplyValuation(economy, "refinedmetals", 0.95f));
|
||||||
AddSupplyOrder(desiredOrders, station, "hullparts", ScaleSupplyTriggerByEconomy(economy, "hullparts", MathF.Max(hullpartsReserve * 1.35f, hullpartsReserve + 40f)), reserveFloor: hullpartsReserve, valuationBase: ScaleSupplyValuation(economy, "hullparts", 1.05f));
|
AddSupplyOrder(desiredOrders, station, "hullparts", ScaleSupplyTriggerByEconomy(economy, "hullparts", MathF.Max(hullpartsReserve * 1.35f, hullpartsReserve + 40f)), reserveFloor: hullpartsReserve, valuationBase: ScaleSupplyValuation(economy, "hullparts", 1.05f));
|
||||||
AddSupplyOrder(desiredOrders, station, "claytronics", ScaleSupplyTriggerByEconomy(economy, "claytronics", MathF.Max(claytronicsReserve * 1.35f, claytronicsReserve + 30f)), reserveFloor: claytronicsReserve, valuationBase: ScaleSupplyValuation(economy, "claytronics", 1.1f));
|
AddSupplyOrder(desiredOrders, station, "claytronics", ScaleSupplyTriggerByEconomy(economy, "claytronics", MathF.Max(claytronicsReserve * 1.35f, claytronicsReserve + 30f)), reserveFloor: claytronicsReserve, valuationBase: ScaleSupplyValuation(economy, "claytronics", 1.1f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "graphene", ScaleSupplyTriggerByEconomy(economy, "graphene", MathF.Max(grapheneReserve * 1.35f, grapheneReserve + 30f)), reserveFloor: grapheneReserve, valuationBase: ScaleSupplyValuation(economy, "graphene", 0.9f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "siliconwafers", ScaleSupplyTriggerByEconomy(economy, "siliconwafers", MathF.Max(siliconWafersReserve * 1.35f, siliconWafersReserve + 30f)), reserveFloor: siliconWafersReserve, valuationBase: ScaleSupplyValuation(economy, "siliconwafers", 0.9f));
|
||||||
|
AddSupplyOrder(desiredOrders, station, "antimattercells", ScaleSupplyTriggerByEconomy(economy, "antimattercells", MathF.Max(antimatterCellsReserve * 1.35f, antimatterCellsReserve + 30f)), reserveFloor: antimatterCellsReserve, valuationBase: ScaleSupplyValuation(economy, "antimattercells", 0.9f));
|
||||||
|
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));
|
||||||
|
|
||||||
ReconcileStationMarketOrders(world, station, desiredOrders);
|
ReconcileStationMarketOrders(world, station, desiredOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static float GetStationReserveFloor(SimulationWorld world, StationRuntime station, string itemId)
|
||||||
|
{
|
||||||
|
var role = DetermineStationRole(station);
|
||||||
|
var site = GetConstructionSiteForStation(world, station.Id);
|
||||||
|
var constructionEnergyReserve = GetConstructionDemandForItem(world, site, "energycells");
|
||||||
|
var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts");
|
||||||
|
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")
|
||||||
|
? 90f
|
||||||
|
: 0f;
|
||||||
|
|
||||||
|
return itemId switch
|
||||||
|
{
|
||||||
|
"water" => MathF.Max(30f, station.Population * 3f),
|
||||||
|
"energycells" => role switch
|
||||||
|
{
|
||||||
|
"power" => 120f,
|
||||||
|
"refinery" => 160f,
|
||||||
|
"hullparts" => 180f,
|
||||||
|
"claytronics" => 220f,
|
||||||
|
"graphene" => 160f,
|
||||||
|
"siliconwafers" => 160f,
|
||||||
|
"antimattercells" => 160f,
|
||||||
|
"superfluidcoolant" => 160f,
|
||||||
|
"quantumtubes" => 160f,
|
||||||
|
"water" => 140f,
|
||||||
|
_ => 60f,
|
||||||
|
} + constructionEnergyReserve,
|
||||||
|
"ice" => role == "water" ? 260f : 0f,
|
||||||
|
"methane" => role == "graphene" ? 320f : 0f,
|
||||||
|
"hydrogen" => role == "antimattercells" ? 320f : 0f,
|
||||||
|
"helium" => role == "superfluidcoolant" ? 320f : 0f,
|
||||||
|
"ore" => role == "refinery" ? 260f : 0f,
|
||||||
|
"silicon" => role == "siliconwafers" ? 240f : 0f,
|
||||||
|
"refinedmetals" => MathF.Max(role switch
|
||||||
|
{
|
||||||
|
"hullparts" => 220f,
|
||||||
|
"shipyard" => 260f,
|
||||||
|
"refinery" => 80f,
|
||||||
|
_ => 0f,
|
||||||
|
}, constructionRefinedReserve),
|
||||||
|
"hullparts" => MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f) + shipPartsReserve,
|
||||||
|
"claytronics" => MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f),
|
||||||
|
"graphene" => MathF.Max(role == "graphene" ? 120f : 0f, role == "quantumtubes" ? 160f : 0f),
|
||||||
|
"siliconwafers" => role == "siliconwafers" ? 120f : 0f,
|
||||||
|
"antimattercells" => MathF.Max(role == "antimattercells" ? 120f : 0f, role == "claytronics" ? 120f : 0f),
|
||||||
|
"superfluidcoolant" => MathF.Max(role == "superfluidcoolant" ? 120f : 0f, role == "quantumtubes" ? 120f : 0f),
|
||||||
|
"quantumtubes" => MathF.Max(role == "quantumtubes" ? 120f : 0f, role == "claytronics" ? 120f : 0f),
|
||||||
|
_ => 0f,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||||
{
|
{
|
||||||
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
|
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
|
||||||
@@ -295,6 +389,11 @@ internal sealed class StationSimulationService
|
|||||||
"refinery" or "refinedmetals" => "refinery",
|
"refinery" or "refinedmetals" => "refinery",
|
||||||
"hullparts" or "hull" => "hullparts",
|
"hullparts" or "hull" => "hullparts",
|
||||||
"claytronics" or "clay" => "claytronics",
|
"claytronics" or "clay" => "claytronics",
|
||||||
|
"graphene" => "graphene",
|
||||||
|
"siliconwafers" or "silicon-wafers" or "silicon" => "siliconwafers",
|
||||||
|
"antimattercells" or "antimatter-cells" => "antimattercells",
|
||||||
|
"superfluidcoolant" or "superfluid-coolant" => "superfluidcoolant",
|
||||||
|
"quantumtubes" or "quantum-tubes" => "quantumtubes",
|
||||||
"shipyard" or "ship-production" => "shipyard",
|
"shipyard" or "ship-production" => "shipyard",
|
||||||
_ => "general",
|
_ => "general",
|
||||||
};
|
};
|
||||||
@@ -318,6 +417,31 @@ internal sealed class StationSimulationService
|
|||||||
return "water";
|
return "water";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (HasStationModules(station, "module_gen_prod_superfluidcoolant_01"))
|
||||||
|
{
|
||||||
|
return "superfluidcoolant";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasStationModules(station, "module_gen_prod_quantumtubes_01"))
|
||||||
|
{
|
||||||
|
return "quantumtubes";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasStationModules(station, "module_gen_prod_antimattercells_01"))
|
||||||
|
{
|
||||||
|
return "antimattercells";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasStationModules(station, "module_gen_prod_siliconwafers_01"))
|
||||||
|
{
|
||||||
|
return "siliconwafers";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasStationModules(station, "module_gen_prod_graphene_01"))
|
||||||
|
{
|
||||||
|
return "graphene";
|
||||||
|
}
|
||||||
|
|
||||||
if (HasStationModules(station, "module_gen_prod_claytronics_01"))
|
if (HasStationModules(station, "module_gen_prod_claytronics_01"))
|
||||||
{
|
{
|
||||||
return "claytronics";
|
return "claytronics";
|
||||||
|
|||||||
16
apps/backend/Universe/Api/GetBalanceHandler.cs
Normal file
16
apps/backend/Universe/Api/GetBalanceHandler.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Api;
|
||||||
|
|
||||||
|
public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/balance");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task HandleAsync(CancellationToken cancellationToken) =>
|
||||||
|
SendOkAsync(worldService.GetBalance(), cancellationToken);
|
||||||
|
}
|
||||||
49
apps/backend/Universe/Api/GetTelemetryHandler.cs
Normal file
49
apps/backend/Universe/Api/GetTelemetryHandler.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using FastEndpoints;
|
||||||
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Api;
|
||||||
|
|
||||||
|
public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService worldService) : EndpointWithoutRequest
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/telemetry");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task HandleAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var status = worldService.GetStatus();
|
||||||
|
var connections = worldService.GetConnectionStats();
|
||||||
|
var uptime = telemetry.Uptime;
|
||||||
|
|
||||||
|
return SendOkAsync(new
|
||||||
|
{
|
||||||
|
process = new
|
||||||
|
{
|
||||||
|
uptimeSeconds = uptime.TotalSeconds,
|
||||||
|
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
|
||||||
|
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
|
||||||
|
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
|
||||||
|
threadCount = telemetry.ThreadCount,
|
||||||
|
processorCount = Environment.ProcessorCount,
|
||||||
|
},
|
||||||
|
simulation = new
|
||||||
|
{
|
||||||
|
sequence = status.Sequence,
|
||||||
|
connectedClients = connections.ConnectedClients,
|
||||||
|
deltaHistoryCount = connections.DeltaHistoryCount,
|
||||||
|
tickIntervalMs = 200,
|
||||||
|
},
|
||||||
|
runtime = new
|
||||||
|
{
|
||||||
|
frameworkDescription = RuntimeInformation.FrameworkDescription,
|
||||||
|
osDescription = RuntimeInformation.OSDescription,
|
||||||
|
gcGen0 = GC.CollectionCount(0),
|
||||||
|
gcGen1 = GC.CollectionCount(1),
|
||||||
|
gcGen2 = GC.CollectionCount(2),
|
||||||
|
},
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/backend/Universe/Api/UpdateBalanceHandler.cs
Normal file
20
apps/backend/Universe/Api/UpdateBalanceHandler.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using SpaceGame.Api.Definitions;
|
||||||
|
using SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Api;
|
||||||
|
|
||||||
|
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Put("/api/balance");
|
||||||
|
AllowAnonymous();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var applied = worldService.UpdateBalance(req);
|
||||||
|
return SendOkAsync(applied, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ public sealed class SimulationWorld
|
|||||||
{
|
{
|
||||||
public required string Label { get; init; }
|
public required string Label { get; init; }
|
||||||
public required int Seed { get; init; }
|
public required int Seed { get; init; }
|
||||||
public required BalanceDefinition Balance { get; init; }
|
public required BalanceDefinition Balance { get; set; }
|
||||||
public required List<SystemRuntime> Systems { get; init; }
|
public required List<SystemRuntime> Systems { get; init; }
|
||||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||||
public required List<CelestialRuntime> Celestials { get; init; }
|
public required List<CelestialRuntime> Celestials { get; init; }
|
||||||
|
|||||||
@@ -224,6 +224,23 @@ internal sealed class SpatialBuilder
|
|||||||
|
|
||||||
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
|
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
|
||||||
|
{
|
||||||
|
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
var reference when reference.StartsWith("star-", StringComparison.Ordinal)
|
||||||
|
=> $"node-{graph.SystemId}-{reference}",
|
||||||
|
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
|
||||||
|
=> $"node-{graph.SystemId}-{reference}",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (anchorId is not null)
|
||||||
|
{
|
||||||
|
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
|
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class SystemGenerationService
|
|||||||
|
|
||||||
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
|
||||||
authoredSystems
|
authoredSystems
|
||||||
.Select(CloneSystemDefinition)
|
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
internal List<SolarSystemDefinition> ExpandSystems(
|
internal List<SolarSystemDefinition> ExpandSystems(
|
||||||
@@ -126,6 +126,7 @@ internal sealed class SystemGenerationService
|
|||||||
.Select(node => new ResourceNodeDefinition
|
.Select(node => new ResourceNodeDefinition
|
||||||
{
|
{
|
||||||
SourceKind = node.SourceKind,
|
SourceKind = node.SourceKind,
|
||||||
|
AnchorReference = node.AnchorReference,
|
||||||
Angle = node.Angle,
|
Angle = node.Angle,
|
||||||
RadiusOffset = node.RadiusOffset,
|
RadiusOffset = node.RadiusOffset,
|
||||||
InclinationDegrees = node.InclinationDegrees,
|
InclinationDegrees = node.InclinationDegrees,
|
||||||
@@ -137,7 +138,7 @@ internal sealed class SystemGenerationService
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return new SolarSystemDefinition
|
return EnsureStrategicResourceCoverage(new SolarSystemDefinition
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Label = label,
|
Label = label,
|
||||||
@@ -161,7 +162,7 @@ internal sealed class SystemGenerationService
|
|||||||
},
|
},
|
||||||
ResourceNodes = resourceNodes,
|
ResourceNodes = resourceNodes,
|
||||||
Planets = planets,
|
Planets = planets,
|
||||||
};
|
}, generatedIndex + 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
|
private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
|
||||||
@@ -182,6 +183,7 @@ internal sealed class SystemGenerationService
|
|||||||
ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
|
ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
|
||||||
{
|
{
|
||||||
SourceKind = node.SourceKind,
|
SourceKind = node.SourceKind,
|
||||||
|
AnchorReference = node.AnchorReference,
|
||||||
Angle = node.Angle,
|
Angle = node.Angle,
|
||||||
RadiusOffset = node.RadiusOffset,
|
RadiusOffset = node.RadiusOffset,
|
||||||
InclinationDegrees = node.InclinationDegrees,
|
InclinationDegrees = node.InclinationDegrees,
|
||||||
@@ -223,6 +225,7 @@ internal sealed class SystemGenerationService
|
|||||||
nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
|
nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
|
||||||
{
|
{
|
||||||
SourceKind = node.SourceKind,
|
SourceKind = node.SourceKind,
|
||||||
|
AnchorReference = node.AnchorReference,
|
||||||
Angle = node.Angle,
|
Angle = node.Angle,
|
||||||
RadiusOffset = node.RadiusOffset,
|
RadiusOffset = node.RadiusOffset,
|
||||||
InclinationDegrees = node.InclinationDegrees,
|
InclinationDegrees = node.InclinationDegrees,
|
||||||
@@ -234,10 +237,30 @@ internal sealed class SystemGenerationService
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SolarSystemDefinition EnsureStrategicResourceCoverage(SolarSystemDefinition system, int seed)
|
||||||
|
{
|
||||||
|
for (var index = 0; index < system.ResourceNodes.Count; index += 1)
|
||||||
|
{
|
||||||
|
system.ResourceNodes[index] = SanitizeResourceNode(system.ResourceNodes[index], system.Planets, seed, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredItems = new[] { "ore", "silicon", "ice", "hydrogen", "helium", "methane" };
|
||||||
|
foreach (var itemId in requiredItems)
|
||||||
|
{
|
||||||
|
if (system.ResourceNodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
system.ResourceNodes.Add(BuildStrategicResourceNode(itemId, system.Planets, seed, system.ResourceNodes.Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
|
||||||
{
|
{
|
||||||
var allPositions = occupiedPositions.ToList();
|
var allPositions = occupiedPositions.ToList();
|
||||||
@@ -303,25 +326,124 @@ internal sealed class SystemGenerationService
|
|||||||
return $"gen-{ordinal}-{slug}";
|
return $"gen-{ordinal}-{slug}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<ResourceNodeDefinition> BuildAsteroidBeltNodes(int generatedIndex, IReadOnlyList<PlanetDefinition> planets)
|
private static ResourceNodeDefinition BuildStrategicResourceNode(
|
||||||
|
string itemId,
|
||||||
|
IReadOnlyList<PlanetDefinition> planets,
|
||||||
|
int seed,
|
||||||
|
int ordinal)
|
||||||
{
|
{
|
||||||
var nodeCount = 4 + (generatedIndex % 4);
|
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
|
||||||
var oreAmount = 1000f;
|
return new ResourceNodeDefinition
|
||||||
|
|
||||||
for (var index = 0; index < nodeCount; index += 1)
|
|
||||||
{
|
{
|
||||||
yield return new ResourceNodeDefinition
|
SourceKind = "local-space",
|
||||||
|
AnchorReference = ResolveStrategicAnchorReference(itemId, planets, ordinal),
|
||||||
|
Angle = (MathF.PI * 2f * ((ordinal % 7) / 7f)) + Jitter(seed, 400 + ordinal, 0.35f),
|
||||||
|
RadiusOffset = 150000f + Jitter(seed, 460 + ordinal, 42000f),
|
||||||
|
InclinationDegrees = Jitter(seed, 520 + ordinal, 10f),
|
||||||
|
AnchorPlanetIndex = anchorPlanetIndex,
|
||||||
|
OreAmount = itemId switch
|
||||||
{
|
{
|
||||||
SourceKind = "asteroid-belt",
|
"ore" => 12000f,
|
||||||
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
|
"silicon" => 10000f,
|
||||||
RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f),
|
"ice" => 9000f,
|
||||||
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
|
_ => 8000f,
|
||||||
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
|
},
|
||||||
OreAmount = oreAmount,
|
ItemId = itemId,
|
||||||
ItemId = "ore",
|
ShardCount = itemId switch
|
||||||
ShardCount = 6 + (index % 4),
|
{
|
||||||
};
|
"ore" or "silicon" or "ice" => 8,
|
||||||
|
_ => 6,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceNodeDefinition SanitizeResourceNode(
|
||||||
|
ResourceNodeDefinition node,
|
||||||
|
IReadOnlyList<PlanetDefinition> planets,
|
||||||
|
int seed,
|
||||||
|
int ordinal)
|
||||||
|
{
|
||||||
|
node.SourceKind = "local-space";
|
||||||
|
node.AnchorReference ??= ResolveLegacyAnchorReference(node, planets, seed, ordinal);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLegacyAnchorReference(
|
||||||
|
ResourceNodeDefinition node,
|
||||||
|
IReadOnlyList<PlanetDefinition> planets,
|
||||||
|
int seed,
|
||||||
|
int ordinal)
|
||||||
|
{
|
||||||
|
if (node.AnchorMoonIndex is int moonIndex && node.AnchorPlanetIndex is int planetIndex && planetIndex >= 0)
|
||||||
|
{
|
||||||
|
return $"planet-{planetIndex + 1}-moon-{moonIndex + 1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.AnchorPlanetIndex is int anchoredPlanetIndex && anchoredPlanetIndex >= 0)
|
||||||
|
{
|
||||||
|
return $"planet-{anchoredPlanetIndex + 1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolveStrategicAnchorReference(node.ItemId, planets, ordinal + seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveStrategicAnchorReference(string itemId, IReadOnlyList<PlanetDefinition> planets, int ordinal)
|
||||||
|
{
|
||||||
|
if (itemId is "hydrogen" or "helium" or "methane")
|
||||||
|
{
|
||||||
|
var gasGiantIndex = planets
|
||||||
|
.Select((planet, index) => (planet, index))
|
||||||
|
.FirstOrDefault(entry => entry.planet.PlanetType is "gas-giant" or "ice-giant")
|
||||||
|
.index;
|
||||||
|
return gasGiantIndex > 0 || (planets.Count > 0 && planets[0].PlanetType is "gas-giant" or "ice-giant")
|
||||||
|
? $"planet-{gasGiantIndex + 1}"
|
||||||
|
: "star-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemId == "ice")
|
||||||
|
{
|
||||||
|
var moonAnchor = planets
|
||||||
|
.Select((planet, index) => (planet, index))
|
||||||
|
.FirstOrDefault(entry => entry.planet.Moons.Count > 0 && entry.planet.PlanetType is "ice" or "ice-giant" or "oceanic");
|
||||||
|
if (moonAnchor.planet is not null && moonAnchor.planet.Moons.Count > 0)
|
||||||
|
{
|
||||||
|
return $"planet-{moonAnchor.index + 1}-moon-1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
|
||||||
|
var lagrange = (ordinal % 3) switch
|
||||||
|
{
|
||||||
|
0 => "l1",
|
||||||
|
1 => "l4",
|
||||||
|
_ => "l5",
|
||||||
|
};
|
||||||
|
return $"planet-{anchorPlanetIndex + 1}-{lagrange}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResolveStrategicResourceAnchorPlanetIndex(string itemId, IReadOnlyList<PlanetDefinition> planets)
|
||||||
|
{
|
||||||
|
if (planets.Count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchesPlanetType(PlanetDefinition planet) => itemId switch
|
||||||
|
{
|
||||||
|
"hydrogen" or "helium" or "methane" => planet.PlanetType is "gas-giant" or "ice-giant",
|
||||||
|
"ice" => planet.PlanetType is "ice" or "ice-giant" or "oceanic",
|
||||||
|
_ => planet.PlanetType is not "gas-giant" and not "ice-giant",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var index = 0; index < planets.Count; index += 1)
|
||||||
|
{
|
||||||
|
if (MatchesPlanetType(planets[index]))
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolveAsteroidAnchorPlanetIndex(planets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
|
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ internal sealed class WorldBuilder(
|
|||||||
systemsById,
|
systemsById,
|
||||||
spatialLayout.SystemGraphs,
|
spatialLayout.SystemGraphs,
|
||||||
spatialLayout.Celestials,
|
spatialLayout.Celestials,
|
||||||
catalog.ModuleDefinitions);
|
catalog.ModuleDefinitions,
|
||||||
|
catalog.ItemDefinitions);
|
||||||
|
|
||||||
seedingService.InitializeStationStockpiles(stations);
|
seedingService.InitializeStationStockpiles(stations);
|
||||||
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
var refinery = seedingService.SelectRefineryStation(stations, scenario);
|
||||||
@@ -106,7 +107,8 @@ internal sealed class WorldBuilder(
|
|||||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||||
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||||
|
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||||
{
|
{
|
||||||
var stations = new List<StationRuntime>();
|
var stations = new List<StationRuntime>();
|
||||||
var stationIdCounter = 0;
|
var stationIdCounter = 0;
|
||||||
@@ -136,9 +138,7 @@ internal sealed class WorldBuilder(
|
|||||||
stations.Add(station);
|
stations.Add(station);
|
||||||
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
placement.AnchorCelestial.OccupyingStructureId = station.Id;
|
||||||
|
|
||||||
var startingModules = plan.StartingModules.Count > 0
|
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
|
||||||
? plan.StartingModules
|
|
||||||
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
|
|
||||||
|
|
||||||
foreach (var moduleId in startingModules)
|
foreach (var moduleId in startingModules)
|
||||||
{
|
{
|
||||||
@@ -149,6 +149,91 @@ internal sealed class WorldBuilder(
|
|||||||
return stations;
|
return stations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> BuildStartingModules(
|
||||||
|
InitialStationDefinition plan,
|
||||||
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||||
|
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||||
|
{
|
||||||
|
var startingModules = new List<string>(plan.StartingModules.Count > 0
|
||||||
|
? plan.StartingModules
|
||||||
|
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
|
||||||
|
|
||||||
|
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
|
||||||
|
|
||||||
|
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
|
||||||
|
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
|
||||||
|
{
|
||||||
|
EnsureStartingModule(startingModules, objectiveModuleId);
|
||||||
|
|
||||||
|
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
|
||||||
|
{
|
||||||
|
EnsureStartingModule(startingModules, storageModuleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return startingModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetObjectiveStartingModuleId(string? objective) =>
|
||||||
|
StationSimulationService.NormalizeStationObjective(objective) switch
|
||||||
|
{
|
||||||
|
"power" => "module_gen_prod_energycells_01",
|
||||||
|
"refinery" => "module_gen_ref_ore_01",
|
||||||
|
"graphene" => "module_gen_prod_graphene_01",
|
||||||
|
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||||
|
"hullparts" => "module_gen_prod_hullparts_01",
|
||||||
|
"claytronics" => "module_gen_prod_claytronics_01",
|
||||||
|
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||||
|
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||||
|
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||||
|
"water" => "module_gen_prod_water_01",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetRequiredStartingStorageModules(
|
||||||
|
string moduleId,
|
||||||
|
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
|
||||||
|
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
|
||||||
|
{
|
||||||
|
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var wareId in moduleDefinition.Production
|
||||||
|
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||||
|
.Concat(moduleDefinition.Products)
|
||||||
|
.Distinct(StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageModuleId = itemDefinition.CargoKind switch
|
||||||
|
{
|
||||||
|
"solid" => "module_arg_stor_solid_m_01",
|
||||||
|
"liquid" => "module_arg_stor_liquid_m_01",
|
||||||
|
_ => "module_arg_stor_container_m_01",
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return storageModuleId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureStartingModule(List<string> modules, string moduleId)
|
||||||
|
{
|
||||||
|
if (!modules.Contains(moduleId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
modules.Add(moduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
|
||||||
ScenarioDefinition scenario,
|
ScenarioDefinition scenario,
|
||||||
IReadOnlyDictionary<string, SystemRuntime> systemsById)
|
IReadOnlyDictionary<string, SystemRuntime> systemsById)
|
||||||
|
|||||||
@@ -65,33 +65,6 @@ internal sealed class WorldSeedingService
|
|||||||
foreach (var station in stations)
|
foreach (var station in stations)
|
||||||
{
|
{
|
||||||
InitializeStationPopulation(station);
|
InitializeStationPopulation(station);
|
||||||
if (station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
station.Inventory["energycells"] = MathF.Max(GetInventoryAmount(station.Inventory, "energycells"), 240f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station.InstalledModules.Contains("module_gen_prod_refinedmetals_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
station.Inventory["ore"] = MathF.Max(GetInventoryAmount(station.Inventory, "ore"), 220f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station.InstalledModules.Contains("module_gen_prod_hullparts_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
station.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(station.Inventory, "refinedmetals"), 240f);
|
|
||||||
station.Inventory["graphene"] = MathF.Max(GetInventoryAmount(station.Inventory, "graphene"), 80f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station.InstalledModules.Contains("module_gen_prod_claytronics_01", StringComparer.Ordinal))
|
|
||||||
{
|
|
||||||
station.Inventory["antimattercells"] = MathF.Max(GetInventoryAmount(station.Inventory, "antimattercells"), 90f);
|
|
||||||
station.Inventory["microchips"] = MathF.Max(GetInventoryAmount(station.Inventory, "microchips"), 120f);
|
|
||||||
station.Inventory["quantumtubes"] = MathF.Max(GetInventoryAmount(station.Inventory, "quantumtubes"), 90f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (station.Population > 0f)
|
|
||||||
{
|
|
||||||
station.Inventory["water"] = MathF.Max(60f, station.Population * 1.5f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +118,11 @@ internal sealed class WorldSeedingService
|
|||||||
|
|
||||||
foreach (var station in world.Stations)
|
foreach (var station in world.Stations)
|
||||||
{
|
{
|
||||||
|
if (HasSatisfiedStarterObjectiveLayout(world, station))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
|
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
|
||||||
if (moduleId is null || station.CelestialId is null)
|
if (moduleId is null || station.CelestialId is null)
|
||||||
{
|
{
|
||||||
@@ -200,6 +178,78 @@ internal sealed class WorldSeedingService
|
|||||||
return (sites, orders);
|
return (sites, orders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
|
||||||
|
{
|
||||||
|
var role = StationSimulationService.DetermineStationRole(station);
|
||||||
|
var objectiveModuleId = role switch
|
||||||
|
{
|
||||||
|
"power" => "module_gen_prod_energycells_01",
|
||||||
|
"refinery" => "module_gen_prod_refinedmetals_01",
|
||||||
|
"graphene" => "module_gen_prod_graphene_01",
|
||||||
|
"siliconwafers" => "module_gen_prod_siliconwafers_01",
|
||||||
|
"hullparts" => "module_gen_prod_hullparts_01",
|
||||||
|
"claytronics" => "module_gen_prod_claytronics_01",
|
||||||
|
"quantumtubes" => "module_gen_prod_quantumtubes_01",
|
||||||
|
"antimattercells" => "module_gen_prod_antimattercells_01",
|
||||||
|
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
|
||||||
|
"water" => "module_gen_prod_water_01",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (objectiveModuleId is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
|
||||||
|
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
|
||||||
|
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var storageModuleId in GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
|
||||||
|
{
|
||||||
|
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
|
||||||
|
{
|
||||||
|
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var wareId in moduleDefinition.Production
|
||||||
|
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
|
||||||
|
.Concat(moduleDefinition.Products)
|
||||||
|
.Distinct(StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return itemDefinition.CargoKind switch
|
||||||
|
{
|
||||||
|
"solid" => "module_arg_stor_solid_m_01",
|
||||||
|
"liquid" => "module_arg_stor_liquid_m_01",
|
||||||
|
_ => "module_arg_stor_container_m_01",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
||||||
{
|
{
|
||||||
var policies = new List<PolicySetRuntime>(factions.Count);
|
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||||
@@ -385,6 +435,13 @@ internal sealed class WorldSeedingService
|
|||||||
Color = "#ff8f70",
|
Color = "#ff8f70",
|
||||||
Credits = MinimumFactionCredits,
|
Credits = MinimumFactionCredits,
|
||||||
},
|
},
|
||||||
|
"nadir-syndicate" => new FactionRuntime
|
||||||
|
{
|
||||||
|
Id = factionId,
|
||||||
|
Label = "Nadir Syndicate",
|
||||||
|
Color = "#91e6a8",
|
||||||
|
Credits = MinimumFactionCredits,
|
||||||
|
},
|
||||||
_ => new FactionRuntime
|
_ => new FactionRuntime
|
||||||
{
|
{
|
||||||
Id = factionId,
|
Id = factionId,
|
||||||
|
|||||||
44
apps/backend/Universe/Simulation/TelemetryService.cs
Normal file
44
apps/backend/Universe/Simulation/TelemetryService.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SpaceGame.Api.Universe.Simulation;
|
||||||
|
|
||||||
|
public sealed class TelemetryService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Process _process = Process.GetCurrentProcess();
|
||||||
|
private readonly Timer _timer;
|
||||||
|
private double _cpuPercent;
|
||||||
|
private DateTime _lastSampleTime;
|
||||||
|
private TimeSpan _lastCpuTime;
|
||||||
|
|
||||||
|
public TelemetryService()
|
||||||
|
{
|
||||||
|
_process.Refresh();
|
||||||
|
_lastSampleTime = DateTime.UtcNow;
|
||||||
|
_lastCpuTime = _process.TotalProcessorTime;
|
||||||
|
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Sample(object? _)
|
||||||
|
{
|
||||||
|
_process.Refresh();
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var cpu = _process.TotalProcessorTime;
|
||||||
|
var elapsed = (now - _lastSampleTime).TotalSeconds;
|
||||||
|
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
|
||||||
|
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
|
||||||
|
_lastSampleTime = now;
|
||||||
|
_lastCpuTime = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double CpuPercent => Volatile.Read(ref _cpuPercent);
|
||||||
|
public long WorkingSetBytes => _process.WorkingSet64;
|
||||||
|
public long GcMemoryBytes => GC.GetTotalMemory(false);
|
||||||
|
public int ThreadCount => _process.Threads.Count;
|
||||||
|
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_timer.Dispose();
|
||||||
|
_process.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ public sealed class WorldService(
|
|||||||
private readonly Queue<WorldDelta> _history = [];
|
private readonly Queue<WorldDelta> _history = [];
|
||||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
|
||||||
private long _sequence;
|
private long _sequence;
|
||||||
|
private BalanceDefinition? _balanceOverride;
|
||||||
|
|
||||||
public WorldSnapshot GetSnapshot()
|
public WorldSnapshot GetSnapshot()
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,44 @@ public sealed class WorldService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
return (_subscribers.Count, _history.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceDefinition GetBalance()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
var b = _world.Balance;
|
||||||
|
return new BalanceDefinition
|
||||||
|
{
|
||||||
|
SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
|
||||||
|
YPlane = b.YPlane,
|
||||||
|
ArrivalThreshold = b.ArrivalThreshold,
|
||||||
|
MiningRate = b.MiningRate,
|
||||||
|
MiningCycleSeconds = b.MiningCycleSeconds,
|
||||||
|
TransferRate = b.TransferRate,
|
||||||
|
DockingDuration = b.DockingDuration,
|
||||||
|
UndockingDuration = b.UndockingDuration,
|
||||||
|
UndockDistance = b.UndockDistance,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_balanceOverride = SanitizeBalance(balance);
|
||||||
|
ApplyBalance(_world, _balanceOverride);
|
||||||
|
return GetBalance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||||
@@ -96,6 +135,10 @@ public sealed class WorldService(
|
|||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
_world = _loader.Load();
|
_world = _loader.Load();
|
||||||
|
if (_balanceOverride is not null)
|
||||||
|
{
|
||||||
|
ApplyBalance(_world, _balanceOverride);
|
||||||
|
}
|
||||||
_sequence += 1;
|
_sequence += 1;
|
||||||
_history.Clear();
|
_history.Clear();
|
||||||
|
|
||||||
@@ -127,6 +170,39 @@ public sealed class WorldService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
|
||||||
|
world.Balance = new BalanceDefinition
|
||||||
|
{
|
||||||
|
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
|
||||||
|
YPlane = balance.YPlane,
|
||||||
|
ArrivalThreshold = balance.ArrivalThreshold,
|
||||||
|
MiningRate = balance.MiningRate,
|
||||||
|
MiningCycleSeconds = balance.MiningCycleSeconds,
|
||||||
|
TransferRate = balance.TransferRate,
|
||||||
|
DockingDuration = balance.DockingDuration,
|
||||||
|
UndockingDuration = balance.UndockingDuration,
|
||||||
|
UndockDistance = balance.UndockDistance,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate)
|
||||||
|
{
|
||||||
|
static float finiteOr(float value, float fallback) =>
|
||||||
|
float.IsFinite(value) ? value : fallback;
|
||||||
|
|
||||||
|
return new BalanceDefinition
|
||||||
|
{
|
||||||
|
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
|
||||||
|
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
|
||||||
|
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
|
||||||
|
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
|
||||||
|
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
|
||||||
|
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
|
||||||
|
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
|
||||||
|
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
|
||||||
|
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||||
delta.RequiresSnapshotRefresh
|
delta.RequiresSnapshotRefresh
|
||||||
|| delta.Events.Count > 0
|
|| delta.Events.Count > 0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WorldGeneration": {
|
"WorldGeneration": {
|
||||||
"TargetSystemCount": 3,
|
"TargetSystemCount": 10,
|
||||||
"IncludeSolSystem": true
|
"IncludeSolSystem": true
|
||||||
},
|
},
|
||||||
"OrbitalSimulation": {
|
"OrbitalSimulation": {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import CollapsibleHudPanel from "./components/CollapsibleHudPanel.vue";
|
|||||||
import HtmlInfoPanel from "./components/HtmlInfoPanel.vue";
|
import HtmlInfoPanel from "./components/HtmlInfoPanel.vue";
|
||||||
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
||||||
import GmOpsWindow from "./components/gm/GmOpsWindow.vue";
|
import GmOpsWindow from "./components/gm/GmOpsWindow.vue";
|
||||||
|
import GmTelemetryWindow from "./components/gm/GmTelemetryWindow.vue";
|
||||||
|
import GmSettingsWindow from "./components/gm/GmSettingsWindow.vue";
|
||||||
import { createViewerHudState } from "./viewerHudState";
|
import { createViewerHudState } from "./viewerHudState";
|
||||||
import { useViewerSelectionStore } from "./ui/stores/viewerSelection";
|
import { useViewerSelectionStore } from "./ui/stores/viewerSelection";
|
||||||
import type { Selectable } from "./viewerTypes";
|
import type { Selectable } from "./viewerTypes";
|
||||||
@@ -22,6 +24,9 @@ const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore);
|
|||||||
let viewer: GameViewer | undefined;
|
let viewer: GameViewer | undefined;
|
||||||
|
|
||||||
const gmOpsOpen = ref(false);
|
const gmOpsOpen = ref(false);
|
||||||
|
const gmTelemetryOpen = ref(false);
|
||||||
|
const gmSettingsOpen = ref(false);
|
||||||
|
const gmMenuOpen = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -145,19 +150,56 @@ function onFocusSelection(selection: Selectable, cameraMode?: "follow" | "tactic
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div class="gm-launcher" @mouseleave="gmMenuOpen = false">
|
||||||
type="button"
|
<div v-if="gmMenuOpen" class="gm-launcher-menu">
|
||||||
class="gm-console-toggle"
|
<button
|
||||||
@click="gmOpsOpen = !gmOpsOpen"
|
type="button"
|
||||||
>
|
class="gm-launcher-item"
|
||||||
{{ gmOpsOpen ? "Close" : "GM Console" }}
|
:class="gmOpsOpen ? 'gm-launcher-item--active' : ''"
|
||||||
</button>
|
@click="gmOpsOpen = !gmOpsOpen; gmMenuOpen = false"
|
||||||
|
>
|
||||||
|
Entities
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gm-launcher-item"
|
||||||
|
:class="gmTelemetryOpen ? 'gm-launcher-item--active' : ''"
|
||||||
|
@click="gmTelemetryOpen = !gmTelemetryOpen; gmMenuOpen = false"
|
||||||
|
>
|
||||||
|
Telemetry
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gm-launcher-item"
|
||||||
|
:class="gmSettingsOpen ? 'gm-launcher-item--active' : ''"
|
||||||
|
@click="gmSettingsOpen = !gmSettingsOpen; gmMenuOpen = false"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gm-launcher-trigger"
|
||||||
|
:class="gmMenuOpen ? 'gm-launcher-trigger--open' : ''"
|
||||||
|
@click="gmMenuOpen = !gmMenuOpen"
|
||||||
|
>
|
||||||
|
GM
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GmOpsWindow
|
<GmOpsWindow
|
||||||
v-if="gmOpsOpen"
|
v-if="gmOpsOpen"
|
||||||
@close="gmOpsOpen = false"
|
@close="gmOpsOpen = false"
|
||||||
@focus="(id, kind) => onFocusSelection({ kind, id }, kind === 'ship' ? 'follow' : 'tactical')"
|
@focus="(id, kind) => onFocusSelection({ kind, id }, kind === 'ship' ? 'follow' : 'tactical')"
|
||||||
/>
|
/>
|
||||||
|
<GmTelemetryWindow
|
||||||
|
v-if="gmTelemetryOpen"
|
||||||
|
@close="gmTelemetryOpen = false"
|
||||||
|
/>
|
||||||
|
<GmSettingsWindow
|
||||||
|
v-if="gmSettingsOpen"
|
||||||
|
@close="gmSettingsOpen = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref="marqueeEl"
|
ref="marqueeEl"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
||||||
|
import type { TelemetrySnapshot } from "./contractsTelemetry";
|
||||||
|
import type { BalanceSettings } from "./contractsBalance";
|
||||||
|
|
||||||
export interface WorldStreamScope {
|
export interface WorldStreamScope {
|
||||||
scopeKind?: string;
|
scopeKind?: string;
|
||||||
@@ -49,6 +51,34 @@ export function openWorldStream(
|
|||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBalance(settings: BalanceSettings) {
|
||||||
|
const response = await fetch("/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() {
|
export async function resetWorld() {
|
||||||
const response = await fetch("/api/world/reset", {
|
const response = await fetch("/api/world/reset", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, h, ref } from "vue";
|
||||||
import {
|
import {
|
||||||
useVueTable,
|
useVueTable,
|
||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
@@ -18,6 +18,7 @@ import { useViewerSelectionStore } from "../../ui/stores/viewerSelection";
|
|||||||
import type { ShipSnapshot } from "../../contractsShips";
|
import type { ShipSnapshot } from "../../contractsShips";
|
||||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
import type { FactionSnapshot } from "../../contractsFactions";
|
import type { FactionSnapshot } from "../../contractsFactions";
|
||||||
|
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
||||||
|
|
||||||
// ── Column ordering composable ─────────────────────────────────────────────
|
// ── Column ordering composable ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -85,6 +86,20 @@ const factionMap = computed(() =>
|
|||||||
new Map(gmStore.factions.map((f) => [f.id, f.label])),
|
new Map(gmStore.factions.map((f) => [f.id, f.label])),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const factionColorMap = computed(() =>
|
||||||
|
new Map(gmStore.factions.map((f) => [f.id, f.color])),
|
||||||
|
);
|
||||||
|
|
||||||
|
function renderColorCell(color: string | null | undefined) {
|
||||||
|
const resolved = color && color !== "—" ? color : "#6b7280";
|
||||||
|
return h("div", { class: "flex items-center justify-center" }, [
|
||||||
|
h("span", {
|
||||||
|
class: "inline-block h-3 w-3 rounded-full border border-white/20",
|
||||||
|
style: { backgroundColor: resolved },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
function titleCaseToken(value: string | null | undefined) {
|
function titleCaseToken(value: string | null | undefined) {
|
||||||
if (!value) return "—";
|
if (!value) return "—";
|
||||||
return value
|
return value
|
||||||
@@ -106,6 +121,13 @@ function compactRate(value: number | null | undefined) {
|
|||||||
return `${sign}${value.toFixed(2)}/s`;
|
return `${sign}${value.toFixed(2)}/s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCargoAmount(value: number | null | undefined) {
|
||||||
|
if (value == null || Number.isNaN(value)) return "—";
|
||||||
|
const rounded = Math.round(value);
|
||||||
|
if (Math.abs(value - rounded) < 0.005) return String(rounded);
|
||||||
|
return value.toFixed(2).replace(/\.?0+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
function getLeadObjective(faction: FactionSnapshot) {
|
function getLeadObjective(faction: FactionSnapshot) {
|
||||||
return [...(faction.objectives ?? [])]
|
return [...(faction.objectives ?? [])]
|
||||||
.sort((left, right) => right.priority - left.priority)
|
.sort((left, right) => right.priority - left.priority)
|
||||||
@@ -184,6 +206,7 @@ type ShipRow = {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
class: string;
|
class: string;
|
||||||
|
factionColor: string;
|
||||||
faction: string;
|
faction: string;
|
||||||
system: string;
|
system: string;
|
||||||
state: string;
|
state: string;
|
||||||
@@ -201,6 +224,7 @@ const shipRows = computed<ShipRow[]>(() =>
|
|||||||
id: s.id,
|
id: s.id,
|
||||||
label: s.label,
|
label: s.label,
|
||||||
class: s.class,
|
class: s.class,
|
||||||
|
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||||
system: s.systemId,
|
system: s.systemId,
|
||||||
state: titleCaseToken(s.state),
|
state: titleCaseToken(s.state),
|
||||||
@@ -218,6 +242,11 @@ const shipColumnHelper = createColumnHelper<ShipRow>();
|
|||||||
const shipColumns = [
|
const shipColumns = [
|
||||||
shipColumnHelper.accessor("label", { header: "Name" }),
|
shipColumnHelper.accessor("label", { header: "Name" }),
|
||||||
shipColumnHelper.accessor("class", { header: "Class" }),
|
shipColumnHelper.accessor("class", { header: "Class" }),
|
||||||
|
shipColumnHelper.accessor("factionColor", {
|
||||||
|
id: "factionColor",
|
||||||
|
header: "Color",
|
||||||
|
cell: (info) => renderColorCell(info.getValue()),
|
||||||
|
}),
|
||||||
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
shipColumnHelper.accessor("faction", { header: "Faction" }),
|
||||||
shipColumnHelper.accessor("system", { header: "System" }),
|
shipColumnHelper.accessor("system", { header: "System" }),
|
||||||
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
shipColumnHelper.accessor("state", { header: "Ship State" }),
|
||||||
@@ -226,13 +255,16 @@ const shipColumns = [
|
|||||||
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
shipColumnHelper.accessor("phase", { header: "Phase" }),
|
||||||
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
shipColumnHelper.accessor("action", { header: "Current Action" }),
|
||||||
shipColumnHelper.accessor("task", { header: "Task" }),
|
shipColumnHelper.accessor("task", { header: "Task" }),
|
||||||
shipColumnHelper.accessor("cargo", { header: "Cargo" }),
|
shipColumnHelper.accessor("cargo", {
|
||||||
|
header: "Cargo",
|
||||||
|
cell: (info) => formatCargoAmount(info.getValue()),
|
||||||
|
}),
|
||||||
shipColumnHelper.accessor("health", { header: "HP" }),
|
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const shipFilter = ref("");
|
const shipFilter = ref("");
|
||||||
const shipSorting = ref<SortingState>([]);
|
const shipSorting = ref<SortingState>([]);
|
||||||
const shipOrder = useColumnOrder(["label", "class", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "objective", "behavior", "phase", "action", "task", "cargo", "health"]);
|
||||||
|
|
||||||
const shipTable = useVueTable({
|
const shipTable = useVueTable({
|
||||||
get data() { return shipRows.value; },
|
get data() { return shipRows.value; },
|
||||||
@@ -259,22 +291,29 @@ type StationRow = {
|
|||||||
label: string;
|
label: string;
|
||||||
category: string;
|
category: string;
|
||||||
objective: string;
|
objective: string;
|
||||||
|
factionColor: string;
|
||||||
faction: string;
|
faction: string;
|
||||||
system: string;
|
system: string;
|
||||||
process: string;
|
process: string;
|
||||||
workforce: string;
|
workforce: string;
|
||||||
docked: string;
|
docked: string;
|
||||||
orders: number;
|
orders: number;
|
||||||
|
orderDetails: MarketOrderSnapshot[];
|
||||||
cargo: number;
|
cargo: number;
|
||||||
modules: number;
|
modules: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const marketOrderMap = computed(() =>
|
||||||
|
new Map(gmStore.marketOrders.map((o) => [o.id, o])),
|
||||||
|
);
|
||||||
|
|
||||||
const stationRows = computed<StationRow[]>(() =>
|
const stationRows = computed<StationRow[]>(() =>
|
||||||
gmStore.stations.map((s) => ({
|
gmStore.stations.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
label: s.label,
|
label: s.label,
|
||||||
category: s.category,
|
category: s.category,
|
||||||
objective: titleCaseToken(s.objective),
|
objective: titleCaseToken(s.objective),
|
||||||
|
factionColor: factionColorMap.value.get(s.factionId) ?? "—",
|
||||||
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
faction: factionMap.value.get(s.factionId) ?? s.factionId,
|
||||||
system: s.systemId,
|
system: s.systemId,
|
||||||
process: s.currentProcesses.length > 0
|
process: s.currentProcesses.length > 0
|
||||||
@@ -283,6 +322,10 @@ const stationRows = computed<StationRow[]>(() =>
|
|||||||
workforce: `${Math.round(s.population)} / ${Math.round(s.populationCapacity)} · ${Math.round(s.workforceEffectiveRatio * 100)}%`,
|
workforce: `${Math.round(s.population)} / ${Math.round(s.populationCapacity)} · ${Math.round(s.workforceEffectiveRatio * 100)}%`,
|
||||||
docked: `${s.dockedShips} / ${s.dockingPads}`,
|
docked: `${s.dockedShips} / ${s.dockingPads}`,
|
||||||
orders: s.marketOrderIds.length,
|
orders: s.marketOrderIds.length,
|
||||||
|
orderDetails: s.marketOrderIds.flatMap((id) => {
|
||||||
|
const order = marketOrderMap.value.get(id);
|
||||||
|
return order ? [order] : [];
|
||||||
|
}),
|
||||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||||
modules: s.installedModules.length,
|
modules: s.installedModules.length,
|
||||||
})),
|
})),
|
||||||
@@ -293,19 +336,27 @@ const stationColumns = [
|
|||||||
stationColumnHelper.accessor("label", { header: "Name" }),
|
stationColumnHelper.accessor("label", { header: "Name" }),
|
||||||
stationColumnHelper.accessor("category", { header: "Category" }),
|
stationColumnHelper.accessor("category", { header: "Category" }),
|
||||||
stationColumnHelper.accessor("objective", { header: "Objective" }),
|
stationColumnHelper.accessor("objective", { header: "Objective" }),
|
||||||
|
stationColumnHelper.accessor("factionColor", {
|
||||||
|
id: "factionColor",
|
||||||
|
header: "Color",
|
||||||
|
cell: (info) => renderColorCell(info.getValue()),
|
||||||
|
}),
|
||||||
stationColumnHelper.accessor("faction", { header: "Faction" }),
|
stationColumnHelper.accessor("faction", { header: "Faction" }),
|
||||||
stationColumnHelper.accessor("system", { header: "System" }),
|
stationColumnHelper.accessor("system", { header: "System" }),
|
||||||
stationColumnHelper.accessor("process", { header: "Production" }),
|
stationColumnHelper.accessor("process", { header: "Production" }),
|
||||||
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
|
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
|
||||||
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||||
stationColumnHelper.accessor("orders", { header: "Orders" }),
|
stationColumnHelper.accessor("orders", { header: "Orders" }),
|
||||||
stationColumnHelper.accessor("cargo", { header: "Cargo" }),
|
stationColumnHelper.accessor("cargo", {
|
||||||
|
header: "Cargo",
|
||||||
|
cell: (info) => formatCargoAmount(info.getValue()),
|
||||||
|
}),
|
||||||
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||||
];
|
];
|
||||||
|
|
||||||
const stationFilter = ref("");
|
const stationFilter = ref("");
|
||||||
const stationSorting = ref<SortingState>([]);
|
const stationSorting = ref<SortingState>([]);
|
||||||
const stationOrder = useColumnOrder(["label", "category", "objective", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
|
const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]);
|
||||||
|
|
||||||
const stationTable = useVueTable({
|
const stationTable = useVueTable({
|
||||||
get data() { return stationRows.value; },
|
get data() { return stationRows.value; },
|
||||||
@@ -330,6 +381,7 @@ const stationTable = useVueTable({
|
|||||||
type FactionRow = {
|
type FactionRow = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
color: string;
|
||||||
planCycle: number;
|
planCycle: number;
|
||||||
priority: string;
|
priority: string;
|
||||||
strategicState: string;
|
strategicState: string;
|
||||||
@@ -353,6 +405,7 @@ const factionRows = computed<FactionRow[]>(() =>
|
|||||||
return {
|
return {
|
||||||
id: f.id,
|
id: f.id,
|
||||||
label: f.label,
|
label: f.label,
|
||||||
|
color: f.color,
|
||||||
planCycle: blackboard?.planCycle ?? 0,
|
planCycle: blackboard?.planCycle ?? 0,
|
||||||
priority: describeFactionPriority(f),
|
priority: describeFactionPriority(f),
|
||||||
strategicState: describeFactionStrategicState(f),
|
strategicState: describeFactionStrategicState(f),
|
||||||
@@ -374,6 +427,10 @@ const factionRows = computed<FactionRow[]>(() =>
|
|||||||
const factionColumnHelper = createColumnHelper<FactionRow>();
|
const factionColumnHelper = createColumnHelper<FactionRow>();
|
||||||
const factionColumns = [
|
const factionColumns = [
|
||||||
factionColumnHelper.accessor("label", { header: "Faction" }),
|
factionColumnHelper.accessor("label", { header: "Faction" }),
|
||||||
|
factionColumnHelper.accessor("color", {
|
||||||
|
header: "Color",
|
||||||
|
cell: (info) => renderColorCell(info.getValue()),
|
||||||
|
}),
|
||||||
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
factionColumnHelper.accessor("planCycle", { header: "Cycle" }),
|
||||||
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
|
factionColumnHelper.accessor("priority", { header: "Top Priority" }),
|
||||||
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
|
factionColumnHelper.accessor("strategicState", { header: "Objective" }),
|
||||||
@@ -392,7 +449,7 @@ const factionColumns = [
|
|||||||
|
|
||||||
const factionFilter = ref("");
|
const factionFilter = ref("");
|
||||||
const factionSorting = ref<SortingState>([]);
|
const factionSorting = ref<SortingState>([]);
|
||||||
const factionOrder = useColumnOrder(["label", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
const factionOrder = useColumnOrder(["label", "color", "planCycle", "priority", "strategicState", "leadStep", "leadTask", "warReadiness", "economy", "threat", "fleets", "systems", "credits", "population", "shipsBuilt", "shipsLost"]);
|
||||||
|
|
||||||
const factionTable = useVueTable({
|
const factionTable = useVueTable({
|
||||||
get data() { return factionRows.value; },
|
get data() { return factionRows.value; },
|
||||||
@@ -472,6 +529,31 @@ function isShipSelected(id: string) {
|
|||||||
function isStationSelected(id: string) {
|
function isStationSelected(id: string) {
|
||||||
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
|
return selectedEntityKind.value === "station" && selectedEntityId.value === id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Orders tooltip ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const ordersTooltip = ref<{
|
||||||
|
visible: boolean;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
orders: MarketOrderSnapshot[];
|
||||||
|
}>({ visible: false, x: 0, y: 0, orders: [] });
|
||||||
|
|
||||||
|
function showOrdersTooltip(e: MouseEvent, orders: MarketOrderSnapshot[]) {
|
||||||
|
if (orders.length === 0) return;
|
||||||
|
ordersTooltip.value = { visible: true, x: e.clientX, y: e.clientY, orders };
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveOrdersTooltip(e: MouseEvent) {
|
||||||
|
if (ordersTooltip.value.visible) {
|
||||||
|
ordersTooltip.value.x = e.clientX;
|
||||||
|
ordersTooltip.value.y = e.clientY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOrdersTooltip() {
|
||||||
|
ordersTooltip.value.visible = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -660,6 +742,15 @@ function isStationSelected(id: string) {
|
|||||||
>
|
>
|
||||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="cell.column.id === 'orders'"
|
||||||
|
:class="row.original.orderDetails.length > 0 ? 'gm-orders-trigger' : ''"
|
||||||
|
@mouseenter="showOrdersTooltip($event, row.original.orderDetails)"
|
||||||
|
@mousemove="moveOrdersTooltip"
|
||||||
|
@mouseleave="hideOrdersTooltip"
|
||||||
|
>
|
||||||
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||||
|
</span>
|
||||||
<FlexRender
|
<FlexRender
|
||||||
v-else
|
v-else
|
||||||
:render="cell.column.columnDef.cell"
|
:render="cell.column.columnDef.cell"
|
||||||
@@ -740,4 +831,35 @@ function isStationSelected(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GmWindow>
|
</GmWindow>
|
||||||
|
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="ordersTooltip.visible && ordersTooltip.orders.length > 0"
|
||||||
|
class="gm-orders-tooltip"
|
||||||
|
:style="{ left: `${ordersTooltip.x + 14}px`, top: `${ordersTooltip.y + 14}px` }"
|
||||||
|
>
|
||||||
|
<table class="gm-orders-tooltip-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Kind</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Remaining</th>
|
||||||
|
<th>Valuation</th>
|
||||||
|
<th>State</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="order in ordersTooltip.orders" :key="order.id">
|
||||||
|
<td>{{ order.itemId }}</td>
|
||||||
|
<td>{{ titleCaseToken(order.kind) }}</td>
|
||||||
|
<td class="tabular-nums">{{ order.amount }}</td>
|
||||||
|
<td class="tabular-nums">{{ order.remainingAmount }}</td>
|
||||||
|
<td class="tabular-nums">{{ order.valuation }}</td>
|
||||||
|
<td>{{ titleCaseToken(order.state) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
134
apps/viewer/src/components/gm/GmSettingsWindow.vue
Normal file
134
apps/viewer/src/components/gm/GmSettingsWindow.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import GmWindow from "./GmWindow.vue";
|
||||||
|
import { fetchBalance, updateBalance } from "../../api";
|
||||||
|
import type { BalanceSettings } from "../../contractsBalance";
|
||||||
|
|
||||||
|
const emit = defineEmits<{ close: [] }>();
|
||||||
|
|
||||||
|
type FieldMeta = {
|
||||||
|
key: keyof BalanceSettings;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
step: number;
|
||||||
|
min: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FIELDS: FieldMeta[] = [
|
||||||
|
{ key: "simulationSpeedMultiplier", label: "Sim Speed Multiplier", description: "Global speed factor applied to every tick", step: 0.1, min: 0.01 },
|
||||||
|
{ key: "arrivalThreshold", label: "Arrival Threshold", description: "Distance at which a ship is considered arrived", step: 1, min: 0.1 },
|
||||||
|
{ key: "miningRate", label: "Mining Rate", description: "Units of ore extracted per mining cycle", step: 1, min: 0 },
|
||||||
|
{ key: "miningCycleSeconds", label: "Mining Cycle (s)", description: "Duration of one mining cycle", step: 0.1, min: 0.1 },
|
||||||
|
{ key: "transferRate", label: "Transfer Rate", description: "Cargo units transferred per second", step: 1, min: 0 },
|
||||||
|
{ key: "dockingDuration", label: "Docking Duration (s)", description: "Time for a ship to complete docking", step: 0.1, min: 0.1 },
|
||||||
|
{ key: "undockingDuration", label: "Undocking Duration (s)", description: "Time for a ship to complete undocking", step: 0.1, min: 0.1 },
|
||||||
|
{ key: "undockDistance", label: "Undock Distance", description: "Distance traveled when undocking", step: 1, min: 0 },
|
||||||
|
{ key: "yPlane", label: "Y Plane", description: "Vertical height for spatial placement", step: 0.5, min: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const draft = reactive<BalanceSettings>({
|
||||||
|
simulationSpeedMultiplier: 1,
|
||||||
|
yPlane: 0,
|
||||||
|
arrivalThreshold: 0,
|
||||||
|
miningRate: 0,
|
||||||
|
miningCycleSeconds: 0,
|
||||||
|
transferRate: 0,
|
||||||
|
dockingDuration: 0,
|
||||||
|
undockingDuration: 0,
|
||||||
|
undockDistance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadError = ref<string | null>(null);
|
||||||
|
const saveError = ref<string | null>(null);
|
||||||
|
const saveStatus = ref<"idle" | "saving" | "saved">("idle");
|
||||||
|
const loaded = ref(false);
|
||||||
|
let savedTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchBalance();
|
||||||
|
Object.assign(draft, data);
|
||||||
|
loadError.value = null;
|
||||||
|
loaded.value = true;
|
||||||
|
} catch {
|
||||||
|
loadError.value = "Failed to load settings";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sanitizeDraft() {
|
||||||
|
for (const field of FIELDS) {
|
||||||
|
const value = draft[field.key];
|
||||||
|
draft[field.key] = Number.isFinite(value) ? Math.max(field.min, value) : field.min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
sanitizeDraft();
|
||||||
|
saveStatus.value = "saving";
|
||||||
|
saveError.value = null;
|
||||||
|
try {
|
||||||
|
const saved = await updateBalance({ ...draft });
|
||||||
|
Object.assign(draft, saved);
|
||||||
|
saveStatus.value = "saved";
|
||||||
|
if (savedTimer !== null) clearTimeout(savedTimer);
|
||||||
|
savedTimer = setTimeout(() => { saveStatus.value = "idle"; }, 2500);
|
||||||
|
} catch {
|
||||||
|
saveError.value = "Failed to save settings";
|
||||||
|
saveStatus.value = "idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GmWindow
|
||||||
|
title="Settings"
|
||||||
|
:initial-width="480"
|
||||||
|
:initial-height="460"
|
||||||
|
:initial-x="260"
|
||||||
|
:initial-y="100"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="gm-settings flex h-full flex-col">
|
||||||
|
<div v-if="loadError" class="gm-settings-error mx-4 mt-3 rounded px-3 py-2 text-xs">
|
||||||
|
{{ loadError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gm-settings-body min-h-0 flex-1 overflow-auto px-4 py-3">
|
||||||
|
<div class="gm-settings-section-title mb-3">Balance</div>
|
||||||
|
|
||||||
|
<div class="gm-settings-grid">
|
||||||
|
<template v-for="field in FIELDS" :key="field.key">
|
||||||
|
<label :for="`gm-setting-${field.key}`" class="gm-settings-label">
|
||||||
|
{{ field.label }}
|
||||||
|
</label>
|
||||||
|
<div class="gm-settings-field-group">
|
||||||
|
<input
|
||||||
|
:id="`gm-setting-${field.key}`"
|
||||||
|
v-model.number="draft[field.key]"
|
||||||
|
type="number"
|
||||||
|
class="gm-settings-input"
|
||||||
|
:step="field.step"
|
||||||
|
:min="field.min"
|
||||||
|
/>
|
||||||
|
<span class="gm-settings-desc">{{ field.description }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gm-settings-footer flex items-center gap-3 px-4 py-3">
|
||||||
|
<span v-if="saveError" class="gm-settings-error-inline flex-1 text-xs">{{ saveError }}</span>
|
||||||
|
<span v-else-if="saveStatus === 'saved'" class="gm-settings-saved flex-1 text-xs">Saved</span>
|
||||||
|
<span v-else class="flex-1" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="gm-settings-save-btn"
|
||||||
|
:disabled="saveStatus === 'saving' || !loaded"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
{{ saveStatus === 'saving' ? 'Saving…' : 'Apply' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GmWindow>
|
||||||
|
</template>
|
||||||
166
apps/viewer/src/components/gm/GmTelemetryWindow.vue
Normal file
166
apps/viewer/src/components/gm/GmTelemetryWindow.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
|
import GmWindow from "./GmWindow.vue";
|
||||||
|
import { fetchTelemetry } from "../../api";
|
||||||
|
import type { TelemetrySnapshot } from "../../contractsTelemetry";
|
||||||
|
|
||||||
|
const emit = defineEmits<{ close: [] }>();
|
||||||
|
|
||||||
|
const data = ref<TelemetrySnapshot | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
const lastUpdatedAt = ref<number | null>(null);
|
||||||
|
const secondsSinceUpdate = ref(0);
|
||||||
|
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let ageTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
async function poll() {
|
||||||
|
try {
|
||||||
|
data.value = await fetchTelemetry();
|
||||||
|
lastUpdatedAt.value = Date.now();
|
||||||
|
error.value = null;
|
||||||
|
} catch {
|
||||||
|
error.value = "Failed to fetch telemetry";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void poll();
|
||||||
|
pollTimer = setInterval(poll, 2000);
|
||||||
|
ageTimer = setInterval(() => {
|
||||||
|
secondsSinceUpdate.value = lastUpdatedAt.value
|
||||||
|
? Math.floor((Date.now() - lastUpdatedAt.value) / 1000)
|
||||||
|
: 0;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollTimer !== null) clearInterval(pollTimer);
|
||||||
|
if (ageTimer !== null) clearInterval(ageTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatUptime(seconds: number) {
|
||||||
|
const h = Math.floor(seconds / 3600);
|
||||||
|
const m = Math.floor((seconds % 3600) / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
if (h > 0) return `${h}h ${m}m ${s}s`;
|
||||||
|
if (m > 0) return `${m}m ${s}s`;
|
||||||
|
return `${s}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(n: number) {
|
||||||
|
return n.toLocaleString("en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cpuBarWidth(pct: number) {
|
||||||
|
return `${Math.min(100, Math.max(0, pct))}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cpuBarClass(pct: number) {
|
||||||
|
if (pct >= 80) return "gm-telemetry-bar--high";
|
||||||
|
if (pct >= 50) return "gm-telemetry-bar--mid";
|
||||||
|
return "gm-telemetry-bar--low";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<GmWindow
|
||||||
|
title="Server Telemetry"
|
||||||
|
:initial-width="460"
|
||||||
|
:initial-height="380"
|
||||||
|
:initial-x="200"
|
||||||
|
:initial-y="120"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="gm-telemetry flex h-full flex-col overflow-auto px-4 py-3">
|
||||||
|
<!-- Error state -->
|
||||||
|
<div v-if="error" class="gm-telemetry-error mb-3 rounded px-3 py-2 text-xs">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div v-else-if="!data" class="flex flex-1 items-center justify-center text-xs opacity-40">
|
||||||
|
Loading…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- PROCESS section -->
|
||||||
|
<div class="gm-telemetry-section mb-4">
|
||||||
|
<div class="gm-telemetry-section-title mb-2">Process</div>
|
||||||
|
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||||
|
<span class="gm-telemetry-label">CPU</span>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<span class="gm-telemetry-value w-10 text-right">{{ data.process.cpuPercent.toFixed(1) }}%</span>
|
||||||
|
<span class="gm-telemetry-bar-track flex-1">
|
||||||
|
<span
|
||||||
|
class="gm-telemetry-bar"
|
||||||
|
:class="cpuBarClass(data.process.cpuPercent)"
|
||||||
|
:style="{ width: cpuBarWidth(data.process.cpuPercent) }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="gm-telemetry-dim">/ {{ data.process.processorCount }} cores</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Working set</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.process.workingSetMb.toFixed(1) }} MB</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">GC memory</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.process.gcMemoryMb.toFixed(1) }} MB</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Threads</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.process.threadCount }}</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Uptime</span>
|
||||||
|
<span class="gm-telemetry-value">{{ formatUptime(data.process.uptimeSeconds) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SIMULATION section -->
|
||||||
|
<div class="gm-telemetry-section mb-4">
|
||||||
|
<div class="gm-telemetry-section-title mb-2">Simulation</div>
|
||||||
|
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||||
|
<span class="gm-telemetry-label">Sequence</span>
|
||||||
|
<span class="gm-telemetry-value font-mono">{{ formatNumber(data.simulation.sequence) }}</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Connected clients</span>
|
||||||
|
<span class="gm-telemetry-value">
|
||||||
|
<span
|
||||||
|
class="gm-telemetry-clients-dot"
|
||||||
|
:class="data.simulation.connectedClients > 0 ? 'gm-telemetry-clients-dot--active' : ''"
|
||||||
|
/>
|
||||||
|
{{ data.simulation.connectedClients }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Delta history</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.simulation.deltaHistoryCount }} / 256</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">Tick interval</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.simulation.tickIntervalMs }} ms</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RUNTIME section -->
|
||||||
|
<div class="gm-telemetry-section mb-4">
|
||||||
|
<div class="gm-telemetry-section-title mb-2">Runtime</div>
|
||||||
|
<div class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5">
|
||||||
|
<span class="gm-telemetry-label">.NET</span>
|
||||||
|
<span class="gm-telemetry-value">{{ data.runtime.frameworkDescription }}</span>
|
||||||
|
|
||||||
|
<span class="gm-telemetry-label">GC collections</span>
|
||||||
|
<span class="gm-telemetry-value font-mono">
|
||||||
|
G0 {{ formatNumber(data.runtime.gcGen0) }} ·
|
||||||
|
G1 {{ formatNumber(data.runtime.gcGen1) }} ·
|
||||||
|
G2 {{ formatNumber(data.runtime.gcGen2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="mt-auto flex items-center justify-between pt-2 text-[10px] opacity-40">
|
||||||
|
<span>Updated {{ secondsSinceUpdate }}s ago</span>
|
||||||
|
<span>Polling every 2s</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</GmWindow>
|
||||||
|
</template>
|
||||||
11
apps/viewer/src/contractsBalance.ts
Normal file
11
apps/viewer/src/contractsBalance.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface BalanceSettings {
|
||||||
|
simulationSpeedMultiplier: number;
|
||||||
|
yPlane: number;
|
||||||
|
arrivalThreshold: number;
|
||||||
|
miningRate: number;
|
||||||
|
miningCycleSeconds: number;
|
||||||
|
transferRate: number;
|
||||||
|
dockingDuration: number;
|
||||||
|
undockingDuration: number;
|
||||||
|
undockDistance: number;
|
||||||
|
}
|
||||||
23
apps/viewer/src/contractsTelemetry.ts
Normal file
23
apps/viewer/src/contractsTelemetry.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface TelemetrySnapshot {
|
||||||
|
process: {
|
||||||
|
uptimeSeconds: number;
|
||||||
|
cpuPercent: number;
|
||||||
|
workingSetMb: number;
|
||||||
|
gcMemoryMb: number;
|
||||||
|
threadCount: number;
|
||||||
|
processorCount: number;
|
||||||
|
};
|
||||||
|
simulation: {
|
||||||
|
sequence: number;
|
||||||
|
connectedClients: number;
|
||||||
|
deltaHistoryCount: number;
|
||||||
|
tickIntervalMs: number;
|
||||||
|
};
|
||||||
|
runtime: {
|
||||||
|
frameworkDescription: string;
|
||||||
|
osDescription: string;
|
||||||
|
gcGen0: number;
|
||||||
|
gcGen1: number;
|
||||||
|
gcGen2: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -539,28 +539,326 @@ canvas {
|
|||||||
box-shadow: inset 2px 0 0 var(--viewer-accent);
|
box-shadow: inset 2px 0 0 var(--viewer-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gm-console-toggle {
|
.gm-launcher {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
z-index: 100;
|
z-index: 300;
|
||||||
padding: 7px 18px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-launcher-trigger {
|
||||||
|
padding: 7px 24px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(127, 214, 255, 0.1);
|
background: rgba(127, 214, 255, 0.1);
|
||||||
border: 1px solid rgba(127, 214, 255, 0.24);
|
border: 1px solid rgba(127, 214, 255, 0.24);
|
||||||
color: var(--viewer-accent);
|
color: var(--viewer-accent);
|
||||||
font-family: "IBM Plex Mono", monospace;
|
font-family: "IBM Plex Mono", monospace;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
letter-spacing: 0.12em;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 140ms ease, border-color 140ms ease;
|
transition: background 140ms ease, border-color 140ms ease;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gm-console-toggle:hover {
|
.gm-launcher-trigger:hover,
|
||||||
|
.gm-launcher-trigger--open {
|
||||||
background: rgba(127, 214, 255, 0.18);
|
background: rgba(127, 214, 255, 0.18);
|
||||||
border-color: rgba(127, 214, 255, 0.4);
|
border-color: rgba(127, 214, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gm-launcher-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 4px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
background: rgba(7, 14, 27, 0.88);
|
||||||
|
border: 1px solid rgba(132, 196, 255, 0.18);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-launcher-item {
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-launcher-item:hover {
|
||||||
|
background: rgba(127, 214, 255, 0.08);
|
||||||
|
color: var(--viewer-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-launcher-item--active {
|
||||||
|
background: rgba(127, 214, 255, 0.12);
|
||||||
|
border-color: rgba(127, 214, 255, 0.22);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-trigger {
|
||||||
|
cursor: default;
|
||||||
|
border-bottom: 1px dashed rgba(127, 214, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
background: rgba(7, 14, 27, 0.95);
|
||||||
|
border: 1px solid rgba(132, 196, 255, 0.22);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 6px 0;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-tooltip-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-tooltip-table th {
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
padding: 2px 12px 4px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.14);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-tooltip-table td {
|
||||||
|
padding: 3px 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-orders-tooltip-table tr:nth-child(even) td {
|
||||||
|
background: rgba(127, 214, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GM Telemetry Window ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.gm-telemetry {
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-error {
|
||||||
|
background: rgba(255, 80, 60, 0.12);
|
||||||
|
border: 1px solid rgba(255, 80, 60, 0.22);
|
||||||
|
color: rgba(255, 160, 140, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-section-title {
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.12);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-label {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-value {
|
||||||
|
color: var(--viewer-text);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-dim {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-bar-track {
|
||||||
|
display: block;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(127, 214, 255, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-bar {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 600ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-bar--low {
|
||||||
|
background: rgba(127, 214, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-bar--mid {
|
||||||
|
background: rgba(255, 191, 105, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-bar--high {
|
||||||
|
background: rgba(255, 80, 60, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-clients-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(127, 214, 255, 0.2);
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-telemetry-clients-dot--active {
|
||||||
|
background: rgba(100, 220, 130, 0.8);
|
||||||
|
box-shadow: 0 0 4px rgba(100, 220, 130, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── GM Settings Window ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.gm-settings {
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-body {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(127, 214, 255, 0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-section-title {
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.12);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px 6px 0;
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.06);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-field-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid rgba(132, 196, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-input {
|
||||||
|
background: rgba(127, 214, 255, 0.05);
|
||||||
|
border: 1px solid rgba(132, 196, 255, 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--viewer-text);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
outline: none;
|
||||||
|
padding: 3px 8px;
|
||||||
|
width: 120px;
|
||||||
|
transition: border-color 120ms ease, background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-input:focus {
|
||||||
|
border-color: rgba(127, 214, 255, 0.4);
|
||||||
|
background: rgba(127, 214, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-desc {
|
||||||
|
color: var(--viewer-muted);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-footer {
|
||||||
|
border-top: 1px solid rgba(132, 196, 255, 0.1);
|
||||||
|
background: rgba(127, 214, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-error {
|
||||||
|
background: rgba(255, 80, 60, 0.12);
|
||||||
|
border: 1px solid rgba(255, 80, 60, 0.22);
|
||||||
|
color: rgba(255, 160, 140, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-error-inline {
|
||||||
|
color: rgba(255, 140, 120, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-saved {
|
||||||
|
color: rgba(100, 220, 130, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-save-btn {
|
||||||
|
padding: 5px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(127, 214, 255, 0.12);
|
||||||
|
border: 1px solid rgba(127, 214, 255, 0.28);
|
||||||
|
color: var(--viewer-accent);
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-save-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(127, 214, 255, 0.2);
|
||||||
|
border-color: rgba(127, 214, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gm-settings-save-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,22 +2,26 @@ import { defineStore } from "pinia";
|
|||||||
import type { ShipSnapshot } from "../../contractsShips";
|
import type { ShipSnapshot } from "../../contractsShips";
|
||||||
import type { StationSnapshot } from "../../contractsInfrastructure";
|
import type { StationSnapshot } from "../../contractsInfrastructure";
|
||||||
import type { FactionSnapshot } from "../../contractsFactions";
|
import type { FactionSnapshot } from "../../contractsFactions";
|
||||||
|
import type { MarketOrderSnapshot } from "../../contractsEconomy";
|
||||||
|
|
||||||
export const useGmStore = defineStore("gm", {
|
export const useGmStore = defineStore("gm", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
ships: [] as ShipSnapshot[],
|
ships: [] as ShipSnapshot[],
|
||||||
stations: [] as StationSnapshot[],
|
stations: [] as StationSnapshot[],
|
||||||
factions: [] as FactionSnapshot[],
|
factions: [] as FactionSnapshot[],
|
||||||
|
marketOrders: [] as MarketOrderSnapshot[],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
updateWorld(
|
updateWorld(
|
||||||
ships: ShipSnapshot[],
|
ships: ShipSnapshot[],
|
||||||
stations: StationSnapshot[],
|
stations: StationSnapshot[],
|
||||||
factions: FactionSnapshot[],
|
factions: FactionSnapshot[],
|
||||||
|
marketOrders: MarketOrderSnapshot[],
|
||||||
) {
|
) {
|
||||||
this.ships = ships;
|
this.ships = ships;
|
||||||
this.stations = stations;
|
this.stations = stations;
|
||||||
this.factions = factions;
|
this.factions = factions;
|
||||||
|
this.marketOrders = marketOrders;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,41 @@ import itemsData from "../../../shared/data/items.json";
|
|||||||
const moduleNameById = new Map<string, string>(
|
const moduleNameById = new Map<string, string>(
|
||||||
(modulesData as { id: string; name: string }[]).map((m) => [m.id, m.name]),
|
(modulesData as { id: string; name: string }[]).map((m) => [m.id, m.name]),
|
||||||
);
|
);
|
||||||
|
const moduleProductionById = new Map<string, {
|
||||||
|
cycleSeconds: number;
|
||||||
|
inputs: { itemId: string; amount: number }[];
|
||||||
|
outputs: { itemId: string; amount: number }[];
|
||||||
|
}>(
|
||||||
|
(modulesData as { id: string; product?: string[] }[])
|
||||||
|
.flatMap((module) => {
|
||||||
|
const productItemId = module.product?.[0];
|
||||||
|
if (!productItemId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = (itemsData as {
|
||||||
|
id: string;
|
||||||
|
production?: { time: number; amount: number; method?: string; wares?: { ware?: string; itemId?: string; amount: number }[] }[];
|
||||||
|
}[]).find((candidate) => candidate.id === productItemId);
|
||||||
|
const production = item?.production?.find((recipe) => (recipe.method ?? "default") === "default")
|
||||||
|
?? item?.production?.[0];
|
||||||
|
if (!production) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[module.id, {
|
||||||
|
cycleSeconds: production.time,
|
||||||
|
inputs: (production.wares ?? []).map((ware) => ({
|
||||||
|
itemId: ware.ware ?? ware.itemId ?? "unknown",
|
||||||
|
amount: ware.amount,
|
||||||
|
})),
|
||||||
|
outputs: [{
|
||||||
|
itemId: productItemId,
|
||||||
|
amount: production.amount,
|
||||||
|
}],
|
||||||
|
}] as const];
|
||||||
|
}),
|
||||||
|
);
|
||||||
const itemTransportById = new Map<string, string>(
|
const itemTransportById = new Map<string, string>(
|
||||||
(itemsData as { id: string; transport: string }[]).map((item) => [item.id, item.transport]),
|
(itemsData as { id: string; transport: string }[]).map((item) => [item.id, item.transport]),
|
||||||
);
|
);
|
||||||
@@ -62,6 +97,23 @@ function renderProgressBar(progress: number): string {
|
|||||||
return `<div class="detail-progress"><div class="detail-progress-track"><div class="detail-progress-fill" style="width: ${(progress * 100).toFixed(1)}%"></div></div></div>`;
|
return `<div class="detail-progress"><div class="detail-progress-track"><div class="detail-progress-fill" style="width: ${(progress * 100).toFixed(1)}%"></div></div></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildStaticModuleTooltip(world: WorldState, stationId: string, moduleId: string): string | null {
|
||||||
|
const production = moduleProductionById.get(moduleId);
|
||||||
|
if (!production) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stationInventory = world.stations.get(stationId)?.inventory ?? [];
|
||||||
|
const inputLines = production.inputs
|
||||||
|
.map((entry) => ` ${entry.itemId}: ${entry.amount.toFixed(0)} required / ${inventoryAmount(stationInventory, entry.itemId).toFixed(0)} available`)
|
||||||
|
.join("\n");
|
||||||
|
const outputLines = production.outputs
|
||||||
|
.map((entry) => ` ${entry.itemId}: ${entry.amount.toFixed(0)}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return `Cycle: ${formatDuration(production.cycleSeconds)}\nInputs:\n${inputLines || " none"}\nOutputs:\n${outputLines || " none"}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatModuleListWithConstruction(
|
function formatModuleListWithConstruction(
|
||||||
world: WorldState,
|
world: WorldState,
|
||||||
stationId: string,
|
stationId: string,
|
||||||
@@ -91,7 +143,10 @@ function formatModuleListWithConstruction(
|
|||||||
renderedProcessCount.set(moduleId, processIndex + 1);
|
renderedProcessCount.set(moduleId, processIndex + 1);
|
||||||
const moduleName = moduleNameById.get(moduleId) ?? moduleId;
|
const moduleName = moduleNameById.get(moduleId) ?? moduleId;
|
||||||
if (!process) {
|
if (!process) {
|
||||||
return moduleName;
|
const tooltip = buildStaticModuleTooltip(world, stationId, moduleId);
|
||||||
|
return tooltip
|
||||||
|
? `<div class="detail-progress-label" title="${escapeAttr(tooltip)}"><span>${moduleName}</span><span>idle</span></div>`
|
||||||
|
: `<div class="detail-progress-label"><span>${moduleName}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputLines = process.inputs.map((e) => ` ${e.itemId}: ${e.amount.toFixed(0)}`).join("\n");
|
const inputLines = process.inputs.map((e) => ` ${e.itemId}: ${e.amount.toFixed(0)}`).join("\n");
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export class ViewerWorldLifecycle {
|
|||||||
[...world.ships.values()],
|
[...world.ships.values()],
|
||||||
[...world.stations.values()],
|
[...world.stations.values()],
|
||||||
[...world.factions.values()],
|
[...world.factions.values()],
|
||||||
|
[...world.marketOrders.values()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"simulationSpeedMultiplier": 1.5,
|
||||||
"yPlane": 4,
|
"yPlane": 4,
|
||||||
"arrivalThreshold": 16,
|
"arrivalThreshold": 16,
|
||||||
"miningRate": 10,
|
"miningRate": 10,
|
||||||
|
|||||||
@@ -15,6 +15,22 @@
|
|||||||
"planetIndex": 1,
|
"planetIndex": 1,
|
||||||
"lagrangeSide": -1
|
"lagrangeSide": -1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Refinery",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "refinery",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_refinedmetals_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Dominion Hullworks",
|
"label": "Dominion Hullworks",
|
||||||
"color": "#7ed4ff",
|
"color": "#7ed4ff",
|
||||||
@@ -27,8 +43,8 @@
|
|||||||
],
|
],
|
||||||
"systemId": "helios",
|
"systemId": "helios",
|
||||||
"factionId": "sol-dominion",
|
"factionId": "sol-dominion",
|
||||||
"planetIndex": 2,
|
"planetIndex": 0,
|
||||||
"lagrangeSide": -1
|
"lagrangeSide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Dominion Clay Grid",
|
"label": "Dominion Clay Grid",
|
||||||
@@ -42,7 +58,102 @@
|
|||||||
],
|
],
|
||||||
"systemId": "helios",
|
"systemId": "helios",
|
||||||
"factionId": "sol-dominion",
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Quantum Yard",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "quantumtubes",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_quantumtubes_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Graphene Array",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "graphene",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_graphene_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
"planetIndex": 0,
|
"planetIndex": 0,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Wafer Foundry",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "siliconwafers",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_siliconwafers_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Antimatter Forge",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "antimattercells",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_antimattercells_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Coolant Loop",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "superfluidcoolant",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_superfluidcoolant_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 3,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dominion Hydro Plant",
|
||||||
|
"color": "#7ed4ff",
|
||||||
|
"objective": "water",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_water_01"
|
||||||
|
],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion",
|
||||||
|
"planetIndex": 3,
|
||||||
"lagrangeSide": 1
|
"lagrangeSide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -60,6 +171,22 @@
|
|||||||
"planetIndex": 1,
|
"planetIndex": 1,
|
||||||
"lagrangeSide": 1
|
"lagrangeSide": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "League Refinery",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "refinery",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_refinedmetals_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "League Hullworks",
|
"label": "League Hullworks",
|
||||||
"color": "#ff8f70",
|
"color": "#ff8f70",
|
||||||
@@ -72,8 +199,8 @@
|
|||||||
],
|
],
|
||||||
"systemId": "sol",
|
"systemId": "sol",
|
||||||
"factionId": "asterion-league",
|
"factionId": "asterion-league",
|
||||||
"planetIndex": 2,
|
"planetIndex": 3,
|
||||||
"lagrangeSide": 1
|
"lagrangeSide": -1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "League Clay Grid",
|
"label": "League Clay Grid",
|
||||||
@@ -87,8 +214,259 @@
|
|||||||
],
|
],
|
||||||
"systemId": "sol",
|
"systemId": "sol",
|
||||||
"factionId": "asterion-league",
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Quantum Yard",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "quantumtubes",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_quantumtubes_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Graphene Array",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "graphene",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_graphene_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 0,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Wafer Foundry",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "siliconwafers",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_siliconwafers_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 0,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Antimatter Forge",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "antimattercells",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_antimattercells_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Coolant Loop",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "superfluidcoolant",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_superfluidcoolant_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "League Hydro Plant",
|
||||||
|
"color": "#ff8f70",
|
||||||
|
"objective": "water",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_water_01"
|
||||||
|
],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league",
|
||||||
|
"planetIndex": 3,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Power Relay",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "power",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Refinery",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "refinery",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_refinedmetals_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Hullworks",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "hullparts",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_hullparts_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 0,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Clay Grid",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "claytronics",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_claytronics_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Quantum Yard",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "quantumtubes",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_quantumtubes_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Graphene Array",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "graphene",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_graphene_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 0,
|
||||||
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Wafer Foundry",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "siliconwafers",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_siliconwafers_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 1,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Antimatter Forge",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "antimattercells",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_antimattercells_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 2,
|
||||||
|
"lagrangeSide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Coolant Loop",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "superfluidcoolant",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_liquid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_superfluidcoolant_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
"planetIndex": 3,
|
"planetIndex": 3,
|
||||||
"lagrangeSide": -1
|
"lagrangeSide": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Syndicate Hydro Plant",
|
||||||
|
"color": "#91e6a8",
|
||||||
|
"objective": "water",
|
||||||
|
"startingModules": [
|
||||||
|
"module_arg_dock_m_01_lowtech",
|
||||||
|
"module_arg_stor_solid_m_01",
|
||||||
|
"module_arg_stor_container_m_01",
|
||||||
|
"module_gen_prod_energycells_01",
|
||||||
|
"module_gen_prod_water_01"
|
||||||
|
],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate",
|
||||||
|
"planetIndex": 3,
|
||||||
|
"lagrangeSide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"shipFormations": [
|
"shipFormations": [
|
||||||
@@ -101,7 +479,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shipId": "miner",
|
"shipId": "miner",
|
||||||
"count": 1,
|
"count": 4,
|
||||||
"center": [ 54, 0, 18 ],
|
"center": [ 54, 0, 18 ],
|
||||||
"systemId": "helios",
|
"systemId": "helios",
|
||||||
"factionId": "sol-dominion"
|
"factionId": "sol-dominion"
|
||||||
@@ -113,6 +491,13 @@
|
|||||||
"systemId": "helios",
|
"systemId": "helios",
|
||||||
"factionId": "sol-dominion"
|
"factionId": "sol-dominion"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"shipId": "gas-miner",
|
||||||
|
"count": 4,
|
||||||
|
"center": [ 74, 0, 14 ],
|
||||||
|
"systemId": "helios",
|
||||||
|
"factionId": "sol-dominion"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"shipId": "constructor",
|
"shipId": "constructor",
|
||||||
"count": 1,
|
"count": 1,
|
||||||
@@ -122,7 +507,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"shipId": "miner",
|
"shipId": "miner",
|
||||||
"count": 1,
|
"count": 4,
|
||||||
"center": [ 56, 0, -12 ],
|
"center": [ 56, 0, -12 ],
|
||||||
"systemId": "sol",
|
"systemId": "sol",
|
||||||
"factionId": "asterion-league"
|
"factionId": "asterion-league"
|
||||||
@@ -133,6 +518,41 @@
|
|||||||
"center": [ 68, 0, -18 ],
|
"center": [ 68, 0, -18 ],
|
||||||
"systemId": "sol",
|
"systemId": "sol",
|
||||||
"factionId": "asterion-league"
|
"factionId": "asterion-league"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "gas-miner",
|
||||||
|
"count": 4,
|
||||||
|
"center": [ 76, 0, -22 ],
|
||||||
|
"systemId": "sol",
|
||||||
|
"factionId": "asterion-league"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "constructor",
|
||||||
|
"count": 1,
|
||||||
|
"center": [ 44, 0, 20 ],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "miner",
|
||||||
|
"count": 4,
|
||||||
|
"center": [ 58, 0, 24 ],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "hauler",
|
||||||
|
"count": 1,
|
||||||
|
"center": [ 68, 0, 18 ],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"shipId": "gas-miner",
|
||||||
|
"count": 4,
|
||||||
|
"center": [ 78, 0, 22 ],
|
||||||
|
"systemId": "perseus",
|
||||||
|
"factionId": "nadir-syndicate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"patrolRoutes": [],
|
"patrolRoutes": [],
|
||||||
|
|||||||
@@ -439,5 +439,71 @@
|
|||||||
"maxEfficiency": 1,
|
"maxEfficiency": 1,
|
||||||
"priority": 8
|
"priority": 8
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gas-miner",
|
||||||
|
"label": "Prospector Gas Miner",
|
||||||
|
"kind": "mining",
|
||||||
|
"class": "industrial",
|
||||||
|
"speed": 75000,
|
||||||
|
"warpSpeed": 0.15,
|
||||||
|
"ftlSpeed": 0.5,
|
||||||
|
"spoolTime": 3.1,
|
||||||
|
"cargoCapacity": 120,
|
||||||
|
"cargoKind": "liquid",
|
||||||
|
"color": "#84e7ff",
|
||||||
|
"hullColor": "#2b5868",
|
||||||
|
"size": 6,
|
||||||
|
"maxHealth": 150,
|
||||||
|
"capabilities": [
|
||||||
|
"warp",
|
||||||
|
"ftl",
|
||||||
|
"mining"
|
||||||
|
],
|
||||||
|
"construction": {
|
||||||
|
"recipeId": "gas-miner-construction",
|
||||||
|
"facilityCategory": "station",
|
||||||
|
"requiredModules": [
|
||||||
|
"module_gen_build_l_01"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
{
|
||||||
|
"itemId": "hullparts",
|
||||||
|
"amount": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "advancedelectronics",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "antimatterconverters",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "shieldcomponents",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "engineparts",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "engineparts",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "turretcomponents",
|
||||||
|
"amount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "hullparts",
|
||||||
|
"amount": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cycleTime": 28,
|
||||||
|
"productsPerHour": 128.6,
|
||||||
|
"maxEfficiency": 1,
|
||||||
|
"priority": 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user