Compare commits

...

3 Commits

79 changed files with 2193 additions and 685 deletions

View File

@@ -1097,14 +1097,14 @@ internal sealed class CommanderPlanningService
{ {
theaters.Add(new FactionTheaterRuntime theaters.Add(new FactionTheaterRuntime
{ {
Id = $"theater-expansion-{expansionProject.SystemId}-{expansionProject.CelestialId}", Id = $"theater-expansion-{expansionProject.SystemId}-{expansionProject.AnchorId}",
Kind = "expansion-front", Kind = "expansion-front",
SystemId = expansionProject.SystemId, SystemId = expansionProject.SystemId,
Status = "active", Status = "active",
Priority = 65f + (economicAssessment.HasShipyard ? 0f : 15f), Priority = 65f + (economicAssessment.HasShipyard ? 0f : 15f),
SupplyRisk = ComputeSystemRisk(world, faction, expansionProject.SystemId), SupplyRisk = ComputeSystemRisk(world, faction, expansionProject.SystemId),
FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, expansionProject.SystemId), FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, expansionProject.SystemId),
AnchorEntityId = expansionProject.SiteId ?? expansionProject.CelestialId, AnchorEntityId = expansionProject.SiteId ?? expansionProject.AnchorId,
AnchorPosition = ResolveExpansionAnchor(world, expansionProject), AnchorPosition = ResolveExpansionAnchor(world, expansionProject),
UpdatedAtUtc = nowUtc, UpdatedAtUtc = nowUtc,
}); });
@@ -1272,7 +1272,7 @@ internal sealed class CommanderPlanningService
], ],
"expansion" => "expansion" =>
[ [
new FactionPlanStepRuntime { Id = $"{campaign.Id}-claim", Kind = "secure-claim", Status = "active", Summary = $"Secure {expansionProject?.CelestialId ?? campaign.TargetEntityId} for construction." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-claim", Kind = "secure-claim", Status = "active", Summary = $"Secure {expansionProject?.AnchorId ?? campaign.TargetEntityId} for construction." },
new FactionPlanStepRuntime { Id = $"{campaign.Id}-supply", Kind = "supply-site", Status = "planned", Summary = "Move construction materials to the site." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-supply", Kind = "supply-site", Status = "planned", Summary = "Move construction materials to the site." },
new FactionPlanStepRuntime { Id = $"{campaign.Id}-guard", Kind = "guard-site", Status = "planned", Summary = "Defend the expansion site until operational." }, new FactionPlanStepRuntime { Id = $"{campaign.Id}-guard", Kind = "guard-site", Status = "planned", Summary = "Defend the expansion site until operational." },
], ],
@@ -2725,7 +2725,7 @@ internal sealed class CommanderPlanningService
AreaSystemId = areaSystemId, AreaSystemId = areaSystemId,
TargetEntityId = objective.TargetEntityId, TargetEntityId = objective.TargetEntityId,
ItemId = objective.ItemId ?? fallback.ItemId, ItemId = objective.ItemId ?? fallback.ItemId,
PreferredNodeId = fallback.PreferredNodeId, PreferredAnchorId = fallback.PreferredAnchorId,
PreferredConstructionSiteId = objective.Kind is "construct-site" or "supply-site" ? objective.TargetEntityId : fallback.PreferredConstructionSiteId, PreferredConstructionSiteId = objective.Kind is "construct-site" or "supply-site" ? objective.TargetEntityId : fallback.PreferredConstructionSiteId,
PreferredModuleId = fallback.PreferredModuleId, PreferredModuleId = fallback.PreferredModuleId,
TargetPosition = objective.TargetPosition ?? fallback.TargetPosition, TargetPosition = objective.TargetPosition ?? fallback.TargetPosition,
@@ -2750,7 +2750,7 @@ internal sealed class CommanderPlanningService
target.AreaSystemId = source.AreaSystemId; target.AreaSystemId = source.AreaSystemId;
target.TargetEntityId = source.TargetEntityId; target.TargetEntityId = source.TargetEntityId;
target.ItemId = source.ItemId; target.ItemId = source.ItemId;
target.PreferredNodeId = source.PreferredNodeId; target.PreferredAnchorId = source.PreferredAnchorId;
target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId;
target.PreferredModuleId = source.PreferredModuleId; target.PreferredModuleId = source.PreferredModuleId;
target.TargetPosition = source.TargetPosition; target.TargetPosition = source.TargetPosition;
@@ -2771,7 +2771,7 @@ internal sealed class CommanderPlanningService
&& string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal)
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredAnchorId, right.PreferredAnchorId, StringComparison.Ordinal)
&& string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal)
&& Nullable.Equals(left.TargetPosition, right.TargetPosition) && Nullable.Equals(left.TargetPosition, right.TargetPosition)
@@ -2792,7 +2792,7 @@ internal sealed class CommanderPlanningService
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds) && left.WaitSeconds.Equals(right.WaitSeconds)
@@ -2863,9 +2863,10 @@ internal sealed class CommanderPlanningService
} }
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId); var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId);
if (site?.CelestialId is { } celestialId) if (site is not null)
{ {
return world.Celestials.FirstOrDefault(celestial => celestial.Id == celestialId)?.Position; return world.Anchors.FirstOrDefault(anchor => anchor.Id == site.AnchorId)?.Position
?? Vector3.Zero;
} }
return null; return null;
@@ -2919,7 +2920,7 @@ internal sealed class CommanderPlanningService
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds) && left.WaitSeconds.Equals(right.WaitSeconds)
@@ -3382,7 +3383,7 @@ internal sealed class CommanderPlanningService
{ {
"defense-front" => $"Defend {theater.SystemId} from hostile pressure.", "defense-front" => $"Defend {theater.SystemId} from hostile pressure.",
"offense-front" => $"Project force into {theater.SystemId}.", "offense-front" => $"Project force into {theater.SystemId}.",
"expansion-front" => $"Expand into {expansionProject?.CelestialId ?? theater.SystemId}.", "expansion-front" => $"Expand into {expansionProject?.AnchorId ?? theater.SystemId}.",
"economic-front" => $"Stabilize commodity shortages around {theater.AnchorEntityId ?? theater.SystemId}.", "economic-front" => $"Stabilize commodity shortages around {theater.AnchorEntityId ?? theater.SystemId}.",
_ => theater.Kind, _ => theater.Kind,
}; };
@@ -3424,13 +3425,13 @@ internal sealed class CommanderPlanningService
private static Vector3 ResolveExpansionAnchor(SimulationWorld world, IndustryExpansionProject project) private static Vector3 ResolveExpansionAnchor(SimulationWorld world, IndustryExpansionProject project)
{ {
if (project.SiteId is not null if (project.SiteId is not null
&& world.ConstructionSites.FirstOrDefault(site => site.Id == project.SiteId) is { } site && world.ConstructionSites.FirstOrDefault(site => site.Id == project.SiteId) is { } site)
&& world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId) is { } siteCelestial)
{ {
return siteCelestial.Position; return world.Anchors.FirstOrDefault(candidate => candidate.Id == site.AnchorId)?.Position
?? Vector3.Zero;
} }
return world.Celestials.FirstOrDefault(candidate => candidate.Id == project.CelestialId)?.Position return world.Anchors.FirstOrDefault(candidate => candidate.Id == project.AnchorId)?.Position
?? ResolveSystemAnchor(world, project.SystemId); ?? ResolveSystemAnchor(world, project.SystemId);
} }

View File

@@ -88,7 +88,7 @@ public sealed record TerritoryClaimSnapshot(
string? SourceClaimId, string? SourceClaimId,
string FactionId, string FactionId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string Status, string Status,
string ClaimKind, string ClaimKind,
float ClaimStrength, float ClaimStrength,

View File

@@ -126,7 +126,7 @@ public sealed class TerritoryClaimRuntime
public string? SourceClaimId { get; set; } public string? SourceClaimId { get; set; }
public required string FactionId { get; set; } public required string FactionId { get; set; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required string CelestialId { get; set; } public required string AnchorId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string ClaimKind { get; set; } = "infrastructure"; public string ClaimKind { get; set; } = "infrastructure";
public float ClaimStrength { get; set; } public float ClaimStrength { get; set; }

View File

@@ -161,7 +161,7 @@ internal sealed class GeopoliticalSimulationService
SourceClaimId = claim.Id, SourceClaimId = claim.Id,
FactionId = claim.FactionId, FactionId = claim.FactionId,
SystemId = claim.SystemId, SystemId = claim.SystemId,
CelestialId = claim.CelestialId, AnchorId = claim.AnchorId,
Status = claim.State, Status = claim.State,
ClaimKind = "infrastructure", ClaimKind = "infrastructure",
ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f, ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,

View File

@@ -21,13 +21,13 @@ internal static class FactionIndustryPlanner
return null; return null;
} }
var targetCelestial = SelectFoundationCelestial(world, factionId, bottleneckCommodity); var targetAnchor = SelectFoundationAnchor(world, factionId, bottleneckCommodity);
if (targetCelestial is null) if (targetAnchor is null)
{ {
return null; return null;
} }
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId); var supportStation = SelectSupportStation(world, factionId, moduleId, targetAnchor.SystemId);
if (supportStation is null) if (supportStation is null)
{ {
return null; return null;
@@ -36,8 +36,8 @@ internal static class FactionIndustryPlanner
return new IndustryExpansionProject( return new IndustryExpansionProject(
bottleneckCommodity, bottleneckCommodity,
moduleId, moduleId,
targetCelestial.SystemId, targetAnchor.SystemId,
targetCelestial.Id, targetAnchor.Id,
supportStation.Id); supportStation.Id);
} }
@@ -93,13 +93,13 @@ internal static class FactionIndustryPlanner
return null; return null;
} }
var targetCelestial = SelectLogisticsFoundationCelestial(world, factionId); var targetAnchor = SelectLogisticsFoundationAnchor(world, factionId);
if (targetCelestial is null) if (targetAnchor is null)
{ {
return null; return null;
} }
var supportStation = SelectSupportStation(world, factionId, shipyardModuleId, targetCelestial.SystemId); var supportStation = SelectSupportStation(world, factionId, shipyardModuleId, targetAnchor.SystemId);
if (supportStation is null) if (supportStation is null)
{ {
return null; return null;
@@ -108,8 +108,8 @@ internal static class FactionIndustryPlanner
return new IndustryExpansionProject( return new IndustryExpansionProject(
"shipyard", "shipyard",
shipyardModuleId, shipyardModuleId,
targetCelestial.SystemId, targetAnchor.SystemId,
targetCelestial.Id, targetAnchor.Id,
supportStation.Id); supportStation.Id);
} }
@@ -129,13 +129,13 @@ internal static class FactionIndustryPlanner
return null; return null;
} }
var bootstrapCelestial = SelectFoundationCelestial(world, factionId, bootstrapCommodity); var bootstrapAnchor = SelectFoundationAnchor(world, factionId, bootstrapCommodity);
if (bootstrapCelestial is null) if (bootstrapAnchor is null)
{ {
return null; return null;
} }
var bootstrapSupportStation = SelectSupportStation(world, factionId, bootstrapModuleId, bootstrapCelestial.SystemId); var bootstrapSupportStation = SelectSupportStation(world, factionId, bootstrapModuleId, bootstrapAnchor.SystemId);
if (bootstrapSupportStation is null) if (bootstrapSupportStation is null)
{ {
return null; return null;
@@ -144,8 +144,8 @@ internal static class FactionIndustryPlanner
return new IndustryExpansionProject( return new IndustryExpansionProject(
bootstrapCommodity, bootstrapCommodity,
bootstrapModuleId, bootstrapModuleId,
bootstrapCelestial.SystemId, bootstrapAnchor.SystemId,
bootstrapCelestial.Id, bootstrapAnchor.Id,
bootstrapSupportStation.Id); bootstrapSupportStation.Id);
} }
@@ -161,13 +161,13 @@ internal static class FactionIndustryPlanner
return null; return null;
} }
var targetCelestial = SelectFoundationCelestial(world, factionId, commodityId); var targetAnchor = SelectFoundationAnchor(world, factionId, commodityId);
if (targetCelestial is null) if (targetAnchor is null)
{ {
return null; return null;
} }
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId); var supportStation = SelectSupportStation(world, factionId, moduleId, targetAnchor.SystemId);
if (supportStation is null) if (supportStation is null)
{ {
return null; return null;
@@ -176,8 +176,8 @@ internal static class FactionIndustryPlanner
return new IndustryExpansionProject( return new IndustryExpansionProject(
commodityId, commodityId,
moduleId, moduleId,
targetCelestial.SystemId, targetAnchor.SystemId,
targetCelestial.Id, targetAnchor.Id,
supportStation.Id); supportStation.Id);
} }
@@ -207,7 +207,7 @@ internal static class FactionIndustryPlanner
site.TargetDefinitionId, site.TargetDefinitionId,
site.BlueprintId, site.BlueprintId,
site.SystemId, site.SystemId,
site.CelestialId, site.AnchorId,
supportStationId, supportStationId,
site.Id); site.Id);
} }
@@ -225,7 +225,7 @@ internal static class FactionIndustryPlanner
} }
var nowUtc = DateTimeOffset.UtcNow; var nowUtc = DateTimeOffset.UtcNow;
var claimId = $"claim-{factionId}-{project.CelestialId}"; var claimId = $"claim-{factionId}-{project.AnchorId}";
if (world.Claims.All(candidate => candidate.Id != claimId)) if (world.Claims.All(candidate => candidate.Id != claimId))
{ {
world.Claims.Add(new ClaimRuntime world.Claims.Add(new ClaimRuntime
@@ -233,7 +233,7 @@ internal static class FactionIndustryPlanner
Id = claimId, Id = claimId,
FactionId = factionId, FactionId = factionId,
SystemId = project.SystemId, SystemId = project.SystemId,
CelestialId = project.CelestialId, AnchorId = project.AnchorId,
PlacedAtUtc = nowUtc, PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8), ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating, State = ClaimStateKinds.Activating,
@@ -246,7 +246,7 @@ internal static class FactionIndustryPlanner
return; return;
} }
var siteId = $"site-{factionId}-{project.CelestialId}"; var siteId = $"site-{factionId}-{project.AnchorId}";
if (world.ConstructionSites.Any(candidate => candidate.Id == siteId)) if (world.ConstructionSites.Any(candidate => candidate.Id == siteId))
{ {
return; return;
@@ -257,7 +257,7 @@ internal static class FactionIndustryPlanner
Id = siteId, Id = siteId,
FactionId = factionId, FactionId = factionId,
SystemId = project.SystemId, SystemId = project.SystemId,
CelestialId = project.CelestialId, AnchorId = project.AnchorId,
TargetKind = "station-foundation", TargetKind = "station-foundation",
TargetDefinitionId = project.CommodityId, TargetDefinitionId = project.CommodityId,
BlueprintId = project.ModuleId, BlueprintId = project.ModuleId,
@@ -450,51 +450,51 @@ internal static class FactionIndustryPlanner
private static float GetTargetLevelSeconds(string itemId) => private static float GetTargetLevelSeconds(string itemId) =>
string.Equals(itemId, "water", StringComparison.Ordinal) ? WaterTargetLevelSeconds : CommodityTargetLevelSeconds; string.Equals(itemId, "water", StringComparison.Ordinal) ? WaterTargetLevelSeconds : CommodityTargetLevelSeconds;
private static CelestialRuntime? SelectFoundationCelestial(SimulationWorld world, string factionId, string commodityId) private static AnchorRuntime? SelectFoundationAnchor(SimulationWorld world, string factionId, string commodityId)
{ {
var resourceItems = ResolveRootResourceItems(world, commodityId); var resourceItems = ResolveRootResourceItems(world, commodityId);
return world.Celestials return world.Anchors
.Where(celestial => .Where(anchor =>
celestial.Kind == SpatialNodeKind.LagrangePoint anchor.Kind == SpatialNodeKind.LagrangePoint
&& celestial.OccupyingStructureId is null && anchor.OccupyingStructureId is null
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed) && world.Claims.All(claim => claim.AnchorId != anchor.Id || claim.State == ClaimStateKinds.Destroyed)
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId)) && IsExpansionSystemEligible(world, factionId, anchor.SystemId))
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems)) .OrderByDescending(anchor => ScoreAnchor(world, factionId, anchor, resourceItems))
.FirstOrDefault(); .FirstOrDefault();
} }
private static CelestialRuntime? SelectLogisticsFoundationCelestial(SimulationWorld world, string factionId) private static AnchorRuntime? SelectLogisticsFoundationAnchor(SimulationWorld world, string factionId)
{ {
return world.Celestials return world.Anchors
.Where(celestial => .Where(anchor =>
celestial.Kind == SpatialNodeKind.LagrangePoint anchor.Kind == SpatialNodeKind.LagrangePoint
&& celestial.OccupyingStructureId is null && anchor.OccupyingStructureId is null
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed) && world.Claims.All(claim => claim.AnchorId != anchor.Id || claim.State == ClaimStateKinds.Destroyed)
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId)) && IsExpansionSystemEligible(world, factionId, anchor.SystemId))
.OrderByDescending(celestial => world.Stations.Count(station => .OrderByDescending(anchor => world.Stations.Count(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal) string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal))) && string.Equals(station.SystemId, anchor.SystemId, StringComparison.Ordinal)))
.ThenByDescending(celestial => world.Stations .ThenByDescending(anchor => world.Stations
.Where(station => .Where(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal) string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)) && string.Equals(station.SystemId, anchor.SystemId, StringComparison.Ordinal))
.Sum(station => station.Inventory.Values.Sum())) .Sum(station => station.Inventory.Values.Sum()))
.FirstOrDefault(); .FirstOrDefault();
} }
private static float ScoreCelestial(SimulationWorld world, string factionId, CelestialRuntime celestial, IReadOnlyCollection<string> resourceItems) private static float ScoreAnchor(SimulationWorld world, string factionId, AnchorRuntime anchor, IReadOnlyCollection<string> resourceItems)
{ {
var resourceScore = world.Nodes var resourceScore = world.Nodes
.Where(node => node.SystemId == celestial.SystemId && resourceItems.Contains(node.ItemId, StringComparer.Ordinal)) .Where(node => node.SystemId == anchor.SystemId && resourceItems.Contains(node.ItemId, StringComparer.Ordinal))
.Sum(node => node.OreRemaining); .Sum(node => node.OreRemaining);
var factionPresence = world.Stations.Count(station => var factionPresence = world.Stations.Count(station =>
string.Equals(station.FactionId, factionId, StringComparison.Ordinal) string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)); && string.Equals(station.SystemId, anchor.SystemId, StringComparison.Ordinal));
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, celestial.SystemId); var controlState = GeopoliticalSimulationService.GetSystemControlState(world, anchor.SystemId);
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, celestial.SystemId); var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, anchor.SystemId);
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == celestial.SystemId); var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == anchor.SystemId);
var pressure = world.Geopolitics?.Territory.Pressures var pressure = world.Geopolitics?.Territory.Pressures
.Where(entry => entry.SystemId == celestial.SystemId && entry.FactionId == factionId) .Where(entry => entry.SystemId == anchor.SystemId && entry.FactionId == factionId)
.OrderByDescending(entry => entry.HostileInfluence) .OrderByDescending(entry => entry.HostileInfluence)
.ThenBy(entry => entry.Id, StringComparer.Ordinal) .ThenBy(entry => entry.Id, StringComparer.Ordinal)
.FirstOrDefault(); .FirstOrDefault();
@@ -515,7 +515,7 @@ internal static class FactionIndustryPlanner
}; };
var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f) var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f)
+ ((strategicProfile?.TerritorialPressure ?? 0f) * 9f) + ((strategicProfile?.TerritorialPressure ?? 0f) * 9f)
+ ((world.Geopolitics is null ? 0f : GeopoliticalSimulationService.GetSystemRouteRisk(world, celestial.SystemId, factionId)) * 250f); + ((world.Geopolitics is null ? 0f : GeopoliticalSimulationService.GetSystemRouteRisk(world, anchor.SystemId, factionId)) * 250f);
return resourceScore return resourceScore
+ (factionPresence * 5_000f) + (factionPresence * 5_000f)
+ controlBias + controlBias
@@ -585,6 +585,6 @@ internal sealed record IndustryExpansionProject(
string CommodityId, string CommodityId,
string ModuleId, string ModuleId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string SupportStationId, string SupportStationId,
string? SiteId = null); string? SiteId = null);

View File

@@ -17,16 +17,15 @@ public sealed class GetPlayerIdentitiesHandler(IAuthRepository authRepository, I
public override async Task HandleAsync(CancellationToken cancellationToken) public override async Task HandleAsync(CancellationToken cancellationToken)
{ {
var users = await authRepository.ListUsersAsync(cancellationToken); var users = await authRepository.ListUsersAsync(cancellationToken);
var playerFactionsById = playerStateStore.GetPlayerFactions() var playerFactionsByPlayerId = playerStateStore.GetPlayerFactionsByPlayerId();
.ToDictionary(player => player.Id, StringComparer.Ordinal);
var responses = new List<PlayerIdentitySummaryResponse>(users.Count + playerFactionsById.Count); var responses = new List<PlayerIdentitySummaryResponse>(users.Count + playerFactionsByPlayerId.Count);
var seenIds = new HashSet<string>(StringComparer.Ordinal); var seenIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var user in users) foreach (var user in users)
{ {
var userId = user.Id.ToString("N"); var userId = user.Id.ToString("N");
playerFactionsById.TryGetValue(userId, out var playerFaction); playerFactionsByPlayerId.TryGetValue(userId, out var playerFaction);
responses.Add(new PlayerIdentitySummaryResponse( responses.Add(new PlayerIdentitySummaryResponse(
userId, userId,
user.Email, user.Email,
@@ -38,19 +37,19 @@ public sealed class GetPlayerIdentitiesHandler(IAuthRepository authRepository, I
seenIds.Add(userId); seenIds.Add(userId);
} }
foreach (var playerFaction in playerStateStore.GetPlayerFactions()) foreach (var (playerId, playerFaction) in playerFactionsByPlayerId)
{ {
if (!seenIds.Add(playerFaction.Id)) if (!seenIds.Add(playerId))
{ {
continue; continue;
} }
responses.Add(new PlayerIdentitySummaryResponse( responses.Add(new PlayerIdentitySummaryResponse(
playerFaction.Id, playerId,
$"{playerFaction.Id}@unknown", $"{playerId}@unknown",
Array.Empty<string>(), Array.Empty<string>(),
true, true,
playerFaction.Id, playerId,
playerFaction.Label, playerFaction.Label,
playerFaction.SovereignFactionId)); playerFaction.SovereignFactionId));
} }

View File

@@ -194,7 +194,7 @@ public sealed record PlayerDirectiveSnapshot(
bool UseOrders, bool UseOrders,
string? StagingOrderKind, string? StagingOrderKind,
string? ItemId, string? ItemId,
string? PreferredNodeId, string? PreferredAnchorId,
string? PreferredConstructionSiteId, string? PreferredConstructionSiteId,
string? PreferredModuleId, string? PreferredModuleId,
int Priority, int Priority,

View File

@@ -45,7 +45,7 @@ public sealed record PlayerDirectiveCommandRequest(
string? SourceStationId, string? SourceStationId,
string? DestinationStationId, string? DestinationStationId,
string? ItemId, string? ItemId,
string? PreferredNodeId, string? PreferredAnchorId,
string? PreferredConstructionSiteId, string? PreferredConstructionSiteId,
string? PreferredModuleId, string? PreferredModuleId,
int Priority, int Priority,

View File

@@ -251,7 +251,7 @@ public sealed class PlayerDirectiveRuntime
public bool UseOrders { get; set; } public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; } public string? StagingOrderKind { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? PreferredNodeId { get; set; } public string? PreferredAnchorId { get; set; }
public string? PreferredConstructionSiteId { get; set; } public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; } public string? PreferredModuleId { get; set; }
public int Priority { get; set; } = 50; public int Priority { get; set; } = 50;

View File

@@ -5,5 +5,6 @@ public interface IPlayerStateStore
bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction); bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction);
PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func<PlayerFactionRuntime> factory); PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func<PlayerFactionRuntime> factory);
IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions(); IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions();
IReadOnlyDictionary<string, PlayerFactionRuntime> GetPlayerFactionsByPlayerId();
void Clear(); void Clear();
} }

View File

@@ -201,7 +201,7 @@ public sealed class PlayerFactionProjectionService
directive.UseOrders, directive.UseOrders,
directive.StagingOrderKind, directive.StagingOrderKind,
directive.ItemId, directive.ItemId,
directive.PreferredNodeId, directive.PreferredAnchorId,
directive.PreferredConstructionSiteId, directive.PreferredConstructionSiteId,
directive.PreferredModuleId, directive.PreferredModuleId,
directive.Priority, directive.Priority,
@@ -261,7 +261,7 @@ public sealed class PlayerFactionProjectionService
template.SourceStationId, template.SourceStationId,
template.DestinationStationId, template.DestinationStationId,
template.ItemId, template.ItemId,
template.NodeId, template.AnchorId,
template.ConstructionSiteId, template.ConstructionSiteId,
template.ModuleId, template.ModuleId,
template.WaitSeconds, template.WaitSeconds,

View File

@@ -329,7 +329,7 @@ internal sealed class PlayerFactionService
directive.SourceStationId = request.SourceStationId; directive.SourceStationId = request.SourceStationId;
directive.DestinationStationId = request.DestinationStationId; directive.DestinationStationId = request.DestinationStationId;
directive.ItemId = request.ItemId; directive.ItemId = request.ItemId;
directive.PreferredNodeId = request.PreferredNodeId; directive.PreferredAnchorId = request.PreferredAnchorId;
directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId; directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
directive.PreferredModuleId = request.PreferredModuleId; directive.PreferredModuleId = request.PreferredModuleId;
directive.Priority = request.Priority; directive.Priority = request.Priority;
@@ -355,7 +355,7 @@ internal sealed class PlayerFactionService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f),
@@ -501,7 +501,7 @@ internal sealed class PlayerFactionService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f),
@@ -692,7 +692,7 @@ internal sealed class PlayerFactionService
SourceStationId = request.SourceStationId, SourceStationId = request.SourceStationId,
DestinationStationId = request.DestinationStationId, DestinationStationId = request.DestinationStationId,
ItemId = request.ItemId, ItemId = request.ItemId,
NodeId = request.NodeId, AnchorId = request.AnchorId,
ConstructionSiteId = request.ConstructionSiteId, ConstructionSiteId = request.ConstructionSiteId,
ModuleId = request.ModuleId, ModuleId = request.ModuleId,
WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f), WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f),
@@ -805,7 +805,7 @@ internal sealed class PlayerFactionService
directive.SourceStationId = request.HomeStationId; directive.SourceStationId = request.HomeStationId;
directive.DestinationStationId = null; directive.DestinationStationId = null;
directive.ItemId = request.ItemId; directive.ItemId = request.ItemId;
directive.PreferredNodeId = request.PreferredNodeId; directive.PreferredAnchorId = request.PreferredAnchorId;
directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId; directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
directive.PreferredModuleId = request.PreferredModuleId; directive.PreferredModuleId = request.PreferredModuleId;
directive.Priority = 100; directive.Priority = 100;
@@ -831,7 +831,7 @@ internal sealed class PlayerFactionService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f),
@@ -1418,7 +1418,7 @@ internal sealed class PlayerFactionService
AreaSystemId = directive?.TargetSystemId ?? directive?.HomeSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId, AreaSystemId = directive?.TargetSystemId ?? directive?.HomeSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId,
TargetEntityId = directive?.TargetEntityId, TargetEntityId = directive?.TargetEntityId,
ItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.ItemId, ItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.ItemId,
PreferredNodeId = directive?.PreferredNodeId ?? ship.DefaultBehavior.PreferredNodeId, PreferredAnchorId = directive?.PreferredAnchorId ?? ship.DefaultBehavior.PreferredAnchorId,
PreferredConstructionSiteId = directive?.PreferredConstructionSiteId ?? ship.DefaultBehavior.PreferredConstructionSiteId, PreferredConstructionSiteId = directive?.PreferredConstructionSiteId ?? ship.DefaultBehavior.PreferredConstructionSiteId,
PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId, PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId,
TargetPosition = directive?.TargetPosition, TargetPosition = directive?.TargetPosition,
@@ -1461,7 +1461,7 @@ internal sealed class PlayerFactionService
SourceStationId = directive.SourceStationId ?? directive.HomeStationId, SourceStationId = directive.SourceStationId ?? directive.HomeStationId,
DestinationStationId = directive.DestinationStationId, DestinationStationId = directive.DestinationStationId,
ItemId = directive.ItemId, ItemId = directive.ItemId,
NodeId = directive.PreferredNodeId, AnchorId = directive.PreferredAnchorId,
ConstructionSiteId = directive.PreferredConstructionSiteId, ConstructionSiteId = directive.PreferredConstructionSiteId,
ModuleId = directive.PreferredModuleId, ModuleId = directive.PreferredModuleId,
WaitSeconds = directive.WaitSeconds, WaitSeconds = directive.WaitSeconds,
@@ -1525,7 +1525,7 @@ internal sealed class PlayerFactionService
target.AreaSystemId = source.AreaSystemId; target.AreaSystemId = source.AreaSystemId;
target.TargetEntityId = source.TargetEntityId; target.TargetEntityId = source.TargetEntityId;
target.ItemId = source.ItemId; target.ItemId = source.ItemId;
target.PreferredNodeId = source.PreferredNodeId; target.PreferredAnchorId = source.PreferredAnchorId;
target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId;
target.PreferredModuleId = source.PreferredModuleId; target.PreferredModuleId = source.PreferredModuleId;
target.TargetPosition = source.TargetPosition; target.TargetPosition = source.TargetPosition;
@@ -1546,7 +1546,7 @@ internal sealed class PlayerFactionService
&& string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal)
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredAnchorId, right.PreferredAnchorId, StringComparison.Ordinal)
&& string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal)
&& Nullable.Equals(left.TargetPosition, right.TargetPosition) && Nullable.Equals(left.TargetPosition, right.TargetPosition)
@@ -1567,7 +1567,7 @@ internal sealed class PlayerFactionService
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds) && left.WaitSeconds.Equals(right.WaitSeconds)
@@ -1589,7 +1589,7 @@ internal sealed class PlayerFactionService
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal)
&& string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds) && left.WaitSeconds.Equals(right.WaitSeconds)
@@ -1634,7 +1634,7 @@ internal sealed class PlayerFactionService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = template.WaitSeconds, WaitSeconds = template.WaitSeconds,

View File

@@ -22,5 +22,8 @@ public sealed class PlayerStateStore : IPlayerStateStore
public IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions() => public IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions() =>
_playerFactions.Values.ToList(); _playerFactions.Values.ToList();
public IReadOnlyDictionary<string, PlayerFactionRuntime> GetPlayerFactionsByPlayerId() =>
new Dictionary<string, PlayerFactionRuntime>(_playerFactions, StringComparer.Ordinal);
public void Clear() => _playerFactions.Clear(); public void Clear() => _playerFactions.Clear();
} }

View File

@@ -6,6 +6,7 @@ public enum SpatialNodeKind
Planet, Planet,
Moon, Moon,
LagrangePoint, LagrangePoint,
ResourceNode,
} }
public enum WorkStatus public enum WorkStatus
@@ -286,6 +287,7 @@ public static class SimulationEnumMappings
SpatialNodeKind.Planet => "planet", SpatialNodeKind.Planet => "planet",
SpatialNodeKind.Moon => "moon", SpatialNodeKind.Moon => "moon",
SpatialNodeKind.LagrangePoint => "lagrange-point", SpatialNodeKind.LagrangePoint => "lagrange-point",
SpatialNodeKind.ResourceNode => "resource-node",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
}; };

View File

@@ -288,7 +288,7 @@ public sealed partial class ShipAiService
TargetSystemId = opportunity.Node.SystemId, TargetSystemId = opportunity.Node.SystemId,
DestinationStationId = opportunity.DropOffStation.Id, DestinationStationId = opportunity.DropOffStation.Id,
ItemId = opportunity.Node.ItemId, ItemId = opportunity.Node.ItemId,
NodeId = opportunity.Node.Id, AnchorId = opportunity.Node.AnchorId,
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
}; };
@@ -509,7 +509,7 @@ public sealed partial class ShipAiService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = template.WaitSeconds, WaitSeconds = template.WaitSeconds,
@@ -561,7 +561,7 @@ public sealed partial class ShipAiService
}; };
} }
var node = SelectLocalMiningNode(world, ship, systemId, itemId); var node = SelectLocalMiningNode(world, ship, systemId, itemId, ship.DefaultBehavior.PreferredAnchorId);
if (node is null) if (node is null)
{ {
ship.LastAccessFailureReason = "no-mineable-node"; ship.LastAccessFailureReason = "no-mineable-node";
@@ -578,8 +578,9 @@ public sealed partial class ShipAiService
Priority = 0, Priority = 0,
InterruptCurrentPlan = false, InterruptCurrentPlan = false,
Label = $"Mine {itemId} in {systemId}", Label = $"Mine {itemId} in {systemId}",
TargetEntityId = node.Id,
TargetSystemId = node.SystemId, TargetSystemId = node.SystemId,
NodeId = node.Id, AnchorId = node.AnchorId,
ItemId = node.ItemId, ItemId = node.ItemId,
WaitSeconds = 0f, WaitSeconds = 0f,
Radius = 0f, Radius = 0f,
@@ -601,7 +602,7 @@ public sealed partial class ShipAiService
&& left.TargetPosition == right.TargetPosition && left.TargetPosition == right.TargetPosition
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.AnchorId, right.AnchorId, StringComparison.Ordinal)
&& left.WaitSeconds.Equals(right.WaitSeconds) && left.WaitSeconds.Equals(right.WaitSeconds)
&& left.Radius.Equals(right.Radius) && left.Radius.Equals(right.Radius)
&& left.MaxSystemRange == right.MaxSystemRange && left.MaxSystemRange == right.MaxSystemRange

View File

@@ -69,7 +69,7 @@ public sealed partial class ShipAiService
} }
var targetPosition = ResolveCurrentTargetPosition(world, subTask); var targetPosition = ResolveCurrentTargetPosition(world, subTask);
var targetCelestial = ResolveTravelTargetCelestial(world, subTask, targetPosition); var targetAnchor = ResolveTravelTargetAnchor(world, subTask, targetPosition);
ship.TargetPosition = targetPosition; ship.TargetPosition = targetPosition;
if (ship.SystemId != subTask.TargetSystemId) if (ship.SystemId != subTask.TargetSystemId)
@@ -81,32 +81,33 @@ public sealed partial class ShipAiService
return SubTaskOutcome.Failed; return SubTaskOutcome.Failed;
} }
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId); var destinationEntryAnchor = ResolveSystemEntryAnchor(world, subTask.TargetSystemId) ?? targetAnchor;
var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition; var destinationEntryPosition = destinationEntryAnchor?.Position ?? targetPosition;
return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition); return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryAnchor, completeOnArrival, targetPosition, targetAnchor);
} }
var currentCelestial = ResolveCurrentCelestial(world, ship); var currentAnchor = ResolveCurrentAnchor(world, ship);
if (targetCelestial is not null if (targetAnchor is not null
&& currentCelestial is not null && currentAnchor is not null
&& !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal)) && !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal))
{ {
if (!CanWarp(ship.Definition)) if (!CanWarp(ship.Definition))
{ {
return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, currentAnchor, targetAnchor, completeOnArrival);
} }
return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, currentAnchor, targetAnchor, completeOnArrival);
} }
if (targetCelestial is not null if (targetAnchor is not null
&& ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers && currentAnchor is not null
&& !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal)
&& CanWarp(ship.Definition)) && CanWarp(ship.Definition))
{ {
return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, currentAnchor, targetAnchor, completeOnArrival);
} }
return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, currentAnchor, targetAnchor, completeOnArrival);
} }
private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
@@ -157,7 +158,7 @@ public sealed partial class ShipAiService
private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds)
{ {
var node = ResolveNode(world, subTask.TargetEntityId ?? subTask.TargetNodeId); var node = ResolveNode(world, subTask.TargetResourceNodeId ?? subTask.TargetEntityId);
if (node is null || !CanExtractNode(ship, node, world)) if (node is null || !CanExtractNode(ship, node, world))
{ {
subTask.BlockingReason = "node-missing"; subTask.BlockingReason = "node-missing";
@@ -165,9 +166,28 @@ public sealed partial class ShipAiService
return SubTaskOutcome.Failed; return SubTaskOutcome.Failed;
} }
var targetPosition = subTask.TargetPosition ?? GetResourceHoldPosition(node.Position, ship.Id, 20f); var deposit = ResolveResourceDeposit(world, subTask.TargetResourceDepositId);
if (deposit is null || !string.Equals(deposit.NodeId, node.Id, StringComparison.Ordinal) || deposit.OreRemaining <= 0.01f)
{
deposit = SelectMiningDeposit(node, ship.Id);
subTask.TargetResourceDepositId = deposit?.Id;
}
if (deposit is null)
{
SyncNodeOreTotals(node);
return SubTaskOutcome.Completed;
}
var targetPosition = GetResourceHoldPosition(deposit.Position, ship.Id, 20f);
subTask.TargetPosition = targetPosition;
var approachThreshold = MathF.Max(subTask.Threshold, 8f);
var distanceToTarget = ship.Position.DistanceTo(targetPosition);
var distanceToDeposit = ship.Position.DistanceTo(deposit.Position);
var effectivelyAtDeposit = string.Equals(ship.SpatialState.CurrentAnchorId, node.AnchorId, StringComparison.Ordinal)
&& distanceToDeposit <= approachThreshold;
ship.TargetPosition = targetPosition; ship.TargetPosition = targetPosition;
if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f)) if (distanceToTarget > approachThreshold && !effectivelyAtDeposit)
{ {
ship.State = ShipState.MiningApproach; ship.State = ShipState.MiningApproach;
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
@@ -188,14 +208,15 @@ public sealed partial class ShipAiService
var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - cargoAmount); var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - cargoAmount);
var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity); var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity);
mined = MathF.Min(mined, node.OreRemaining); mined = MathF.Min(mined, deposit.OreRemaining);
if (mined <= 0.01f) if (mined <= 0.01f)
{ {
return SubTaskOutcome.Completed; return SubTaskOutcome.Completed;
} }
AddInventory(ship.Inventory, node.ItemId, mined); AddInventory(ship.Inventory, node.ItemId, mined);
node.OreRemaining = MathF.Max(0f, node.OreRemaining - mined); deposit.OreRemaining = MathF.Max(0f, deposit.OreRemaining - mined);
SyncNodeOreTotals(node);
if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f || node.OreRemaining <= 0.01f) if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f || node.OreRemaining <= 0.01f)
{ {
return SubTaskOutcome.Completed; return SubTaskOutcome.Completed;
@@ -605,15 +626,22 @@ public sealed partial class ShipAiService
float deltaSeconds, float deltaSeconds,
string targetSystemId, string targetSystemId,
Vector3 targetPosition, Vector3 targetPosition,
CelestialRuntime? targetCelestial, AnchorRuntime? currentAnchor,
AnchorRuntime? targetAnchor,
bool completeOnArrival) bool completeOnArrival)
{ {
var distance = ship.Position.DistanceTo(targetPosition); var distance = ship.Position.DistanceTo(targetPosition);
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationAnchorId = targetAnchor?.Id ?? currentAnchor?.Id;
subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f);
ship.SpatialState.SystemPosition = currentAnchor is null
? ship.Position
: new Vector3(
currentAnchor.Position.X + ship.Position.X,
currentAnchor.Position.Y + ship.Position.Y,
currentAnchor.Position.Z + ship.Position.Z);
if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold)) if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold))
{ {
@@ -621,13 +649,26 @@ public sealed partial class ShipAiService
ship.TargetPosition = targetPosition; ship.TargetPosition = targetPosition;
ship.SystemId = targetSystemId; ship.SystemId = targetSystemId;
ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.CurrentAnchorId = targetAnchor?.Id ?? currentAnchor?.Id;
ship.SpatialState.SystemPosition = targetAnchor is null
? targetPosition
: new Vector3(
targetAnchor.Position.X + targetPosition.X,
targetAnchor.Position.Y + targetPosition.Y,
targetAnchor.Position.Z + targetPosition.Z);
ship.State = ShipState.Arriving; ship.State = ShipState.Arriving;
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
} }
ship.State = ShipState.LocalFlight; ship.State = ShipState.LocalFlight;
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds);
ship.SpatialState.SystemPosition = currentAnchor is null
? ship.Position
: new Vector3(
currentAnchor.Position.X + ship.Position.X,
currentAnchor.Position.Y + ship.Position.Y,
currentAnchor.Position.Z + ship.Position.Z);
return SubTaskOutcome.Active; return SubTaskOutcome.Active;
} }
@@ -637,18 +678,24 @@ public sealed partial class ShipAiService
ShipSubTaskRuntime subTask, ShipSubTaskRuntime subTask,
float deltaSeconds, float deltaSeconds,
Vector3 targetPosition, Vector3 targetPosition,
CelestialRuntime targetCelestial, AnchorRuntime currentAnchor,
AnchorRuntime targetAnchor,
bool completeOnArrival) bool completeOnArrival)
{ {
var transit = ship.SpatialState.Transit; var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id) if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationAnchorId != targetAnchor.Id)
{ {
var originAnchorPosition = currentAnchor.Position;
var destinationAnchorPosition = targetAnchor.Position;
var initialSpoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
var initialTravelDuration = MathF.Max(0.1f, originAnchorPosition.DistanceTo(destinationAnchorPosition) / MathF.Max(GetWarpTravelSpeed(ship), 0.001f));
transit = new ShipTransitRuntime transit = new ShipTransitRuntime
{ {
Regime = MovementRegimeKind.Warp, Regime = MovementRegimeKind.Warp,
OriginNodeId = ship.SpatialState.CurrentCelestialId, OriginAnchorId = currentAnchor.Id,
DestinationNodeId = targetCelestial.Id, DestinationAnchorId = targetAnchor.Id,
StartedAtUtc = world.GeneratedAtUtc, StartedAtUtc = world.GeneratedAtUtc,
ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration),
}; };
ship.SpatialState.Transit = transit; ship.SpatialState.Transit = transit;
subTask.ElapsedSeconds = 0f; subTask.ElapsedSeconds = 0f;
@@ -656,33 +703,47 @@ public sealed partial class ShipAiService
ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.Warp; ship.SpatialState.MovementRegime = MovementRegimeKind.Warp;
ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.CurrentAnchorId = null;
ship.SpatialState.DestinationNodeId = targetCelestial.Id; ship.SpatialState.DestinationAnchorId = targetAnchor.Id;
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); var spoolDurationSeconds = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
if (ship.State != ShipState.Warping) var startedAtUtc = transit.StartedAtUtc ?? world.GeneratedAtUtc;
var arrivalDueAtUtc = transit.ArrivalDueAtUtc ?? world.GeneratedAtUtc;
var totalDuration = MathF.Max(0.1f, (float)(arrivalDueAtUtc - startedAtUtc).TotalSeconds);
var elapsedSeconds = Math.Clamp((float)(world.GeneratedAtUtc - startedAtUtc).TotalSeconds, 0f, totalDuration);
var originPosition = ResolveAnchorPosition(world, transit.OriginAnchorId, currentAnchor.Position);
var destinationPosition = ResolveAnchorPosition(world, transit.DestinationAnchorId, targetAnchor.Position);
if (elapsedSeconds < spoolDurationSeconds)
{ {
ship.State = ShipState.SpoolingWarp; ship.State = ShipState.SpoolingWarp;
if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration)) ship.Position = Vector3.Zero;
{ ship.TargetPosition = Vector3.Zero;
return SubTaskOutcome.Active; ship.SpatialState.SystemPosition = originPosition;
} transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f);
subTask.Progress = transit.Progress;
ship.State = ShipState.Warping; return SubTaskOutcome.Active;
} }
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null ship.State = ShipState.Warping;
? ship.Position.DistanceTo(targetPosition) var warpTravelDuration = MathF.Max(0.001f, totalDuration - spoolDurationSeconds);
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); var travelElapsed = Math.Clamp(elapsedSeconds - spoolDurationSeconds, 0f, warpTravelDuration);
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds); var travelProgress = Math.Clamp(travelElapsed / warpTravelDuration, 0f, 1f);
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); var travelDelta = destinationPosition.Subtract(originPosition);
ship.Position = Vector3.Zero;
ship.TargetPosition = Vector3.Zero;
ship.SpatialState.SystemPosition = new Vector3(
originPosition.X + (travelDelta.X * travelProgress),
originPosition.Y + (travelDelta.Y * travelProgress),
originPosition.Z + (travelDelta.Z * travelProgress));
transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f);
subTask.Progress = transit.Progress; subTask.Progress = transit.Progress;
if (ship.Position.DistanceTo(targetPosition) > 18f) if (elapsedSeconds < totalDuration - 0.001f)
{ {
return SubTaskOutcome.Active; return SubTaskOutcome.Active;
} }
return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetCelestial, completeOnArrival); return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetAnchor, completeOnArrival);
} }
private SubTaskOutcome UpdateFtlTransit( private SubTaskOutcome UpdateFtlTransit(
@@ -692,20 +753,24 @@ public sealed partial class ShipAiService
float deltaSeconds, float deltaSeconds,
string targetSystemId, string targetSystemId,
Vector3 entryPosition, Vector3 entryPosition,
CelestialRuntime? targetCelestial, AnchorRuntime? entryAnchor,
bool completeOnArrival, bool completeOnArrival,
Vector3 finalTargetPosition) Vector3 finalTargetPosition,
AnchorRuntime? finalTargetAnchor)
{ {
var destinationNodeId = targetCelestial?.Id; var destinationAnchorId = entryAnchor?.Id;
var transit = ship.SpatialState.Transit; var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId) if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationAnchorId != destinationAnchorId)
{ {
var initialTravelDuration = MathF.Max(0.1f, ResolveSystemGalaxyPosition(world, ship.SystemId).DistanceTo(ResolveSystemGalaxyPosition(world, targetSystemId)) / MathF.Max(ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation), 0.001f));
var initialSpoolDuration = MathF.Max(ship.Definition.SpoolTime, 0.1f);
transit = new ShipTransitRuntime transit = new ShipTransitRuntime
{ {
Regime = MovementRegimeKind.FtlTransit, Regime = MovementRegimeKind.FtlTransit,
OriginNodeId = ship.SpatialState.CurrentCelestialId, OriginAnchorId = ship.SpatialState.CurrentAnchorId,
DestinationNodeId = destinationNodeId, DestinationAnchorId = destinationAnchorId,
StartedAtUtc = world.GeneratedAtUtc, StartedAtUtc = world.GeneratedAtUtc,
ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration),
}; };
ship.SpatialState.Transit = transit; ship.SpatialState.Transit = transit;
subTask.ElapsedSeconds = 0f; subTask.ElapsedSeconds = 0f;
@@ -713,39 +778,32 @@ public sealed partial class ShipAiService
ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit; ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit;
ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.CurrentAnchorId = null;
ship.SpatialState.DestinationNodeId = destinationNodeId; ship.SpatialState.DestinationAnchorId = destinationAnchorId;
if (ship.State != ShipState.Ftl) var spoolDurationSeconds = MathF.Max(ship.Definition.SpoolTime, 0.1f);
{ var startedAtUtc = transit.StartedAtUtc ?? world.GeneratedAtUtc;
ship.State = ShipState.SpoolingFtl; var arrivalDueAtUtc = transit.ArrivalDueAtUtc ?? world.GeneratedAtUtc;
if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f))) var totalDuration = MathF.Max(0.1f, (float)(arrivalDueAtUtc - startedAtUtc).TotalSeconds);
{ var elapsedSeconds = Math.Clamp((float)(world.GeneratedAtUtc - startedAtUtc).TotalSeconds, 0f, totalDuration);
return SubTaskOutcome.Active; ship.State = elapsedSeconds < spoolDurationSeconds ? ShipState.SpoolingFtl : ShipState.Ftl;
} transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f);
ship.State = ShipState.Ftl;
}
var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId);
var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId);
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation)) * deltaSeconds / totalDistance));
subTask.Progress = transit.Progress; subTask.Progress = transit.Progress;
if (transit.Progress < 0.999f) if (elapsedSeconds < totalDuration - 0.001f)
{ {
return SubTaskOutcome.Active; return SubTaskOutcome.Active;
} }
ship.Position = entryPosition; ship.Position = Vector3.Zero;
ship.TargetPosition = finalTargetPosition; ship.TargetPosition = finalTargetPosition;
ship.SystemId = targetSystemId; ship.SystemId = targetSystemId;
ship.SpatialState.CurrentSystemId = targetSystemId; ship.SpatialState.CurrentSystemId = targetSystemId;
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.CurrentAnchorId = entryAnchor?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationAnchorId = finalTargetAnchor?.Id ?? entryAnchor?.Id;
ship.SpatialState.SystemPosition = entryPosition;
ship.State = ShipState.Arriving; ship.State = ShipState.Arriving;
// Cross-system travel is only complete once the ship finishes the // Cross-system travel is only complete once the ship finishes the
@@ -753,7 +811,7 @@ public sealed partial class ShipAiService
return SubTaskOutcome.Active; return SubTaskOutcome.Active;
} }
private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial, bool completeOnArrival) private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, AnchorRuntime? targetAnchor, bool completeOnArrival)
{ {
ship.Position = targetPosition; ship.Position = targetPosition;
ship.TargetPosition = targetPosition; ship.TargetPosition = targetPosition;
@@ -762,8 +820,14 @@ public sealed partial class ShipAiService
ship.SpatialState.Transit = null; ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; ship.SpatialState.CurrentAnchorId = targetAnchor?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id; ship.SpatialState.DestinationAnchorId = targetAnchor?.Id;
ship.SpatialState.SystemPosition = targetAnchor is null
? targetPosition
: new Vector3(
targetAnchor.Position.X + targetPosition.X,
targetAnchor.Position.Y + targetPosition.Y,
targetAnchor.Position.Z + targetPosition.Z);
ship.State = ShipState.Arriving; ship.State = ShipState.Arriving;
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
} }

View File

@@ -9,6 +9,11 @@ public sealed partial class ShipAiService
{ {
private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask) private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask)
{ {
if (!string.IsNullOrWhiteSpace(subTask.TargetAnchorId) && ResolveAnchor(world, subTask.TargetAnchorId) is not null)
{
return subTask.TargetPosition ?? Vector3.Zero;
}
if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId))
{ {
var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
@@ -44,15 +49,20 @@ public sealed partial class ShipAiService
if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId))
{ {
var station = ResolveStation(world, subTask.TargetEntityId); var station = ResolveStation(world, subTask.TargetEntityId);
if (station?.CelestialId is not null) if (station?.AnchorId is not null)
{ {
return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId); return ResolveAnchorBackedCelestial(world, station.AnchorId);
} }
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
if (site?.CelestialId is not null) if (site?.AnchorId is not null)
{ {
return world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); return ResolveAnchorBackedCelestial(world, site.AnchorId);
}
if (ResolveAnchor(world, subTask.TargetEntityId) is { } anchorBackedCelestialTarget)
{
return ResolveAnchorBackedCelestial(world, anchorBackedCelestialTarget.Id);
} }
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
@@ -76,25 +86,145 @@ public sealed partial class ShipAiService
.FirstOrDefault(); .FirstOrDefault();
} }
private static AnchorRuntime? ResolveTravelTargetAnchor(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition)
{
if (!string.IsNullOrWhiteSpace(subTask.TargetAnchorId) && ResolveAnchor(world, subTask.TargetAnchorId) is { } explicitTargetAnchor)
{
return explicitTargetAnchor;
}
if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId))
{
var station = ResolveStation(world, subTask.TargetEntityId);
if (station?.AnchorId is not null)
{
return ResolveAnchor(world, station.AnchorId);
}
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
if (site?.AnchorId is not null)
{
return ResolveAnchor(world, site.AnchorId);
}
var node = ResolveNode(world, subTask.TargetEntityId);
if (node is not null)
{
return ResolveAnchor(world, node.AnchorId);
}
if (ResolveAnchor(world, subTask.TargetEntityId) is { } directAnchor)
{
return directAnchor;
}
if (world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } celestial)
{
return ResolveAnchor(world, celestial.Id);
}
if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck)
{
return world.Anchors
.Where(candidate => candidate.SystemId == wreck.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position))
.FirstOrDefault();
}
}
return world.Anchors
.Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
.FirstOrDefault();
}
private static AnchorRuntime? ResolveCurrentAnchor(SimulationWorld world, ShipRuntime ship)
{
if (ship.SpatialState.CurrentAnchorId is not null && ResolveAnchor(world, ship.SpatialState.CurrentAnchorId) is { } explicitAnchor)
{
return explicitAnchor;
}
if (ship.DockedStationId is not null && ResolveStation(world, ship.DockedStationId)?.AnchorId is { } dockAnchorId)
{
return ResolveAnchor(world, dockAnchorId);
}
return world.Anchors
.Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ResolveShipSystemPosition(world, ship)))
.FirstOrDefault();
}
private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship) private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
{ {
if (ship.SpatialState.CurrentCelestialId is not null) if (ship.SpatialState.CurrentAnchorId is not null && ResolveAnchorBackedCelestial(world, ship.SpatialState.CurrentAnchorId) is { } currentAnchorCelestial)
{ {
return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId); return currentAnchorCelestial;
} }
return world.Celestials return world.Celestials
.Where(candidate => candidate.SystemId == ship.SystemId) .Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) .OrderBy(candidate => candidate.Position.DistanceTo(ResolveShipSystemPosition(world, ship)))
.FirstOrDefault(); .FirstOrDefault();
} }
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) => private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star); world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star);
private static AnchorRuntime? ResolveSystemEntryAnchor(SimulationWorld world, string systemId) =>
world.Anchors.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star);
private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) =>
world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero; world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero;
private static Vector3 ResolveAnchorPosition(SimulationWorld world, string? anchorId, Vector3 fallbackPosition) =>
ResolveAnchor(world, anchorId)?.Position ?? fallbackPosition;
private static Vector3 ResolveStationSystemPosition(SimulationWorld world, StationRuntime station)
{
if (station.AnchorId is not null && ResolveAnchor(world, station.AnchorId) is { } anchor)
{
return new Vector3(
anchor.Position.X + station.Position.X,
anchor.Position.Y + station.Position.Y,
anchor.Position.Z + station.Position.Z);
}
return station.Position;
}
private static Vector3 ResolveNodeSystemPosition(SimulationWorld world, ResourceNodeRuntime node)
{
if (ResolveAnchor(world, node.AnchorId) is { } anchor)
{
return new Vector3(
anchor.Position.X + node.Position.X,
anchor.Position.Y + node.Position.Y,
anchor.Position.Z + node.Position.Z);
}
return node.Position;
}
private static Vector3 ResolveShipSystemPosition(SimulationWorld world, ShipRuntime ship)
{
if (ship.SpatialState.SystemPosition is { } systemPosition)
{
return systemPosition;
}
if (ResolveCurrentAnchor(world, ship) is { } anchor)
{
return new Vector3(
anchor.Position.X + ship.Position.X,
anchor.Position.Y + ship.Position.Y,
anchor.Position.Z + ship.Position.Z);
}
return ship.Position;
}
private static float GetLocalTravelSpeed(ShipRuntime ship) => private static float GetLocalTravelSpeed(ShipRuntime ship) =>
SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation); SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation);
@@ -183,6 +313,7 @@ public sealed partial class ShipAiService
{ {
var policy = ResolvePolicy(world, ship.PolicySetId); var policy = ResolvePolicy(world, ship.PolicySetId);
var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId; var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId;
var preferredAnchorId = ship.DefaultBehavior.PreferredAnchorId;
var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange);
var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination);
string? deniedReason = null; string? deniedReason = null;
@@ -194,6 +325,11 @@ public sealed partial class ShipAiService
return false; return false;
} }
if (preferredAnchorId is not null && !string.Equals(node.AnchorId, preferredAnchorId, StringComparison.Ordinal))
{
return false;
}
if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason)) if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason))
{ {
deniedReason ??= reason; deniedReason ??= reason;
@@ -214,7 +350,7 @@ public sealed partial class ShipAiService
+ (effectiveMiningSkill * 10f) + (effectiveMiningSkill * 10f)
- distancePenalty - distancePenalty
- routeRiskPenalty - routeRiskPenalty
- node.Position.DistanceTo(ship.Position); - ResolveNodeSystemPosition(world, node).DistanceTo(ResolveShipSystemPosition(world, ship));
return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}"); return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}");
}) })
.OrderByDescending(candidate => candidate.Score) .OrderByDescending(candidate => candidate.Score)
@@ -452,7 +588,7 @@ public sealed partial class ShipAiService
?? homeStation; ?? homeStation;
} }
private static ResourceNodeRuntime? SelectLocalMiningNode(SimulationWorld world, ShipRuntime ship, string systemId, string itemId) private static ResourceNodeRuntime? SelectLocalMiningNode(SimulationWorld world, ShipRuntime ship, string systemId, string itemId, string? anchorId = null)
{ {
var policy = ResolvePolicy(world, ship.PolicySetId); var policy = ResolvePolicy(world, ship.PolicySetId);
string? deniedReason = null; string? deniedReason = null;
@@ -467,6 +603,11 @@ public sealed partial class ShipAiService
return false; return false;
} }
if (anchorId is not null && !string.Equals(candidate.AnchorId, anchorId, StringComparison.Ordinal))
{
return false;
}
if (!TryCheckSystemAllowed(world, policy, ship.FactionId, candidate.SystemId, "military", out var reason)) if (!TryCheckSystemAllowed(world, policy, ship.FactionId, candidate.SystemId, "military", out var reason))
{ {
deniedReason ??= reason; deniedReason ??= reason;
@@ -487,6 +628,54 @@ public sealed partial class ShipAiService
return node; return node;
} }
private static ResourceDepositRuntime? ResolveResourceDeposit(SimulationWorld world, string? depositId)
{
if (string.IsNullOrWhiteSpace(depositId))
{
return null;
}
foreach (var node in world.Nodes)
{
var deposit = node.Deposits.FirstOrDefault(candidate => string.Equals(candidate.Id, depositId, StringComparison.Ordinal));
if (deposit is not null)
{
return deposit;
}
}
return null;
}
private static ResourceDepositRuntime? SelectMiningDeposit(ResourceNodeRuntime node, string shipId)
{
return node.Deposits
.Where(candidate => candidate.OreRemaining > 0.01f)
.OrderByDescending(candidate => candidate.OreRemaining)
.ThenBy(candidate => candidate.Id, StringComparer.Ordinal)
.FirstOrDefault();
}
private static void SyncNodeOreTotals(ResourceNodeRuntime node)
{
node.OreRemaining = node.Deposits.Sum(candidate => candidate.OreRemaining);
}
private static AnchorRuntime? ResolveMiningAnchor(SimulationWorld world, string? anchorId, string? nodeId)
{
if (anchorId is not null)
{
return ResolveAnchor(world, anchorId);
}
if (nodeId is not null && ResolveNode(world, nodeId) is { } node)
{
return ResolveAnchor(world, node.AnchorId);
}
return null;
}
private static StationRuntime? SelectLocalAutoMineBuyer(SimulationWorld world, ShipRuntime ship, string systemId, string itemId) private static StationRuntime? SelectLocalAutoMineBuyer(SimulationWorld world, ShipRuntime ship, string systemId, string itemId)
{ {
var policy = ResolvePolicy(world, ship.PolicySetId); var policy = ResolvePolicy(world, ship.PolicySetId);
@@ -686,9 +875,14 @@ public sealed partial class ShipAiService
return (celestial.SystemId, celestial.Position); return (celestial.SystemId, celestial.Position);
} }
if (ResolveAnchor(world, entityId) is { } anchor)
{
return (anchor.SystemId, anchor.Position);
}
if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site) if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site)
{ {
var position = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? Vector3.Zero; var position = ResolveAnchor(world, site.AnchorId)?.Position ?? Vector3.Zero;
return (site.SystemId, position); return (site.SystemId, position);
} }
@@ -720,6 +914,16 @@ public sealed partial class ShipAiService
private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) => private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) =>
stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId); stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId);
private static AnchorRuntime? ResolveAnchor(SimulationWorld world, string? anchorId) =>
anchorId is null ? null : world.Anchors.FirstOrDefault(candidate => candidate.Id == anchorId);
private static CelestialRuntime? ResolveAnchorBackedCelestial(SimulationWorld world, string? anchorId)
{
var anchor = ResolveAnchor(world, anchorId);
var celestialId = SpatialBuilder.ResolveCompatibleCelestialId(anchor);
return celestialId is null ? null : world.Celestials.FirstOrDefault(candidate => candidate.Id == celestialId);
}
private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) => private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) =>
nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId); nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId);
@@ -815,7 +1019,8 @@ public sealed partial class ShipAiService
if (site?.StationId is null && site is not null) if (site?.StationId is null && site is not null)
{ {
var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position; var anchorPosition = ResolveAnchor(world, site.AnchorId)?.Position
?? station.Position;
return GetResourceHoldPosition(anchorPosition, ship.Id, 78f); return GetResourceHoldPosition(anchorPosition, ship.Id, 78f);
} }
@@ -867,7 +1072,7 @@ public sealed partial class ShipAiService
private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site) private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site)
{ {
var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); var anchor = ResolveAnchor(world, site.AnchorId);
if (anchor is null || site.BlueprintId is null) if (anchor is null || site.BlueprintId is null)
{ {
site.State = ConstructionSiteStateKinds.Destroyed; site.State = ConstructionSiteStateKinds.Destroyed;
@@ -878,13 +1083,13 @@ public sealed partial class ShipAiService
{ {
Id = $"station-{world.Stations.Count + 1}", Id = $"station-{world.Stations.Count + 1}",
SystemId = site.SystemId, SystemId = site.SystemId,
AnchorId = site.AnchorId,
Label = BuildFoundedStationLabel(site.TargetDefinitionId), Label = BuildFoundedStationLabel(site.TargetDefinitionId),
Category = "station", Category = "station",
Objective = DetermineFoundationObjective(site.TargetDefinitionId), Objective = DetermineFoundationObjective(site.TargetDefinitionId),
Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color, Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color,
Position = anchor.Position, Position = Vector3.Zero,
FactionId = site.FactionId, FactionId = site.FactionId,
CelestialId = site.CelestialId,
Health = 600f, Health = 600f,
MaxHealth = 600f, MaxHealth = 600f,
}; };

View File

@@ -128,11 +128,11 @@ public sealed partial class ShipAiService
CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}", CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}",
[ [
CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f), CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f),
CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f)
]), ]),
CreateStep("step-construction-build", "build-site", $"Build {site.Id}", CreateStep("step-construction-build", "build-site", $"Build {site.Id}",
[ [
CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, supportStation.Position, site.Id, 12f, 0f)
]) ])
]); ]);
} }
@@ -301,7 +301,9 @@ public sealed partial class ShipAiService
float amount, float amount,
string? itemId = null, string? itemId = null,
string? moduleId = null, string? moduleId = null,
string? targetNodeId = null) => string? targetAnchorId = null,
string? targetResourceNodeId = null,
string? targetResourceDepositId = null) =>
new() new()
{ {
Id = id, Id = id,
@@ -310,7 +312,9 @@ public sealed partial class ShipAiService
TargetSystemId = targetSystemId, TargetSystemId = targetSystemId,
TargetPosition = targetPosition, TargetPosition = targetPosition,
TargetEntityId = targetEntityId, TargetEntityId = targetEntityId,
TargetNodeId = targetNodeId, TargetAnchorId = targetAnchorId,
TargetResourceNodeId = targetResourceNodeId,
TargetResourceDepositId = targetResourceDepositId,
ItemId = itemId, ItemId = itemId,
ModuleId = moduleId, ModuleId = moduleId,
Threshold = threshold, Threshold = threshold,

View File

@@ -171,7 +171,8 @@ public sealed partial class ShipAiService
return null; return null;
} }
var node = ResolveNode(world, order.NodeId); var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId);
var node = ResolveNode(world, order.TargetEntityId);
if (node is not null) if (node is not null)
{ {
if (!string.Equals(node.SystemId, systemId, StringComparison.Ordinal)) if (!string.Equals(node.SystemId, systemId, StringComparison.Ordinal))
@@ -188,7 +189,7 @@ public sealed partial class ShipAiService
} }
else else
{ {
node = SelectLocalMiningNode(world, ship, systemId, itemId); node = SelectLocalMiningNode(world, ship, systemId, itemId, anchor?.Id);
} }
if (node is null) if (node is null)
@@ -197,24 +198,30 @@ public sealed partial class ShipAiService
return null; return null;
} }
return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {itemId} in {systemId}"); return BuildLocalMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {itemId} in {systemId}");
} }
private ShipPlanRuntime? BuildMineLocalOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) private ShipPlanRuntime? BuildMineLocalOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
{ {
var node = ResolveNode(world, order.NodeId); var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId);
var node = ResolveNode(world, order.TargetEntityId)
?? SelectLocalMiningNode(world, ship, order.TargetSystemId ?? ship.SystemId, order.ItemId ?? ship.DefaultBehavior.ItemId ?? string.Empty, anchor?.Id);
if (node is null) if (node is null)
{ {
order.FailureReason = "mine-order-incomplete"; order.FailureReason = "mine-order-incomplete";
return null; return null;
} }
return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {node.ItemId}"); return BuildLocalMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {node.ItemId}");
} }
private ShipPlanRuntime? BuildMineAndDeliverRunOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) private ShipPlanRuntime? BuildMineAndDeliverRunOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
{ {
var node = ResolveNode(world, order.NodeId); var anchor = ResolveMiningAnchor(world, order.AnchorId, order.TargetEntityId);
var node = ResolveNode(world, order.TargetEntityId)
?? (string.IsNullOrWhiteSpace(order.ItemId)
? null
: SelectLocalMiningNode(world, ship, order.TargetSystemId ?? ship.SystemId, order.ItemId, anchor?.Id));
var buyer = ResolveStation(world, order.DestinationStationId); var buyer = ResolveStation(world, order.DestinationStationId);
if (node is null || buyer is null) if (node is null || buyer is null)
{ {
@@ -222,7 +229,7 @@ public sealed partial class ShipAiService
return null; return null;
} }
return BuildMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, buyer, order.Label ?? $"Mine {node.ItemId} for {buyer.Label}"); return BuildMiningPlan(world, ship, AiPlanSourceKind.Order, order.Id, node, buyer, order.Label ?? $"Mine {node.ItemId} for {buyer.Label}");
} }
private ShipPlanRuntime? BuildSellMinedCargoOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) private ShipPlanRuntime? BuildSellMinedCargoOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order)
@@ -396,9 +403,10 @@ public sealed partial class ShipAiService
return BuildFollowShipPlan(ship, AiPlanSourceKind.Order, order.Id, targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}"); return BuildFollowShipPlan(ship, AiPlanSourceKind.Order, order.Id, targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}");
} }
private ShipPlanRuntime BuildMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary) private ShipPlanRuntime BuildMiningPlan(SimulationWorld world, ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary)
{ {
var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); var deposit = SelectMiningDeposit(node, ship.Id);
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f);
return CreatePlan( return CreatePlan(
ship, ship,
sourceKind, sourceKind,
@@ -408,8 +416,8 @@ public sealed partial class ShipAiService
[ [
CreateStep("step-mine", "mine", $"Mine {node.ItemId}", CreateStep("step-mine", "mine", $"Mine {node.ItemId}",
[ [
CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} field", node.SystemId, Vector3.Zero, node.AnchorId, 8f, 0f, targetAnchorId: node.AnchorId),
CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity()) CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id)
]), ]),
CreateStep("step-deliver", "deliver", $"Deliver {node.ItemId} to {homeStation.Label}", CreateStep("step-deliver", "deliver", $"Deliver {node.ItemId} to {homeStation.Label}",
[ [
@@ -421,9 +429,10 @@ public sealed partial class ShipAiService
]); ]);
} }
private ShipPlanRuntime BuildLocalMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, string summary) private ShipPlanRuntime BuildLocalMiningPlan(SimulationWorld world, ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, string summary)
{ {
var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); var deposit = SelectMiningDeposit(node, ship.Id);
var extractionPosition = GetResourceHoldPosition(deposit?.Position ?? Vector3.Zero, ship.Id, 20f);
return CreatePlan( return CreatePlan(
ship, ship,
sourceKind, sourceKind,
@@ -433,8 +442,8 @@ public sealed partial class ShipAiService
[ [
CreateStep("step-mine", "mine", $"Mine {node.ItemId}", CreateStep("step-mine", "mine", $"Mine {node.ItemId}",
[ [
CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} field", node.SystemId, Vector3.Zero, node.AnchorId, 8f, 0f, targetAnchorId: node.AnchorId),
CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId) CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id)
]) ])
]); ]);
} }

View File

@@ -11,7 +11,7 @@ public sealed record ShipOrderCommandRequest(
string? SourceStationId, string? SourceStationId,
string? DestinationStationId, string? DestinationStationId,
string? ItemId, string? ItemId,
string? NodeId, string? AnchorId,
string? ConstructionSiteId, string? ConstructionSiteId,
string? ModuleId, string? ModuleId,
float? WaitSeconds, float? WaitSeconds,
@@ -28,7 +28,7 @@ public sealed record ShipOrderTemplateCommandRequest(
string? SourceStationId, string? SourceStationId,
string? DestinationStationId, string? DestinationStationId,
string? ItemId, string? ItemId,
string? NodeId, string? AnchorId,
string? ConstructionSiteId, string? ConstructionSiteId,
string? ModuleId, string? ModuleId,
float? WaitSeconds, float? WaitSeconds,
@@ -43,7 +43,7 @@ public sealed record ShipDefaultBehaviorCommandRequest(
string? AreaSystemId, string? AreaSystemId,
string? TargetEntityId, string? TargetEntityId,
string? ItemId, string? ItemId,
string? PreferredNodeId, string? PreferredAnchorId,
string? PreferredConstructionSiteId, string? PreferredConstructionSiteId,
string? PreferredModuleId, string? PreferredModuleId,
Vector3Dto? TargetPosition, Vector3Dto? TargetPosition,

View File

@@ -23,7 +23,7 @@ public sealed record ShipOrderSnapshot(
string? SourceStationId, string? SourceStationId,
string? DestinationStationId, string? DestinationStationId,
string? ItemId, string? ItemId,
string? NodeId, string? AnchorId,
string? ConstructionSiteId, string? ConstructionSiteId,
string? ModuleId, string? ModuleId,
float WaitSeconds, float WaitSeconds,
@@ -41,7 +41,7 @@ public sealed record ShipOrderTemplateSnapshot(
string? SourceStationId, string? SourceStationId,
string? DestinationStationId, string? DestinationStationId,
string? ItemId, string? ItemId,
string? NodeId, string? AnchorId,
string? ConstructionSiteId, string? ConstructionSiteId,
string? ModuleId, string? ModuleId,
float WaitSeconds, float WaitSeconds,
@@ -56,7 +56,7 @@ public sealed record DefaultBehaviorSnapshot(
string? AreaSystemId, string? AreaSystemId,
string? TargetEntityId, string? TargetEntityId,
string? ItemId, string? ItemId,
string? PreferredNodeId, string? PreferredAnchorId,
string? PreferredConstructionSiteId, string? PreferredConstructionSiteId,
string? PreferredModuleId, string? PreferredModuleId,
Vector3Dto? TargetPosition, Vector3Dto? TargetPosition,
@@ -95,7 +95,9 @@ public sealed record ShipSubTaskSnapshot(
string Summary, string Summary,
string? TargetEntityId, string? TargetEntityId,
string? TargetSystemId, string? TargetSystemId,
string? TargetNodeId, string? TargetAnchorId,
string? TargetResourceNodeId,
string? TargetResourceDepositId,
Vector3Dto? TargetPosition, Vector3Dto? TargetPosition,
string? ItemId, string? ItemId,
string? ModuleId, string? ModuleId,
@@ -135,6 +137,7 @@ public sealed record ShipSnapshot(
string Purpose, string Purpose,
string Type, string Type,
string SystemId, string SystemId,
string? AnchorId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
Vector3Dto LocalVelocity, Vector3Dto LocalVelocity,
Vector3Dto TargetLocalPosition, Vector3Dto TargetLocalPosition,
@@ -151,11 +154,11 @@ public sealed record ShipSnapshot(
string? ControlReason, string? ControlReason,
string? LastReplanReason, string? LastReplanReason,
string? LastAccessFailureReason, string? LastAccessFailureReason,
string? CelestialId,
string? DockedStationId, string? DockedStationId,
string? CommanderId, string? CommanderId,
string? PolicySetId, string? PolicySetId,
float CargoCapacity, float CargoCapacity,
IReadOnlyList<string> CargoTypes,
float TravelSpeed, float TravelSpeed,
string TravelSpeedUnit, string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,
@@ -170,6 +173,7 @@ public sealed record ShipDelta(
string Purpose, string Purpose,
string Type, string Type,
string SystemId, string SystemId,
string? AnchorId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
Vector3Dto LocalVelocity, Vector3Dto LocalVelocity,
Vector3Dto TargetLocalPosition, Vector3Dto TargetLocalPosition,
@@ -186,11 +190,11 @@ public sealed record ShipDelta(
string? ControlReason, string? ControlReason,
string? LastReplanReason, string? LastReplanReason,
string? LastAccessFailureReason, string? LastAccessFailureReason,
string? CelestialId,
string? DockedStationId, string? DockedStationId,
string? CommanderId, string? CommanderId,
string? PolicySetId, string? PolicySetId,
float CargoCapacity, float CargoCapacity,
IReadOnlyList<string> CargoTypes,
float TravelSpeed, float TravelSpeed,
string TravelSpeedUnit, string TravelSpeedUnit,
IReadOnlyList<InventoryEntry> Inventory, IReadOnlyList<InventoryEntry> Inventory,
@@ -202,17 +206,17 @@ public sealed record ShipDelta(
public sealed record ShipSpatialStateSnapshot( public sealed record ShipSpatialStateSnapshot(
string SpaceLayer, string SpaceLayer,
string CurrentSystemId, string CurrentSystemId,
string? CurrentCelestialId, string? CurrentAnchorId,
Vector3Dto? LocalPosition, Vector3Dto? LocalPosition,
Vector3Dto? SystemPosition, Vector3Dto? SystemPosition,
string MovementRegime, string MovementRegime,
string? DestinationNodeId, string? DestinationAnchorId,
ShipTransitSnapshot? Transit); ShipTransitSnapshot? Transit);
public sealed record ShipTransitSnapshot( public sealed record ShipTransitSnapshot(
string Regime, string Regime,
string? OriginNodeId, string? OriginAnchorId,
string? DestinationNodeId, string? DestinationAnchorId,
DateTimeOffset? StartedAtUtc, DateTimeOffset? StartedAtUtc,
DateTimeOffset? ArrivalDueAtUtc, DateTimeOffset? ArrivalDueAtUtc,
float Progress); float Progress);

View File

@@ -60,7 +60,7 @@ public sealed class ShipOrderRuntime
public string? SourceStationId { get; set; } public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; } public string? DestinationStationId { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? NodeId { get; set; } public string? AnchorId { get; set; }
public string? ConstructionSiteId { get; set; } public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public float WaitSeconds { get; set; } public float WaitSeconds { get; set; }
@@ -78,7 +78,7 @@ public sealed class DefaultBehaviorRuntime
public string? AreaSystemId { get; set; } public string? AreaSystemId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? PreferredNodeId { get; set; } public string? PreferredAnchorId { get; set; }
public string? PreferredConstructionSiteId { get; set; } public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; } public string? PreferredModuleId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
@@ -102,7 +102,7 @@ public sealed class ShipOrderTemplateRuntime
public string? SourceStationId { get; set; } public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; } public string? DestinationStationId { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? NodeId { get; set; } public string? AnchorId { get; set; }
public string? ConstructionSiteId { get; set; } public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public float WaitSeconds { get; set; } public float WaitSeconds { get; set; }
@@ -146,7 +146,9 @@ public sealed class ShipSubTaskRuntime
public WorkStatus Status { get; set; } = WorkStatus.Pending; public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetNodeId { get; set; } public string? TargetAnchorId { get; set; }
public string? TargetResourceNodeId { get; set; }
public string? TargetResourceDepositId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }

View File

@@ -104,12 +104,12 @@ internal sealed class SimulationEngine
CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f); CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
world.Stations.Remove(station); world.Stations.Remove(station);
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial) if (station.AnchorId is not null && world.Anchors.FirstOrDefault(candidate => candidate.Id == station.AnchorId) is { } anchor)
{ {
celestial.OccupyingStructureId = null; anchor.OccupyingStructureId = null;
} }
foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId)) foreach (var claim in world.Claims.Where(candidate => candidate.AnchorId == station.AnchorId))
{ {
claim.Health = 0f; claim.Health = 0f;
claim.State = ClaimStateKinds.Destroyed; claim.State = ClaimStateKinds.Destroyed;

View File

@@ -24,6 +24,7 @@ internal sealed class SimulationProjectionService
false, false,
events, events,
BuildCelestialDeltas(world), BuildCelestialDeltas(world),
BuildAnchorDeltas(world),
BuildNodeDeltas(world), BuildNodeDeltas(world),
BuildStationDeltas(world), BuildStationDeltas(world),
BuildClaimDeltas(world), BuildClaimDeltas(world),
@@ -87,26 +88,37 @@ internal sealed class SimulationProjectionService
c.Kind, c.Kind,
c.OrbitalAnchor, c.OrbitalAnchor,
c.LocalSpaceRadius, c.LocalSpaceRadius,
c.ParentNodeId, c.ParentAnchorId,
c.OccupyingStructureId, c.OccupyingStructureId,
c.OrbitReferenceId)).ToList(), c.OrbitReferenceId)).ToList(),
world.Anchors.Select(ToAnchorDelta).Select(anchor => new AnchorSnapshot(
anchor.Id,
anchor.SystemId,
anchor.Kind,
anchor.SystemPosition,
anchor.LocalSpaceRadius,
anchor.ParentAnchorId,
anchor.OccupyingStructureId,
anchor.OrbitReferenceId)).ToList(),
world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot( world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot(
node.Id, node.Id,
node.AnchorId,
node.SystemId, node.SystemId,
node.LocalPosition, node.LocalPosition,
node.CelestialId, node.LocalSpaceRadius,
node.SourceKind, node.SourceKind,
node.OreRemaining, node.OreRemaining,
node.MaxOre, node.MaxOre,
node.ItemId)).ToList(), node.ItemId,
node.Deposits)).ToList(),
world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot( world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot(
station.Id, station.Id,
station.Label, station.Label,
station.Category, station.Category,
station.Objective, station.Objective,
station.SystemId, station.SystemId,
station.AnchorId,
station.LocalPosition, station.LocalPosition,
station.CelestialId,
station.Color, station.Color,
station.DockedShips, station.DockedShips,
station.DockedShipIds, station.DockedShipIds,
@@ -127,7 +139,7 @@ internal sealed class SimulationProjectionService
claim.Id, claim.Id,
claim.FactionId, claim.FactionId,
claim.SystemId, claim.SystemId,
claim.CelestialId, claim.AnchorId,
claim.State, claim.State,
claim.Health, claim.Health,
claim.PlacedAtUtc, claim.PlacedAtUtc,
@@ -136,7 +148,7 @@ internal sealed class SimulationProjectionService
site.Id, site.Id,
site.FactionId, site.FactionId,
site.SystemId, site.SystemId,
site.CelestialId, site.AnchorId,
site.TargetKind, site.TargetKind,
site.TargetDefinitionId, site.TargetDefinitionId,
site.BlueprintId, site.BlueprintId,
@@ -180,6 +192,7 @@ internal sealed class SimulationProjectionService
ship.Purpose, ship.Purpose,
ship.Type, ship.Type,
ship.SystemId, ship.SystemId,
ship.AnchorId,
ship.LocalPosition, ship.LocalPosition,
ship.LocalVelocity, ship.LocalVelocity,
ship.TargetLocalPosition, ship.TargetLocalPosition,
@@ -196,11 +209,11 @@ internal sealed class SimulationProjectionService
ship.ControlReason, ship.ControlReason,
ship.LastReplanReason, ship.LastReplanReason,
ship.LastAccessFailureReason, ship.LastAccessFailureReason,
ship.CelestialId,
ship.DockedStationId, ship.DockedStationId,
ship.CommanderId, ship.CommanderId,
ship.PolicySetId, ship.PolicySetId,
ship.CargoCapacity, ship.CargoCapacity,
ship.CargoTypes,
ship.TravelSpeed, ship.TravelSpeed,
ship.TravelSpeedUnit, ship.TravelSpeedUnit,
ship.Inventory, ship.Inventory,
@@ -239,6 +252,11 @@ internal sealed class SimulationProjectionService
celestial.LastDeltaSignature = BuildCelestialSignature(celestial); celestial.LastDeltaSignature = BuildCelestialSignature(celestial);
} }
foreach (var anchor in world.Anchors)
{
anchor.LastDeltaSignature = BuildAnchorSignature(anchor);
}
foreach (var station in world.Stations) foreach (var station in world.Stations)
{ {
station.LastDeltaSignature = BuildStationSignature(world, station); station.LastDeltaSignature = BuildStationSignature(world, station);
@@ -298,6 +316,24 @@ internal sealed class SimulationProjectionService
return deltas; return deltas;
} }
private static IReadOnlyList<AnchorDelta> BuildAnchorDeltas(SimulationWorld world)
{
var deltas = new List<AnchorDelta>();
foreach (var anchor in world.Anchors)
{
var signature = BuildAnchorSignature(anchor);
if (signature == anchor.LastDeltaSignature)
{
continue;
}
anchor.LastDeltaSignature = signature;
deltas.Add(ToAnchorDelta(anchor));
}
return deltas;
}
private static IReadOnlyList<CelestialDelta> BuildCelestialDeltas(SimulationWorld world) private static IReadOnlyList<CelestialDelta> BuildCelestialDeltas(SimulationWorld world)
{ {
var deltas = new List<CelestialDelta>(); var deltas = new List<CelestialDelta>();
@@ -466,17 +502,30 @@ internal sealed class SimulationProjectionService
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal)); string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
private static string BuildNodeSignature(ResourceNodeRuntime node) => private static string BuildNodeSignature(ResourceNodeRuntime node) =>
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.CelestialId}|{node.OreRemaining:0.###}"; string.Join("|",
node.SystemId,
node.AnchorId,
$"{node.Position.X:0.###}",
$"{node.Position.Y:0.###}",
$"{node.Position.Z:0.###}",
$"{node.OreRemaining:0.###}",
string.Join(",",
node.Deposits
.OrderBy(deposit => deposit.Id, StringComparer.Ordinal)
.Select(deposit => $"{deposit.Id}:{deposit.Position.X:0.###}:{deposit.Position.Y:0.###}:{deposit.Position.Z:0.###}:{deposit.OreRemaining:0.###}")));
private static string BuildCelestialSignature(CelestialRuntime celestial) => private static string BuildCelestialSignature(CelestialRuntime celestial) =>
$"{celestial.SystemId}|{celestial.Kind.ToContractValue()}|{celestial.Position.X:0.###}|{celestial.Position.Y:0.###}|{celestial.Position.Z:0.###}|{celestial.LocalSpaceRadius:0.###}|{celestial.ParentNodeId}|{celestial.OccupyingStructureId}|{celestial.OrbitReferenceId}"; $"{celestial.SystemId}|{celestial.Kind.ToContractValue()}|{celestial.Position.X:0.###}|{celestial.Position.Y:0.###}|{celestial.Position.Z:0.###}|{celestial.LocalSpaceRadius:0.###}|{celestial.ParentAnchorId}|{celestial.OccupyingStructureId}|{celestial.OrbitReferenceId}";
private static string BuildAnchorSignature(AnchorRuntime anchor) =>
$"{anchor.SystemId}|{anchor.Kind.ToContractValue()}|{anchor.Position.X:0.###}|{anchor.Position.Y:0.###}|{anchor.Position.Z:0.###}|{anchor.LocalSpaceRadius:0.###}|{anchor.ParentAnchorId}|{anchor.OccupyingStructureId}|{anchor.OrbitReferenceId}|{anchor.SourceEntityKind}|{anchor.SourceEntityId}";
private static string BuildStationSignature(SimulationWorld world, StationRuntime station) private static string BuildStationSignature(SimulationWorld world, StationRuntime station)
{ {
var processes = ToStationActionProgressSnapshots(world, station); var processes = ToStationActionProgressSnapshots(world, station);
return string.Join("|", return string.Join("|",
station.SystemId, station.SystemId,
station.CelestialId ?? "none", station.AnchorId ?? "none",
station.CommanderId ?? "none", station.CommanderId ?? "none",
station.PolicySetId ?? "none", station.PolicySetId ?? "none",
BuildInventorySignature(station.Inventory), BuildInventorySignature(station.Inventory),
@@ -495,10 +544,10 @@ internal sealed class SimulationProjectionService
} }
private static string BuildClaimSignature(ClaimRuntime claim) => private static string BuildClaimSignature(ClaimRuntime claim) =>
$"{claim.FactionId}|{claim.SystemId}|{claim.CelestialId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}"; $"{claim.FactionId}|{claim.SystemId}|{claim.AnchorId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) => private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) =>
$"{site.FactionId}|{site.SystemId}|{site.CelestialId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}"; $"{site.FactionId}|{site.SystemId}|{site.AnchorId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildMarketOrderSignature(MarketOrderRuntime order) => private static string BuildMarketOrderSignature(MarketOrderRuntime order) =>
$"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}"; $"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}";
@@ -552,17 +601,17 @@ internal sealed class SimulationProjectionService
string.Join(",", string.Join(",",
ToActiveSubTaskSnapshots(ship).Select(subTask => ToActiveSubTaskSnapshots(ship).Select(subTask =>
$"{subTask.Id}:{subTask.Kind}:{subTask.Status}:{subTask.Progress:0.###}:{subTask.ElapsedSeconds:0.###}:{subTask.BlockingReason ?? "none"}")), $"{subTask.Id}:{subTask.Kind}:{subTask.Status}:{subTask.Progress:0.###}:{subTask.ElapsedSeconds:0.###}:{subTask.BlockingReason ?? "none"}")),
ship.SpatialState.CurrentCelestialId ?? "none", ship.SpatialState.CurrentAnchorId ?? "none",
ship.DockedStationId ?? "none", ship.DockedStationId ?? "none",
ship.CommanderId ?? "none", ship.CommanderId ?? "none",
ship.PolicySetId ?? "none", ship.PolicySetId ?? "none",
ship.SpatialState.SpaceLayer.ToContractValue(), ship.SpatialState.SpaceLayer.ToContractValue(),
ship.SpatialState.CurrentCelestialId ?? "none", ship.SpatialState.CurrentAnchorId ?? "none",
ship.SpatialState.MovementRegime.ToContractValue(), ship.SpatialState.MovementRegime.ToContractValue(),
ship.SpatialState.DestinationNodeId ?? "none", ship.SpatialState.DestinationAnchorId ?? "none",
ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none", ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none",
ship.SpatialState.Transit?.OriginNodeId ?? "none", ship.SpatialState.Transit?.OriginAnchorId ?? "none",
ship.SpatialState.Transit?.DestinationNodeId ?? "none", ship.SpatialState.Transit?.DestinationAnchorId ?? "none",
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0", ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"), GetShipCargoAmount(ship).ToString("0.###"),
ship.Skills.Navigation.ToString(CultureInfo.InvariantCulture), ship.Skills.Navigation.ToString(CultureInfo.InvariantCulture),
@@ -653,13 +702,33 @@ internal sealed class SimulationProjectionService
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new( private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
node.Id, node.Id,
node.AnchorId,
node.SystemId, node.SystemId,
ToDto(node.Position), ToDto(node.Position),
node.CelestialId, node.LocalSpaceRadius,
node.SourceKind, node.SourceKind,
node.OreRemaining, node.OreRemaining,
node.MaxOre, node.MaxOre,
node.ItemId); node.ItemId,
node.Deposits.Select(ToResourceDepositSnapshot).ToList());
private static ResourceDepositSnapshot ToResourceDepositSnapshot(ResourceDepositRuntime deposit) => new(
deposit.Id,
deposit.NodeId,
deposit.AnchorId,
ToDto(deposit.Position),
deposit.OreRemaining,
deposit.MaxOre);
private static AnchorDelta ToAnchorDelta(AnchorRuntime anchor) => new(
anchor.Id,
anchor.SystemId,
anchor.Kind.ToContractValue(),
ToDto(anchor.Position),
anchor.LocalSpaceRadius,
anchor.ParentAnchorId,
anchor.OccupyingStructureId,
anchor.OrbitReferenceId);
private static CelestialDelta ToCelestialDelta(CelestialRuntime celestial) => new( private static CelestialDelta ToCelestialDelta(CelestialRuntime celestial) => new(
celestial.Id, celestial.Id,
@@ -667,7 +736,7 @@ internal sealed class SimulationProjectionService
celestial.Kind.ToContractValue(), celestial.Kind.ToContractValue(),
ToDto(celestial.Position), ToDto(celestial.Position),
celestial.LocalSpaceRadius, celestial.LocalSpaceRadius,
celestial.ParentNodeId, celestial.ParentAnchorId,
celestial.OccupyingStructureId, celestial.OccupyingStructureId,
celestial.OrbitReferenceId); celestial.OrbitReferenceId);
@@ -677,8 +746,8 @@ internal sealed class SimulationProjectionService
station.Category, station.Category,
station.Objective, station.Objective,
station.SystemId, station.SystemId,
station.AnchorId,
ToDto(station.Position), ToDto(station.Position),
station.CelestialId,
station.Color, station.Color,
station.DockedShipIds.Count, station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
@@ -737,7 +806,7 @@ internal sealed class SimulationProjectionService
claim.Id, claim.Id,
claim.FactionId, claim.FactionId,
claim.SystemId, claim.SystemId,
claim.CelestialId, claim.AnchorId,
claim.State, claim.State,
claim.Health, claim.Health,
claim.PlacedAtUtc, claim.PlacedAtUtc,
@@ -747,7 +816,7 @@ internal sealed class SimulationProjectionService
site.Id, site.Id,
site.FactionId, site.FactionId,
site.SystemId, site.SystemId,
site.CelestialId, site.AnchorId,
site.TargetKind, site.TargetKind,
site.TargetDefinitionId, site.TargetDefinitionId,
site.BlueprintId, site.BlueprintId,
@@ -811,6 +880,7 @@ internal sealed class SimulationProjectionService
ship.Definition.Purpose.ToDataValue(), ship.Definition.Purpose.ToDataValue(),
ship.Definition.Type.ToDataValue(), ship.Definition.Type.ToDataValue(),
ship.SystemId, ship.SystemId,
ship.SpatialState.CurrentAnchorId,
ToDto(ship.Position), ToDto(ship.Position),
ToDto(ship.Velocity), ToDto(ship.Velocity),
ToDto(ship.TargetPosition), ToDto(ship.TargetPosition),
@@ -827,11 +897,16 @@ internal sealed class SimulationProjectionService
ship.ControlReason, ship.ControlReason,
ship.LastReplanReason, ship.LastReplanReason,
ship.LastAccessFailureReason, ship.LastAccessFailureReason,
ship.SpatialState.CurrentCelestialId,
ship.DockedStationId, ship.DockedStationId,
ship.CommanderId, ship.CommanderId,
ship.PolicySetId, ship.PolicySetId,
ship.Definition.GetTotalCargoCapacity(), ship.Definition.GetTotalCargoCapacity(),
ship.Definition.Cargo
.SelectMany(entry => entry.Types)
.Where(type => !string.IsNullOrWhiteSpace(type))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(type => type, StringComparer.OrdinalIgnoreCase)
.ToList(),
ToShipTravelSpeed(ship).Speed, ToShipTravelSpeed(ship).Speed,
ToShipTravelSpeed(ship).Unit, ToShipTravelSpeed(ship).Unit,
@@ -880,7 +955,7 @@ internal sealed class SimulationProjectionService
order.SourceStationId, order.SourceStationId,
order.DestinationStationId, order.DestinationStationId,
order.ItemId, order.ItemId,
order.NodeId, order.AnchorId,
order.ConstructionSiteId, order.ConstructionSiteId,
order.ModuleId, order.ModuleId,
order.WaitSeconds, order.WaitSeconds,
@@ -906,7 +981,7 @@ internal sealed class SimulationProjectionService
behavior.AreaSystemId, behavior.AreaSystemId,
behavior.TargetEntityId, behavior.TargetEntityId,
behavior.ItemId, behavior.ItemId,
behavior.PreferredNodeId, behavior.PreferredAnchorId,
behavior.PreferredConstructionSiteId, behavior.PreferredConstructionSiteId,
behavior.PreferredModuleId, behavior.PreferredModuleId,
behavior.TargetPosition is null ? null : ToDto(behavior.TargetPosition.Value), behavior.TargetPosition is null ? null : ToDto(behavior.TargetPosition.Value),
@@ -929,7 +1004,7 @@ internal sealed class SimulationProjectionService
template.SourceStationId, template.SourceStationId,
template.DestinationStationId, template.DestinationStationId,
template.ItemId, template.ItemId,
template.NodeId, template.AnchorId,
template.ConstructionSiteId, template.ConstructionSiteId,
template.ModuleId, template.ModuleId,
template.WaitSeconds, template.WaitSeconds,
@@ -1002,10 +1077,12 @@ internal sealed class SimulationProjectionService
subTask.Kind, subTask.Kind,
subTask.Status.ToContractValue(), subTask.Status.ToContractValue(),
subTask.Summary, subTask.Summary,
subTask.TargetEntityId, subTask.TargetEntityId,
subTask.TargetSystemId, subTask.TargetSystemId,
subTask.TargetNodeId, subTask.TargetAnchorId,
subTask.TargetPosition is null ? null : ToDto(subTask.TargetPosition.Value), subTask.TargetResourceNodeId,
subTask.TargetResourceDepositId,
subTask.TargetPosition is null ? null : ToDto(subTask.TargetPosition.Value),
subTask.ItemId, subTask.ItemId,
subTask.ModuleId, subTask.ModuleId,
subTask.Threshold, subTask.Threshold,
@@ -1408,7 +1485,7 @@ internal sealed class SimulationProjectionService
claim.SourceClaimId, claim.SourceClaimId,
claim.FactionId, claim.FactionId,
claim.SystemId, claim.SystemId,
claim.CelestialId, claim.AnchorId,
claim.Status, claim.Status,
claim.ClaimKind, claim.ClaimKind,
claim.ClaimStrength, claim.ClaimStrength,
@@ -1564,15 +1641,15 @@ internal sealed class SimulationProjectionService
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new( private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
state.SpaceLayer.ToContractValue(), state.SpaceLayer.ToContractValue(),
state.CurrentSystemId, state.CurrentSystemId,
state.CurrentCelestialId, state.CurrentAnchorId,
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value), state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value), state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
state.MovementRegime.ToContractValue(), state.MovementRegime.ToContractValue(),
state.DestinationNodeId, state.DestinationAnchorId,
state.Transit is null ? null : new ShipTransitSnapshot( state.Transit is null ? null : new ShipTransitSnapshot(
state.Transit.Regime.ToContractValue(), state.Transit.Regime.ToContractValue(),
state.Transit.OriginNodeId, state.Transit.OriginAnchorId,
state.Transit.DestinationNodeId, state.Transit.DestinationAnchorId,
state.Transit.StartedAtUtc, state.Transit.StartedAtUtc,
state.Transit.ArrivalDueAtUtc, state.Transit.ArrivalDueAtUtc,
state.Transit.Progress)); state.Transit.Progress));

View File

@@ -10,8 +10,8 @@ public sealed record StationSnapshot(
string Category, string Category,
string Objective, string Objective,
string SystemId, string SystemId,
string? AnchorId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
string? CelestialId,
string Color, string Color,
int DockedShips, int DockedShips,
IReadOnlyList<string> DockedShipIds, IReadOnlyList<string> DockedShipIds,
@@ -35,8 +35,8 @@ public sealed record StationDelta(
string Category, string Category,
string Objective, string Objective,
string SystemId, string SystemId,
string? AnchorId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
string? CelestialId,
string Color, string Color,
int DockedShips, int DockedShips,
IReadOnlyList<string> DockedShipIds, IReadOnlyList<string> DockedShipIds,
@@ -74,7 +74,7 @@ public sealed record ClaimSnapshot(
string Id, string Id,
string FactionId, string FactionId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string State, string State,
float Health, float Health,
DateTimeOffset PlacedAtUtc, DateTimeOffset PlacedAtUtc,
@@ -84,7 +84,7 @@ public sealed record ClaimDelta(
string Id, string Id,
string FactionId, string FactionId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string State, string State,
float Health, float Health,
DateTimeOffset PlacedAtUtc, DateTimeOffset PlacedAtUtc,
@@ -94,7 +94,7 @@ public sealed record ConstructionSiteSnapshot(
string Id, string Id,
string FactionId, string FactionId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string TargetKind, string TargetKind,
string TargetDefinitionId, string TargetDefinitionId,
string? BlueprintId, string? BlueprintId,
@@ -112,7 +112,7 @@ public sealed record ConstructionSiteDelta(
string Id, string Id,
string FactionId, string FactionId,
string SystemId, string SystemId,
string CelestialId, string AnchorId,
string TargetKind, string TargetKind,
string TargetDefinitionId, string TargetDefinitionId,
string? BlueprintId, string? BlueprintId,

View File

@@ -5,7 +5,7 @@ public sealed class ClaimRuntime
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required string CelestialId { get; init; } public required string AnchorId { get; init; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public DateTimeOffset PlacedAtUtc { get; init; } public DateTimeOffset PlacedAtUtc { get; init; }
public DateTimeOffset ActivatesAtUtc { get; set; } public DateTimeOffset ActivatesAtUtc { get; set; }
@@ -19,7 +19,7 @@ public sealed class ConstructionSiteRuntime
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required string CelestialId { get; init; } public required string AnchorId { get; init; }
public required string TargetKind { get; init; } public required string TargetKind { get; init; }
public required string TargetDefinitionId { get; init; } public required string TargetDefinitionId { get; init; }
public string? BlueprintId { get; set; } public string? BlueprintId { get; set; }

View File

@@ -7,6 +7,7 @@ public sealed class StationRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public string? AnchorId { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public string Category { get; set; } = "station"; public string Category { get; set; } = "station";
public string Objective { get; set; } = "general"; public string Objective { get; set; } = "general";
@@ -14,7 +15,6 @@ public sealed class StationRuntime
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public float Radius { get; set; } = 24f; public float Radius { get; set; } = 24f;
public required string FactionId { get; init; } public required string FactionId { get; init; }
public string? CelestialId { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public List<StationModuleRuntime> Modules { get; } = []; public List<StationModuleRuntime> Modules { get; } = [];

View File

@@ -100,7 +100,7 @@ internal sealed class StationLifecycleService
{ {
CurrentSystemId = station.SystemId, CurrentSystemId = station.SystemId,
SpaceLayer = SpaceLayerKind.LocalSpace, SpaceLayer = SpaceLayerKind.LocalSpace,
CurrentCelestialId = station.CelestialId, CurrentAnchorId = station.AnchorId,
LocalPosition = position, LocalPosition = position,
SystemPosition = position, SystemPosition = position,
MovementRegime = MovementRegimeKind.LocalFlight, MovementRegime = MovementRegimeKind.LocalFlight,

View File

@@ -33,11 +33,11 @@ public sealed class StreamWorldHandler(WorldService worldService) : EndpointWith
} }
var systemId = HttpContext.Request.Query["systemId"].ToString(); var systemId = HttpContext.Request.Query["systemId"].ToString();
var bubbleId = HttpContext.Request.Query["bubbleId"].ToString(); var anchorId = HttpContext.Request.Query["anchorId"].ToString();
var scope = new ObserverScope( var scope = new ObserverScope(
scopeKind, scopeKind,
string.IsNullOrWhiteSpace(systemId) ? null : systemId, string.IsNullOrWhiteSpace(systemId) ? null : systemId,
string.IsNullOrWhiteSpace(bubbleId) ? null : bubbleId); string.IsNullOrWhiteSpace(anchorId) ? null : anchorId);
var stream = worldService.Subscribe(scope, afterSequence, cancellationToken); var stream = worldService.Subscribe(scope, afterSequence, cancellationToken);
await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken); await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken);

View File

@@ -42,25 +42,57 @@ public sealed record PlanetSnapshot(
string Color, string Color,
bool HasRing); bool HasRing);
public sealed record ResourceDepositSnapshot(
string Id,
string NodeId,
string AnchorId,
Vector3Dto LocalPosition,
float OreRemaining,
float MaxOre);
public sealed record ResourceNodeSnapshot( public sealed record ResourceNodeSnapshot(
string Id, string Id,
string AnchorId,
string SystemId, string SystemId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
string? CelestialId, float LocalSpaceRadius,
string SourceKind, string SourceKind,
float OreRemaining, float OreRemaining,
float MaxOre, float MaxOre,
string ItemId); string ItemId,
IReadOnlyList<ResourceDepositSnapshot> Deposits);
public sealed record ResourceNodeDelta( public sealed record ResourceNodeDelta(
string Id, string Id,
string AnchorId,
string SystemId, string SystemId,
Vector3Dto LocalPosition, Vector3Dto LocalPosition,
string? CelestialId, float LocalSpaceRadius,
string SourceKind, string SourceKind,
float OreRemaining, float OreRemaining,
float MaxOre, float MaxOre,
string ItemId); string ItemId,
IReadOnlyList<ResourceDepositSnapshot> Deposits);
public sealed record AnchorSnapshot(
string Id,
string SystemId,
string Kind,
Vector3Dto SystemPosition,
float LocalSpaceRadius,
string? ParentAnchorId,
string? OccupyingStructureId,
string? OrbitReferenceId);
public sealed record AnchorDelta(
string Id,
string SystemId,
string Kind,
Vector3Dto SystemPosition,
float LocalSpaceRadius,
string? ParentAnchorId,
string? OccupyingStructureId,
string? OrbitReferenceId);
public sealed record CelestialSnapshot( public sealed record CelestialSnapshot(
string Id, string Id,
@@ -68,7 +100,7 @@ public sealed record CelestialSnapshot(
string Kind, string Kind,
Vector3Dto OrbitalAnchor, Vector3Dto OrbitalAnchor,
float LocalSpaceRadius, float LocalSpaceRadius,
string? ParentNodeId, string? ParentAnchorId,
string? OccupyingStructureId, string? OccupyingStructureId,
string? OrbitReferenceId); string? OrbitReferenceId);
@@ -78,6 +110,6 @@ public sealed record CelestialDelta(
string Kind, string Kind,
Vector3Dto OrbitalAnchor, Vector3Dto OrbitalAnchor,
float LocalSpaceRadius, float LocalSpaceRadius,
string? ParentNodeId, string? ParentAnchorId,
string? OccupyingStructureId, string? OccupyingStructureId,
string? OrbitReferenceId); string? OrbitReferenceId);

View File

@@ -10,6 +10,7 @@ public sealed record WorldSnapshot(
DateTimeOffset GeneratedAtUtc, DateTimeOffset GeneratedAtUtc,
IReadOnlyList<SystemSnapshot> Systems, IReadOnlyList<SystemSnapshot> Systems,
IReadOnlyList<CelestialSnapshot> Celestials, IReadOnlyList<CelestialSnapshot> Celestials,
IReadOnlyList<AnchorSnapshot> Anchors,
IReadOnlyList<ResourceNodeSnapshot> Nodes, IReadOnlyList<ResourceNodeSnapshot> Nodes,
IReadOnlyList<StationSnapshot> Stations, IReadOnlyList<StationSnapshot> Stations,
IReadOnlyList<ClaimSnapshot> Claims, IReadOnlyList<ClaimSnapshot> Claims,
@@ -29,6 +30,7 @@ public sealed record WorldDelta(
bool RequiresSnapshotRefresh, bool RequiresSnapshotRefresh,
IReadOnlyList<SimulationEventRecord> Events, IReadOnlyList<SimulationEventRecord> Events,
IReadOnlyList<CelestialDelta> Celestials, IReadOnlyList<CelestialDelta> Celestials,
IReadOnlyList<AnchorDelta> Anchors,
IReadOnlyList<ResourceNodeDelta> Nodes, IReadOnlyList<ResourceNodeDelta> Nodes,
IReadOnlyList<StationDelta> Stations, IReadOnlyList<StationDelta> Stations,
IReadOnlyList<ClaimDelta> Claims, IReadOnlyList<ClaimDelta> Claims,
@@ -54,7 +56,7 @@ public sealed record SimulationEventRecord(
public sealed record ObserverScope( public sealed record ObserverScope(
string ScopeKind, string ScopeKind,
string? SystemId = null, string? SystemId = null,
string? CelestialId = null); string? AnchorId = null);
public sealed record OrbitalSimulationSnapshot( public sealed record OrbitalSimulationSnapshot(
double SimulatedSecondsPerRealSecond); double SimulatedSecondsPerRealSecond);

View File

@@ -6,6 +6,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 List<SystemRuntime> Systems { get; init; } public required List<SystemRuntime> Systems { get; init; }
public required List<AnchorRuntime> Anchors { 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; }
public required List<WreckRuntime> Wrecks { get; init; } public required List<WreckRuntime> Wrecks { get; init; }

View File

@@ -7,22 +7,49 @@ public sealed class SystemRuntime
public required Vector3 Position { get; init; } public required Vector3 Position { get; init; }
} }
public sealed class AnchorRuntime
{
public required string Id { get; init; }
public required string SystemId { get; init; }
public required SpatialNodeKind Kind { get; init; }
public required Vector3 Position { get; set; }
public float LocalSpaceRadius { get; set; }
public string? ParentAnchorId { get; set; }
public string? OrbitReferenceId { get; set; }
public string? OccupyingStructureId { get; set; }
public required string SourceEntityKind { get; init; }
public required string SourceEntityId { get; init; }
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class ResourceNodeRuntime public sealed class ResourceNodeRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string AnchorId { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public required string SourceKind { get; init; } public required string SourceKind { get; init; }
public required string ItemId { get; init; } public required string ItemId { get; init; }
public string? CelestialId { get; set; } public float LocalSpaceRadius { get; init; }
public float OrbitRadius { get; init; } public float OrbitRadius { get; init; }
public float OrbitPhase { get; init; } public float OrbitPhase { get; init; }
public float OrbitInclination { get; init; } public float OrbitInclination { get; init; }
public float OreRemaining { get; set; } public float OreRemaining { get; set; }
public float MaxOre { get; init; } public float MaxOre { get; init; }
public List<ResourceDepositRuntime> Deposits { get; } = [];
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class ResourceDepositRuntime
{
public required string Id { get; init; }
public required string NodeId { get; init; }
public required string AnchorId { get; init; }
public required Vector3 Position { get; set; }
public float OreRemaining { get; set; }
public float MaxOre { get; init; }
}
public sealed class CelestialRuntime public sealed class CelestialRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
@@ -30,7 +57,7 @@ public sealed class CelestialRuntime
public required SpatialNodeKind Kind { get; init; } public required SpatialNodeKind Kind { get; init; }
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public float LocalSpaceRadius { get; init; } public float LocalSpaceRadius { get; init; }
public string? ParentNodeId { get; set; } public string? ParentAnchorId { get; set; }
public string? OccupyingStructureId { get; set; } public string? OccupyingStructureId { get; set; }
public string? OrbitReferenceId { get; set; } public string? OrbitReferenceId { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
@@ -52,19 +79,19 @@ public sealed class ShipSpatialStateRuntime
{ {
public SpaceLayerKind SpaceLayer { get; set; } = SpaceLayerKind.LocalSpace; public SpaceLayerKind SpaceLayer { get; set; } = SpaceLayerKind.LocalSpace;
public required string CurrentSystemId { get; set; } public required string CurrentSystemId { get; set; }
public string? CurrentCelestialId { get; set; } public string? CurrentAnchorId { get; set; }
public Vector3? LocalPosition { get; set; } public Vector3? LocalPosition { get; set; }
public Vector3? SystemPosition { get; set; } public Vector3? SystemPosition { get; set; }
public MovementRegimeKind MovementRegime { get; set; } = MovementRegimeKind.LocalFlight; public MovementRegimeKind MovementRegime { get; set; } = MovementRegimeKind.LocalFlight;
public string? DestinationNodeId { get; set; } public string? DestinationAnchorId { get; set; }
public ShipTransitRuntime? Transit { get; set; } public ShipTransitRuntime? Transit { get; set; }
} }
public sealed class ShipTransitRuntime public sealed class ShipTransitRuntime
{ {
public required MovementRegimeKind Regime { get; init; } public required MovementRegimeKind Regime { get; init; }
public string? OriginNodeId { get; init; } public string? OriginAnchorId { get; init; }
public string? DestinationNodeId { get; init; } public string? DestinationAnchorId { get; init; }
public DateTimeOffset? StartedAtUtc { get; set; } public DateTimeOffset? StartedAtUtc { get; set; }
public DateTimeOffset? ArrivalDueAtUtc { get; set; } public DateTimeOffset? ArrivalDueAtUtc { get; set; }
public float Progress { get; set; } public float Progress { get; set; }

View File

@@ -18,13 +18,13 @@ public sealed class ScenarioContentBuilder(
scenario, scenario,
topology.SystemsById, topology.SystemsById,
topology.SpatialLayout.SystemGraphs, topology.SpatialLayout.SystemGraphs,
topology.SpatialLayout.Celestials); topology.SpatialLayout.Anchors);
var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById); var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById);
var ships = CreateShips( var ships = CreateShips(
scenario, scenario,
topology.SystemsById, topology.SystemsById,
topology.SpatialLayout.Celestials, topology.SpatialLayout.Anchors,
patrolRoutes, patrolRoutes,
stations); stations);
@@ -35,7 +35,7 @@ public sealed class ScenarioContentBuilder(
ScenarioDefinition scenario, ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById, IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs, IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
IReadOnlyCollection<CelestialRuntime> celestials) IReadOnlyCollection<AnchorRuntime> anchors)
{ {
var stations = new List<StationRuntime>(); var stations = new List<StationRuntime>();
var stationIdCounter = 0; var stationIdCounter = 0;
@@ -47,23 +47,27 @@ public sealed class ScenarioContentBuilder(
throw new InvalidOperationException($"Scenario station '{plan.Label}' references unknown system '{plan.SystemId}'."); throw new InvalidOperationException($"Scenario station '{plan.Label}' references unknown system '{plan.SystemId}'.");
} }
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials); var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], anchors);
var station = new StationRuntime var station = new StationRuntime
{ {
Id = $"station-{++stationIdCounter}", Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
AnchorId = placement.Anchor.Id,
Label = plan.Label, Label = plan.Label,
Color = plan.Color, Color = plan.Color,
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective), Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
Position = placement.Position, Position = Vector3.Zero,
FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"), FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"),
CelestialId = placement.AnchorCelestial.Id,
Health = 600f, Health = 600f,
MaxHealth = 600f, MaxHealth = 600f,
}; };
stations.Add(station); stations.Add(station);
placement.AnchorCelestial.OccupyingStructureId = station.Id; placement.Anchor.OccupyingStructureId = station.Id;
if (placement.Celestial is not null)
{
placement.Celestial.OccupyingStructureId = station.Id;
}
var startingModules = BuildStartingModules(plan); var startingModules = BuildStartingModules(plan);
foreach (var moduleId in startingModules) foreach (var moduleId in startingModules)
@@ -162,7 +166,7 @@ public sealed class ScenarioContentBuilder(
private List<ShipRuntime> CreateShips( private List<ShipRuntime> CreateShips(
ScenarioDefinition scenario, ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById, IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyCollection<CelestialRuntime> celestials, IReadOnlyCollection<AnchorRuntime> anchors,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes, IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations) IReadOnlyCollection<StationRuntime> stations)
{ {
@@ -181,6 +185,8 @@ public sealed class ScenarioContentBuilder(
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f); var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset); var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
var factionId = GetRequiredFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'"); var factionId = GetRequiredFactionId(formation.FactionId, $"ship formation '{formation.ShipId}' in system '{formation.SystemId}'");
var spatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, anchors);
var localPosition = spatialState.LocalPosition ?? Vector3.Zero;
ships.Add(new ShipRuntime ships.Add(new ShipRuntime
{ {
@@ -188,9 +194,9 @@ public sealed class ScenarioContentBuilder(
SystemId = formation.SystemId, SystemId = formation.SystemId,
Definition = definition, Definition = definition,
FactionId = factionId, FactionId = factionId,
Position = position, Position = localPosition,
TargetPosition = position, TargetPosition = localPosition,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials), SpatialState = spatialState,
DefaultBehavior = CreateBehavior( DefaultBehavior = CreateBehavior(
definition, definition,
formation.SystemId, formation.SystemId,

View File

@@ -2,8 +2,15 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
namespace SpaceGame.Api.Universe.Scenario; namespace SpaceGame.Api.Universe.Scenario;
public sealed class SpatialBuilder(IBalanceService balance) public sealed class SpatialBuilder
{ {
internal static bool IsConstructibleAnchorKind(SpatialNodeKind kind) => kind is SpatialNodeKind.Planet or SpatialNodeKind.Moon or SpatialNodeKind.LagrangePoint;
internal static string? ResolveCompatibleCelestialId(AnchorRuntime? anchor) =>
anchor is not null && string.Equals(anchor.SourceEntityKind, "celestial", StringComparison.Ordinal)
? anchor.SourceEntityId
: null;
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems) internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems)
{ {
var systemGraphs = systems.ToDictionary( var systemGraphs = systems.ToDictionary(
@@ -11,6 +18,19 @@ public sealed class SpatialBuilder(IBalanceService balance)
BuildSystemSpatialGraph, BuildSystemSpatialGraph,
StringComparer.Ordinal); StringComparer.Ordinal);
var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList(); var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
var anchors = celestials.Select(celestial => new AnchorRuntime
{
Id = celestial.Id,
SystemId = celestial.SystemId,
Kind = celestial.Kind,
Position = celestial.Position,
LocalSpaceRadius = celestial.LocalSpaceRadius,
ParentAnchorId = celestial.ParentAnchorId,
OrbitReferenceId = celestial.OrbitReferenceId,
OccupyingStructureId = celestial.OccupyingStructureId,
SourceEntityKind = "celestial",
SourceEntityId = celestial.Id,
}).ToList();
var nodes = new List<ResourceNodeRuntime>(); var nodes = new List<ResourceNodeRuntime>();
var nodeIdCounter = 0; var nodeIdCounter = 0;
@@ -20,24 +40,43 @@ public sealed class SpatialBuilder(IBalanceService balance)
foreach (var node in system.Definition.ResourceNodes) foreach (var node in system.Definition.ResourceNodes)
{ {
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node); var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
var nodeId = $"node-{++nodeIdCounter}";
var localPosition = ComputeResourceNodeLocalPosition(node);
var anchorPosition = anchorCelestial is null
? localPosition
: Add(anchorCelestial.Position, localPosition);
nodes.Add(new ResourceNodeRuntime nodes.Add(new ResourceNodeRuntime
{ {
Id = $"node-{++nodeIdCounter}", Id = nodeId,
AnchorId = nodeId,
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane), Position = localPosition,
SourceKind = node.SourceKind, SourceKind = node.SourceKind,
ItemId = node.ItemId, ItemId = node.ItemId,
CelestialId = anchorCelestial?.Id, LocalSpaceRadius = LocalSpaceRadius,
OrbitRadius = node.RadiusOffset, OrbitRadius = node.RadiusOffset,
OrbitPhase = node.Angle, OrbitPhase = node.Angle,
OrbitInclination = DegreesToRadians(node.InclinationDegrees), OrbitInclination = DegreesToRadians(node.InclinationDegrees),
OreRemaining = node.OreAmount, OreRemaining = node.OreAmount,
MaxOre = node.OreAmount, MaxOre = node.OreAmount,
}); });
nodes[^1].Deposits.AddRange(BuildResourceDeposits(system.Definition.Id, nodeId, node, node.OreAmount));
anchors.Add(new AnchorRuntime
{
Id = nodeId,
SystemId = system.Definition.Id,
Kind = SpatialNodeKind.ResourceNode,
Position = anchorPosition,
LocalSpaceRadius = LocalSpaceRadius,
ParentAnchorId = anchorCelestial?.Id,
SourceEntityKind = "resource-node",
SourceEntityId = nodeId,
});
} }
} }
return new ScenarioSpatialLayout(systemGraphs, celestials, nodes); return new ScenarioSpatialLayout(systemGraphs, anchors, celestials, nodes);
} }
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system) private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
@@ -70,7 +109,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.Planet, kind: SpatialNodeKind.Planet,
position: planetPosition, position: planetPosition,
localSpaceRadius: LocalSpaceRadius, localSpaceRadius: LocalSpaceRadius,
parentNodeId: primaryStarNodeId); parentAnchorId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal); var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
@@ -82,7 +121,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.LagrangePoint, kind: SpatialNodeKind.LagrangePoint,
position: point.Position, position: point.Position,
localSpaceRadius: LocalSpaceRadius, localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id, parentAnchorId: planetCelestial.Id,
orbitReferenceId: point.Designation); orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeCelestial; lagrangeNodes[point.Designation] = lagrangeCelestial;
} }
@@ -100,7 +139,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
kind: SpatialNodeKind.Moon, kind: SpatialNodeKind.Moon,
position: moonPosition, position: moonPosition,
localSpaceRadius: LocalSpaceRadius, localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id); parentAnchorId: planetCelestial.Id);
} }
} }
@@ -114,7 +153,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
SpatialNodeKind kind, SpatialNodeKind kind,
Vector3 position, Vector3 position,
float localSpaceRadius, float localSpaceRadius,
string? parentNodeId = null, string? parentAnchorId = null,
string? orbitReferenceId = null) string? orbitReferenceId = null)
{ {
var celestial = new CelestialRuntime var celestial = new CelestialRuntime
@@ -124,7 +163,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
Kind = kind, Kind = kind,
Position = position, Position = position,
LocalSpaceRadius = localSpaceRadius, LocalSpaceRadius = localSpaceRadius,
ParentNodeId = parentNodeId, ParentAnchorId = parentAnchorId,
OrbitReferenceId = orbitReferenceId, OrbitReferenceId = orbitReferenceId,
}; };
@@ -183,7 +222,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
InitialStationDefinition plan, InitialStationDefinition plan,
SystemRuntime system, SystemRuntime system,
SystemSpatialGraph graph, SystemSpatialGraph graph,
IReadOnlyCollection<CelestialRuntime> existingCelestials) IReadOnlyCollection<AnchorRuntime> existingAnchors)
{ {
if (plan.PlanetIndex is int planetIndex && if (plan.PlanetIndex is int planetIndex &&
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes)) graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
@@ -191,28 +230,32 @@ public sealed class SpatialBuilder(IBalanceService balance)
var designation = ResolveLagrangeDesignation(plan.LagrangeSide); var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial)) if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
{ {
return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position); var lagrangeAnchor = existingAnchors.First(anchor => string.Equals(anchor.Id, lagrangeCelestial.Id, StringComparison.Ordinal));
return new StationPlacement(lagrangeAnchor, lagrangeCelestial, lagrangeAnchor.Position);
} }
} }
if (plan.Position is { Length: 3 }) if (plan.Position is { Length: 3 })
{ {
var targetPosition = NormalizeScenarioPoint(system, plan.Position); var targetPosition = NormalizeScenarioPoint(system, plan.Position);
var preferredCelestial = existingCelestials var preferredAnchor = existingAnchors
.Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint) .Where(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.LagrangePoint)
.OrderBy(c => c.Position.DistanceTo(targetPosition)) .OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
.FirstOrDefault() .FirstOrDefault()
?? existingCelestials ?? existingAnchors
.Where(c => c.SystemId == system.Definition.Id) .Where(anchor => anchor.SystemId == system.Definition.Id && IsConstructibleAnchorKind(anchor.Kind))
.OrderBy(c => c.Position.DistanceTo(targetPosition)) .OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
.First(); .First();
return new StationPlacement(preferredCelestial, preferredCelestial.Position); var preferredCelestial = graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, ResolveCompatibleCelestialId(preferredAnchor), StringComparison.Ordinal));
return new StationPlacement(preferredAnchor, preferredCelestial, preferredAnchor.Position);
} }
var fallbackCelestial = graph.Celestials var fallbackAnchor = existingAnchors
.FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId)) .Where(anchor => anchor.SystemId == system.Definition.Id)
?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet); .FirstOrDefault(anchor => anchor.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(anchor.OccupyingStructureId))
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position); ?? existingAnchors.First(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.Planet);
var fallbackCelestial = graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, ResolveCompatibleCelestialId(fallbackAnchor), StringComparison.Ordinal));
return new StationPlacement(fallbackAnchor, fallbackCelestial, fallbackAnchor.Position);
} }
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
@@ -256,20 +299,80 @@ public sealed class SpatialBuilder(IBalanceService balance)
return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId); return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
} }
private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane) private static Vector3 ComputeResourceNodeLocalPosition(ResourceNodeDefinition definition)
{ {
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f); var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
var offset = new Vector3( return new Vector3(
MathF.Cos(definition.Angle) * definition.RadiusOffset, MathF.Cos(definition.Angle) * definition.RadiusOffset,
verticalOffset, verticalOffset,
MathF.Sin(definition.Angle) * definition.RadiusOffset); MathF.Sin(definition.Angle) * definition.RadiusOffset);
}
if (anchorCelestial is null) private static IReadOnlyList<ResourceDepositRuntime> BuildResourceDeposits(
string systemId,
string nodeId,
ResourceNodeDefinition definition,
float oreAmount)
{
var depositCount = Math.Clamp((int)MathF.Round(MathF.Sqrt(MathF.Max(oreAmount, 1f)) / 18f), 4, 12);
var deposits = new List<ResourceDepositRuntime>(depositCount);
var weightTotal = 0f;
var weights = new float[depositCount];
for (var index = 0; index < depositCount; index += 1)
{ {
return new Vector3(offset.X, yPlane + offset.Y, offset.Z); var weight = 0.8f + (Hash01(systemId, nodeId, $"weight-{index}") * 1.6f);
weights[index] = weight;
weightTotal += weight;
} }
return Add(anchorCelestial.Position, offset); var scatterRadius = MathF.Max(140f, LocalSpaceRadius * 0.58f);
for (var index = 0; index < depositCount; index += 1)
{
var angle = Hash01(systemId, nodeId, $"angle-{index}") * MathF.PI * 2f;
var radiusFactor = 0.22f + (Hash01(systemId, nodeId, $"radius-{index}") * 0.74f);
var radius = scatterRadius * MathF.Sqrt(radiusFactor);
var vertical = (Hash01(systemId, nodeId, $"vertical-{index}") - 0.5f) * MathF.Max(60f, scatterRadius * 0.14f);
var localPosition = new Vector3(
MathF.Cos(angle) * radius,
vertical,
MathF.Sin(angle) * radius);
var maxOre = oreAmount * (weights[index] / MathF.Max(weightTotal, 0.001f));
deposits.Add(new ResourceDepositRuntime
{
Id = $"{nodeId}-deposit-{index + 1}",
NodeId = nodeId,
AnchorId = nodeId,
Position = localPosition,
OreRemaining = maxOre,
MaxOre = maxOre,
});
}
return deposits;
}
private static float Hash01(string systemId, string nodeId, string salt)
{
unchecked
{
var hash = 17;
foreach (var character in systemId)
{
hash = (hash * 31) + character;
}
foreach (var character in nodeId)
{
hash = (hash * 31) + character;
}
foreach (var character in salt)
{
hash = (hash * 31) + character;
}
return (hash & 0x7fffffff) / (float)int.MaxValue;
}
} }
private static Vector3 ComputePlanetPosition(PlanetDefinition planet) private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
@@ -286,19 +389,22 @@ public sealed class SpatialBuilder(IBalanceService balance)
return Add(planetPosition, local); return Add(planetPosition, local);
} }
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials) internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<AnchorRuntime> anchors)
{ {
var nearestCelestial = celestials var nearestAnchor = anchors
.Where(c => c.SystemId == systemId) .Where(anchor => anchor.SystemId == systemId)
.OrderBy(c => c.Position.DistanceTo(position)) .OrderBy(anchor => anchor.Position.DistanceTo(position))
.FirstOrDefault(); .FirstOrDefault();
var localPosition = nearestAnchor is null
? position
: position.Subtract(nearestAnchor.Position);
return new ShipSpatialStateRuntime return new ShipSpatialStateRuntime
{ {
CurrentSystemId = systemId, CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKind.LocalSpace, SpaceLayer = SpaceLayerKind.LocalSpace,
CurrentCelestialId = nearestCelestial?.Id, CurrentAnchorId = nearestAnchor?.Id,
LocalPosition = position, LocalPosition = localPosition,
SystemPosition = position, SystemPosition = position,
MovementRegime = MovementRegimeKind.LocalFlight, MovementRegime = MovementRegimeKind.LocalFlight,
}; };
@@ -307,6 +413,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
public sealed record ScenarioSpatialLayout( public sealed record ScenarioSpatialLayout(
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs, IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
List<AnchorRuntime> Anchors,
List<CelestialRuntime> Celestials, List<CelestialRuntime> Celestials,
List<ResourceNodeRuntime> Nodes); List<ResourceNodeRuntime> Nodes);
@@ -317,4 +424,4 @@ public sealed record SystemSpatialGraph(
internal sealed record LagrangePointPlacement(string Designation, Vector3 Position); internal sealed record LagrangePointPlacement(string Designation, Vector3 Position);
internal sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position); internal sealed record StationPlacement(AnchorRuntime Anchor, CelestialRuntime? Celestial, Vector3 Position);

View File

@@ -18,13 +18,14 @@ public sealed class WorldRuntimeAssembler(
var policies = seedingService.CreatePolicies(factions); var policies = seedingService.CreatePolicies(factions);
var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships); var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships);
var nowUtc = DateTimeOffset.UtcNow; var nowUtc = DateTimeOffset.UtcNow;
var claims = seedingService.CreateClaims(content.Stations, topology.SpatialLayout.Celestials, nowUtc); var claims = seedingService.CreateClaims(content.Stations, topology.SpatialLayout.Anchors, nowUtc);
var world = new SimulationWorld var world = new SimulationWorld
{ {
Label = "Split Viewer / Simulation World", Label = "Split Viewer / Simulation World",
Seed = worldGenerationOptions.Seed, Seed = worldGenerationOptions.Seed,
Systems = topology.SystemRuntimes.ToList(), Systems = topology.SystemRuntimes.ToList(),
Anchors = topology.SpatialLayout.Anchors,
Celestials = topology.SpatialLayout.Celestials, Celestials = topology.SpatialLayout.Celestials,
Nodes = topology.SpatialLayout.Nodes, Nodes = topology.SpatialLayout.Nodes,
Wrecks = [], Wrecks = [],

View File

@@ -74,27 +74,27 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
internal List<ClaimRuntime> CreateClaims( internal List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations, IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<CelestialRuntime> celestials, IReadOnlyCollection<AnchorRuntime> anchors,
DateTimeOffset nowUtc) DateTimeOffset nowUtc)
{ {
var stationsByCelestialId = stations var stationsByAnchorId = stations
.Where(station => station.CelestialId is not null) .Where(station => !string.IsNullOrWhiteSpace(station.AnchorId))
.ToDictionary(station => station.CelestialId!, StringComparer.Ordinal); .ToDictionary(station => station.AnchorId!, StringComparer.Ordinal);
var claims = new List<ClaimRuntime>(); var claims = new List<ClaimRuntime>();
foreach (var celestial in celestials.Where(c => c.Kind == SpatialNodeKind.LagrangePoint)) foreach (var anchor in anchors.Where(candidate => candidate.Kind == SpatialNodeKind.LagrangePoint))
{ {
if (!stationsByCelestialId.TryGetValue(celestial.Id, out var station)) if (!stationsByAnchorId.TryGetValue(anchor.Id, out var station))
{ {
continue; continue;
} }
claims.Add(new ClaimRuntime claims.Add(new ClaimRuntime
{ {
Id = $"claim-{celestial.Id}", Id = $"claim-{anchor.Id}",
FactionId = station.FactionId, FactionId = station.FactionId,
SystemId = celestial.SystemId, SystemId = anchor.SystemId,
CelestialId = celestial.Id, AnchorId = anchor.Id,
PlacedAtUtc = nowUtc, PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8), ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating, State = ClaimStateKinds.Activating,
@@ -119,12 +119,12 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
} }
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.AnchorId is null)
{ {
continue; continue;
} }
var claim = world.Claims.FirstOrDefault(candidate => candidate.CelestialId == station.CelestialId); var claim = world.Claims.FirstOrDefault(candidate => string.Equals(candidate.AnchorId, station.AnchorId, StringComparison.Ordinal));
if (claim is null || !world.ModuleRecipes.TryGetValue(moduleId, out var recipe)) if (claim is null || !world.ModuleRecipes.TryGetValue(moduleId, out var recipe))
{ {
continue; continue;
@@ -135,7 +135,7 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
Id = $"site-{station.Id}", Id = $"site-{station.Id}",
FactionId = station.FactionId, FactionId = station.FactionId,
SystemId = station.SystemId, SystemId = station.SystemId,
CelestialId = station.CelestialId, AnchorId = station.AnchorId,
TargetKind = "station-module", TargetKind = "station-module",
TargetDefinitionId = "station", TargetDefinitionId = "station",
BlueprintId = moduleId, BlueprintId = moduleId,

View File

@@ -1,5 +1,7 @@
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
using SpaceGame.Api.Universe.Scenario;
namespace SpaceGame.Api.Universe.Simulation; namespace SpaceGame.Api.Universe.Simulation;
internal sealed class OrbitalStateUpdater internal sealed class OrbitalStateUpdater
@@ -223,22 +225,47 @@ internal sealed class OrbitalStateUpdater
foreach (var station in world.Stations) foreach (var station in world.Stations)
{ {
if (station.CelestialId is null || !celestialsById.TryGetValue(station.CelestialId, out var anchorCelestial)) if (station.AnchorId is not null && world.Anchors.Any(candidate => candidate.Id == station.AnchorId))
{ {
continue; station.Position = Vector3.Zero;
} }
station.Position = anchorCelestial.Position;
} }
foreach (var node in world.Nodes) foreach (var node in world.Nodes)
{ {
if (node.CelestialId is null || !celestialsById.TryGetValue(node.CelestialId, out var anchorCelestial)) node.Position = ComputeResourceNodeOffset(node, worldTimeSeconds);
}
var nodeAnchorsById = world.Nodes.ToDictionary(node => node.AnchorId, StringComparer.Ordinal);
foreach (var anchor in world.Anchors)
{
if (string.Equals(anchor.SourceEntityKind, "resource-node", StringComparison.Ordinal))
{ {
if (nodeAnchorsById.TryGetValue(anchor.Id, out var node))
{
if (anchor.ParentAnchorId is not null && celestialsById.TryGetValue(anchor.ParentAnchorId, out var anchorCelestial))
{
anchor.Position = Add(anchorCelestial.Position, node.Position);
}
else
{
anchor.Position = node.Position;
}
anchor.LocalSpaceRadius = node.LocalSpaceRadius;
}
continue; continue;
} }
node.Position = Add(anchorCelestial.Position, ComputeResourceNodeOffset(node, worldTimeSeconds)); if (celestialsById.TryGetValue(anchor.Id, out var celestial))
{
anchor.Position = celestial.Position;
anchor.LocalSpaceRadius = celestial.LocalSpaceRadius;
anchor.ParentAnchorId = celestial.ParentAnchorId;
anchor.OccupyingStructureId = celestial.OccupyingStructureId;
anchor.OrbitReferenceId = celestial.OrbitReferenceId;
}
} }
foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null)) foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null))
@@ -261,20 +288,29 @@ internal sealed class OrbitalStateUpdater
{ {
ship.SpatialState.CurrentSystemId = ship.SystemId; ship.SpatialState.CurrentSystemId = ship.SystemId;
ship.SpatialState.LocalPosition = ship.Position; ship.SpatialState.LocalPosition = ship.Position;
ship.SpatialState.SystemPosition = ship.Position;
if (ship.SpatialState.Transit is not null) if (ship.SpatialState.Transit is not null)
{ {
ship.SpatialState.CurrentCelestialId = null; ship.SpatialState.CurrentAnchorId = null;
continue; continue;
} }
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
var nearestCelestial = world.Celestials var currentAnchor = ship.SpatialState.CurrentAnchorId is not null
.Where(candidate => candidate.SystemId == ship.SystemId) ? world.Anchors.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentAnchorId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) : null;
.FirstOrDefault(); if (currentAnchor is null || !string.Equals(currentAnchor.SystemId, ship.SystemId, StringComparison.Ordinal))
ship.SpatialState.CurrentCelestialId = nearestCelestial?.Id; {
currentAnchor = world.Anchors
.Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
.FirstOrDefault();
}
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
ship.SpatialState.SystemPosition = currentAnchor is null
? ship.Position
: Add(currentAnchor.Position, ship.Position);
if (ship.DockedStationId is null) if (ship.DockedStationId is null)
{ {
@@ -282,9 +318,9 @@ internal sealed class OrbitalStateUpdater
} }
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station?.CelestialId is not null) if (station is not null)
{ {
ship.SpatialState.CurrentCelestialId = station.CelestialId; ship.SpatialState.CurrentAnchorId = station.AnchorId;
} }
} }
} }

View File

@@ -315,6 +315,8 @@ public sealed class WorldService
string.Equals(candidate.FactionId, faction.Id, StringComparison.Ordinal) string.Equals(candidate.FactionId, faction.Id, StringComparison.Ordinal)
&& string.Equals(candidate.SystemId, system.Definition.Id, StringComparison.Ordinal)); && string.Equals(candidate.SystemId, system.Definition.Id, StringComparison.Ordinal));
var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, homeStation); var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, homeStation);
var spatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Anchors);
var localPosition = spatialState.LocalPosition ?? Vector3.Zero;
var ship = new ShipRuntime var ship = new ShipRuntime
{ {
@@ -322,9 +324,9 @@ public sealed class WorldService
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
Definition = definition, Definition = definition,
FactionId = faction.Id, FactionId = faction.Id,
Position = spawnPosition, Position = localPosition,
TargetPosition = spawnPosition, TargetPosition = localPosition,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials), SpatialState = spatialState,
DefaultBehavior = defaultBehavior, DefaultBehavior = defaultBehavior,
Skills = ShipBootstrapPolicy.CreateSkills(definition), Skills = ShipBootstrapPolicy.CreateSkills(definition),
Health = definition.Hull, Health = definition.Hull,
@@ -352,15 +354,18 @@ public sealed class WorldService
? $"{faction.Label} {ToTitleCaseToken(objective)} {CountFactionStationsInSystem(faction.Id, system.Definition.Id) + 1}" ? $"{faction.Label} {ToTitleCaseToken(objective)} {CountFactionStationsInSystem(faction.Id, system.Definition.Id) + 1}"
: request.Label.Trim(); : request.Label.Trim();
var stationId = $"station-{faction.Id}-{objective}-{Guid.NewGuid():N}".ToLowerInvariant(); var stationId = $"station-{faction.Id}-{objective}-{Guid.NewGuid():N}".ToLowerInvariant();
var position = ResolveStationSpawnPosition(system.Definition.Id); var requestedPosition = ResolveStationSpawnPosition(system.Definition.Id);
var anchor = ResolveNearestConstructibleAnchor(system.Definition.Id, requestedPosition)
?? throw new InvalidOperationException($"System '{system.Definition.Id}' does not have a valid constructible anchor for station spawning.");
var station = new StationRuntime var station = new StationRuntime
{ {
Id = stationId, Id = stationId,
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
AnchorId = anchor.Id,
Label = label, Label = label,
Color = faction.Color, Color = faction.Color,
Objective = objective, Objective = objective,
Position = position, Position = Vector3.Zero,
FactionId = faction.Id, FactionId = faction.Id,
PolicySetId = faction.DefaultPolicySetId, PolicySetId = faction.DefaultPolicySetId,
Health = 600f, Health = 600f,
@@ -375,6 +380,7 @@ public sealed class WorldService
station.PopulationCapacity = GetStationSupportedPopulation(_world.ModuleDefinitions, station); station.PopulationCapacity = GetStationSupportedPopulation(_world.ModuleDefinitions, station);
station.WorkforceRequired = GetStationRequiredWorkforce(_world.ModuleDefinitions, station); station.WorkforceRequired = GetStationRequiredWorkforce(_world.ModuleDefinitions, station);
_world.Stations.Add(station); _world.Stations.Add(station);
anchor.OccupyingStructureId = station.Id;
new GeopoliticalSimulationService().Update(_world, 0f, []); new GeopoliticalSimulationService().Update(_world, 0f, []);
PublishSnapshotRefreshUnsafe("spawn-station", $"Spawned station {station.Id}", "station", station.Id); PublishSnapshotRefreshUnsafe("spawn-station", $"Spawned station {station.Id}", "station", station.Id);
@@ -490,6 +496,7 @@ public sealed class WorldService
[], [],
[], [],
[], [],
[],
null); null);
_history.Enqueue(worldDelta); _history.Enqueue(worldDelta);
@@ -526,6 +533,7 @@ public sealed class WorldService
[], [],
[], [],
[], [],
[],
null); null);
_history.Enqueue(worldDelta); _history.Enqueue(worldDelta);
@@ -608,6 +616,8 @@ public sealed class WorldService
var shipId = $"ship-{playerFaction.Id}-{definition.Id}-{Guid.NewGuid():N}".ToLowerInvariant(); var shipId = $"ship-{playerFaction.Id}-{definition.Id}-{Guid.NewGuid():N}".ToLowerInvariant();
var spawnPosition = ResolveSpawnPosition(system.Definition.Id); var spawnPosition = ResolveSpawnPosition(system.Definition.Id);
var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, null); var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, null);
var spatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Anchors);
var localPosition = spatialState.LocalPosition ?? Vector3.Zero;
var ship = new ShipRuntime var ship = new ShipRuntime
{ {
@@ -615,9 +625,9 @@ public sealed class WorldService
SystemId = system.Definition.Id, SystemId = system.Definition.Id,
Definition = definition, Definition = definition,
FactionId = playerFaction.Id, FactionId = playerFaction.Id,
Position = spawnPosition, Position = localPosition,
TargetPosition = spawnPosition, TargetPosition = localPosition,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials), SpatialState = spatialState,
DefaultBehavior = defaultBehavior, DefaultBehavior = defaultBehavior,
Skills = ShipBootstrapPolicy.CreateSkills(definition), Skills = ShipBootstrapPolicy.CreateSkills(definition),
Health = definition.Hull, Health = definition.Hull,
@@ -712,7 +722,7 @@ public sealed class WorldService
SourceStationId = request.SourceStationId, SourceStationId = request.SourceStationId,
DestinationStationId = request.DestinationStationId, DestinationStationId = request.DestinationStationId,
ItemId = request.ItemId, ItemId = request.ItemId,
NodeId = request.NodeId, AnchorId = request.AnchorId,
ConstructionSiteId = request.ConstructionSiteId, ConstructionSiteId = request.ConstructionSiteId,
ModuleId = request.ModuleId, ModuleId = request.ModuleId,
WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f), WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f),
@@ -780,7 +790,7 @@ public sealed class WorldService
ship.DefaultBehavior.AreaSystemId = request.AreaSystemId; ship.DefaultBehavior.AreaSystemId = request.AreaSystemId;
ship.DefaultBehavior.TargetEntityId = request.TargetEntityId; ship.DefaultBehavior.TargetEntityId = request.TargetEntityId;
ship.DefaultBehavior.ItemId = request.ItemId; ship.DefaultBehavior.ItemId = request.ItemId;
ship.DefaultBehavior.PreferredNodeId = request.PreferredNodeId; ship.DefaultBehavior.PreferredAnchorId = request.PreferredAnchorId;
ship.DefaultBehavior.PreferredConstructionSiteId = request.PreferredConstructionSiteId; ship.DefaultBehavior.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
ship.DefaultBehavior.PreferredModuleId = request.PreferredModuleId; ship.DefaultBehavior.PreferredModuleId = request.PreferredModuleId;
ship.DefaultBehavior.TargetPosition = request.TargetPosition is null ship.DefaultBehavior.TargetPosition = request.TargetPosition is null
@@ -807,7 +817,7 @@ public sealed class WorldService
SourceStationId = template.SourceStationId, SourceStationId = template.SourceStationId,
DestinationStationId = template.DestinationStationId, DestinationStationId = template.DestinationStationId,
ItemId = template.ItemId, ItemId = template.ItemId,
NodeId = template.NodeId, AnchorId = template.AnchorId,
ConstructionSiteId = template.ConstructionSiteId, ConstructionSiteId = template.ConstructionSiteId,
ModuleId = template.ModuleId, ModuleId = template.ModuleId,
WaitSeconds = template.WaitSeconds ?? 0f, WaitSeconds = template.WaitSeconds ?? 0f,
@@ -905,6 +915,16 @@ public sealed class WorldService
return new Vector3(MathF.Cos(angle) * radius, 0f, MathF.Sin(angle) * radius); return new Vector3(MathF.Cos(angle) * radius, 0f, MathF.Sin(angle) * radius);
} }
private AnchorRuntime? ResolveNearestConstructibleAnchor(string systemId, Vector3 position) =>
_world.Anchors
.Where(candidate => string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal))
.Where(candidate => SpatialBuilder.IsConstructibleAnchorKind(candidate.Kind))
.OrderBy(candidate => candidate.Position.DistanceTo(position))
.FirstOrDefault();
private string? ResolveNearestAnchorId(string systemId, Vector3 position) =>
ResolveNearestConstructibleAnchor(systemId, position)?.Id;
private IReadOnlyList<string> BuildStarterStationModules(string factionId, string objective) private IReadOnlyList<string> BuildStarterStationModules(string factionId, string objective)
{ {
var modules = new List<string>(); var modules = new List<string>();
@@ -1079,9 +1099,9 @@ public sealed class WorldService
} }
var systemFilter = scope.SystemId; var systemFilter = scope.SystemId;
if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null) if (string.Equals(scope.ScopeKind, "local-anchor", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.AnchorId is not null)
{ {
systemFilter = ResolveCelestialSystemId(scope.CelestialId); systemFilter = ResolveAnchorSystemId(scope.AnchorId);
} }
return delta with return delta with
@@ -1091,6 +1111,7 @@ public sealed class WorldService
.Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter)) .Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter))
.ToList(), .ToList(),
Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(), Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(),
Anchors = delta.Anchors.Where((anchor) => systemFilter is null || anchor.SystemId == systemFilter).ToList(),
Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(), Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(),
Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(), Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(),
Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(), Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(),
@@ -1136,8 +1157,8 @@ public sealed class WorldService
ScopeEntityId = scopeEntityId, ScopeEntityId = scopeEntityId,
}; };
private string? ResolveCelestialSystemId(string celestialId) => private string? ResolveAnchorSystemId(string anchorId) =>
_world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId; _world.Anchors.FirstOrDefault((anchor) => anchor.Id == anchorId)?.SystemId;
private string? ResolveMarketOrderSystemId(string orderId) private string? ResolveMarketOrderSystemId(string orderId)
{ {
@@ -1181,7 +1202,7 @@ public sealed class WorldService
{ {
"universe" => true, "universe" => true,
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter, "system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
"local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter, "local-anchor" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
_ => true, _ => true,
}; };
} }

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { GameViewer } from "./GameViewer"; import { GameViewer } from "./GameViewer";
import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue"; import ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
import ViewerEntityBrowserPanel from "./components/ViewerEntityBrowserPanel.vue"; import ViewerEntityBrowserPanel from "./components/ViewerEntityBrowserPanel.vue";
@@ -31,8 +31,8 @@ const authStore = useAuthStore();
const playerFactionStore = usePlayerFactionStore(); const playerFactionStore = usePlayerFactionStore();
const automationCatalogStore = useShipAutomationCatalogStore(); const automationCatalogStore = useShipAutomationCatalogStore();
const selectionStore = useViewerSelectionStore(); const selectionStore = useViewerSelectionStore();
const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore); const { selectedEntityId } = storeToRefs(selectionStore);
const { canAccessGm, effectivePlayerId } = storeToRefs(authStore); const { canAccessGm, effectivePlayerId, isActingAsAlternateIdentity } = storeToRefs(authStore);
const { playerFaction } = storeToRefs(playerFactionStore); const { playerFaction } = storeToRefs(playerFactionStore);
let viewer: GameViewer | undefined; let viewer: GameViewer | undefined;
@@ -42,14 +42,27 @@ const gmSettingsOpen = ref(false);
const gmMenuOpen = ref(false); const gmMenuOpen = ref(false);
const leftSidebarTab = ref<"player" | "entities">("player"); const leftSidebarTab = ref<"player" | "entities">("player");
const playerContextReady = ref(false); const playerContextReady = ref(false);
const rightSidebarWidth = ref(380);
const rightSidebarResizing = ref(false);
const shouldShowOnboarding = computed(() =>
!!playerContextReady.value
&& !!playerFaction.value?.requiresOnboarding
&& (!canAccessGm.value || isActingAsAlternateIdentity.value),
);
onMounted(async () => { onMounted(async () => {
window.addEventListener("pointermove", onWindowPointerMove);
window.addEventListener("pointerup", stopRightSidebarResize);
window.addEventListener("pointercancel", stopRightSidebarResize);
void automationCatalogStore.load(); void automationCatalogStore.load();
await refreshPlayerContext(); await refreshPlayerContext();
await startViewerIfAuthenticated(); await startViewerIfAuthenticated();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("pointermove", onWindowPointerMove);
window.removeEventListener("pointerup", stopRightSidebarResize);
window.removeEventListener("pointercancel", stopRightSidebarResize);
viewer?.dispose(); viewer?.dispose();
}); });
@@ -71,7 +84,7 @@ watch(
); );
watch( watch(
() => playerFaction.value?.requiresOnboarding ?? false, () => shouldShowOnboarding.value,
async (requiresOnboarding) => { async (requiresOnboarding) => {
if (requiresOnboarding) { if (requiresOnboarding) {
viewer?.dispose(); viewer?.dispose();
@@ -101,8 +114,31 @@ function onFocusSelection(selection: Selectable, cameraMode?: "follow" | "tactic
viewer?.focusSelection(selection, cameraMode); viewer?.focusSelection(selection, cameraMode);
} }
function startRightSidebarResize(event: PointerEvent) {
if (window.innerWidth <= 760 || event.button !== 0) {
return;
}
rightSidebarResizing.value = true;
event.preventDefault();
}
function onWindowPointerMove(event: PointerEvent) {
if (!rightSidebarResizing.value) {
return;
}
const minWidth = 280;
const maxWidth = Math.min(720, Math.max(window.innerWidth - 240, minWidth));
rightSidebarWidth.value = Math.min(maxWidth, Math.max(minWidth, window.innerWidth - event.clientX));
}
function stopRightSidebarResize() {
rightSidebarResizing.value = false;
}
async function startViewerIfAuthenticated() { async function startViewerIfAuthenticated() {
if (!authStore.isAuthenticated || viewer || !playerContextReady.value || playerFaction.value?.requiresOnboarding) { if (!authStore.isAuthenticated || viewer || !playerContextReady.value || shouldShowOnboarding.value) {
return; return;
} }
@@ -155,7 +191,7 @@ async function refreshPlayerContext() {
<p>Loading your in-universe identity and ownership state.</p> <p>Loading your in-universe identity and ownership state.</p>
</div> </div>
</div> </div>
<PlayerOnboardingPanel v-else-if="playerContextReady && playerFaction?.requiresOnboarding" /> <PlayerOnboardingPanel v-else-if="shouldShowOnboarding" />
<div v-else class="viewer-app"> <div v-else class="viewer-app">
<div <div
ref="canvasHostEl" ref="canvasHostEl"
@@ -232,27 +268,28 @@ async function refreshPlayerContext() {
</div> </div>
</div> </div>
<div class="absolute right-5 top-5 flex max-h-[calc(100vh-40px)] w-[min(380px,calc(100vw-40px))] flex-col gap-4 overflow-hidden max-[760px]:bottom-[148px] max-[760px]:left-5 max-[760px]:right-5 max-[760px]:top-auto max-[760px]:max-h-[38vh] max-[760px]:w-auto"> <div class="viewer-right-sidebar-dock" :style="{ width: `${rightSidebarWidth}px` }">
<ViewerEntityInspectorPanel <section class="viewer-right-sidebar pointer-events-auto">
class="min-h-0 flex-1" <div
:fallback-title="hudState.detailPanel.title" class="viewer-right-sidebar__resize-handle"
:fallback-html="hudState.detailPanel.bodyHtml" :class="rightSidebarResizing ? 'viewer-right-sidebar__resize-handle--active' : ''"
@focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)" @pointerdown="startRightSidebarResize"
/> />
<div <div class="viewer-right-sidebar__body">
class="pointer-events-auto rounded-xl bg-[rgba(255,116,88,0.14)] px-3.5 py-3 text-[#ffd8cf]" <ViewerEntityInspectorPanel
:hidden="hudState.error.hidden" class="viewer-right-sidebar__panel"
> :fallback-title="hudState.detailPanel.title"
{{ hudState.error.message }} :fallback-html="hudState.detailPanel.bodyHtml"
</div> @focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)"
<button />
v-if="selectedEntityId" </div>
type="button" <div
class="selection-action-button pointer-events-auto self-end rounded-full border border-white/10 bg-white/5 px-3.5 py-2.5 text-sm text-[color:var(--viewer-text)] transition hover:bg-white/10" class="viewer-right-sidebar__error"
@click="selectionStore.clearSelection('ui')" :hidden="hudState.error.hidden"
> >
Clear {{ selectedEntityLabel ?? "Selection" }} {{ hudState.error.message }}
</button> </div>
</section>
</div> </div>
<div ref="historyLayerHostEl"> <div ref="historyLayerHostEl">

View File

@@ -8,7 +8,7 @@ import { updatePanFromKeyboard } from "./viewerCamera";
import { setShellReticleOpacity } from "./viewerControls"; import { setShellReticleOpacity } from "./viewerControls";
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop"; import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
import { updateSystemStarPresentation } from "./viewerPresentation"; import { updateSystemStarPresentation } from "./viewerPresentation";
import { resolveFocusedCelestialId } from "./viewerSelection"; import { resolveFocusedAnchorId } from "./viewerSelection";
import { describeSelectionParent } from "./viewerPanels"; import { describeSelectionParent } from "./viewerPanels";
import { import {
createInitialNetworkStats, createInitialNetworkStats,
@@ -195,6 +195,7 @@ export class ViewerAppController {
return this.sceneDataController.createWorldPresentationContext({ return this.sceneDataController.createWorldPresentationContext({
world: this.world, world: this.world,
activeSystemId: this.activeSystemId, activeSystemId: this.activeSystemId,
focusedAnchorId: this.resolveFocusedAnchorId(),
cameraMode: this.cameraMode, cameraMode: this.cameraMode,
povLevel: this.povLevel, povLevel: this.povLevel,
orbitYaw: this.orbitYaw, orbitYaw: this.orbitYaw,
@@ -284,6 +285,7 @@ export class ViewerAppController {
this.sceneStore.setViewContext(this.activeSystemId ?? null, this.povLevel); this.sceneStore.setViewContext(this.activeSystemId ?? null, this.povLevel);
} }
this.navigationController.updateActiveSystem(); this.navigationController.updateActiveSystem();
this.navigationController.syncGalaxyAnchorToActiveSystem();
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) { if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
// Follow camera directly controls systemLayer.camera in updateFollowCamera. // Follow camera directly controls systemLayer.camera in updateFollowCamera.
@@ -350,8 +352,8 @@ export class ViewerAppController {
this.interactionController.refreshHistoryWindows(); this.interactionController.refreshHistoryWindows();
} }
private resolveFocusedCelestialId() { private resolveFocusedAnchorId() {
return resolveFocusedCelestialId(this.world, this.selectedItems); return resolveFocusedAnchorId(this.world, this.selectedItems);
} }
private onResize(width: number, height: number) { private onResize(width: number, height: number) {

View File

@@ -28,7 +28,7 @@ import type {
export interface WorldStreamScope { export interface WorldStreamScope {
scopeKind?: string; scopeKind?: string;
systemId?: string | null; systemId?: string | null;
bubbleId?: string | null; anchorId?: string | null;
} }
async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit, options?: { skipAuth?: boolean; skipRefresh?: boolean }): Promise<T> { async function fetchJson<T>(input: RequestInfo | URL, init?: RequestInit, options?: { skipAuth?: boolean; skipRefresh?: boolean }): Promise<T> {
@@ -105,8 +105,8 @@ export function openWorldStream(
if (scope?.systemId) { if (scope?.systemId) {
query.set("systemId", scope.systemId); query.set("systemId", scope.systemId);
} }
if (scope?.bubbleId) { if (scope?.anchorId) {
query.set("bubbleId", scope.bubbleId); query.set("anchorId", scope.anchorId);
} }
const stream = new EventSource(`/api/world/stream?${query.toString()}`); const stream = new EventSource(`/api/world/stream?${query.toString()}`);

View File

@@ -115,12 +115,13 @@ function formatShipLocation(ship: ShipSnapshot) {
return `Docked ${dockedStation.label}`; return `Docked ${dockedStation.label}`;
} }
if (ship.spatialState.transit?.destinationNodeId) { const transitAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
return `Transit ${ship.systemId}`; if (transitAnchorId) {
return `Transit ${titleCase(transitAnchorId)}`;
} }
if (ship.celestialId) { if (ship.spatialState.currentAnchorId) {
return `Orbit ${titleCase(ship.celestialId)}`; return `Anchor ${compactAnchorId(ship.spatialState.currentAnchorId)}`;
} }
const system = systemById.value.get(ship.systemId); const system = systemById.value.get(ship.systemId);
@@ -129,13 +130,32 @@ function formatShipLocation(ship: ShipSnapshot) {
function formatStationLocation(station: StationSnapshot) { function formatStationLocation(station: StationSnapshot) {
const system = systemById.value.get(station.systemId); const system = systemById.value.get(station.systemId);
if (station.celestialId) { if (station.anchorId) {
return `${system?.label ?? station.systemId} · ${titleCase(station.celestialId)}`; return `${system?.label ?? station.systemId} · ${compactAnchorId(station.anchorId)}`;
} }
return system?.label ?? station.systemId; return system?.label ?? station.systemId;
} }
function compactAnchorId(value: string) {
const lagrangeMatch = value.match(/(l[1-5])$/i);
if (lagrangeMatch) {
return lagrangeMatch[1].toUpperCase();
}
const moonMatch = value.match(/moon-(\d+)$/i);
if (moonMatch) {
return `Moon ${moonMatch[1]}`;
}
const planetMatch = value.match(/planet-(\d+)$/i);
if (planetMatch) {
return `Planet ${planetMatch[1]}`;
}
return titleCase(value);
}
function shipAiStates(ship: ShipSnapshot) { function shipAiStates(ship: ShipSnapshot) {
const travelToken = ship.spatialState.transit ? "TRV" : ""; const travelToken = ship.spatialState.transit ? "TRV" : "";
const dockToken = ship.dockedStationId ? "DCK" : ""; const dockToken = ship.dockedStationId ? "DCK" : "";

View File

@@ -80,6 +80,14 @@ function formatPercent(value: number) {
return `${Math.round(value * 100)}%`; return `${Math.round(value * 100)}%`;
} }
function formatCargoTypeLabel(types: string[] | null | undefined) {
if (!types || types.length === 0) {
return "general";
}
return types.join(" / ");
}
function joinDetail(parts: Array<string | null | undefined>) { function joinDetail(parts: Array<string | null | undefined>) {
return parts.filter((part): part is string => !!part && part.trim().length > 0).join(" · "); return parts.filter((part): part is string => !!part && part.trim().length > 0).join(" · ");
} }
@@ -88,7 +96,7 @@ function describeOrderTarget(order: {
itemId?: string | null; itemId?: string | null;
targetEntityId?: string | null; targetEntityId?: string | null;
targetSystemId?: string | null; targetSystemId?: string | null;
nodeId?: string | null; anchorId?: string | null;
constructionSiteId?: string | null; constructionSiteId?: string | null;
sourceStationId?: string | null; sourceStationId?: string | null;
destinationStationId?: string | null; destinationStationId?: string | null;
@@ -97,7 +105,7 @@ function describeOrderTarget(order: {
return order.itemId return order.itemId
?? order.targetEntityId ?? order.targetEntityId
?? order.targetSystemId ?? order.targetSystemId
?? order.nodeId ?? order.anchorId
?? order.constructionSiteId ?? order.constructionSiteId
?? order.destinationStationId ?? order.destinationStationId
?? order.sourceStationId ?? order.sourceStationId
@@ -109,13 +117,15 @@ function describeSubTaskTarget(subTask: {
itemId?: string | null; itemId?: string | null;
targetEntityId?: string | null; targetEntityId?: string | null;
targetSystemId?: string | null; targetSystemId?: string | null;
targetNodeId?: string | null; targetAnchorId?: string | null;
targetResourceNodeId?: string | null;
moduleId?: string | null; moduleId?: string | null;
}) { }) {
return subTask.itemId return subTask.itemId
?? subTask.targetEntityId ?? subTask.targetEntityId
?? subTask.targetSystemId ?? subTask.targetSystemId
?? subTask.targetNodeId ?? subTask.targetAnchorId
?? subTask.targetResourceNodeId
?? subTask.moduleId ?? subTask.moduleId
?? "—"; ?? "—";
} }
@@ -184,8 +194,14 @@ const shipStatusRows = computed(() => {
return []; return [];
} }
const shipLocation = selectedShip.value.spatialState.currentAnchorId
?? selectedShip.value.anchorId
?? selectedShip.value.systemId
?? "unknown";
return [ return [
{ label: "State", value: titleCase(selectedShip.value.state) }, { label: "State", value: titleCase(selectedShip.value.state) },
{ label: "Location", value: shipLocation },
{ label: "Behavior", value: getShipBehaviorLabel(selectedShip.value.defaultBehavior.kind) }, { label: "Behavior", value: getShipBehaviorLabel(selectedShip.value.defaultBehavior.kind) },
{ label: "Control", value: titleCase(selectedShip.value.controlSourceKind) }, { label: "Control", value: titleCase(selectedShip.value.controlSourceKind) },
{ label: "Assignment", value: selectedShip.value.assignment?.kind ?? "unassigned" }, { label: "Assignment", value: selectedShip.value.assignment?.kind ?? "unassigned" },
@@ -201,20 +217,21 @@ const shipStatusRows = computed(() => {
]; ];
}); });
const shipCargoSummaryRows = computed(() => { const shipCargoBarRows = computed(() => {
if (!selectedShip.value) { if (!selectedShip.value) {
return []; return [];
} }
const usedCargo = selectedShip.value.inventory.reduce((sum, entry) => sum + entry.amount, 0); const usedCargo = selectedShip.value.inventory.reduce((sum, entry) => sum + entry.amount, 0);
return [ return [{
{ label: "Used", value: formatAmount(usedCargo) }, key: "cargo",
{ label: "Capacity", value: formatAmount(selectedShip.value.cargoCapacity) }, label: `${formatCargoTypeLabel(selectedShip.value.cargoTypes)}`,
{ label: "Free", value: formatAmount(Math.max(selectedShip.value.cargoCapacity - usedCargo, 0)) }, value: usedCargo,
{ label: "Travel", value: `${formatAmount(selectedShip.value.travelSpeed)} ${selectedShip.value.travelSpeedUnit}` }, valueLabel: formatAmount(usedCargo),
{ label: "Hull", value: formatAmount(selectedShip.value.health) }, max: selectedShip.value.cargoCapacity,
{ label: "Regime", value: titleCase(selectedShip.value.spatialState.movementRegime) }, maxLabel: formatAmount(selectedShip.value.cargoCapacity),
]; fillRatio: selectedShip.value.cargoCapacity > 0 ? usedCargo / selectedShip.value.cargoCapacity : 0,
}];
}); });
const shipCargoRows = computed(() => const shipCargoRows = computed(() =>
@@ -309,8 +326,13 @@ const stationStatusRows = computed(() => {
return []; return [];
} }
const stationLocation = selectedStation.value.anchorId
?? selectedStation.value.systemId
?? "unknown";
return [ return [
{ label: "Category", value: titleCase(selectedStation.value.category) }, { label: "Category", value: titleCase(selectedStation.value.category) },
{ label: "Location", value: stationLocation },
{ label: "Objective", value: titleCase(selectedStation.value.objective) }, { label: "Objective", value: titleCase(selectedStation.value.objective) },
{ label: "Docked", value: `${selectedStation.value.dockedShips} / ${selectedStation.value.dockingPads}` }, { label: "Docked", value: `${selectedStation.value.dockedShips} / ${selectedStation.value.dockingPads}` },
{ {
@@ -335,10 +357,12 @@ const stationModuleRows = computed(() =>
const stationStorageRows = computed(() => const stationStorageRows = computed(() =>
selectedStation.value?.storageUsage.map((entry) => ({ selectedStation.value?.storageUsage.map((entry) => ({
key: entry.storageClass, key: entry.storageClass,
storageClass: titleCase(entry.storageClass), label: titleCase(entry.storageClass),
used: formatAmount(entry.used), value: entry.used,
capacity: formatAmount(entry.capacity), valueLabel: formatAmount(entry.used),
fill: entry.capacity > 0 ? formatPercent(entry.used / entry.capacity) : "0%", max: entry.capacity,
maxLabel: formatAmount(entry.capacity),
fillRatio: entry.capacity > 0 ? entry.used / entry.capacity : 0,
})) ?? [], })) ?? [],
); );
@@ -429,7 +453,7 @@ async function saveBehavior() {
itemId: behaviorForm.kind === "local-auto-mine" itemId: behaviorForm.kind === "local-auto-mine"
? (behaviorForm.itemId.trim() || null) ? (behaviorForm.itemId.trim() || null)
: null, : null,
preferredNodeId: null, preferredAnchorId: null,
preferredConstructionSiteId: null, preferredConstructionSiteId: null,
preferredModuleId: null, preferredModuleId: null,
targetPosition: null, targetPosition: null,
@@ -461,7 +485,7 @@ async function queueHoldPositionOrder() {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: null, itemId: null,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 0, waitSeconds: 0,
@@ -497,7 +521,7 @@ async function queueMoveOrder() {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: null, itemId: null,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 0, waitSeconds: 0,
@@ -540,7 +564,7 @@ async function queueMineResourceOrder() {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId, itemId,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 0, waitSeconds: 0,
@@ -609,15 +633,20 @@ async function clearOrders() {
<div class="entity-inspector-section"> <div class="entity-inspector-section">
<h4>Cargo</h4> <h4>Cargo</h4>
<div class="entity-inspector-table-wrap"> <div class="entity-inspector-capacity-list">
<table class="entity-inspector-table entity-inspector-table--kv"> <div v-for="row in shipCargoBarRows" :key="row.key" class="entity-inspector-capacity">
<tbody> <div class="entity-inspector-capacity__header">
<tr v-for="row in shipCargoSummaryRows" :key="row.label"> <span class="entity-inspector-capacity__label">{{ row.label }}</span>
<th scope="row">{{ row.label }}</th> <span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
<td>{{ row.value }}</td> </div>
</tr> <div class="entity-inspector-capacity__scale">
</tbody> <span>0</span>
</table> <div class="entity-inspector-capacity__track">
<div class="entity-inspector-capacity__fill" :style="{ width: `${Math.max(0, Math.min(100, row.fillRatio * 100))}%` }"></div>
</div>
<span>{{ row.maxLabel }}</span>
</div>
</div>
</div> </div>
<div v-if="shipCargoRows.length > 0" class="entity-inspector-table-wrap"> <div v-if="shipCargoRows.length > 0" class="entity-inspector-table-wrap">
<table class="entity-inspector-table"> <table class="entity-inspector-table">
@@ -635,7 +664,7 @@ async function clearOrders() {
</tbody> </tbody>
</table> </table>
</div> </div>
<div v-else class="entity-inspector-empty">No cargo.</div> <div v-else class="entity-inspector-empty">No wares loaded.</div>
</div> </div>
<div class="entity-inspector-section"> <div class="entity-inspector-section">
@@ -856,25 +885,20 @@ async function clearOrders() {
<div class="entity-inspector-section"> <div class="entity-inspector-section">
<h4>Storage</h4> <h4>Storage</h4>
<div v-if="stationStorageRows.length > 0" class="entity-inspector-table-wrap"> <div v-if="stationStorageRows.length > 0" class="entity-inspector-capacity-list">
<table class="entity-inspector-table"> <div v-for="row in stationStorageRows" :key="row.key" class="entity-inspector-capacity">
<thead> <div class="entity-inspector-capacity__header">
<tr> <span class="entity-inspector-capacity__label">{{ row.label }}</span>
<th scope="col">Class</th> <span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
<th scope="col" class="entity-inspector-table__numeric">Used</th> </div>
<th scope="col" class="entity-inspector-table__numeric">Capacity</th> <div class="entity-inspector-capacity__scale">
<th scope="col" class="entity-inspector-table__numeric">Fill</th> <span>0</span>
</tr> <div class="entity-inspector-capacity__track">
</thead> <div class="entity-inspector-capacity__fill" :style="{ width: `${Math.max(0, Math.min(100, row.fillRatio * 100))}%` }"></div>
<tbody> </div>
<tr v-for="row in stationStorageRows" :key="row.key"> <span>{{ row.maxLabel }}</span>
<td>{{ row.storageClass }}</td> </div>
<td class="entity-inspector-table__numeric">{{ row.used }}</td> </div>
<td class="entity-inspector-table__numeric">{{ row.capacity }}</td>
<td class="entity-inspector-table__numeric">{{ row.fill }}</td>
</tr>
</tbody>
</table>
</div> </div>
<div v-if="stationInventoryRows.length > 0" class="entity-inspector-table-wrap"> <div v-if="stationInventoryRows.length > 0" class="entity-inspector-table-wrap">
<table class="entity-inspector-table"> <table class="entity-inspector-table">

View File

@@ -157,7 +157,7 @@ async function runAction(action: MenuAction) {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId, itemId,
nodeId: target.value.selection.kind === "node" ? target.value.selection.id : null, anchorId: target.value.anchorId ?? null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 0, waitSeconds: 0,
@@ -182,7 +182,7 @@ async function runAction(action: MenuAction) {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: null, itemId: null,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 8, waitSeconds: 8,
@@ -207,7 +207,7 @@ async function runAction(action: MenuAction) {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: null, itemId: null,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 6, waitSeconds: 6,
@@ -232,7 +232,7 @@ async function runAction(action: MenuAction) {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: null, itemId: null,
nodeId: null, anchorId: null,
constructionSiteId: null, constructionSiteId: null,
moduleId: null, moduleId: null,
waitSeconds: 0, waitSeconds: 0,

View File

@@ -149,13 +149,6 @@ 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 formatPercent(value: number | null | undefined) { function formatPercent(value: number | null | undefined) {
if (value == null || Number.isNaN(value)) return "—"; if (value == null || Number.isNaN(value)) return "—";
return `${Math.round(value * 100)}%`; return `${Math.round(value * 100)}%`;
@@ -281,8 +274,6 @@ type ShipRow = {
plan: string; plan: string;
step: string; step: string;
subtask: string; subtask: string;
cargo: number;
health: number;
}; };
const shipRows = computed<ShipRow[]>(() => const shipRows = computed<ShipRow[]>(() =>
@@ -305,8 +296,6 @@ const shipRows = computed<ShipRow[]>(() =>
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—", plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—", step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—",
subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—", subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—",
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
health: Math.round(s.health),
}; };
}), }),
); );
@@ -329,16 +318,11 @@ const shipColumns = [
shipColumnHelper.accessor("plan", { header: "Plan" }), shipColumnHelper.accessor("plan", { header: "Plan" }),
shipColumnHelper.accessor("step", { header: "Current Step" }), shipColumnHelper.accessor("step", { header: "Current Step" }),
shipColumnHelper.accessor("subtask", { header: "SubTask" }), shipColumnHelper.accessor("subtask", { header: "SubTask" }),
shipColumnHelper.accessor("cargo", {
header: "Cargo",
cell: (info) => formatCargoAmount(info.getValue()),
}),
shipColumnHelper.accessor("health", { header: "HP" }),
]; ];
const shipFilter = ref(""); const shipFilter = ref("");
const shipSorting = ref<SortingState>([]); const shipSorting = ref<SortingState>([]);
const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask", "cargo", "health"]); const shipOrder = useColumnOrder(["label", "class", "factionColor", "faction", "system", "state", "assignment", "behavior", "orders", "plan", "step", "subtask"]);
const shipTable = useVueTable({ const shipTable = useVueTable({
get data() { return shipRows.value; }, get data() { return shipRows.value; },
@@ -373,7 +357,6 @@ type StationRow = {
docked: string; docked: string;
orders: number; orders: number;
orderDetails: MarketOrderSnapshot[]; orderDetails: MarketOrderSnapshot[];
cargo: number;
modules: number; modules: number;
}; };
@@ -400,7 +383,6 @@ const stationRows = computed<StationRow[]>(() =>
const order = marketOrderMap.value.get(id); const order = marketOrderMap.value.get(id);
return order ? [order] : []; return order ? [order] : [];
}), }),
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
modules: s.installedModules.length, modules: s.installedModules.length,
})), })),
); );
@@ -421,16 +403,12 @@ const stationColumns = [
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",
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", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "cargo", "modules"]); const stationOrder = useColumnOrder(["label", "category", "objective", "factionColor", "faction", "system", "process", "workforce", "docked", "orders", "modules"]);
const stationTable = useVueTable({ const stationTable = useVueTable({
get data() { return stationRows.value; }, get data() { return stationRows.value; },

View File

@@ -251,7 +251,7 @@ const behaviorForm = reactive({
areaSystemId: "", areaSystemId: "",
targetEntityId: "", targetEntityId: "",
itemId: "", itemId: "",
preferredNodeId: "", preferredAnchorId: "",
preferredConstructionSiteId: "", preferredConstructionSiteId: "",
preferredModuleId: "", preferredModuleId: "",
waitSeconds: 3, waitSeconds: 3,
@@ -268,7 +268,7 @@ const orderForm = reactive({
targetEntityId: "", targetEntityId: "",
targetSystemId: "", targetSystemId: "",
itemId: "", itemId: "",
nodeId: "", anchorId: "",
constructionSiteId: "", constructionSiteId: "",
moduleId: "", moduleId: "",
waitSeconds: 3, waitSeconds: 3,
@@ -344,7 +344,7 @@ watch(selectedShip, (ship) => {
behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId; behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId;
behaviorForm.targetEntityId = ship.defaultBehavior.targetEntityId ?? ""; behaviorForm.targetEntityId = ship.defaultBehavior.targetEntityId ?? "";
behaviorForm.itemId = ship.defaultBehavior.itemId ?? ""; behaviorForm.itemId = ship.defaultBehavior.itemId ?? "";
behaviorForm.preferredNodeId = ship.defaultBehavior.preferredNodeId ?? ""; behaviorForm.preferredAnchorId = ship.defaultBehavior.preferredAnchorId ?? "";
behaviorForm.preferredConstructionSiteId = ship.defaultBehavior.preferredConstructionSiteId ?? ""; behaviorForm.preferredConstructionSiteId = ship.defaultBehavior.preferredConstructionSiteId ?? "";
behaviorForm.preferredModuleId = ship.defaultBehavior.preferredModuleId ?? ""; behaviorForm.preferredModuleId = ship.defaultBehavior.preferredModuleId ?? "";
behaviorForm.waitSeconds = ship.defaultBehavior.waitSeconds; behaviorForm.waitSeconds = ship.defaultBehavior.waitSeconds;
@@ -484,7 +484,7 @@ async function submitDirective() {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: directiveForm.itemId || null, itemId: directiveForm.itemId || null,
preferredNodeId: null, preferredAnchorId: null,
preferredConstructionSiteId: null, preferredConstructionSiteId: null,
preferredModuleId: null, preferredModuleId: null,
priority: directiveForm.priority, priority: directiveForm.priority,
@@ -612,7 +612,7 @@ async function submitDirectBehavior() {
areaSystemId: behaviorForm.areaSystemId || null, areaSystemId: behaviorForm.areaSystemId || null,
targetEntityId: behaviorForm.targetEntityId || null, targetEntityId: behaviorForm.targetEntityId || null,
itemId: behaviorForm.itemId || null, itemId: behaviorForm.itemId || null,
preferredNodeId: behaviorForm.preferredNodeId || null, preferredAnchorId: behaviorForm.preferredAnchorId || null,
preferredConstructionSiteId: behaviorForm.preferredConstructionSiteId || null, preferredConstructionSiteId: behaviorForm.preferredConstructionSiteId || null,
preferredModuleId: behaviorForm.preferredModuleId || null, preferredModuleId: behaviorForm.preferredModuleId || null,
targetPosition: null, targetPosition: null,
@@ -646,7 +646,7 @@ async function submitDirectOrder() {
sourceStationId: null, sourceStationId: null,
destinationStationId: null, destinationStationId: null,
itemId: orderForm.itemId || null, itemId: orderForm.itemId || null,
nodeId: orderForm.nodeId || null, anchorId: orderForm.anchorId || null,
constructionSiteId: orderForm.constructionSiteId || null, constructionSiteId: orderForm.constructionSiteId || null,
moduleId: orderForm.moduleId || null, moduleId: orderForm.moduleId || null,
waitSeconds: orderForm.waitSeconds, waitSeconds: orderForm.waitSeconds,
@@ -706,7 +706,7 @@ async function submitDirectOrder() {
<label><span>Area System</span><input v-model="behaviorForm.areaSystemId" type="text"></label> <label><span>Area System</span><input v-model="behaviorForm.areaSystemId" type="text"></label>
<label><span>Target Entity</span><input v-model="behaviorForm.targetEntityId" type="text"></label> <label><span>Target Entity</span><input v-model="behaviorForm.targetEntityId" type="text"></label>
<label><span>Item</span><input v-model="behaviorForm.itemId" type="text"></label> <label><span>Item</span><input v-model="behaviorForm.itemId" type="text"></label>
<label><span>Preferred Node</span><input v-model="behaviorForm.preferredNodeId" type="text"></label> <label><span>Preferred Anchor</span><input v-model="behaviorForm.preferredAnchorId" type="text"></label>
<label><span>Construction Site</span><input v-model="behaviorForm.preferredConstructionSiteId" type="text"></label> <label><span>Construction Site</span><input v-model="behaviorForm.preferredConstructionSiteId" type="text"></label>
<label><span>Module</span><input v-model="behaviorForm.preferredModuleId" type="text"></label> <label><span>Module</span><input v-model="behaviorForm.preferredModuleId" type="text"></label>
<label><span>Radius</span><input v-model.number="behaviorForm.radius" type="number" min="0" step="1"></label> <label><span>Radius</span><input v-model.number="behaviorForm.radius" type="number" min="0" step="1"></label>
@@ -723,7 +723,7 @@ async function submitDirectOrder() {
<label><span>Target System</span><input v-model="orderForm.targetSystemId" type="text"></label> <label><span>Target System</span><input v-model="orderForm.targetSystemId" type="text"></label>
<label><span>Target Entity</span><input v-model="orderForm.targetEntityId" type="text"></label> <label><span>Target Entity</span><input v-model="orderForm.targetEntityId" type="text"></label>
<label><span>Item</span><input v-model="orderForm.itemId" type="text"></label> <label><span>Item</span><input v-model="orderForm.itemId" type="text"></label>
<label><span>Node</span><input v-model="orderForm.nodeId" type="text"></label> <label><span>Anchor</span><input v-model="orderForm.anchorId" type="text"></label>
<label><span>Construction Site</span><input v-model="orderForm.constructionSiteId" type="text"></label> <label><span>Construction Site</span><input v-model="orderForm.constructionSiteId" type="text"></label>
<label><span>Module</span><input v-model="orderForm.moduleId" type="text"></label> <label><span>Module</span><input v-model="orderForm.moduleId" type="text"></label>
<label><span>Priority</span><input v-model.number="orderForm.priority" type="number" min="0" step="1"></label> <label><span>Priority</span><input v-model.number="orderForm.priority" type="number" min="0" step="1"></label>

View File

@@ -7,10 +7,13 @@ export type {
OrbitalSimulationSnapshot, OrbitalSimulationSnapshot,
} from "./contractsWorld"; } from "./contractsWorld";
export type { export type {
AnchorSnapshot,
AnchorDelta,
StarSnapshot, StarSnapshot,
MoonSnapshot, MoonSnapshot,
SystemSnapshot, SystemSnapshot,
PlanetSnapshot, PlanetSnapshot,
ResourceDepositSnapshot,
ResourceNodeSnapshot, ResourceNodeSnapshot,
ResourceNodeDelta, ResourceNodeDelta,
CelestialSnapshot, CelestialSnapshot,

View File

@@ -46,26 +46,50 @@ export interface PlanetSnapshot {
hasRing: boolean; hasRing: boolean;
} }
export interface ResourceDepositSnapshot {
id: string;
nodeId: string;
anchorId: string;
localPosition: Vector3Dto;
oreRemaining: number;
maxOre: number;
}
export interface ResourceNodeSnapshot { export interface ResourceNodeSnapshot {
id: string; id: string;
anchorId: string;
systemId: string; systemId: string;
localPosition: Vector3Dto; localPosition: Vector3Dto;
celestialId?: string | null; localSpaceRadius: number;
sourceKind: string; sourceKind: string;
oreRemaining: number; oreRemaining: number;
maxOre: number; maxOre: number;
itemId: string; itemId: string;
deposits: ResourceDepositSnapshot[];
} }
export interface ResourceNodeDelta extends ResourceNodeSnapshot {} export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
export interface AnchorSnapshot {
id: string;
systemId: string;
kind: string;
systemPosition: Vector3Dto;
localSpaceRadius: number;
parentAnchorId?: string | null;
occupyingStructureId?: string | null;
orbitReferenceId?: string | null;
}
export interface AnchorDelta extends AnchorSnapshot {}
export interface CelestialSnapshot { export interface CelestialSnapshot {
id: string; id: string;
systemId: string; systemId: string;
kind: string; kind: string;
orbitalAnchor: Vector3Dto; orbitalAnchor: Vector3Dto;
localSpaceRadius: number; localSpaceRadius: number;
parentNodeId?: string | null; parentAnchorId?: string | null;
occupyingStructureId?: string | null; occupyingStructureId?: string | null;
orbitReferenceId?: string | null; orbitReferenceId?: string | null;
} }

View File

@@ -93,7 +93,7 @@ export interface TerritoryClaimSnapshot {
sourceClaimId?: string | null; sourceClaimId?: string | null;
factionId: string; factionId: string;
systemId: string; systemId: string;
celestialId?: string | null; anchorId: string;
status: string; status: string;
claimKind: string; claimKind: string;
claimStrength: number; claimStrength: number;

View File

@@ -27,8 +27,8 @@ export interface StationSnapshot {
category: string; category: string;
objective: string; objective: string;
systemId: string; systemId: string;
anchorId?: string | null;
localPosition: Vector3Dto; localPosition: Vector3Dto;
celestialId?: string | null;
color: string; color: string;
dockedShips: number; dockedShips: number;
dockedShipIds: string[]; dockedShipIds: string[];
@@ -53,7 +53,7 @@ export interface ClaimSnapshot {
id: string; id: string;
factionId: string; factionId: string;
systemId: string; systemId: string;
celestialId: string; anchorId: string;
state: string; state: string;
health: number; health: number;
placedAtUtc: string; placedAtUtc: string;
@@ -66,7 +66,7 @@ export interface ConstructionSiteSnapshot {
id: string; id: string;
factionId: string; factionId: string;
systemId: string; systemId: string;
celestialId: string; anchorId: string;
targetKind: string; targetKind: string;
targetDefinitionId: string; targetDefinitionId: string;
blueprintId?: string | null; blueprintId?: string | null;

View File

@@ -207,7 +207,7 @@ export interface PlayerDirectiveSnapshot {
useOrders: boolean; useOrders: boolean;
stagingOrderKind?: string | null; stagingOrderKind?: string | null;
itemId?: string | null; itemId?: string | null;
preferredNodeId?: string | null; preferredAnchorId?: string | null;
preferredConstructionSiteId?: string | null; preferredConstructionSiteId?: string | null;
preferredModuleId?: string | null; preferredModuleId?: string | null;
priority: number; priority: number;

View File

@@ -24,7 +24,7 @@ export interface ShipOrderSnapshot {
sourceStationId?: string | null; sourceStationId?: string | null;
destinationStationId?: string | null; destinationStationId?: string | null;
itemId?: string | null; itemId?: string | null;
nodeId?: string | null; anchorId?: string | null;
constructionSiteId?: string | null; constructionSiteId?: string | null;
moduleId?: string | null; moduleId?: string | null;
waitSeconds: number; waitSeconds: number;
@@ -43,7 +43,7 @@ export interface ShipOrderTemplateSnapshot {
sourceStationId?: string | null; sourceStationId?: string | null;
destinationStationId?: string | null; destinationStationId?: string | null;
itemId?: string | null; itemId?: string | null;
nodeId?: string | null; anchorId?: string | null;
constructionSiteId?: string | null; constructionSiteId?: string | null;
moduleId?: string | null; moduleId?: string | null;
waitSeconds: number; waitSeconds: number;
@@ -59,7 +59,7 @@ export interface DefaultBehaviorSnapshot {
areaSystemId?: string | null; areaSystemId?: string | null;
targetEntityId?: string | null; targetEntityId?: string | null;
itemId?: string | null; itemId?: string | null;
preferredNodeId?: string | null; preferredAnchorId?: string | null;
preferredConstructionSiteId?: string | null; preferredConstructionSiteId?: string | null;
preferredModuleId?: string | null; preferredModuleId?: string | null;
targetPosition?: Vector3Dto | null; targetPosition?: Vector3Dto | null;
@@ -100,7 +100,9 @@ export interface ShipSubTaskSnapshot {
summary: string; summary: string;
targetEntityId?: string | null; targetEntityId?: string | null;
targetSystemId?: string | null; targetSystemId?: string | null;
targetNodeId?: string | null; targetAnchorId?: string | null;
targetResourceNodeId?: string | null;
targetResourceDepositId?: string | null;
targetPosition?: Vector3Dto | null; targetPosition?: Vector3Dto | null;
itemId?: string | null; itemId?: string | null;
moduleId?: string | null; moduleId?: string | null;
@@ -143,6 +145,7 @@ export interface ShipSnapshot {
purpose: string; purpose: string;
type: string; type: string;
systemId: string; systemId: string;
anchorId?: string | null;
localPosition: Vector3Dto; localPosition: Vector3Dto;
localVelocity: Vector3Dto; localVelocity: Vector3Dto;
targetLocalPosition: Vector3Dto; targetLocalPosition: Vector3Dto;
@@ -159,11 +162,11 @@ export interface ShipSnapshot {
controlReason?: string | null; controlReason?: string | null;
lastReplanReason?: string | null; lastReplanReason?: string | null;
lastAccessFailureReason?: string | null; lastAccessFailureReason?: string | null;
celestialId?: string | null;
dockedStationId?: string | null; dockedStationId?: string | null;
commanderId?: string | null; commanderId?: string | null;
policySetId?: string | null; policySetId?: string | null;
cargoCapacity: number; cargoCapacity: number;
cargoTypes: string[];
travelSpeed: number; travelSpeed: number;
travelSpeedUnit: string; travelSpeedUnit: string;
inventory: InventoryEntry[]; inventory: InventoryEntry[];
@@ -178,18 +181,18 @@ export interface ShipDelta extends ShipSnapshot {}
export interface ShipSpatialStateSnapshot { export interface ShipSpatialStateSnapshot {
spaceLayer: string; spaceLayer: string;
currentSystemId: string; currentSystemId: string;
currentCelestialId?: string | null; currentAnchorId?: string | null;
localPosition?: Vector3Dto | null; localPosition?: Vector3Dto | null;
systemPosition?: Vector3Dto | null; systemPosition?: Vector3Dto | null;
movementRegime: string; movementRegime: string;
destinationNodeId?: string | null; destinationAnchorId?: string | null;
transit?: ShipTransitSnapshot | null; transit?: ShipTransitSnapshot | null;
} }
export interface ShipTransitSnapshot { export interface ShipTransitSnapshot {
regime: string; regime: string;
originNodeId?: string | null; originAnchorId?: string | null;
destinationNodeId?: string | null; destinationAnchorId?: string | null;
startedAtUtc?: string | null; startedAtUtc?: string | null;
arrivalDueAtUtc?: string | null; arrivalDueAtUtc?: string | null;
progress: number; progress: number;

View File

@@ -9,6 +9,8 @@ import type {
FactionSnapshot, FactionSnapshot,
} from "./contractsFactions"; } from "./contractsFactions";
import type { import type {
AnchorDelta,
AnchorSnapshot,
CelestialDelta, CelestialDelta,
CelestialSnapshot, CelestialSnapshot,
ResourceNodeDelta, ResourceNodeDelta,
@@ -37,6 +39,7 @@ export interface WorldSnapshot {
generatedAtUtc: string; generatedAtUtc: string;
systems: SystemSnapshot[]; systems: SystemSnapshot[];
celestials: CelestialSnapshot[]; celestials: CelestialSnapshot[];
anchors: AnchorSnapshot[];
nodes: ResourceNodeSnapshot[]; nodes: ResourceNodeSnapshot[];
stations: import("./contractsInfrastructure").StationSnapshot[]; stations: import("./contractsInfrastructure").StationSnapshot[];
claims: ClaimSnapshot[]; claims: ClaimSnapshot[];
@@ -57,6 +60,7 @@ export interface WorldDelta {
requiresSnapshotRefresh: boolean; requiresSnapshotRefresh: boolean;
events: SimulationEventRecord[]; events: SimulationEventRecord[];
celestials: CelestialDelta[]; celestials: CelestialDelta[];
anchors: AnchorDelta[];
nodes: ResourceNodeDelta[]; nodes: ResourceNodeDelta[];
stations: import("./contractsInfrastructure").StationDelta[]; stations: import("./contractsInfrastructure").StationDelta[];
claims: ClaimDelta[]; claims: ClaimDelta[];
@@ -84,7 +88,7 @@ export interface SimulationEventRecord {
export interface ObserverScope { export interface ObserverScope {
scopeKind: string; scopeKind: string;
systemId?: string | null; systemId?: string | null;
celestialId?: string | null; anchorId?: string | null;
} }
export interface OrbitalSimulationSnapshot { export interface OrbitalSimulationSnapshot {

View File

@@ -44,7 +44,7 @@ export interface PlayerDirectiveCommandRequest {
sourceStationId?: string | null; sourceStationId?: string | null;
destinationStationId?: string | null; destinationStationId?: string | null;
itemId?: string | null; itemId?: string | null;
preferredNodeId?: string | null; preferredAnchorId?: string | null;
preferredConstructionSiteId?: string | null; preferredConstructionSiteId?: string | null;
preferredModuleId?: string | null; preferredModuleId?: string | null;
priority: number; priority: number;

View File

@@ -12,7 +12,7 @@ export interface ShipOrderCommandRequest {
sourceStationId?: string | null; sourceStationId?: string | null;
destinationStationId?: string | null; destinationStationId?: string | null;
itemId?: string | null; itemId?: string | null;
nodeId?: string | null; anchorId?: string | null;
constructionSiteId?: string | null; constructionSiteId?: string | null;
moduleId?: string | null; moduleId?: string | null;
waitSeconds?: number | null; waitSeconds?: number | null;
@@ -28,7 +28,7 @@ export interface ShipDefaultBehaviorCommandRequest {
areaSystemId?: string | null; areaSystemId?: string | null;
targetEntityId?: string | null; targetEntityId?: string | null;
itemId?: string | null; itemId?: string | null;
preferredNodeId?: string | null; preferredAnchorId?: string | null;
preferredConstructionSiteId?: string | null; preferredConstructionSiteId?: string | null;
preferredModuleId?: string | null; preferredModuleId?: string | null;
targetPosition?: Vector3Dto | null; targetPosition?: Vector3Dto | null;

View File

@@ -366,6 +366,74 @@ canvas {
backdrop-filter: none; backdrop-filter: none;
} }
.viewer-right-sidebar-dock {
position: absolute;
inset: 0 0 0 auto;
width: min(380px, 100vw);
padding: 0;
}
.viewer-right-sidebar {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
min-height: 0;
padding: 16px;
background:
linear-gradient(180deg, rgba(7, 14, 27, 0.9), rgba(7, 14, 27, 0.78)),
radial-gradient(circle at top right, rgba(127, 214, 255, 0.08), transparent 34%);
border-left: 1px solid rgba(132, 196, 255, 0.14);
backdrop-filter: blur(18px);
box-shadow: -18px 0 42px rgba(0, 0, 0, 0.18);
}
.viewer-right-sidebar__resize-handle {
position: absolute;
inset: 0 auto 0 -8px;
width: 16px;
cursor: ew-resize;
}
.viewer-right-sidebar__resize-handle::before {
content: "";
position: absolute;
inset: 0 6px;
background: rgba(132, 196, 255, 0.06);
transition: background 120ms ease;
}
.viewer-right-sidebar__resize-handle:hover::before,
.viewer-right-sidebar__resize-handle--active::before {
background: rgba(132, 196, 255, 0.18);
}
.viewer-right-sidebar__body {
display: flex;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.viewer-right-sidebar__panel.entity-inspector-panel {
height: 100%;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
backdrop-filter: none;
}
.viewer-right-sidebar__error {
margin-top: 0.9rem;
border-radius: 1rem;
background: rgba(255, 116, 88, 0.14);
padding: 0.85rem 0.95rem;
color: #ffd8cf;
}
.viewer-stats-overlay { .viewer-stats-overlay {
font-family: "IBM Plex Mono", "SFMono-Regular", monospace; font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.8rem; font-size: 0.8rem;
@@ -1705,6 +1773,62 @@ canvas {
color: rgba(173, 220, 255, 0.58); color: rgba(173, 220, 255, 0.58);
} }
.entity-inspector-capacity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.entity-inspector-capacity {
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.03);
padding: 0.8rem 0.9rem;
}
.entity-inspector-capacity__header,
.entity-inspector-capacity__scale {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.7rem;
}
.entity-inspector-capacity__header {
margin-bottom: 0.45rem;
}
.entity-inspector-capacity__label {
font-size: 0.74rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(173, 220, 255, 0.72);
}
.entity-inspector-capacity__value,
.entity-inspector-capacity__scale span {
font-family: var(--viewer-mono-font);
font-size: 0.76rem;
color: rgba(255, 255, 255, 0.78);
}
.entity-inspector-capacity__track {
position: relative;
flex: 1 1 auto;
min-width: 0;
height: 0.65rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.entity-inspector-capacity__fill {
position: absolute;
inset: 0 auto 0 0;
border-radius: inherit;
background: linear-gradient(90deg, rgba(116, 196, 255, 0.5), rgba(116, 196, 255, 0.9));
}
.entity-inspector-panel__fallback { .entity-inspector-panel__fallback {
margin-top: 0.9rem; margin-top: 0.9rem;
font-size: 0.83rem; font-size: 0.83rem;
@@ -1910,6 +2034,23 @@ canvas {
padding: 14px; padding: 14px;
} }
.viewer-right-sidebar-dock {
inset: auto 20px 148px 20px;
width: auto;
max-height: 38vh;
}
.viewer-right-sidebar {
padding: 14px;
border-left: none;
border-top: 1px solid rgba(132, 196, 255, 0.14);
box-shadow: 0 -18px 42px rgba(0, 0, 0, 0.18);
}
.viewer-right-sidebar__resize-handle {
display: none;
}
.viewer-stats-overlay-dock { .viewer-stats-overlay-dock {
top: 96px; top: 96px;
left: 20px; left: 20px;

View File

@@ -6,6 +6,7 @@ export interface ViewerOrderContextMenuTarget {
selection: Selectable; selection: Selectable;
label: string; label: string;
systemId?: string | null; systemId?: string | null;
anchorId?: string | null;
itemId?: string | null; itemId?: string | null;
targetPosition?: Vector3Dto | null; targetPosition?: Vector3Dto | null;
} }

View File

@@ -18,7 +18,7 @@ interface ResolveSelectionPositionParams {
nodeVisuals: Map<string, NodeVisual>; nodeVisuals: Map<string, NodeVisual>;
planetVisuals: PlanetVisual[]; planetVisuals: PlanetVisual[];
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3; computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3; resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
} }
interface FocusOnSelectionParams extends ResolveSelectionPositionParams { interface FocusOnSelectionParams extends ResolveSelectionPositionParams {
@@ -47,7 +47,7 @@ interface SeedSystemFocusParams {
nodeVisuals: Map<string, NodeVisual>; nodeVisuals: Map<string, NodeVisual>;
planetVisuals: PlanetVisual[]; planetVisuals: PlanetVisual[];
computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3; computeNodeLocalPosition: (visual: NodeVisual, timeSeconds: number) => THREE.Vector3;
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3; resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
} }
interface CameraFocusParams { interface CameraFocusParams {
@@ -107,6 +107,7 @@ export function applyPanFromScreenDelta(
delta: THREE.Vector2, delta: THREE.Vector2,
orbitYaw: number, orbitYaw: number,
currentDistance: number, currentDistance: number,
cameraFovDegrees: number,
povLevel: PovLevel, povLevel: PovLevel,
activeSystemId: string | undefined, activeSystemId: string | undefined,
systemAnchor: THREE.Vector3, systemAnchor: THREE.Vector3,
@@ -125,18 +126,19 @@ export function applyPanFromScreenDelta(
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw)); const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
const right = new THREE.Vector3(-forward.z, 0, forward.x); const right = new THREE.Vector3(-forward.z, 0, forward.x);
const pan = right.multiplyScalar(-normalized.x).add(forward.multiplyScalar(-normalized.y)); const visibleHeight = 2 * Math.tan(THREE.MathUtils.degToRad(cameraFovDegrees) * 0.5) * currentDistance;
const visibleWidth = visibleHeight * (safeWidth / safeHeight);
const horizontalDistance = normalized.x * visibleWidth;
const verticalDistance = -normalized.y * visibleHeight;
const pan = right.multiplyScalar(horizontalDistance).add(forward.multiplyScalar(verticalDistance));
if (activeSystemId) { if (activeSystemId) {
const scale = povLevel === "system" const systemDisplayToKilometers = 1 / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.35, KILOMETERS_PER_AU * 6.5) systemAnchor.addScaledVector(pan, systemDisplayToKilometers);
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 1200, 180000);
systemAnchor.addScaledVector(pan, scale);
return; return;
} }
const galaxyScale = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 1800, 22000); galaxyAnchor.add(pan);
galaxyAnchor.addScaledVector(pan, galaxyScale);
} }
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined { export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
@@ -235,11 +237,11 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
} }
if (selection.kind === "claim") { if (selection.kind === "claim") {
const claim = world.claims.get(selection.id); const claim = world.claims.get(selection.id);
return claim ? resolvePointPosition(claim.systemId, claim.celestialId) : undefined; return claim ? resolvePointPosition(claim.systemId, null, claim.anchorId) : undefined;
} }
if (selection.kind === "construction-site") { if (selection.kind === "construction-site") {
const site = world.constructionSites.get(selection.id); const site = world.constructionSites.get(selection.id);
return site ? resolvePointPosition(site.systemId, site.celestialId) : undefined; return site ? resolvePointPosition(site.systemId, null, site.anchorId) : undefined;
} }
if (selection.kind === "planet") { if (selection.kind === "planet") {
const system = world.systems.get(selection.systemId); const system = world.systems.get(selection.systemId);

View File

@@ -30,8 +30,14 @@ export function createViewerControllers(host: any) {
claimGroup: host.systemLayer.claimGroup, claimGroup: host.systemLayer.claimGroup,
constructionSiteGroup: host.systemLayer.constructionSiteGroup, constructionSiteGroup: host.systemLayer.constructionSiteGroup,
shipGroup: host.systemLayer.shipGroup, shipGroup: host.systemLayer.shipGroup,
localNodeGroup: host.localLayer.nodeGroup,
localStationGroup: host.localLayer.stationGroup,
localClaimGroup: host.localLayer.claimGroup,
localConstructionSiteGroup: host.localLayer.constructionSiteGroup,
localShipGroup: host.localLayer.shipGroup,
galaxySelectableTargets: host.galaxyLayer.selectableTargets, galaxySelectableTargets: host.galaxyLayer.selectableTargets,
systemSelectableTargets: host.systemLayer.selectableTargets, systemSelectableTargets: host.systemLayer.selectableTargets,
localSelectableTargets: host.localLayer.selectableTargets,
systemVisuals: host.galaxyLayer.systemVisuals, systemVisuals: host.galaxyLayer.systemVisuals,
planetVisuals: host.systemLayer.planetVisuals, planetVisuals: host.systemLayer.planetVisuals,
celestialVisuals: host.systemLayer.celestialVisuals, celestialVisuals: host.systemLayer.celestialVisuals,
@@ -40,6 +46,11 @@ export function createViewerControllers(host: any) {
claimVisuals: host.systemLayer.claimVisuals, claimVisuals: host.systemLayer.claimVisuals,
constructionSiteVisuals: host.systemLayer.constructionSiteVisuals, constructionSiteVisuals: host.systemLayer.constructionSiteVisuals,
shipVisuals: host.systemLayer.shipVisuals, shipVisuals: host.systemLayer.shipVisuals,
localNodeVisuals: host.localLayer.nodeVisuals,
localStationVisuals: host.localLayer.stationVisuals,
localClaimVisuals: host.localLayer.claimVisuals,
localConstructionSiteVisuals: host.localLayer.constructionSiteVisuals,
localShipVisuals: host.localLayer.shipVisuals,
}); });
const navigationController = new ViewerNavigationController({ const navigationController = new ViewerNavigationController({
@@ -152,8 +163,9 @@ export function createViewerControllers(host: any) {
applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims), applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims),
applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites), applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites),
applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs), applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs),
refreshLocalLayer: () => sceneDataController.refreshLocalLayer(host.world, host.resolveFocusedAnchorId()),
refreshHistoryWindows: () => host.refreshHistoryWindows(), refreshHistoryWindows: () => host.refreshHistoryWindows(),
resolveFocusedCelestialId: () => host.resolveFocusedCelestialId(), resolveFocusedAnchorId: () => host.resolveFocusedAnchorId(),
updateSystemSummaries: () => host.updateSystemSummaries(), updateSystemSummaries: () => host.updateSystemSummaries(),
applyZoomPresentation: () => presentationController.applyZoomPresentation(), applyZoomPresentation: () => presentationController.applyZoomPresentation(),
updateNetworkPanel: () => presentationController.updateNetworkPanel(), updateNetworkPanel: () => presentationController.updateNetworkPanel(),
@@ -191,8 +203,10 @@ export function createViewerControllers(host: any) {
mouse: host.mouse, mouse: host.mouse,
galaxyCamera: host.galaxyLayer.camera, galaxyCamera: host.galaxyLayer.camera,
systemCamera: host.systemLayer.camera, systemCamera: host.systemLayer.camera,
localCamera: host.localLayer.camera,
galaxySelectableTargets: host.galaxyLayer.selectableTargets, galaxySelectableTargets: host.galaxyLayer.selectableTargets,
systemSelectableTargets: host.systemLayer.selectableTargets, systemSelectableTargets: host.systemLayer.selectableTargets,
localSelectableTargets: host.localLayer.selectableTargets,
hoverLabelEl: host.hoverLabelEl, hoverLabelEl: host.hoverLabelEl,
hoverConnectorLineEl: host.hoverConnectorLineEl, hoverConnectorLineEl: host.hoverConnectorLineEl,
marqueeEl: host.marqueeEl, marqueeEl: host.marqueeEl,
@@ -244,6 +258,9 @@ export function createViewerControllers(host: any) {
delta, delta,
host.orbitYaw, host.orbitYaw,
host.currentDistance, host.currentDistance,
host.activeSystemId
? (host.povLevel === "local" ? host.localLayer.camera.fov : host.systemLayer.camera.fov)
: host.galaxyLayer.camera.fov,
host.povLevel, host.povLevel,
host.activeSystemId, host.activeSystemId,
host.systemAnchor, host.systemAnchor,

View File

@@ -148,9 +148,11 @@ export function updateFollowCamera(params: {
if (ship.spatialState.movementRegime === "ftl-transit") { if (ship.spatialState.movementRegime === "ftl-transit") {
systemAnchor.set(0, 0, 0); systemAnchor.set(0, 0, 0);
const destinationNodeId = ship.spatialState.transit?.destinationNodeId; const destinationAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
const destinationCelestial = destinationNodeId ? world.celestials.get(destinationNodeId) : undefined; const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
const destinationSystem = destinationCelestial ? world.systems.get(destinationCelestial.systemId) : undefined; const destinationSystem = destinationAnchor
? world.systems.get(destinationAnchor.systemId)
: undefined;
const originSystem = world.systems.get(ship.systemId); const originSystem = world.systems.get(ship.systemId);
if (originSystem && destinationSystem) { if (originSystem && destinationSystem) {
followCameraDesiredDirection followCameraDesiredDirection

View File

@@ -36,14 +36,17 @@ export function pickSelectableAtClientPosition(
renderer: THREE.WebGLRenderer, renderer: THREE.WebGLRenderer,
raycaster: THREE.Raycaster, raycaster: THREE.Raycaster,
mouse: THREE.Vector2, mouse: THREE.Vector2,
povLevel: PovLevel,
galaxyCamera: THREE.Camera, galaxyCamera: THREE.Camera,
galaxySelectableTargets: Map<THREE.Object3D, Selectable>, galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
systemCamera: THREE.Camera, systemCamera: THREE.Camera,
systemSelectableTargets: Map<THREE.Object3D, Selectable>, systemSelectableTargets: Map<THREE.Object3D, Selectable>,
localCamera: THREE.Camera,
localSelectableTargets: Map<THREE.Object3D, Selectable>,
clientX: number, clientX: number,
clientY: number, clientY: number,
) { ) {
const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, clientX, clientY); const hit = pickSelectableHitAtClientPosition(renderer, raycaster, mouse, povLevel, galaxyCamera, galaxySelectableTargets, systemCamera, systemSelectableTargets, localCamera, localSelectableTargets, clientX, clientY);
return hit?.selection; return hit?.selection;
} }
@@ -51,13 +54,23 @@ export function pickSelectableHitAtClientPosition(
renderer: THREE.WebGLRenderer, renderer: THREE.WebGLRenderer,
raycaster: THREE.Raycaster, raycaster: THREE.Raycaster,
mouse: THREE.Vector2, mouse: THREE.Vector2,
povLevel: PovLevel,
galaxyCamera: THREE.Camera, galaxyCamera: THREE.Camera,
galaxySelectableTargets: Map<THREE.Object3D, Selectable>, galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
systemCamera: THREE.Camera, systemCamera: THREE.Camera,
systemSelectableTargets: Map<THREE.Object3D, Selectable>, systemSelectableTargets: Map<THREE.Object3D, Selectable>,
localCamera: THREE.Camera,
localSelectableTargets: Map<THREE.Object3D, Selectable>,
clientX: number, clientX: number,
clientY: number, clientY: number,
): HoverPickResult | undefined { ): HoverPickResult | undefined {
if (povLevel === "local") {
const localHit = pickOneCamera(renderer, raycaster, mouse, localCamera, localSelectableTargets, clientX, clientY);
if (localHit) {
return localHit;
}
}
// Try system camera first (higher priority when in a system) // Try system camera first (higher priority when in a system)
const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY); const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY);
if (systemHit) { if (systemHit) {

View File

@@ -28,8 +28,10 @@ export interface ViewerInteractionContext {
mouse: THREE.Vector2; mouse: THREE.Vector2;
galaxyCamera: THREE.PerspectiveCamera; galaxyCamera: THREE.PerspectiveCamera;
systemCamera: THREE.PerspectiveCamera; systemCamera: THREE.PerspectiveCamera;
localCamera: THREE.PerspectiveCamera;
galaxySelectableTargets: Map<THREE.Object3D, Selectable>; galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
systemSelectableTargets: Map<THREE.Object3D, Selectable>; systemSelectableTargets: Map<THREE.Object3D, Selectable>;
localSelectableTargets: Map<THREE.Object3D, Selectable>;
hoverLabelEl: HTMLDivElement; hoverLabelEl: HTMLDivElement;
hoverConnectorLineEl: SVGLineElement; hoverConnectorLineEl: SVGLineElement;
marqueeEl: HTMLDivElement; marqueeEl: HTMLDivElement;
@@ -391,10 +393,13 @@ export class ViewerInteractionController {
this.context.renderer, this.context.renderer,
this.context.raycaster, this.context.raycaster,
this.context.mouse, this.context.mouse,
this.context.getPovLevel(),
this.context.galaxyCamera, this.context.galaxyCamera,
this.context.galaxySelectableTargets, this.context.galaxySelectableTargets,
this.context.systemCamera, this.context.systemCamera,
this.context.systemSelectableTargets, this.context.systemSelectableTargets,
this.context.localCamera,
this.context.localSelectableTargets,
clientX, clientX,
clientY, clientY,
); );
@@ -405,10 +410,13 @@ export class ViewerInteractionController {
this.context.renderer, this.context.renderer,
this.context.raycaster, this.context.raycaster,
this.context.mouse, this.context.mouse,
this.context.getPovLevel(),
this.context.galaxyCamera, this.context.galaxyCamera,
this.context.galaxySelectableTargets, this.context.galaxySelectableTargets,
this.context.systemCamera, this.context.systemCamera,
this.context.systemSelectableTargets, this.context.systemSelectableTargets,
this.context.localCamera,
this.context.localSelectableTargets,
clientX, clientX,
clientY, clientY,
); );
@@ -466,6 +474,7 @@ export class ViewerInteractionController {
selection, selection,
label: node.itemId, label: node.itemId,
systemId: node.systemId, systemId: node.systemId,
anchorId: node.anchorId,
itemId: node.itemId, itemId: node.itemId,
targetPosition: node.localPosition, targetPosition: node.localPosition,
} : null; } : null;

View File

@@ -1,17 +1,50 @@
import * as THREE from "three"; import * as THREE from "three";
import type {
ClaimVisual,
ConstructionSiteVisual,
NodeVisual,
Selectable,
ShipVisual,
StructureVisual,
} from "./viewerTypes";
/** /**
* Local rendering layer. * Local rendering layer.
* Scene coordinate unit: reserved for future close-up detail. * Scene coordinate unit: reserved for future close-up detail.
* Camera far plane covers immediate surroundings. * Camera far plane covers immediate surroundings.
* Currently empty — populated when local-space objects are introduced.
*/ */
export class LocalLayer { export class LocalLayer {
readonly scene = new THREE.Scene(); readonly scene = new THREE.Scene();
readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 2000); readonly camera = new THREE.PerspectiveCamera(50, 1, 0.1, 2000);
readonly nodeGroup = new THREE.Group();
readonly stationGroup = new THREE.Group();
readonly claimGroup = new THREE.Group();
readonly constructionSiteGroup = new THREE.Group();
readonly shipGroup = new THREE.Group();
readonly selectableTargets = new Map<THREE.Object3D, Selectable>();
readonly shipVisuals = new Map<string, ShipVisual>();
readonly nodeVisuals = new Map<string, NodeVisual>();
readonly stationVisuals = new Map<string, StructureVisual>();
readonly claimVisuals = new Map<string, ClaimVisual>();
readonly constructionSiteVisuals = new Map<string, ConstructionSiteVisual>();
private static readonly ORIGIN = new THREE.Vector3(0, 0, 0); private static readonly ORIGIN = new THREE.Vector3(0, 0, 0);
constructor() {
this.scene.add(new THREE.AmbientLight(0x90a6c0, 0.8));
const keyLight = new THREE.DirectionalLight(0xdcecff, 1.4);
keyLight.position.set(180, 220, 140);
this.scene.add(keyLight);
this.scene.add(
this.nodeGroup,
this.stationGroup,
this.claimGroup,
this.constructionSiteGroup,
this.shipGroup,
);
}
updateCamera(orbitOffset: THREE.Vector3) { updateCamera(orbitOffset: THREE.Vector3) {
this.camera.position.copy(orbitOffset); this.camera.position.copy(orbitOffset);
this.camera.lookAt(LocalLayer.ORIGIN); this.camera.lookAt(LocalLayer.ORIGIN);

View File

@@ -1,4 +1,5 @@
import * as THREE from "three"; import * as THREE from "three";
import { scaleGalaxyVector, toThreeVector } from "./viewerMath";
import { import {
determineActiveSystemId, determineActiveSystemId,
focusOnSelection, focusOnSelection,
@@ -62,6 +63,22 @@ export interface ViewerNavigationContext {
export class ViewerNavigationController { export class ViewerNavigationController {
constructor(private readonly context: ViewerNavigationContext) {} constructor(private readonly context: ViewerNavigationContext) {}
syncGalaxyAnchorToActiveSystem() {
const world = this.context.getWorld();
const activeSystemId = this.context.getActiveSystemId();
const povLevel = this.context.getPovLevel();
if (!world || !activeSystemId || povLevel === "galaxy") {
return;
}
const system = world.systems.get(activeSystemId);
if (!system) {
return;
}
this.context.galaxyAnchor.copy(scaleGalaxyVector(toThreeVector(system.galaxyPosition)));
}
focusOnSelection(selection: Selectable) { focusOnSelection(selection: Selectable) {
focusOnSelection({ focusOnSelection({
world: this.context.getWorld(), world: this.context.getWorld(),
@@ -70,7 +87,7 @@ export class ViewerNavigationController {
nodeVisuals: this.context.nodeVisuals, nodeVisuals: this.context.nodeVisuals,
planetVisuals: this.context.planetVisuals, planetVisuals: this.context.planetVisuals,
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId), resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId),
activeSystemId: this.context.getActiveSystemId(), activeSystemId: this.context.getActiveSystemId(),
galaxyAnchor: this.context.galaxyAnchor, galaxyAnchor: this.context.galaxyAnchor,
systemAnchor: this.context.systemAnchor, systemAnchor: this.context.systemAnchor,
@@ -85,7 +102,7 @@ export class ViewerNavigationController {
nodeVisuals: this.context.nodeVisuals, nodeVisuals: this.context.nodeVisuals,
planetVisuals: this.context.planetVisuals, planetVisuals: this.context.planetVisuals,
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
resolvePointPosition: (systemId, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId), resolvePointPosition: (systemId, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemId, celestialId, anchorId),
}); });
} }
@@ -171,7 +188,7 @@ export class ViewerNavigationController {
nodeVisuals: this.context.nodeVisuals, nodeVisuals: this.context.nodeVisuals,
planetVisuals: this.context.planetVisuals, planetVisuals: this.context.planetVisuals,
computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds), computeNodeLocalPosition: (node, timeSeconds) => computeNodeLocalPosition(this.context.createWorldPresentationContext(), node, timeSeconds),
resolvePointPosition: (systemIdValue, celestialId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId), resolvePointPosition: (systemIdValue, celestialId, anchorId) => resolvePointPosition(this.context.createWorldPresentationContext(), systemIdValue, celestialId, anchorId),
}); });
} }

View File

@@ -52,7 +52,7 @@ const moduleProductionById = new Map<string, {
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]),
); );
import { describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection"; import { describeAnchorPathWithinSystem, describeCelestialPathWithinSystem, describeOrbitalParent, describeSelectable, describeShipBehavior, describeShipCurrentAction, describeShipOrder, describeShipState, getSelectionGroup, renderSystemDetails } from "./viewerSelection";
import type { import type {
CameraMode, CameraMode,
NodeVisual, NodeVisual,
@@ -461,7 +461,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
title: `${celestial.kind} celestial`, title: `${celestial.kind} celestial`,
bodyHtml: ` bodyHtml: `
<p>${celestial.systemId}</p> <p>${celestial.systemId}</p>
<p>Parent ${celestial.parentNodeId ?? "none"}<br>Orbit ref ${celestial.orbitReferenceId ?? "none"}</p> <p>Parent ${celestial.parentAnchorId ?? "none"}<br>Orbit ref ${celestial.orbitReferenceId ?? "none"}</p>
<p>Occupying structure ${celestial.occupyingStructureId ?? "none"}</p> <p>Occupying structure ${celestial.occupyingStructureId ?? "none"}</p>
<p>Local space radius ${celestial.localSpaceRadius.toFixed(0)} km</p> <p>Local space radius ${celestial.localSpaceRadius.toFixed(0)} km</p>
`, `,
@@ -477,7 +477,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
title: `Claim ${claim.id}`, title: `Claim ${claim.id}`,
bodyHtml: ` bodyHtml: `
<p>${claim.systemId}</p> <p>${claim.systemId}</p>
<p>Celestial ${claim.celestialId}</p> <p>Anchor ${describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId) ?? claim.anchorId}</p>
<p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p> <p>State ${claim.state}<br>Health ${claim.health.toFixed(0)}</p>
<p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p> <p>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
`, `,
@@ -494,7 +494,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
title: `Construction ${site.id}`, title: `Construction ${site.id}`,
bodyHtml: ` bodyHtml: `
<p>${site.systemId}</p> <p>${site.systemId}</p>
<p>Celestial ${site.celestialId}</p> <p>Anchor ${describeAnchorPathWithinSystem(world, site.systemId, site.anchorId) ?? site.anchorId}</p>
<p>${site.targetKind} ${site.targetDefinitionId}</p> <p>${site.targetKind} ${site.targetDefinitionId}</p>
<p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p> <p>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p> <p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
@@ -608,24 +608,33 @@ export function describeSelectionParent(
return "unknown"; return "unknown";
} }
return station.celestialId return station.anchorId
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId) ?? `${station.systemId} network` ? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId) ?? `${station.systemId} network`
: "unknown"; : "unknown";
} }
if (selection.kind === "node") { if (selection.kind === "node") {
const node = world.nodes.get(selection.id); const node = world.nodes.get(selection.id);
const visual = node ? nodeVisuals.get(selection.id) : undefined; return node
return describeOrbitalParent(world, node?.systemId, visual?.anchor); ? describeAnchorPathWithinSystem(world, node.systemId, node.anchorId) ?? node.anchorId
: "unknown";
} }
if (selection.kind === "celestial") { if (selection.kind === "celestial") {
const celestial = world.celestials.get(selection.id); const celestial = world.celestials.get(selection.id);
return celestial?.parentNodeId ?? `${celestial?.systemId ?? "unknown"} network`; return celestial
? describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id) ?? `${celestial.systemId} network`
: "unknown";
} }
if (selection.kind === "claim") { if (selection.kind === "claim") {
return world.claims.get(selection.id)?.celestialId ?? "unknown"; const claim = world.claims.get(selection.id);
return claim
? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId) ?? claim.anchorId
: "unknown";
} }
if (selection.kind === "construction-site") { if (selection.kind === "construction-site") {
return world.constructionSites.get(selection.id)?.celestialId ?? "unknown"; const site = world.constructionSites.get(selection.id);
return site
? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId) ?? site.anchorId
: "unknown";
} }
return "unknown"; return "unknown";

View File

@@ -14,6 +14,16 @@ import {
syncShips as syncShipScene, syncShips as syncShipScene,
syncStations as syncStationScene, syncStations as syncStationScene,
} from "./viewerSceneSync"; } from "./viewerSceneSync";
import {
createClaimMesh,
createConstructionSiteMesh,
createNodeMesh,
createResourceDepositMesh,
createShipMesh,
createShipTacticalIcon,
createStationMesh,
createTacticalIcon,
} from "./viewerSceneFactory";
import { import {
deriveNodeOrbital, deriveNodeOrbital,
deriveOrbitalFromLocalPosition, deriveOrbitalFromLocalPosition,
@@ -43,7 +53,7 @@ import type {
SystemSnapshot, SystemSnapshot,
} from "./contracts"; } from "./contracts";
import type { OrbitalAnchor, Selectable } from "./viewerTypes"; import type { OrbitalAnchor, Selectable } from "./viewerTypes";
import { rawObject } from "./viewerScenePrimitives"; import { rawObject, registerSelectableTarget } from "./viewerScenePrimitives";
export interface ViewerSceneDataContext { export interface ViewerSceneDataContext {
documentRef: Document; documentRef: Document;
@@ -61,8 +71,14 @@ export interface ViewerSceneDataContext {
claimGroup: THREE.Group; claimGroup: THREE.Group;
constructionSiteGroup: THREE.Group; constructionSiteGroup: THREE.Group;
shipGroup: THREE.Group; shipGroup: THREE.Group;
localNodeGroup: THREE.Group;
localStationGroup: THREE.Group;
localClaimGroup: THREE.Group;
localConstructionSiteGroup: THREE.Group;
localShipGroup: THREE.Group;
galaxySelectableTargets: Map<THREE.Object3D, Selectable>; galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
systemSelectableTargets: Map<THREE.Object3D, Selectable>; systemSelectableTargets: Map<THREE.Object3D, Selectable>;
localSelectableTargets: Map<THREE.Object3D, Selectable>;
systemVisuals: Map<any, any>; systemVisuals: Map<any, any>;
planetVisuals: any[]; planetVisuals: any[];
celestialVisuals: Map<any, any>; celestialVisuals: Map<any, any>;
@@ -71,6 +87,11 @@ export interface ViewerSceneDataContext {
claimVisuals: Map<any, any>; claimVisuals: Map<any, any>;
constructionSiteVisuals: Map<any, any>; constructionSiteVisuals: Map<any, any>;
shipVisuals: Map<any, any>; shipVisuals: Map<any, any>;
localNodeVisuals: Map<any, any>;
localStationVisuals: Map<any, any>;
localClaimVisuals: Map<any, any>;
localConstructionSiteVisuals: Map<any, any>;
localShipVisuals: Map<any, any>;
} }
export class ViewerSceneDataController { export class ViewerSceneDataController {
@@ -136,6 +157,162 @@ export class ViewerSceneDataController {
applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId()); applyShipDeltaUpdates(this.createSceneSyncContext(), ships, tickIntervalMs, this.context.getActiveSystemId());
} }
refreshLocalLayer(world: any, focusedAnchorId?: string) {
this.context.localNodeGroup.clear();
this.context.localStationGroup.clear();
this.context.localClaimGroup.clear();
this.context.localConstructionSiteGroup.clear();
this.context.localShipGroup.clear();
this.context.localSelectableTargets.clear();
this.context.localNodeVisuals.clear();
this.context.localStationVisuals.clear();
this.context.localClaimVisuals.clear();
this.context.localConstructionSiteVisuals.clear();
this.context.localShipVisuals.clear();
if (!world || !focusedAnchorId) {
return;
}
const activeSystemId = this.context.getActiveSystemId();
const inFocusedSystem = (systemId: string) => !activeSystemId || systemId === activeSystemId;
for (const node of world.nodes.values()) {
if (node.anchorId !== focusedAnchorId || !inFocusedSystem(node.systemId)) {
continue;
}
const mesh = createNodeMesh(node);
const icon = createTacticalIcon(this.context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
const localPosition = new THREE.Vector3(0, 0, 0);
mesh.setPosition(localPosition);
icon.setPosition(localPosition);
this.context.localNodeVisuals.set(node.id, {
systemId: node.systemId,
anchorId: node.anchorId,
mesh,
icon,
sourceKind: node.sourceKind,
anchor: { kind: "star" as const },
localPosition,
orbitRadius: 0,
orbitPhase: 0,
orbitInclination: 0,
});
this.context.localNodeGroup.add(rawObject(mesh), rawObject(icon));
for (const deposit of node.deposits) {
const depositMesh = createResourceDepositMesh(deposit, node);
this.context.localNodeGroup.add(rawObject(depositMesh));
}
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "node", id: node.id });
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "node", id: node.id });
}
for (const station of world.stations.values()) {
if (station.anchorId !== focusedAnchorId || !inFocusedSystem(station.systemId)) {
continue;
}
const mesh = createStationMesh(station);
const icon = createTacticalIcon(this.context.documentRef, station.color, 130);
const localPosition = new THREE.Vector3(station.localPosition.x, station.localPosition.y, station.localPosition.z);
mesh.setPosition(localPosition);
icon.setPosition(localPosition);
this.context.localStationVisuals.set(station.id, {
id: station.id,
systemId: station.systemId,
anchorId: station.anchorId,
mesh,
icon,
anchor: { kind: "star" as const },
orbitRadius: 0,
orbitPhase: 0,
orbitInclination: 0,
localPosition,
});
this.context.localStationGroup.add(rawObject(mesh), rawObject(icon));
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "station", id: station.id });
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "station", id: station.id });
}
for (const claim of world.claims.values()) {
if (claim.anchorId !== focusedAnchorId || !inFocusedSystem(claim.systemId)) {
continue;
}
const mesh = createClaimMesh(claim);
const icon = createTacticalIcon(this.context.documentRef, "#ff5b5b", 90);
const localPosition = new THREE.Vector3(0, 0, 0);
mesh.setPosition(localPosition);
icon.setPosition(localPosition);
this.context.localClaimVisuals.set(claim.id, {
id: claim.id,
anchorId: claim.anchorId,
systemId: claim.systemId,
mesh,
icon,
localPosition,
});
this.context.localClaimGroup.add(rawObject(mesh), rawObject(icon));
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "claim", id: claim.id });
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "claim", id: claim.id });
}
for (const site of world.constructionSites.values()) {
if (site.anchorId !== focusedAnchorId || !inFocusedSystem(site.systemId)) {
continue;
}
const mesh = createConstructionSiteMesh(site);
const icon = createTacticalIcon(this.context.documentRef, "#9df29c", 90);
const localPosition = new THREE.Vector3(0, 0, 0);
mesh.setPosition(localPosition);
icon.setPosition(localPosition);
this.context.localConstructionSiteVisuals.set(site.id, {
id: site.id,
anchorId: site.anchorId,
systemId: site.systemId,
mesh,
icon,
localPosition,
});
this.context.localConstructionSiteGroup.add(rawObject(mesh), rawObject(icon));
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "construction-site", id: site.id });
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "construction-site", id: site.id });
}
for (const ship of world.ships.values()) {
const shipAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
if (shipAnchorId !== focusedAnchorId || !inFocusedSystem(ship.systemId) || ship.spatialState.spaceLayer !== "local-space") {
continue;
}
const shipColor = shipPresentationColor(ship);
const mesh = createShipMesh(ship, shipSize(ship), shipLength(ship), shipColor);
const icon = createShipTacticalIcon(this.context.documentRef, shipColor, 78);
const localPosition = new THREE.Vector3(ship.localPosition.x, ship.localPosition.y, ship.localPosition.z);
mesh.setPosition(localPosition);
icon.setPosition(localPosition);
icon.setColor(shipColor);
this.context.localShipVisuals.set(ship.id, {
systemId: ship.systemId,
anchorId: shipAnchorId,
mesh,
icon,
iconBaseScale: 78,
startPosition: localPosition.clone(),
authoritativePosition: localPosition.clone(),
targetPosition: new THREE.Vector3(ship.targetLocalPosition.x, ship.targetLocalPosition.y, ship.targetLocalPosition.z),
velocity: new THREE.Vector3(ship.localVelocity.x, ship.localVelocity.y, ship.localVelocity.z),
receivedAtMs: performance.now(),
blendDurationMs: Math.max(world.tickIntervalMs ?? 80, 80),
});
this.context.localShipGroup.add(rawObject(mesh), rawObject(icon));
registerSelectableTarget(this.context.localSelectableTargets, mesh, { kind: "ship", id: ship.id });
registerSelectableTarget(this.context.localSelectableTargets, icon, { kind: "ship", id: ship.id });
}
}
/** /**
* Called when the active system changes. Swaps which system's root is in systemScene * Called when the active system changes. Swaps which system's root is in systemScene
* and updates visibility of all system-filtered objects. * and updates visibility of all system-filtered objects.
@@ -202,6 +379,7 @@ export class ViewerSceneDataController {
createWorldPresentationContext(overrides: { createWorldPresentationContext(overrides: {
world: any; world: any;
activeSystemId?: string; activeSystemId?: string;
focusedAnchorId?: string;
cameraMode: any; cameraMode: any;
povLevel: any; povLevel: any;
orbitYaw: number; orbitYaw: number;
@@ -215,17 +393,23 @@ export class ViewerSceneDataController {
worldTimeSyncMs: this.context.getWorldTimeSyncMs(), worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
worldSeed: this.context.getWorldSeed(), worldSeed: this.context.getWorldSeed(),
activeSystemId: overrides.activeSystemId, activeSystemId: overrides.activeSystemId,
focusedAnchorId: overrides.focusedAnchorId,
cameraMode: overrides.cameraMode, cameraMode: overrides.cameraMode,
povLevel: overrides.povLevel, povLevel: overrides.povLevel,
orbitYaw: overrides.orbitYaw, orbitYaw: overrides.orbitYaw,
camera: overrides.systemCamera, camera: overrides.systemCamera,
systemAnchor: overrides.systemAnchor, systemAnchor: overrides.systemAnchor,
shipVisuals: this.context.shipVisuals, shipVisuals: this.context.shipVisuals,
localShipVisuals: this.context.localShipVisuals,
nodeVisuals: this.context.nodeVisuals, nodeVisuals: this.context.nodeVisuals,
localNodeVisuals: this.context.localNodeVisuals,
celestialVisuals: this.context.celestialVisuals, celestialVisuals: this.context.celestialVisuals,
stationVisuals: this.context.stationVisuals, stationVisuals: this.context.stationVisuals,
localStationVisuals: this.context.localStationVisuals,
claimVisuals: this.context.claimVisuals, claimVisuals: this.context.claimVisuals,
localClaimVisuals: this.context.localClaimVisuals,
constructionSiteVisuals: this.context.constructionSiteVisuals, constructionSiteVisuals: this.context.constructionSiteVisuals,
localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
systemVisuals: this.context.systemVisuals, systemVisuals: this.context.systemVisuals,
systemSummaryVisuals: new Map(), systemSummaryVisuals: new Map(),
toDisplayLocalPosition: overrides.toDisplayLocalPosition, toDisplayLocalPosition: overrides.toDisplayLocalPosition,
@@ -248,8 +432,14 @@ export class ViewerSceneDataController {
claimGroup: this.context.claimGroup, claimGroup: this.context.claimGroup,
constructionSiteGroup: this.context.constructionSiteGroup, constructionSiteGroup: this.context.constructionSiteGroup,
shipGroup: this.context.shipGroup, shipGroup: this.context.shipGroup,
localNodeGroup: this.context.localNodeGroup,
localStationGroup: this.context.localStationGroup,
localClaimGroup: this.context.localClaimGroup,
localConstructionSiteGroup: this.context.localConstructionSiteGroup,
localShipGroup: this.context.localShipGroup,
galaxySelectableTargets: this.context.galaxySelectableTargets, galaxySelectableTargets: this.context.galaxySelectableTargets,
systemSelectableTargets: this.context.systemSelectableTargets, systemSelectableTargets: this.context.systemSelectableTargets,
localSelectableTargets: this.context.localSelectableTargets,
systemVisuals: this.context.systemVisuals, systemVisuals: this.context.systemVisuals,
planetVisuals: this.context.planetVisuals, planetVisuals: this.context.planetVisuals,
celestialVisuals: this.context.celestialVisuals, celestialVisuals: this.context.celestialVisuals,
@@ -258,12 +448,17 @@ export class ViewerSceneDataController {
claimVisuals: this.context.claimVisuals, claimVisuals: this.context.claimVisuals,
constructionSiteVisuals: this.context.constructionSiteVisuals, constructionSiteVisuals: this.context.constructionSiteVisuals,
shipVisuals: this.context.shipVisuals, shipVisuals: this.context.shipVisuals,
localNodeVisuals: this.context.localNodeVisuals,
localStationVisuals: this.context.localStationVisuals,
localClaimVisuals: this.context.localClaimVisuals,
localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
localShipVisuals: this.context.localShipVisuals,
shipSize, shipSize,
shipLength, shipLength,
shipPresentationColor, shipPresentationColor,
celestialColor, celestialColor,
createCirclePoints, createCirclePoints,
resolvePointPosition: (systemId: string, celestialId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId), resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => resolvePointPosition(this.context.getWorldPresentationContext(), systemId, celestialId, anchorId),
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition), resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => resolveOrbitalAnchor(this.context.getWorldPresentationContext(), systemId, localPosition),
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor), deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: OrbitalAnchor) => deriveNodeOrbital(this.context.getWorldPresentationContext(), node, anchor),
deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor), deriveOrbitalFromLocalPosition: (localPosition: THREE.Vector3, systemId: string, anchor: OrbitalAnchor) => deriveOrbitalFromLocalPosition(this.context.getWorldPresentationContext(), localPosition, systemId, anchor),

View File

@@ -11,6 +11,7 @@ import type {
ConstructionSiteSnapshot, ConstructionSiteSnapshot,
MoonSnapshot, MoonSnapshot,
PlanetSnapshot, PlanetSnapshot,
ResourceDepositSnapshot,
ResourceNodeSnapshot, ResourceNodeSnapshot,
ShipSnapshot, ShipSnapshot,
StationSnapshot, StationSnapshot,
@@ -46,6 +47,23 @@ export function createNodeMesh(node: ResourceNodeSnapshot): SceneNode {
return createSceneNode(mesh); return createSceneNode(mesh);
} }
export function createResourceDepositMesh(deposit: ResourceDepositSnapshot, node: ResourceNodeSnapshot): SceneNode {
const isGas = node.sourceKind === "gas-cloud" || node.itemId === "gas";
const oreRatio = deposit.maxOre <= 0.01 ? 0 : deposit.oreRemaining / deposit.maxOre;
const mesh = new THREE.Mesh(
isGas ? new THREE.SphereGeometry(10, 12, 12) : new THREE.DodecahedronGeometry(8 + (oreRatio * 5), 0),
new THREE.MeshStandardMaterial({
color: isGas ? 0x7fd6ff : 0xc9a165,
flatShading: !isGas,
transparent: isGas,
opacity: isGas ? 0.55 : 1,
emissive: new THREE.Color(isGas ? 0x7fd6ff : 0xc9a165).multiplyScalar(isGas ? 0.18 : 0.05),
}),
);
mesh.position.copy(toThreeVector(deposit.localPosition));
return createSceneNode(mesh);
}
export function createCelestialMesh(node: CelestialSnapshot, celestialColor: (kind: string) => string): SceneNode { export function createCelestialMesh(node: CelestialSnapshot, celestialColor: (kind: string) => string): SceneNode {
const color = celestialColor(node.kind); const color = celestialColor(node.kind);
return createSceneNode(new THREE.Mesh( return createSceneNode(new THREE.Mesh(

View File

@@ -70,6 +70,18 @@ function toSystemPos(localPosition: THREE.Vector3): THREE.Vector3 {
return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE); return localPosition.clone().multiplyScalar(DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
} }
function resolveShipSystemPosition(ship: ShipSnapshot | ShipDelta, context: SceneSyncContext) {
if (ship.spatialState.systemPosition) {
return toThreeVector(ship.spatialState.systemPosition);
}
if (ship.anchorId) {
return context.resolvePointPosition(ship.systemId, null, ship.anchorId);
}
return toThreeVector(ship.localPosition);
}
interface SceneSyncContext { interface SceneSyncContext {
documentRef: Document; documentRef: Document;
worldOrbitalTimeSeconds?: number; worldOrbitalTimeSeconds?: number;
@@ -98,7 +110,7 @@ interface SceneSyncContext {
shipPresentationColor: (ship: ShipSnapshot) => string; shipPresentationColor: (ship: ShipSnapshot) => string;
celestialColor: (kind: string) => string; celestialColor: (kind: string) => string;
createCirclePoints: (radius: number, segments: number) => THREE.Vector3[]; createCirclePoints: (radius: number, segments: number) => THREE.Vector3[];
resolvePointPosition: (systemId: string, celestialId?: string | null) => THREE.Vector3; resolvePointPosition: (systemId: string, celestialId?: string | null, anchorId?: string | null) => THREE.Vector3;
resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => NodeVisual["anchor"]; resolveOrbitalAnchor: (systemId: string, localPosition: THREE.Vector3) => NodeVisual["anchor"];
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => { deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => {
radius: number; radius: number;
@@ -250,7 +262,8 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
const mesh = createNodeMesh(node); const mesh = createNodeMesh(node);
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100); const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
const localPosition = toThreeVector(node.localPosition); const localPosition = toThreeVector(node.localPosition);
const displayPos = toSystemPos(localPosition); const systemPosition = context.resolvePointPosition(node.systemId, null, node.anchorId);
const displayPos = toSystemPos(systemPosition);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
icon.setPosition(displayPos); icon.setPosition(displayPos);
const isActive = node.systemId === activeSystemId; const isActive = node.systemId === activeSystemId;
@@ -260,6 +273,7 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
const orbital = context.deriveNodeOrbital(node, anchor); const orbital = context.deriveNodeOrbital(node, anchor);
context.nodeVisuals.set(node.id, { context.nodeVisuals.set(node.id, {
systemId: node.systemId, systemId: node.systemId,
anchorId: node.anchorId,
mesh, mesh,
icon, icon,
sourceKind: node.sourceKind, sourceKind: node.sourceKind,
@@ -283,7 +297,8 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
const mesh = createStationMesh(station); const mesh = createStationMesh(station);
const icon = createTacticalIcon(context.documentRef, station.color, 130); const icon = createTacticalIcon(context.documentRef, station.color, 130);
const localPosition = toThreeVector(station.localPosition); const localPosition = toThreeVector(station.localPosition);
const displayPos = toSystemPos(localPosition); const systemPosition = context.resolvePointPosition(station.systemId, null, station.anchorId);
const displayPos = toSystemPos(systemPosition);
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
icon.setPosition(displayPos); icon.setPosition(displayPos);
const isActive = station.systemId === activeSystemId; const isActive = station.systemId === activeSystemId;
@@ -294,6 +309,7 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
context.stationVisuals.set(station.id, { context.stationVisuals.set(station.id, {
id: station.id, id: station.id,
systemId: station.systemId, systemId: station.systemId,
anchorId: station.anchorId,
mesh, mesh,
icon, icon,
anchor, anchor,
@@ -313,7 +329,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
context.claimVisuals.clear(); context.claimVisuals.clear();
for (const claim of claims) { for (const claim of claims) {
const localPosition = context.resolvePointPosition(claim.systemId, claim.celestialId); const localPosition = context.resolvePointPosition(claim.systemId, null, claim.anchorId);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
const mesh = createClaimMesh(claim); const mesh = createClaimMesh(claim);
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90); const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90);
@@ -324,7 +340,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
icon.setVisible(isActive); icon.setVisible(isActive);
context.claimVisuals.set(claim.id, { context.claimVisuals.set(claim.id, {
id: claim.id, id: claim.id,
celestialId: claim.celestialId, anchorId: claim.anchorId,
systemId: claim.systemId, systemId: claim.systemId,
mesh, mesh,
icon, icon,
@@ -341,7 +357,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
context.constructionSiteVisuals.clear(); context.constructionSiteVisuals.clear();
for (const site of sites) { for (const site of sites) {
const localPosition = context.resolvePointPosition(site.systemId, site.celestialId); const localPosition = context.resolvePointPosition(site.systemId, null, site.anchorId);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(localPosition);
const mesh = createConstructionSiteMesh(site); const mesh = createConstructionSiteMesh(site);
const icon = createTacticalIcon(context.documentRef, "#9df29c", 90); const icon = createTacticalIcon(context.documentRef, "#9df29c", 90);
@@ -352,7 +368,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
icon.setVisible(isActive); icon.setVisible(isActive);
context.constructionSiteVisuals.set(site.id, { context.constructionSiteVisuals.set(site.id, {
id: site.id, id: site.id,
celestialId: site.celestialId, anchorId: site.anchorId,
systemId: site.systemId, systemId: site.systemId,
mesh, mesh,
icon, icon,
@@ -374,7 +390,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
const iconBaseScale = 78; const iconBaseScale = 78;
const icon = createShipTacticalIcon(context.documentRef, shipColor, iconBaseScale); const icon = createShipTacticalIcon(context.documentRef, shipColor, iconBaseScale);
const localPosition = toThreeVector(ship.localPosition); const localPosition = toThreeVector(ship.localPosition);
const displayPos = toSystemPos(localPosition); const displayPos = toSystemPos(resolveShipSystemPosition(ship, context));
mesh.setPosition(displayPos); mesh.setPosition(displayPos);
icon.setPosition(displayPos); icon.setPosition(displayPos);
icon.setColor(shipColor); icon.setColor(shipColor);
@@ -386,6 +402,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "ship", id: ship.id }); registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "ship", id: ship.id });
context.shipVisuals.set(ship.id, { context.shipVisuals.set(ship.id, {
systemId: ship.systemId, systemId: ship.systemId,
anchorId: ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined,
mesh, mesh,
icon, icon,
iconBaseScale, iconBaseScale,
@@ -430,6 +447,7 @@ export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDe
} }
visual.systemId = node.systemId; visual.systemId = node.systemId;
visual.anchorId = node.anchorId;
visual.sourceKind = node.sourceKind; visual.sourceKind = node.sourceKind;
visual.localPosition.copy(toThreeVector(node.localPosition)); visual.localPosition.copy(toThreeVector(node.localPosition));
visual.anchor = context.resolveOrbitalAnchor(node.systemId, visual.localPosition); visual.anchor = context.resolveOrbitalAnchor(node.systemId, visual.localPosition);
@@ -452,6 +470,7 @@ export function applyStationDeltas(context: SceneSyncContext, stations: StationD
} }
visual.systemId = station.systemId; visual.systemId = station.systemId;
visual.anchorId = station.anchorId;
visual.localPosition.copy(toThreeVector(station.localPosition)); visual.localPosition.copy(toThreeVector(station.localPosition));
visual.anchor = context.resolveOrbitalAnchor(station.systemId, visual.localPosition); visual.anchor = context.resolveOrbitalAnchor(station.systemId, visual.localPosition);
const orbital = context.deriveOrbitalFromLocalPosition(visual.localPosition, station.systemId, visual.anchor); const orbital = context.deriveOrbitalFromLocalPosition(visual.localPosition, station.systemId, visual.anchor);
@@ -474,7 +493,8 @@ export function applyClaimDeltas(context: SceneSyncContext, claims: ClaimDelta[]
} }
visual.systemId = claim.systemId; visual.systemId = claim.systemId;
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, claim.celestialId)); visual.anchorId = claim.anchorId;
visual.localPosition.copy(context.resolvePointPosition(claim.systemId, null, claim.anchorId));
const displayPos = toSystemPos(visual.localPosition); const displayPos = toSystemPos(visual.localPosition);
visual.mesh.setPosition(displayPos); visual.mesh.setPosition(displayPos);
visual.icon.setPosition(displayPos); visual.icon.setPosition(displayPos);
@@ -494,7 +514,8 @@ export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: Co
} }
visual.systemId = site.systemId; visual.systemId = site.systemId;
visual.localPosition.copy(context.resolvePointPosition(site.systemId, site.celestialId)); visual.anchorId = site.anchorId;
visual.localPosition.copy(context.resolvePointPosition(site.systemId, null, site.anchorId));
const displayPos = toSystemPos(visual.localPosition); const displayPos = toSystemPos(visual.localPosition);
visual.mesh.setPosition(displayPos); visual.mesh.setPosition(displayPos);
visual.icon.setPosition(displayPos); visual.icon.setPosition(displayPos);
@@ -514,6 +535,7 @@ export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], t
} }
visual.systemId = ship.systemId; visual.systemId = ship.systemId;
visual.anchorId = ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined;
visual.startPosition.copy(getAnimatedShipLocalPosition(visual)); visual.startPosition.copy(getAnimatedShipLocalPosition(visual));
visual.authoritativePosition.copy(toThreeVector(ship.localPosition)); visual.authoritativePosition.copy(toThreeVector(ship.localPosition));
visual.targetPosition.copy(toThreeVector(ship.targetLocalPosition)); visual.targetPosition.copy(toThreeVector(ship.targetLocalPosition));

View File

@@ -20,10 +20,17 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
return world.stations.get(item.id)?.label ?? item.id; return world.stations.get(item.id)?.label ?? item.id;
} }
if (item.kind === "node") { if (item.kind === "node") {
return item.id; const node = world.nodes.get(item.id);
return node ? `${node.itemId} source` : item.id;
} }
if (item.kind === "celestial") { if (item.kind === "celestial") {
return `${world.celestials.get(item.id)?.kind ?? "celestial"} ${item.id}`; const celestial = world.celestials.get(item.id);
if (!celestial) {
return item.id;
}
return describeCelestialPathWithinSystem(world, celestial.systemId, celestial.id)
?? `${world.systems.get(celestial.systemId)?.label ?? celestial.systemId} / ${celestial.kind}`;
} }
if (item.kind === "claim") { if (item.kind === "claim") {
return `claim ${item.id}`; return `claim ${item.id}`;
@@ -113,9 +120,7 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
return item.id; return item.id;
} }
const anchorPath = node.celestialId const anchorPath = describeAnchorPathWithinSystem(world, node.systemId, node.anchorId);
? describeCelestialPathWithinSystem(world, node.systemId, node.celestialId)
: undefined;
return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`; return anchorPath ? `${anchorPath} / ${node.itemId}` : `${node.systemId} / ${node.itemId}`;
} }
@@ -135,16 +140,16 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
if (item.kind === "claim") { if (item.kind === "claim") {
const claim = world.claims.get(item.id); const claim = world.claims.get(item.id);
const anchorPath = claim?.celestialId const anchorPath = claim
? describeCelestialPathWithinSystem(world, claim.systemId, claim.celestialId) ? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId)
: undefined; : undefined;
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`; return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
} }
if (item.kind === "construction-site") { if (item.kind === "construction-site") {
const site = world.constructionSites.get(item.id); const site = world.constructionSites.get(item.id);
const anchorPath = site?.celestialId const anchorPath = site
? describeCelestialPathWithinSystem(world, site.systemId, site.celestialId) ? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId)
: undefined; : undefined;
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id; const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`; return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
@@ -210,20 +215,74 @@ export function resolveFocusedCelestialId(world: WorldState | undefined, selecte
return selected.id; return selected.id;
} }
if (selected.kind === "ship") { if (selected.kind === "ship") {
return world.ships.get(selected.id)?.spatialState.currentCelestialId ?? world.ships.get(selected.id)?.celestialId ?? undefined; const ship = world.ships.get(selected.id);
return ship?.spatialState.currentAnchorId && world.celestials.has(ship.spatialState.currentAnchorId)
? ship.spatialState.currentAnchorId
: (ship?.anchorId && world.celestials.has(ship.anchorId) ? ship.anchorId : undefined);
} }
if (selected.kind === "station") { if (selected.kind === "station") {
return world.stations.get(selected.id)?.celestialId ?? undefined; const station = world.stations.get(selected.id);
return station?.anchorId && world.celestials.has(station.anchorId) ? station.anchorId : undefined;
} }
if (selected.kind === "claim") { if (selected.kind === "claim") {
return world.claims.get(selected.id)?.celestialId ?? undefined; const claim = world.claims.get(selected.id);
return claim && world.celestials.has(claim.anchorId) ? claim.anchorId : undefined;
} }
if (selected.kind === "construction-site") { if (selected.kind === "construction-site") {
return world.constructionSites.get(selected.id)?.celestialId ?? undefined; const site = world.constructionSites.get(selected.id);
return site && world.celestials.has(site.anchorId) ? site.anchorId : undefined;
} }
return undefined; return undefined;
} }
export function resolveFocusedAnchorId(world: WorldState | undefined, selectedItems: Selectable[]): string | undefined {
if (!world || selectedItems.length !== 1) {
return undefined;
}
const selected = selectedItems[0];
if (selected.kind === "node") {
return world.nodes.get(selected.id)?.anchorId;
}
if (selected.kind === "ship") {
const ship = world.ships.get(selected.id);
return ship?.spatialState.currentAnchorId
?? ship?.anchorId
?? resolveFocusedCelestialId(world, selectedItems);
}
if (selected.kind === "station") {
const station = world.stations.get(selected.id);
return station?.anchorId
?? resolveFocusedCelestialId(world, selectedItems);
}
if (selected.kind === "claim") {
const claim = world.claims.get(selected.id);
return claim?.anchorId
?? resolveFocusedCelestialId(world, selectedItems);
}
if (selected.kind === "construction-site") {
const site = world.constructionSites.get(selected.id);
return site?.anchorId
?? resolveFocusedCelestialId(world, selectedItems);
}
if (selected.kind === "celestial") {
if (world.anchors.has(selected.id)) {
return selected.id;
}
const orbitBackedAnchor = [...world.anchors.values()].find((anchor) => anchor.orbitReferenceId === selected.id);
return orbitBackedAnchor?.id;
}
if (selected.kind === "planet") {
return `${selected.systemId}-planet-${selected.planetIndex + 1}`;
}
if (selected.kind === "moon") {
return `${selected.systemId}-planet-${selected.planetIndex + 1}-moon-${selected.moonIndex + 1}`;
}
return undefined;
}
export function describeOrbitalParent(world: WorldState | undefined, systemId?: string, anchor?: OrbitalAnchor): string { export function describeOrbitalParent(world: WorldState | undefined, systemId?: string, anchor?: OrbitalAnchor): string {
if (!world || !systemId) { if (!world || !systemId) {
return "unknown"; return "unknown";
@@ -330,23 +389,31 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
return baseState; return baseState;
} }
const destinationNodeId = ship.spatialState.destinationNodeId ?? ship.spatialState.transit?.destinationNodeId; const destinationAnchorId = ship.spatialState.destinationAnchorId ?? ship.spatialState.transit?.destinationAnchorId;
if (!destinationNodeId) { if (!destinationAnchorId) {
return baseState; return baseState;
} }
const destinationCelestial = world.celestials.get(destinationNodeId); const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
if (!destinationCelestial) {
return `${baseState} -> ${destinationNodeId}`;
}
if (baseState === "warping" || baseState === "spooling-warp") { if (baseState === "warping" || baseState === "spooling-warp") {
const destinationPath = describeCelestialPathWithinSystem(world, destinationCelestial.systemId, destinationNodeId); const destinationSystemId = destinationAnchor?.systemId ?? ship.spatialState.currentSystemId ?? ship.systemId;
return `${baseState} -> ${destinationPath ?? destinationNodeId}`; const destinationPath = describeAnchorPathWithinSystem(
world,
destinationSystemId,
destinationAnchorId,
);
return `${baseState} -> ${destinationPath ?? destinationAnchorId}`;
} }
const destinationSystem = world.systems.get(destinationCelestial.systemId); const destinationSystemId = destinationAnchor?.systemId
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`; ?? ship.spatialState.currentSystemId
?? ship.systemId;
const destinationSystem = world.systems.get(destinationSystemId);
if (!destinationSystem) {
return `${baseState} -> ${destinationAnchorId}`;
}
return `${baseState} -> ${destinationSystem.label}`;
} }
export function describeShipObjective(objective: string): string { export function describeShipObjective(objective: string): string {
@@ -406,8 +473,8 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
if (ship.dockedStationId) { if (ship.dockedStationId) {
const station = world.stations.get(ship.dockedStationId); const station = world.stations.get(ship.dockedStationId);
if (station) { if (station) {
const anchorPath = station.celestialId const anchorPath = station.anchorId
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId) ? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId)
: undefined; : undefined;
return { return {
system: systemLabel, system: systemLabel,
@@ -416,11 +483,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
} }
} }
const currentCelestialId = ship.spatialState.currentCelestialId ?? ship.celestialId; const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
if (currentCelestialId) { if (currentAnchorId) {
const celestialPath = describeCelestialPathWithinSystem(world, systemId, currentCelestialId); const anchorPath = describeAnchorPathWithinSystem(world, systemId, currentAnchorId);
if (celestialPath) { if (anchorPath) {
return { system: systemLabel, local: celestialPath }; return { system: systemLabel, local: anchorPath };
} }
} }
@@ -446,9 +513,9 @@ export function describeActiveSpace(
return activeSystem.label; return activeSystem.label;
} }
const celestialId = resolveFocusedCelestialId(world, selectedItems); const anchorId = resolveFocusedAnchorId(world, selectedItems);
if (celestialId) { if (anchorId) {
const localPath = describeCelestialPathWithinSystem(world, activeSystem.id, celestialId); const localPath = describeAnchorPathWithinSystem(world, activeSystem.id, anchorId);
return localPath return localPath
? `${activeSystem.label} / ${localPath}` ? `${activeSystem.label} / ${localPath}`
: activeSystem.label; : activeSystem.label;
@@ -472,10 +539,9 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
return undefined; return undefined;
} }
if (celestial.parentNodeId) { const anchorId = resolveAnchorIdForCelestial(world, celestialId);
const parentPath = describeCelestialPathWithinSystem(world, systemId, celestial.parentNodeId); if (anchorId) {
const segment = describeCelestialSegment(system, celestial); return describeAnchorPathWithinSystem(world, systemId, anchorId);
return parentPath ? `${parentPath}/${segment}` : segment;
} }
if (celestial.kind === "star") { if (celestial.kind === "star") {
@@ -485,6 +551,60 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
return describeCelestialSegment(system, celestial); return describeCelestialSegment(system, celestial);
} }
export function describeAnchorPathWithinSystem(world: WorldState, systemId: string, anchorId: string, celestialId?: string | null): string | undefined {
const anchor = world.anchors.get(anchorId);
if (anchor?.parentAnchorId) {
const parentPath = describeAnchorPathWithinSystem(world, systemId, anchor.parentAnchorId);
const segment = describeAnchorSegment(anchor);
return parentPath ? `${parentPath}/${segment}` : segment;
}
if (celestialId) {
return describeCelestialPathWithinSystem(world, systemId, celestialId);
}
if (!anchor) {
return undefined;
}
return describeAnchorSegment(anchor);
}
function describeAnchorSegment(anchor: { kind: string; id: string; orbitReferenceId?: string | null }): string {
if (anchor.orbitReferenceId) {
return describeAnchorOrbitReference(anchor.orbitReferenceId);
}
if (anchor.kind === "resource-node") {
return anchor.id;
}
return anchor.kind.replace(/-/g, " ");
}
function resolveAnchorIdForCelestial(world: WorldState, celestialId: string): string | undefined {
return world.anchors.has(celestialId) ? celestialId : undefined;
}
function describeAnchorOrbitReference(referenceId: string): string {
const lagrangeMatch = referenceId.match(/(l[1-5])$/i);
if (lagrangeMatch) {
return lagrangeMatch[1].toUpperCase();
}
const moonMatch = referenceId.match(/moon-(\d+)$/i);
if (moonMatch) {
return `Moon ${moonMatch[1]}`;
}
const planetMatch = referenceId.match(/planet-(\d+)$/i);
if (planetMatch) {
return `Planet ${planetMatch[1]}`;
}
return referenceId;
}
function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string { function describeCelestialSegment(system: SystemSnapshot, celestial: CelestialSnapshot): string {
const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/); const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/);
if (moonMatch) { if (moonMatch) {

View File

@@ -41,6 +41,7 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
generatedAtUtc: snapshot.generatedAtUtc, generatedAtUtc: snapshot.generatedAtUtc,
systems: new Map(snapshot.systems.map((system) => [system.id, system])), systems: new Map(snapshot.systems.map((system) => [system.id, system])),
celestials: new Map(snapshot.celestials.map((celestial) => [celestial.id, celestial])), celestials: new Map(snapshot.celestials.map((celestial) => [celestial.id, celestial])),
anchors: new Map(snapshot.anchors.map((anchor) => [anchor.id, anchor])),
nodes: new Map(snapshot.nodes.map((node) => [node.id, node])), nodes: new Map(snapshot.nodes.map((node) => [node.id, node])),
stations: new Map(snapshot.stations.map((station) => [station.id, station])), stations: new Map(snapshot.stations.map((station) => [station.id, station])),
claims: new Map(snapshot.claims.map((claim) => [claim.id, claim])), claims: new Map(snapshot.claims.map((claim) => [claim.id, claim])),
@@ -65,6 +66,9 @@ export function applyDeltaToWorld(world: WorldState, delta: WorldDelta): boolean
for (const celestial of delta.celestials) { for (const celestial of delta.celestials) {
world.celestials.set(celestial.id, celestial); world.celestials.set(celestial.id, celestial);
} }
for (const anchor of delta.anchors) {
world.anchors.set(anchor.id, anchor);
}
for (const node of delta.nodes) { for (const node of delta.nodes) {
world.nodes.set(node.id, node); world.nodes.set(node.id, node);
} }
@@ -101,6 +105,7 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
+ delta.stations.length + delta.stations.length
+ delta.nodes.length + delta.nodes.length
+ delta.celestials.length + delta.celestials.length
+ delta.anchors.length
+ delta.claims.length + delta.claims.length
+ delta.constructionSites.length + delta.constructionSites.length
+ delta.marketOrders.length + delta.marketOrders.length

View File

@@ -1,6 +1,7 @@
import * as THREE from "three"; import * as THREE from "three";
import type { SceneNode } from "./viewerScenePrimitives"; import type { SceneNode } from "./viewerScenePrimitives";
import type { import type {
AnchorSnapshot,
CelestialSnapshot, CelestialSnapshot,
ClaimSnapshot, ClaimSnapshot,
ConstructionSiteSnapshot, ConstructionSiteSnapshot,
@@ -35,6 +36,7 @@ export type Selectable =
export interface ShipVisual { export interface ShipVisual {
systemId: string; systemId: string;
anchorId?: string;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
iconBaseScale: number; iconBaseScale: number;
@@ -74,6 +76,7 @@ export type OrbitalAnchor =
export interface NodeVisual { export interface NodeVisual {
systemId: string; systemId: string;
anchorId: string;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
sourceKind: string; sourceKind: string;
@@ -96,7 +99,8 @@ export interface CelestialVisual {
export interface ClaimVisual { export interface ClaimVisual {
id: string; id: string;
celestialId: string; anchorId: string;
celestialId?: string | null;
systemId: string; systemId: string;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
@@ -105,7 +109,8 @@ export interface ClaimVisual {
export interface ConstructionSiteVisual { export interface ConstructionSiteVisual {
id: string; id: string;
celestialId: string; anchorId: string;
celestialId?: string | null;
systemId: string; systemId: string;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
@@ -115,6 +120,7 @@ export interface ConstructionSiteVisual {
export interface StructureVisual { export interface StructureVisual {
id: string; id: string;
systemId: string; systemId: string;
anchorId?: string | null;
mesh: SceneNode; mesh: SceneNode;
icon: SceneNode; icon: SceneNode;
anchor: OrbitalAnchor; anchor: OrbitalAnchor;
@@ -145,6 +151,7 @@ export interface WorldState {
generatedAtUtc: string; generatedAtUtc: string;
systems: Map<string, SystemSnapshot>; systems: Map<string, SystemSnapshot>;
celestials: Map<string, CelestialSnapshot>; celestials: Map<string, CelestialSnapshot>;
anchors: Map<string, AnchorSnapshot>;
nodes: Map<string, ResourceNodeSnapshot>; nodes: Map<string, ResourceNodeSnapshot>;
stations: Map<string, StationSnapshot>; stations: Map<string, StationSnapshot>;
claims: Map<string, ClaimSnapshot>; claims: Map<string, ClaimSnapshot>;

View File

@@ -65,8 +65,9 @@ export interface ViewerWorldLifecycleContext {
applyClaimDeltas: (claims: ClaimDelta[]) => void; applyClaimDeltas: (claims: ClaimDelta[]) => void;
applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void; applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void;
applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void; applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void;
refreshLocalLayer: () => void;
refreshHistoryWindows: () => void; refreshHistoryWindows: () => void;
resolveFocusedCelestialId: () => string | undefined; resolveFocusedAnchorId: () => string | undefined;
updateSystemSummaries: () => void; updateSystemSummaries: () => void;
applyZoomPresentation: () => void; applyZoomPresentation: () => void;
updateNetworkPanel: () => void; updateNetworkPanel: () => void;
@@ -165,6 +166,7 @@ export class ViewerWorldLifecycle {
this.context.syncClaims(snapshot.claims); this.context.syncClaims(snapshot.claims);
this.context.syncConstructionSites(snapshot.constructionSites); this.context.syncConstructionSites(snapshot.constructionSites);
this.context.syncShips(snapshot.ships, snapshot.tickIntervalMs); this.context.syncShips(snapshot.ships, snapshot.tickIntervalMs);
this.context.refreshLocalLayer();
this.rebuildFactions(snapshot.factions); this.rebuildFactions(snapshot.factions);
this.context.updateSystemSummaries(); this.context.updateSystemSummaries();
this.context.applyZoomPresentation(); this.context.applyZoomPresentation();
@@ -185,6 +187,7 @@ export class ViewerWorldLifecycle {
this.context.applyClaimDeltas(delta.claims); this.context.applyClaimDeltas(delta.claims);
this.context.applyConstructionSiteDeltas(delta.constructionSites); this.context.applyConstructionSiteDeltas(delta.constructionSites);
this.context.applyShipDeltas(delta.ships, delta.tickIntervalMs); this.context.applyShipDeltas(delta.ships, delta.tickIntervalMs);
this.context.refreshLocalLayer();
this.rebuildFactions(cloneFactions(world)); this.rebuildFactions(cloneFactions(world));
this.context.updateSystemSummaries(); this.context.updateSystemSummaries();
} }
@@ -219,6 +222,7 @@ export class ViewerWorldLifecycle {
} }
this.context.refreshHistoryWindows(); this.context.refreshHistoryWindows();
this.context.refreshLocalLayer();
this.context.updateSystemPanel(); this.context.updateSystemPanel();
this.refreshStreamScopeIfNeeded(); this.refreshStreamScopeIfNeeded();
const detailState = buildDetailPanelState({ const detailState = buildDetailPanelState({
@@ -241,12 +245,12 @@ export class ViewerWorldLifecycle {
return { scopeKind: "universe" as const }; return { scopeKind: "universe" as const };
} }
const celestialId = this.context.resolveFocusedCelestialId(); const anchorId = this.context.resolveFocusedAnchorId();
if (this.context.getPovLevel() === "local" && celestialId) { if (this.context.getPovLevel() === "local" && anchorId) {
return { return {
scopeKind: "local-celestial" as const, scopeKind: "local-anchor" as const,
systemId: activeSystemId, systemId: activeSystemId,
celestialId, anchorId,
}; };
} }

View File

@@ -10,7 +10,7 @@ import {
toThreeVector, toThreeVector,
} from "./viewerMath"; } from "./viewerMath";
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants"; import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection"; import { describeActiveSpace, resolveFocusedAnchorId } from "./viewerSelection";
import { import {
resolveShipHeading, resolveShipHeading,
updateSystemStarPresentation, updateSystemStarPresentation,
@@ -58,15 +58,21 @@ export interface WorldOrbitalContext {
export interface WorldPresentationContext extends WorldOrbitalContext { export interface WorldPresentationContext extends WorldOrbitalContext {
activeSystemId?: string; activeSystemId?: string;
focusedAnchorId?: string;
cameraMode: CameraMode; cameraMode: CameraMode;
povLevel: PovLevel; povLevel: PovLevel;
orbitYaw: number; orbitYaw: number;
camera: THREE.PerspectiveCamera; camera: THREE.PerspectiveCamera;
systemAnchor: THREE.Vector3; systemAnchor: THREE.Vector3;
shipVisuals: Map<string, ShipVisual>; shipVisuals: Map<string, ShipVisual>;
localShipVisuals: Map<string, ShipVisual>;
claimVisuals: Map<string, ClaimVisual>; claimVisuals: Map<string, ClaimVisual>;
localClaimVisuals: Map<string, ClaimVisual>;
constructionSiteVisuals: Map<string, ConstructionSiteVisual>; constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
localConstructionSiteVisuals: Map<string, ConstructionSiteVisual>;
systemVisuals: Map<string, SystemVisual>; systemVisuals: Map<string, SystemVisual>;
localNodeVisuals: Map<string, NodeVisual>;
localStationVisuals: Map<string, StructureVisual>;
systemSummaryVisuals: Map<string, any>; systemSummaryVisuals: Map<string, any>;
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3; toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
updateSystemDetailVisibility: () => void; updateSystemDetailVisibility: () => void;
@@ -95,7 +101,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
continue; continue;
} }
const worldPosition = getAnimatedShipLocalPosition(visual, now); const worldPosition = resolveShipRenderPosition(context, ship, visual, now, renderMode);
const displayPosition = context.toDisplayLocalPosition(worldPosition); const displayPosition = context.toDisplayLocalPosition(worldPosition);
visual.mesh.setPosition(displayPosition); visual.mesh.setPosition(displayPosition);
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
@@ -124,8 +130,22 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
} }
} }
for (const [shipId, visual] of context.localShipVisuals.entries()) {
const ship = context.world?.ships.get(shipId);
if (!ship) {
continue;
}
const localPosition = getAnimatedShipLocalPosition(visual, now);
const displayPosition = context.toDisplayLocalPosition(localPosition);
visual.mesh.setPosition(displayPosition);
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(renderMode === "local");
visual.icon.setVisible(renderMode === "local");
}
for (const visual of context.nodeVisuals.values()) { for (const visual of context.nodeVisuals.values()) {
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds); const animatedLocalPosition = resolveNodeRenderPosition(context, visual, worldTimeSeconds, renderMode);
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition)); visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(visual.systemId === context.activeSystemId); visual.mesh.setVisible(visual.systemId === context.activeSystemId);
@@ -148,7 +168,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
} }
for (const visual of context.stationVisuals.values()) { for (const visual of context.stationVisuals.values()) {
const animatedLocalPosition = resolveStructureAnimatedLocalPosition(context, visual, worldTimeSeconds); const animatedLocalPosition = resolveStructureRenderPosition(context, visual, worldTimeSeconds, renderMode);
const displayPosition = context.toDisplayLocalPosition(animatedLocalPosition); const displayPosition = context.toDisplayLocalPosition(animatedLocalPosition);
visual.mesh.setPosition(displayPosition); visual.mesh.setPosition(displayPosition);
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
@@ -165,7 +185,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
} }
for (const visual of context.claimVisuals.values()) { for (const visual of context.claimVisuals.values()) {
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone(); const animatedLocalPosition = visual.localPosition.clone();
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition)); visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(visual.systemId === context.activeSystemId); visual.mesh.setVisible(visual.systemId === context.activeSystemId);
@@ -173,12 +193,41 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
} }
for (const visual of context.constructionSiteVisuals.values()) { for (const visual of context.constructionSiteVisuals.values()) {
const animatedLocalPosition = computeCelestialLocalPositionById(context, visual.celestialId, worldTimeSeconds) ?? visual.localPosition.clone(); const animatedLocalPosition = visual.localPosition.clone();
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition)); visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
visual.icon.setPosition(rawObject(visual.mesh).position.clone()); visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(visual.systemId === context.activeSystemId); visual.mesh.setVisible(visual.systemId === context.activeSystemId);
visual.icon.setVisible(visual.systemId === context.activeSystemId); visual.icon.setVisible(visual.systemId === context.activeSystemId);
} }
for (const visual of context.localNodeVisuals.values()) {
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(renderMode === "local");
visual.icon.setVisible(renderMode === "local");
}
for (const visual of context.localStationVisuals.values()) {
const displayPosition = context.toDisplayLocalPosition(visual.localPosition.clone());
visual.mesh.setPosition(displayPosition);
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(renderMode === "local");
visual.icon.setVisible(renderMode === "local");
}
for (const visual of context.localClaimVisuals.values()) {
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(renderMode === "local");
visual.icon.setVisible(renderMode === "local");
}
for (const visual of context.localConstructionSiteVisuals.values()) {
visual.mesh.setPosition(context.toDisplayLocalPosition(visual.localPosition.clone()));
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
visual.mesh.setVisible(renderMode === "local");
visual.icon.setVisible(renderMode === "local");
}
} }
type RenderSpaceMode = "galaxy" | "system" | "local"; type RenderSpaceMode = "galaxy" | "system" | "local";
@@ -213,6 +262,60 @@ export function resolveShipWorldPosition(
return context.toDisplayLocalPosition(animatedLocalPosition); return context.toDisplayLocalPosition(animatedLocalPosition);
} }
function resolveAnchorSystemPosition(context: WorldOrbitalContext, anchorId?: string | null) {
if (!anchorId) {
return undefined;
}
const anchor = context.world?.anchors.get(anchorId);
return anchor ? toThreeVector(anchor.systemPosition) : undefined;
}
function resolveShipRenderPosition(
context: WorldPresentationContext,
ship: ShipSnapshot,
visual: ShipVisual,
now: number,
renderMode: RenderSpaceMode,
) {
const animatedLocalPosition = getAnimatedShipLocalPosition(visual, now);
const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId ?? visual.anchorId;
const anchoredSystemPosition = resolveAnchorSystemPosition(context, currentAnchorId)
?? (ship.spatialState.systemPosition ? toThreeVector(ship.spatialState.systemPosition) : undefined);
if (renderMode === "local" && currentAnchorId && currentAnchorId === context.focusedAnchorId) {
return animatedLocalPosition;
}
return anchoredSystemPosition ?? animatedLocalPosition;
}
function resolveNodeRenderPosition(
context: WorldPresentationContext,
visual: NodeVisual,
timeSeconds: number,
renderMode: RenderSpaceMode,
) {
if (renderMode === "local" && visual.anchorId === context.focusedAnchorId) {
return computeNodeLocalPosition(context, visual, timeSeconds);
}
return resolveAnchorSystemPosition(context, visual.anchorId) ?? visual.localPosition.clone();
}
function resolveStructureRenderPosition(
context: WorldPresentationContext,
visual: StructureVisual,
timeSeconds: number,
renderMode: RenderSpaceMode,
) {
if (renderMode === "local" && visual.anchorId && visual.anchorId === context.focusedAnchorId) {
return resolveStructureAnimatedLocalPosition(context, visual, timeSeconds);
}
return resolveAnchorSystemPosition(context, visual.anchorId) ?? visual.localPosition.clone();
}
export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) { export function updateSystemSummaries(world: WorldState | undefined, systemSummaryVisuals: Map<string, any>) {
if (!world) { if (!world) {
return; return;
@@ -310,9 +413,9 @@ export function describeGameStatus(params: GameStatusParams) {
? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU` ? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
: ""; : "";
// Local space: position relative to the focused celestial's orbital anchor in km // Local space: position relative to the focused celestial's orbital anchor in km
const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems); const focusedAnchorId = resolveFocusedAnchorId(world, selectedItems);
const celestialAnchor = focusedCelestialId const celestialAnchor = focusedAnchorId
? world?.celestials.get(focusedCelestialId)?.orbitalAnchor ? (world?.anchors.get(focusedAnchorId)?.systemPosition ?? world?.celestials.get(focusedAnchorId)?.orbitalAnchor)
: undefined; : undefined;
const locPos = systemAnchor && celestialAnchor const locPos = systemAnchor && celestialAnchor
? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km` ? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
@@ -415,7 +518,14 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
return bestAnchor; return bestAnchor;
} }
export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null) { export function resolvePointPosition(context: WorldOrbitalContext, _systemId: string, celestialId?: string | null, anchorId?: string | null) {
if (anchorId) {
const anchor = context.world?.anchors.get(anchorId);
if (anchor) {
return toThreeVector(anchor.systemPosition);
}
}
if (celestialId) { if (celestialId) {
const celestial = context.world?.celestials.get(celestialId); const celestial = context.world?.celestials.get(celestialId);
if (celestial) { if (celestial) {
@@ -446,17 +556,17 @@ export function computeCelestialLocalPositionById(
} }
const basePosition = toThreeVector(celestial.orbitalAnchor); const basePosition = toThreeVector(celestial.orbitalAnchor);
if (!celestial.parentNodeId) { if (!celestial.parentAnchorId) {
return basePosition; return basePosition;
} }
const parentCelestial = context.world.celestials.get(celestial.parentNodeId); const parentCelestial = context.world.celestials.get(celestial.parentAnchorId);
if (!parentCelestial) { if (!parentCelestial) {
return basePosition; return basePosition;
} }
visiting.add(celestialId); visiting.add(celestialId);
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting); const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentAnchorId, timeSeconds, visiting);
visiting.delete(celestialId); visiting.delete(celestialId);
if (!parentCurrentPosition) { if (!parentCurrentPosition) {
return basePosition; return basePosition;
@@ -548,9 +658,16 @@ function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, vis
} }
const station = context.world.stations.get(visual.id); const station = context.world.stations.get(visual.id);
if (!station?.celestialId) { if (!station) {
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14); return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
} }
return computeCelestialLocalPositionById(context, station.celestialId, timeSeconds) ?? visual.localPosition.clone(); if (station.anchorId) {
const anchor = context.world.anchors.get(station.anchorId);
if (anchor) {
return toThreeVector(anchor.systemPosition);
}
}
return computeStructureLocalPosition(context, visual, timeSeconds, 0.14);
} }

View File

@@ -12,7 +12,7 @@ export default defineConfig({
port: 5174, port: 5174,
allowedHosts: ["sobina.local"], allowedHosts: ["sobina.local"],
proxy: { proxy: {
"/api": "http://127.0.0.1:5080", "/api": "http://127.0.0.1:5079",
}, },
}, },
build: { build: {

View File

@@ -40,7 +40,6 @@ Recommended global ID families:
- `commanderId` - `commanderId`
- `systemId` - `systemId`
- `anchorId` - `anchorId`
- `localspaceId`
- `stationId` - `stationId`
- `shipId` - `shipId`
- `moduleId` - `moduleId`
@@ -59,15 +58,14 @@ The intended core entities are:
2. `Commander` 2. `Commander`
3. `System` 3. `System`
4. `Anchor` 4. `Anchor`
5. `Localspace` 5. `Station`
6. `Station` 6. `Ship`
7. `Ship` 7. `ModuleInstance`
8. `ModuleInstance` 8. `Claim`
9. `Claim` 9. `ConstructionSite`
10. `ConstructionSite` 10. `MarketOrder`
11. `MarketOrder` 11. `Recipe`
12. `Recipe` 12. `PolicySet`
13. `PolicySet`
## Faction ## Faction
@@ -120,12 +118,12 @@ Suggested fields:
- `label` - `label`
- `galaxyPosition` - `galaxyPosition`
- `star definition` - `star definition`
- `nodeIds` - `anchorIds`
- `faction influence later` - `faction influence later`
## Anchor ## Anchor
An anchor is a meaningful location in a system that owns a localspace. An anchor is a meaningful location in a system.
Suggested fields: Suggested fields:
@@ -133,7 +131,7 @@ Suggested fields:
- `systemId` - `systemId`
- `kind` - `kind`
- `systemPosition` - `systemPosition`
- `localspaceId` - `localspaceRadius`
- `parentAnchorId?` - `parentAnchorId?`
- `orbital metadata?` - `orbital metadata?`
- `constructionIds?` - `constructionIds?`
@@ -146,25 +144,9 @@ Recommended anchor kinds:
- `lagrange-point` - `lagrange-point`
- `resource-node` - `resource-node`
## Localspace
A localspace is the tactical simulation context attached to one anchor.
Suggested fields:
- `localspaceId`
- `anchorId`
- `systemId`
- `radius`
- `occupantShipIds`
- `occupantStationIds`
- `occupantClaimIds`
- `occupantConstructionSiteIds`
- `serverAuthorityId later`
## Station ## Station
A station is a constructed structure that lives inside one localspace. A station is a constructed structure that lives at one anchor.
Suggested fields: Suggested fields:
@@ -173,7 +155,6 @@ Suggested fields:
- `commanderId?` - `commanderId?`
- `anchorId` - `anchorId`
- `systemId` - `systemId`
- `localspaceId`
- `moduleIds` - `moduleIds`
- `inventory` - `inventory`
- `population` - `population`
@@ -236,7 +217,6 @@ Suggested fields:
- `commanderId?` - `commanderId?`
- `systemId` - `systemId`
- `anchorId` - `anchorId`
- `localspaceId`
- `placedAt` - `placedAt`
- `activatesAt` - `activatesAt`
- `state` - `state`
@@ -258,7 +238,6 @@ Suggested fields:
- `constructionSiteId` - `constructionSiteId`
- `ownerFactionId` - `ownerFactionId`
- `anchorId` - `anchorId`
- `localspaceId`
- `targetKind` - `targetKind`
- `targetDefinitionId` - `targetDefinitionId`
- `requiredItems` - `requiredItems`
@@ -351,7 +330,6 @@ Recommended ship spatial state fields:
- `spaceLayer` - `spaceLayer`
- `currentSystemId` - `currentSystemId`
- `currentAnchorId?` - `currentAnchorId?`
- `currentLocalspaceId?`
- `localPosition?` - `localPosition?`
- `systemPosition?` - `systemPosition?`
- `movementRegime` - `movementRegime`

View File

@@ -34,7 +34,6 @@ Every event should conceptually have:
- `kind` - `kind`
- `spaceLayer` - `spaceLayer`
- `systemId?` - `systemId?`
- `localspaceId?`
- `anchorId?` - `anchorId?`
- `primaryEntityKind` - `primaryEntityKind`
- `primaryEntityId` - `primaryEntityId`

View File

@@ -73,7 +73,7 @@ Current state:
Primary gaps: Primary gaps:
- [`RuntimeModels.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs) has no `AnchorRuntime`, `LocalspaceRuntime`, `ClaimRuntime`, or `ConstructionSiteRuntime`. - [`RuntimeModels.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs) has no first-class `AnchorRuntime` aligned with the target design, plus no fully anchor-native `ClaimRuntime` or `ConstructionSiteRuntime`.
- [`ScenarioLoader.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/ScenarioLoader.cs) computes station positions directly instead of creating anchor-backed placement. - [`ScenarioLoader.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/ScenarioLoader.cs) computes station positions directly instead of creating anchor-backed placement.
- [`SimulationEngine.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/SimulationEngine.cs) still treats travel as raw coordinate movement rather than anchor-to-anchor transit between spaces. - [`SimulationEngine.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/SimulationEngine.cs) still treats travel as raw coordinate movement rather than anchor-to-anchor transit between spaces.
@@ -247,7 +247,6 @@ Work:
- extend [`RuntimeModels.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs) with: - extend [`RuntimeModels.cs`](/home/jbourdon/repos/space-game/apps/backend/Simulation/RuntimeModels.cs) with:
- `AnchorRuntime` - `AnchorRuntime`
- `LocalspaceRuntime`
- `CommanderRuntime` - `CommanderRuntime`
- `ClaimRuntime` - `ClaimRuntime`
- `ConstructionSiteRuntime` - `ConstructionSiteRuntime`
@@ -262,7 +261,6 @@ Work:
- add structured ship spatial state: - add structured ship spatial state:
- current space layer - current space layer
- current anchor - current anchor
- current localspace
- current transit - current transit
Why first: Why first:

View File

@@ -8,7 +8,7 @@ It is the canonical reference for:
- solar systems - solar systems
- celestials - celestials
- anchors - anchors
- localspaces - localspaces as the tactical space around anchors
- ship and station placement - ship and station placement
- intra-system travel - intra-system travel
- inter-system travel - inter-system travel
@@ -27,14 +27,13 @@ The structure can be understood as a tree:
- the galaxy contains solar systems - the galaxy contains solar systems
- each solar system contains celestials and other derived locations - each solar system contains celestials and other derived locations
- each meaningful location is an anchor - each meaningful location is an anchor
- each anchor owns one localspace - each anchor has a localspace around it
The intended structure is: The intended structure is:
1. `galaxy` 1. `galaxy`
2. `solar system` 2. `solar system`
3. `anchor` 3. `anchor`
4. `localspace`
Ships and stations do not live in arbitrary free-floating "system local space". Ships and stations do not live in arbitrary free-floating "system local space".
@@ -99,7 +98,7 @@ Systems remain important for:
## Anchors ## Anchors
An anchor is a meaningful object in a system that owns a localspace. An anchor is a meaningful object in a system.
Anchors are first-class world entities. Anchors are first-class world entities.
@@ -120,7 +119,7 @@ Each anchor should have:
- an anchor type - an anchor type
- a position in system space - a position in system space
- optional orbital metadata - optional orbital metadata
- an associated localspace definition - a localspace around it
- optional parent/child relationships - optional parent/child relationships
Examples: Examples:
@@ -131,7 +130,7 @@ Examples:
- a resource node is an anchor but not a celestial - a resource node is an anchor but not a celestial
- a Lagrange point can be the child of a moon, which is the child of a planet, which is the child of a star - a Lagrange point can be the child of a moon, which is the child of a planet, which is the child of a star
Every anchor has exactly one localspace. Every anchor has exactly one localspace around it.
## Celestials ## Celestials
@@ -147,9 +146,9 @@ Celestials exist for three reasons:
1. they structure the solar system visually and strategically 1. they structure the solar system visually and strategically
2. they define orbital relationships 2. they define orbital relationships
3. they provide valid anchors for localspaces and derived locations such as Lagrange points 3. they provide valid anchors and derived locations such as Lagrange points
Every star, planet, and moon gets a localspace. Every star, planet, and moon has a localspace around it.
Not all anchors are celestials, but all celestials are anchors. Not all anchors are celestials, but all celestials are anchors.
@@ -165,7 +164,7 @@ Initial assumptions:
- major orbitals can expose `L1` through `L5` - major orbitals can expose `L1` through `L5`
- each exposed Lagrange point is its own anchor - each exposed Lagrange point is its own anchor
- each exposed Lagrange point has its own localspace - each exposed Lagrange point has its own localspace around it
- Lagrange points are valid construction sites - Lagrange points are valid construction sites
For now, all five may exist for supported orbitals, but the intended direction is that only major planets should necessarily expose all five. For now, all five may exist for supported orbitals, but the intended direction is that only major planets should necessarily expose all five.
@@ -186,7 +185,7 @@ That means a resource node can have:
- a stable identity - a stable identity
- a place in a solar system - a place in a solar system
- its own localspace - its own localspace around it
This is desirable because it allows resources to exist anywhere meaningful in a system while still fitting the anchored localspace model. This is desirable because it allows resources to exist anywhere meaningful in a system while still fitting the anchored localspace model.
@@ -198,6 +197,22 @@ Resource nodes are not construction sites.
That is intentional so they can be spawned, depleted, despawned, and regenerated more freely than permanent infrastructure anchors. That is intentional so they can be spawned, depleted, despawned, and regenerated more freely than permanent infrastructure anchors.
Resource nodes are strategic mining destinations.
That means:
- outside localspace, a miner travels to a resource-node anchor
- inside that localspace, the miner chooses a concrete extractable target
The concrete extractable target is a tactical mining concern, not a strategic travel identity.
So:
- travel uses `anchorId`
- local extraction uses a localspace-level target such as a rock, cluster, or gas pocket
Do not use a generic `NodeId` to mean both.
## Localspace ## Localspace
`localspace` is the tactical simulation term and should be the only term used for this concept. `localspace` is the tactical simulation term and should be the only term used for this concept.
@@ -212,7 +227,7 @@ Use:
- `localspace` - `localspace`
Each localspace belongs to exactly one anchor. Each localspace is the tactical space around exactly one anchor.
Examples: Examples:
@@ -233,16 +248,26 @@ Localspace is where close simulation happens:
- local logistics - local logistics
- tactical defense - tactical defense
For mining specifically:
- the destination localspace is chosen by anchor
- the final extractable target is chosen only after the ship is inside that localspace
This preserves the distinction between:
- strategic travel to a mining site
- tactical mining behavior inside that site
Ships and constructions do not exist directly in system space. They exist in one localspace at a time unless they are explicitly traveling between anchors. Ships and constructions do not exist directly in system space. They exist in one localspace at a time unless they are explicitly traveling between anchors.
## Ship Placement ## Ship Placement
A ship always belongs to exactly one localspace unless it is actively transitioning between anchors. A ship always belongs to exactly one anchor unless it is actively transitioning between anchors.
Normal ship state should be one of: Normal ship state should be one of:
- in a localspace - at an anchor
- traveling between localspaces in the same system - traveling between anchors in the same system
- traveling between systems - traveling between systems
Inside a localspace, ships use tactical movement with thrusters. Inside a localspace, ships use tactical movement with thrusters.
@@ -444,6 +469,82 @@ Localspace view should show:
The viewer should not imply that the full solar system is one continuous local battlefield. The viewer should not imply that the full solar system is one continuous local battlefield.
Viewer zoom transitions are a client-side presentation effect.
That means:
- zooming from galaxy to system does not imply one shared coordinate space
- zooming from system to localspace does not imply one shared coordinate space
- the client may animate or blend between views for readability
- those effects do not define simulation truth
## Scales And Units
Each spatial layer owns its own units.
Those units should be native to that layer, not projections of one universal master coordinate system.
### Galaxy
The galaxy layer uses:
- `light-years`
This is the scale for:
- system positions
- inter-system distances
- strategic map layout
### System
The system layer uses:
- `AU`
This is the scale for:
- anchor positions inside a system
- orbital relationships
- intra-system routing and warp travel
Variation at localspace scale is intentionally insignificant at this layer.
### Localspace
The localspace layer uses:
- `meters`
- `m/s`
This is the scale for:
- tactical movement
- combat
- docking
- mining
- construction
Variation at system scale is intentionally insignificant at this layer.
## Cross-Layer Rule
The simulation should not try to preserve one continuous coordinate frame across galaxy, system, and localspace.
Cross-layer relationships should be modeled through:
- containment
- references
- travel state
Not through:
- universal coordinate conversion
- raw projection of localspace offsets into system space
- raw projection of system offsets into galaxy space
The viewer may animate transitions between layers, but that is presentation only. It does not mean the simulation uses one continuous spatial frame.
## Ownership And Sovereignty ## Ownership And Sovereignty
Ownership and sovereignty should primarily be tracked at system level. Ownership and sovereignty should primarily be tracked at system level.
@@ -451,7 +552,7 @@ Ownership and sovereignty should primarily be tracked at system level.
This is not fully defined yet, but the current design direction is: This is not fully defined yet, but the current design direction is:
- systems are the main sovereignty unit - systems are the main sovereignty unit
- localspaces and constructions exist inside systems - anchors and constructions exist inside systems
- local conflicts and control still matter tactically - local conflicts and control still matter tactically
## Simulation Implications ## Simulation Implications
@@ -466,7 +567,7 @@ This model supports:
It also gives a cleaner authority boundary for later scaling: It also gives a cleaner authority boundary for later scaling:
- one localspace can become one simulation partition - one anchor can become one tactical simulation partition
- one system can remain a higher-level strategic container - one system can remain a higher-level strategic container
This supports: This supports:
@@ -493,8 +594,8 @@ Until the implementation is updated, the following terms should be used consiste
- `galaxy`: the top-level strategic star map - `galaxy`: the top-level strategic star map
- `system`: the solar system container - `system`: the solar system container
- `celestial`: star, planet, or moon - `celestial`: star, planet, or moon
- `anchor`: anything that owns a localspace - `anchor`: a meaningful location in a system and the primary tactical simulation container
- `localspace`: the tactical simulation bubble attached to one anchor - `localspace`: the tactical simulation space around an anchor, not a separately identified entity
- `intra-system warp`: movement between anchors in the same system - `intra-system warp`: movement between anchors in the same system
- `inter-system FTL`: movement between systems - `inter-system FTL`: movement between systems