Compare commits
3 Commits
fdcf83ccec
...
6c92ab50c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c92ab50c8 | |||
| d0c6e30304 | |||
| 75568324f5 |
@@ -1097,14 +1097,14 @@ internal sealed class CommanderPlanningService
|
||||
{
|
||||
theaters.Add(new FactionTheaterRuntime
|
||||
{
|
||||
Id = $"theater-expansion-{expansionProject.SystemId}-{expansionProject.CelestialId}",
|
||||
Id = $"theater-expansion-{expansionProject.SystemId}-{expansionProject.AnchorId}",
|
||||
Kind = "expansion-front",
|
||||
SystemId = expansionProject.SystemId,
|
||||
Status = "active",
|
||||
Priority = 65f + (economicAssessment.HasShipyard ? 0f : 15f),
|
||||
SupplyRisk = ComputeSystemRisk(world, faction, expansionProject.SystemId),
|
||||
FriendlyAssetValue = EstimateFriendlyAssetValue(world, faction.Id, expansionProject.SystemId),
|
||||
AnchorEntityId = expansionProject.SiteId ?? expansionProject.CelestialId,
|
||||
AnchorEntityId = expansionProject.SiteId ?? expansionProject.AnchorId,
|
||||
AnchorPosition = ResolveExpansionAnchor(world, expansionProject),
|
||||
UpdatedAtUtc = nowUtc,
|
||||
});
|
||||
@@ -1272,7 +1272,7 @@ internal sealed class CommanderPlanningService
|
||||
],
|
||||
"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}-guard", Kind = "guard-site", Status = "planned", Summary = "Defend the expansion site until operational." },
|
||||
],
|
||||
@@ -2725,7 +2725,7 @@ internal sealed class CommanderPlanningService
|
||||
AreaSystemId = areaSystemId,
|
||||
TargetEntityId = objective.TargetEntityId,
|
||||
ItemId = objective.ItemId ?? fallback.ItemId,
|
||||
PreferredNodeId = fallback.PreferredNodeId,
|
||||
PreferredAnchorId = fallback.PreferredAnchorId,
|
||||
PreferredConstructionSiteId = objective.Kind is "construct-site" or "supply-site" ? objective.TargetEntityId : fallback.PreferredConstructionSiteId,
|
||||
PreferredModuleId = fallback.PreferredModuleId,
|
||||
TargetPosition = objective.TargetPosition ?? fallback.TargetPosition,
|
||||
@@ -2750,7 +2750,7 @@ internal sealed class CommanderPlanningService
|
||||
target.AreaSystemId = source.AreaSystemId;
|
||||
target.TargetEntityId = source.TargetEntityId;
|
||||
target.ItemId = source.ItemId;
|
||||
target.PreferredNodeId = source.PreferredNodeId;
|
||||
target.PreferredAnchorId = source.PreferredAnchorId;
|
||||
target.PreferredConstructionSiteId = source.PreferredConstructionSiteId;
|
||||
target.PreferredModuleId = source.PreferredModuleId;
|
||||
target.TargetPosition = source.TargetPosition;
|
||||
@@ -2771,7 +2771,7 @@ internal sealed class CommanderPlanningService
|
||||
&& string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.TargetEntityId, right.TargetEntityId, 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.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal)
|
||||
&& 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.DestinationStationId, right.DestinationStationId, 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.ModuleId, right.ModuleId, StringComparison.Ordinal)
|
||||
&& left.WaitSeconds.Equals(right.WaitSeconds)
|
||||
@@ -2863,9 +2863,10 @@ internal sealed class CommanderPlanningService
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -2919,7 +2920,7 @@ internal sealed class CommanderPlanningService
|
||||
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.DestinationStationId, right.DestinationStationId, 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.ModuleId, right.ModuleId, StringComparison.Ordinal)
|
||||
&& left.WaitSeconds.Equals(right.WaitSeconds)
|
||||
@@ -3382,7 +3383,7 @@ internal sealed class CommanderPlanningService
|
||||
{
|
||||
"defense-front" => $"Defend {theater.SystemId} from hostile pressure.",
|
||||
"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}.",
|
||||
_ => theater.Kind,
|
||||
};
|
||||
@@ -3424,13 +3425,13 @@ internal sealed class CommanderPlanningService
|
||||
private static Vector3 ResolveExpansionAnchor(SimulationWorld world, IndustryExpansionProject project)
|
||||
{
|
||||
if (project.SiteId is not null
|
||||
&& world.ConstructionSites.FirstOrDefault(site => site.Id == project.SiteId) is { } site
|
||||
&& world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId) is { } siteCelestial)
|
||||
&& world.ConstructionSites.FirstOrDefault(site => site.Id == project.SiteId) is { } site)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ public sealed record TerritoryClaimSnapshot(
|
||||
string? SourceClaimId,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string Status,
|
||||
string ClaimKind,
|
||||
float ClaimStrength,
|
||||
|
||||
@@ -126,7 +126,7 @@ public sealed class TerritoryClaimRuntime
|
||||
public string? SourceClaimId { get; set; }
|
||||
public required string FactionId { 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 ClaimKind { get; set; } = "infrastructure";
|
||||
public float ClaimStrength { get; set; }
|
||||
|
||||
@@ -161,7 +161,7 @@ internal sealed class GeopoliticalSimulationService
|
||||
SourceClaimId = claim.Id,
|
||||
FactionId = claim.FactionId,
|
||||
SystemId = claim.SystemId,
|
||||
CelestialId = claim.CelestialId,
|
||||
AnchorId = claim.AnchorId,
|
||||
Status = claim.State,
|
||||
ClaimKind = "infrastructure",
|
||||
ClaimStrength = claim.State == ClaimStateKinds.Active ? 1f : 0.65f,
|
||||
|
||||
@@ -21,13 +21,13 @@ internal static class FactionIndustryPlanner
|
||||
return null;
|
||||
}
|
||||
|
||||
var targetCelestial = SelectFoundationCelestial(world, factionId, bottleneckCommodity);
|
||||
if (targetCelestial is null)
|
||||
var targetAnchor = SelectFoundationAnchor(world, factionId, bottleneckCommodity);
|
||||
if (targetAnchor is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId);
|
||||
var supportStation = SelectSupportStation(world, factionId, moduleId, targetAnchor.SystemId);
|
||||
if (supportStation is null)
|
||||
{
|
||||
return null;
|
||||
@@ -36,8 +36,8 @@ internal static class FactionIndustryPlanner
|
||||
return new IndustryExpansionProject(
|
||||
bottleneckCommodity,
|
||||
moduleId,
|
||||
targetCelestial.SystemId,
|
||||
targetCelestial.Id,
|
||||
targetAnchor.SystemId,
|
||||
targetAnchor.Id,
|
||||
supportStation.Id);
|
||||
}
|
||||
|
||||
@@ -93,13 +93,13 @@ internal static class FactionIndustryPlanner
|
||||
return null;
|
||||
}
|
||||
|
||||
var targetCelestial = SelectLogisticsFoundationCelestial(world, factionId);
|
||||
if (targetCelestial is null)
|
||||
var targetAnchor = SelectLogisticsFoundationAnchor(world, factionId);
|
||||
if (targetAnchor is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var supportStation = SelectSupportStation(world, factionId, shipyardModuleId, targetCelestial.SystemId);
|
||||
var supportStation = SelectSupportStation(world, factionId, shipyardModuleId, targetAnchor.SystemId);
|
||||
if (supportStation is null)
|
||||
{
|
||||
return null;
|
||||
@@ -108,8 +108,8 @@ internal static class FactionIndustryPlanner
|
||||
return new IndustryExpansionProject(
|
||||
"shipyard",
|
||||
shipyardModuleId,
|
||||
targetCelestial.SystemId,
|
||||
targetCelestial.Id,
|
||||
targetAnchor.SystemId,
|
||||
targetAnchor.Id,
|
||||
supportStation.Id);
|
||||
}
|
||||
|
||||
@@ -129,13 +129,13 @@ internal static class FactionIndustryPlanner
|
||||
return null;
|
||||
}
|
||||
|
||||
var bootstrapCelestial = SelectFoundationCelestial(world, factionId, bootstrapCommodity);
|
||||
if (bootstrapCelestial is null)
|
||||
var bootstrapAnchor = SelectFoundationAnchor(world, factionId, bootstrapCommodity);
|
||||
if (bootstrapAnchor is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bootstrapSupportStation = SelectSupportStation(world, factionId, bootstrapModuleId, bootstrapCelestial.SystemId);
|
||||
var bootstrapSupportStation = SelectSupportStation(world, factionId, bootstrapModuleId, bootstrapAnchor.SystemId);
|
||||
if (bootstrapSupportStation is null)
|
||||
{
|
||||
return null;
|
||||
@@ -144,8 +144,8 @@ internal static class FactionIndustryPlanner
|
||||
return new IndustryExpansionProject(
|
||||
bootstrapCommodity,
|
||||
bootstrapModuleId,
|
||||
bootstrapCelestial.SystemId,
|
||||
bootstrapCelestial.Id,
|
||||
bootstrapAnchor.SystemId,
|
||||
bootstrapAnchor.Id,
|
||||
bootstrapSupportStation.Id);
|
||||
}
|
||||
|
||||
@@ -161,13 +161,13 @@ internal static class FactionIndustryPlanner
|
||||
return null;
|
||||
}
|
||||
|
||||
var targetCelestial = SelectFoundationCelestial(world, factionId, commodityId);
|
||||
if (targetCelestial is null)
|
||||
var targetAnchor = SelectFoundationAnchor(world, factionId, commodityId);
|
||||
if (targetAnchor is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var supportStation = SelectSupportStation(world, factionId, moduleId, targetCelestial.SystemId);
|
||||
var supportStation = SelectSupportStation(world, factionId, moduleId, targetAnchor.SystemId);
|
||||
if (supportStation is null)
|
||||
{
|
||||
return null;
|
||||
@@ -176,8 +176,8 @@ internal static class FactionIndustryPlanner
|
||||
return new IndustryExpansionProject(
|
||||
commodityId,
|
||||
moduleId,
|
||||
targetCelestial.SystemId,
|
||||
targetCelestial.Id,
|
||||
targetAnchor.SystemId,
|
||||
targetAnchor.Id,
|
||||
supportStation.Id);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ internal static class FactionIndustryPlanner
|
||||
site.TargetDefinitionId,
|
||||
site.BlueprintId,
|
||||
site.SystemId,
|
||||
site.CelestialId,
|
||||
site.AnchorId,
|
||||
supportStationId,
|
||||
site.Id);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ internal static class FactionIndustryPlanner
|
||||
}
|
||||
|
||||
var nowUtc = DateTimeOffset.UtcNow;
|
||||
var claimId = $"claim-{factionId}-{project.CelestialId}";
|
||||
var claimId = $"claim-{factionId}-{project.AnchorId}";
|
||||
if (world.Claims.All(candidate => candidate.Id != claimId))
|
||||
{
|
||||
world.Claims.Add(new ClaimRuntime
|
||||
@@ -233,7 +233,7 @@ internal static class FactionIndustryPlanner
|
||||
Id = claimId,
|
||||
FactionId = factionId,
|
||||
SystemId = project.SystemId,
|
||||
CelestialId = project.CelestialId,
|
||||
AnchorId = project.AnchorId,
|
||||
PlacedAtUtc = nowUtc,
|
||||
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
||||
State = ClaimStateKinds.Activating,
|
||||
@@ -246,7 +246,7 @@ internal static class FactionIndustryPlanner
|
||||
return;
|
||||
}
|
||||
|
||||
var siteId = $"site-{factionId}-{project.CelestialId}";
|
||||
var siteId = $"site-{factionId}-{project.AnchorId}";
|
||||
if (world.ConstructionSites.Any(candidate => candidate.Id == siteId))
|
||||
{
|
||||
return;
|
||||
@@ -257,7 +257,7 @@ internal static class FactionIndustryPlanner
|
||||
Id = siteId,
|
||||
FactionId = factionId,
|
||||
SystemId = project.SystemId,
|
||||
CelestialId = project.CelestialId,
|
||||
AnchorId = project.AnchorId,
|
||||
TargetKind = "station-foundation",
|
||||
TargetDefinitionId = project.CommodityId,
|
||||
BlueprintId = project.ModuleId,
|
||||
@@ -450,51 +450,51 @@ internal static class FactionIndustryPlanner
|
||||
private static float GetTargetLevelSeconds(string itemId) =>
|
||||
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);
|
||||
return world.Celestials
|
||||
.Where(celestial =>
|
||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& celestial.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||
.OrderByDescending(celestial => ScoreCelestial(world, factionId, celestial, resourceItems))
|
||||
return world.Anchors
|
||||
.Where(anchor =>
|
||||
anchor.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& anchor.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.AnchorId != anchor.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, anchor.SystemId))
|
||||
.OrderByDescending(anchor => ScoreAnchor(world, factionId, anchor, resourceItems))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static CelestialRuntime? SelectLogisticsFoundationCelestial(SimulationWorld world, string factionId)
|
||||
private static AnchorRuntime? SelectLogisticsFoundationAnchor(SimulationWorld world, string factionId)
|
||||
{
|
||||
return world.Celestials
|
||||
.Where(celestial =>
|
||||
celestial.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& celestial.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.CelestialId != celestial.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, celestial.SystemId))
|
||||
.OrderByDescending(celestial => world.Stations.Count(station =>
|
||||
return world.Anchors
|
||||
.Where(anchor =>
|
||||
anchor.Kind == SpatialNodeKind.LagrangePoint
|
||||
&& anchor.OccupyingStructureId is null
|
||||
&& world.Claims.All(claim => claim.AnchorId != anchor.Id || claim.State == ClaimStateKinds.Destroyed)
|
||||
&& IsExpansionSystemEligible(world, factionId, anchor.SystemId))
|
||||
.OrderByDescending(anchor => world.Stations.Count(station =>
|
||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal)))
|
||||
.ThenByDescending(celestial => world.Stations
|
||||
&& string.Equals(station.SystemId, anchor.SystemId, StringComparison.Ordinal)))
|
||||
.ThenByDescending(anchor => world.Stations
|
||||
.Where(station =>
|
||||
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()))
|
||||
.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
|
||||
.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);
|
||||
var factionPresence = world.Stations.Count(station =>
|
||||
string.Equals(station.FactionId, factionId, StringComparison.Ordinal)
|
||||
&& string.Equals(station.SystemId, celestial.SystemId, StringComparison.Ordinal));
|
||||
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, celestial.SystemId);
|
||||
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, celestial.SystemId);
|
||||
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == celestial.SystemId);
|
||||
&& string.Equals(station.SystemId, anchor.SystemId, StringComparison.Ordinal));
|
||||
var controlState = GeopoliticalSimulationService.GetSystemControlState(world, anchor.SystemId);
|
||||
var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, anchor.SystemId);
|
||||
var strategicProfile = world.Geopolitics?.Territory.StrategicProfiles.FirstOrDefault(profile => profile.SystemId == anchor.SystemId);
|
||||
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)
|
||||
.ThenBy(entry => entry.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
@@ -515,7 +515,7 @@ internal static class FactionIndustryPlanner
|
||||
};
|
||||
var securityPenalty = ((pressure?.HostileInfluence ?? 0f) * 14f)
|
||||
+ ((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
|
||||
+ (factionPresence * 5_000f)
|
||||
+ controlBias
|
||||
@@ -585,6 +585,6 @@ internal sealed record IndustryExpansionProject(
|
||||
string CommodityId,
|
||||
string ModuleId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string SupportStationId,
|
||||
string? SiteId = null);
|
||||
|
||||
@@ -17,16 +17,15 @@ public sealed class GetPlayerIdentitiesHandler(IAuthRepository authRepository, I
|
||||
public override async Task HandleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var users = await authRepository.ListUsersAsync(cancellationToken);
|
||||
var playerFactionsById = playerStateStore.GetPlayerFactions()
|
||||
.ToDictionary(player => player.Id, StringComparer.Ordinal);
|
||||
var playerFactionsByPlayerId = playerStateStore.GetPlayerFactionsByPlayerId();
|
||||
|
||||
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);
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
var userId = user.Id.ToString("N");
|
||||
playerFactionsById.TryGetValue(userId, out var playerFaction);
|
||||
playerFactionsByPlayerId.TryGetValue(userId, out var playerFaction);
|
||||
responses.Add(new PlayerIdentitySummaryResponse(
|
||||
userId,
|
||||
user.Email,
|
||||
@@ -38,19 +37,19 @@ public sealed class GetPlayerIdentitiesHandler(IAuthRepository authRepository, I
|
||||
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;
|
||||
}
|
||||
|
||||
responses.Add(new PlayerIdentitySummaryResponse(
|
||||
playerFaction.Id,
|
||||
$"{playerFaction.Id}@unknown",
|
||||
playerId,
|
||||
$"{playerId}@unknown",
|
||||
Array.Empty<string>(),
|
||||
true,
|
||||
playerFaction.Id,
|
||||
playerId,
|
||||
playerFaction.Label,
|
||||
playerFaction.SovereignFactionId));
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ public sealed record PlayerDirectiveSnapshot(
|
||||
bool UseOrders,
|
||||
string? StagingOrderKind,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredAnchorId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
int Priority,
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed record PlayerDirectiveCommandRequest(
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredAnchorId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
int Priority,
|
||||
|
||||
@@ -251,7 +251,7 @@ public sealed class PlayerDirectiveRuntime
|
||||
public bool UseOrders { get; set; }
|
||||
public string? StagingOrderKind { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? PreferredNodeId { get; set; }
|
||||
public string? PreferredAnchorId { get; set; }
|
||||
public string? PreferredConstructionSiteId { get; set; }
|
||||
public string? PreferredModuleId { get; set; }
|
||||
public int Priority { get; set; } = 50;
|
||||
|
||||
@@ -5,5 +5,6 @@ public interface IPlayerStateStore
|
||||
bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction);
|
||||
PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func<PlayerFactionRuntime> factory);
|
||||
IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions();
|
||||
IReadOnlyDictionary<string, PlayerFactionRuntime> GetPlayerFactionsByPlayerId();
|
||||
void Clear();
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ public sealed class PlayerFactionProjectionService
|
||||
directive.UseOrders,
|
||||
directive.StagingOrderKind,
|
||||
directive.ItemId,
|
||||
directive.PreferredNodeId,
|
||||
directive.PreferredAnchorId,
|
||||
directive.PreferredConstructionSiteId,
|
||||
directive.PreferredModuleId,
|
||||
directive.Priority,
|
||||
@@ -261,7 +261,7 @@ public sealed class PlayerFactionProjectionService
|
||||
template.SourceStationId,
|
||||
template.DestinationStationId,
|
||||
template.ItemId,
|
||||
template.NodeId,
|
||||
template.AnchorId,
|
||||
template.ConstructionSiteId,
|
||||
template.ModuleId,
|
||||
template.WaitSeconds,
|
||||
|
||||
@@ -329,7 +329,7 @@ internal sealed class PlayerFactionService
|
||||
directive.SourceStationId = request.SourceStationId;
|
||||
directive.DestinationStationId = request.DestinationStationId;
|
||||
directive.ItemId = request.ItemId;
|
||||
directive.PreferredNodeId = request.PreferredNodeId;
|
||||
directive.PreferredAnchorId = request.PreferredAnchorId;
|
||||
directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
|
||||
directive.PreferredModuleId = request.PreferredModuleId;
|
||||
directive.Priority = request.Priority;
|
||||
@@ -355,7 +355,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f),
|
||||
@@ -501,7 +501,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f),
|
||||
@@ -692,7 +692,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = request.SourceStationId,
|
||||
DestinationStationId = request.DestinationStationId,
|
||||
ItemId = request.ItemId,
|
||||
NodeId = request.NodeId,
|
||||
AnchorId = request.AnchorId,
|
||||
ConstructionSiteId = request.ConstructionSiteId,
|
||||
ModuleId = request.ModuleId,
|
||||
WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f),
|
||||
@@ -805,7 +805,7 @@ internal sealed class PlayerFactionService
|
||||
directive.SourceStationId = request.HomeStationId;
|
||||
directive.DestinationStationId = null;
|
||||
directive.ItemId = request.ItemId;
|
||||
directive.PreferredNodeId = request.PreferredNodeId;
|
||||
directive.PreferredAnchorId = request.PreferredAnchorId;
|
||||
directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
|
||||
directive.PreferredModuleId = request.PreferredModuleId;
|
||||
directive.Priority = 100;
|
||||
@@ -831,7 +831,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
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,
|
||||
TargetEntityId = directive?.TargetEntityId,
|
||||
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,
|
||||
PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId,
|
||||
TargetPosition = directive?.TargetPosition,
|
||||
@@ -1461,7 +1461,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = directive.SourceStationId ?? directive.HomeStationId,
|
||||
DestinationStationId = directive.DestinationStationId,
|
||||
ItemId = directive.ItemId,
|
||||
NodeId = directive.PreferredNodeId,
|
||||
AnchorId = directive.PreferredAnchorId,
|
||||
ConstructionSiteId = directive.PreferredConstructionSiteId,
|
||||
ModuleId = directive.PreferredModuleId,
|
||||
WaitSeconds = directive.WaitSeconds,
|
||||
@@ -1525,7 +1525,7 @@ internal sealed class PlayerFactionService
|
||||
target.AreaSystemId = source.AreaSystemId;
|
||||
target.TargetEntityId = source.TargetEntityId;
|
||||
target.ItemId = source.ItemId;
|
||||
target.PreferredNodeId = source.PreferredNodeId;
|
||||
target.PreferredAnchorId = source.PreferredAnchorId;
|
||||
target.PreferredConstructionSiteId = source.PreferredConstructionSiteId;
|
||||
target.PreferredModuleId = source.PreferredModuleId;
|
||||
target.TargetPosition = source.TargetPosition;
|
||||
@@ -1546,7 +1546,7 @@ internal sealed class PlayerFactionService
|
||||
&& string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.TargetEntityId, right.TargetEntityId, 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.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal)
|
||||
&& 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.DestinationStationId, right.DestinationStationId, 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.ModuleId, right.ModuleId, StringComparison.Ordinal)
|
||||
&& left.WaitSeconds.Equals(right.WaitSeconds)
|
||||
@@ -1589,7 +1589,7 @@ internal sealed class PlayerFactionService
|
||||
&& string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.DestinationStationId, right.DestinationStationId, 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.ModuleId, right.ModuleId, StringComparison.Ordinal)
|
||||
&& left.WaitSeconds.Equals(right.WaitSeconds)
|
||||
@@ -1634,7 +1634,7 @@ internal sealed class PlayerFactionService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
WaitSeconds = template.WaitSeconds,
|
||||
|
||||
@@ -22,5 +22,8 @@ public sealed class PlayerStateStore : IPlayerStateStore
|
||||
public IReadOnlyCollection<PlayerFactionRuntime> GetPlayerFactions() =>
|
||||
_playerFactions.Values.ToList();
|
||||
|
||||
public IReadOnlyDictionary<string, PlayerFactionRuntime> GetPlayerFactionsByPlayerId() =>
|
||||
new Dictionary<string, PlayerFactionRuntime>(_playerFactions, StringComparer.Ordinal);
|
||||
|
||||
public void Clear() => _playerFactions.Clear();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public enum SpatialNodeKind
|
||||
Planet,
|
||||
Moon,
|
||||
LagrangePoint,
|
||||
ResourceNode,
|
||||
}
|
||||
|
||||
public enum WorkStatus
|
||||
@@ -286,6 +287,7 @@ public static class SimulationEnumMappings
|
||||
SpatialNodeKind.Planet => "planet",
|
||||
SpatialNodeKind.Moon => "moon",
|
||||
SpatialNodeKind.LagrangePoint => "lagrange-point",
|
||||
SpatialNodeKind.ResourceNode => "resource-node",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
|
||||
};
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ public sealed partial class ShipAiService
|
||||
TargetSystemId = opportunity.Node.SystemId,
|
||||
DestinationStationId = opportunity.DropOffStation.Id,
|
||||
ItemId = opportunity.Node.ItemId,
|
||||
NodeId = opportunity.Node.Id,
|
||||
AnchorId = opportunity.Node.AnchorId,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
@@ -509,7 +509,7 @@ public sealed partial class ShipAiService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
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)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-mineable-node";
|
||||
@@ -578,8 +578,9 @@ public sealed partial class ShipAiService
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Mine {itemId} in {systemId}",
|
||||
TargetEntityId = node.Id,
|
||||
TargetSystemId = node.SystemId,
|
||||
NodeId = node.Id,
|
||||
AnchorId = node.AnchorId,
|
||||
ItemId = node.ItemId,
|
||||
WaitSeconds = 0f,
|
||||
Radius = 0f,
|
||||
@@ -601,7 +602,7 @@ public sealed partial class ShipAiService
|
||||
&& left.TargetPosition == right.TargetPosition
|
||||
&& string.Equals(left.DestinationStationId, right.DestinationStationId, 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.Radius.Equals(right.Radius)
|
||||
&& left.MaxSystemRange == right.MaxSystemRange
|
||||
|
||||
@@ -69,7 +69,7 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
|
||||
var targetPosition = ResolveCurrentTargetPosition(world, subTask);
|
||||
var targetCelestial = ResolveTravelTargetCelestial(world, subTask, targetPosition);
|
||||
var targetAnchor = ResolveTravelTargetAnchor(world, subTask, targetPosition);
|
||||
ship.TargetPosition = targetPosition;
|
||||
|
||||
if (ship.SystemId != subTask.TargetSystemId)
|
||||
@@ -81,32 +81,33 @@ public sealed partial class ShipAiService
|
||||
return SubTaskOutcome.Failed;
|
||||
}
|
||||
|
||||
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId);
|
||||
var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition;
|
||||
return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition);
|
||||
var destinationEntryAnchor = ResolveSystemEntryAnchor(world, subTask.TargetSystemId) ?? targetAnchor;
|
||||
var destinationEntryPosition = destinationEntryAnchor?.Position ?? targetPosition;
|
||||
return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryAnchor, completeOnArrival, targetPosition, targetAnchor);
|
||||
}
|
||||
|
||||
var currentCelestial = ResolveCurrentCelestial(world, ship);
|
||||
if (targetCelestial is not null
|
||||
&& currentCelestial is not null
|
||||
&& !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
|
||||
var currentAnchor = ResolveCurrentAnchor(world, ship);
|
||||
if (targetAnchor is not null
|
||||
&& currentAnchor is not null
|
||||
&& !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal))
|
||||
{
|
||||
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
|
||||
&& ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers
|
||||
if (targetAnchor is not null
|
||||
&& currentAnchor is not null
|
||||
&& !string.Equals(currentAnchor.Id, targetAnchor.Id, StringComparison.Ordinal)
|
||||
&& 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)
|
||||
@@ -157,7 +158,7 @@ public sealed partial class ShipAiService
|
||||
|
||||
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))
|
||||
{
|
||||
subTask.BlockingReason = "node-missing";
|
||||
@@ -165,9 +166,28 @@ public sealed partial class ShipAiService
|
||||
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;
|
||||
if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f))
|
||||
if (distanceToTarget > approachThreshold && !effectivelyAtDeposit)
|
||||
{
|
||||
ship.State = ShipState.MiningApproach;
|
||||
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 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)
|
||||
{
|
||||
return SubTaskOutcome.Completed;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return SubTaskOutcome.Completed;
|
||||
@@ -605,15 +626,22 @@ public sealed partial class ShipAiService
|
||||
float deltaSeconds,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
CelestialRuntime? targetCelestial,
|
||||
AnchorRuntime? currentAnchor,
|
||||
AnchorRuntime? targetAnchor,
|
||||
bool completeOnArrival)
|
||||
{
|
||||
var distance = ship.Position.DistanceTo(targetPosition);
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
||||
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);
|
||||
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))
|
||||
{
|
||||
@@ -621,13 +649,26 @@ public sealed partial class ShipAiService
|
||||
ship.TargetPosition = targetPosition;
|
||||
ship.SystemId = 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;
|
||||
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
ship.State = ShipState.LocalFlight;
|
||||
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -637,18 +678,24 @@ public sealed partial class ShipAiService
|
||||
ShipSubTaskRuntime subTask,
|
||||
float deltaSeconds,
|
||||
Vector3 targetPosition,
|
||||
CelestialRuntime targetCelestial,
|
||||
AnchorRuntime currentAnchor,
|
||||
AnchorRuntime targetAnchor,
|
||||
bool completeOnArrival)
|
||||
{
|
||||
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
|
||||
{
|
||||
Regime = MovementRegimeKind.Warp,
|
||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
||||
DestinationNodeId = targetCelestial.Id,
|
||||
OriginAnchorId = currentAnchor.Id,
|
||||
DestinationAnchorId = targetAnchor.Id,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration),
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
subTask.ElapsedSeconds = 0f;
|
||||
@@ -656,33 +703,47 @@ public sealed partial class ShipAiService
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.Warp;
|
||||
ship.SpatialState.CurrentCelestialId = null;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial.Id;
|
||||
ship.SpatialState.CurrentAnchorId = null;
|
||||
ship.SpatialState.DestinationAnchorId = targetAnchor.Id;
|
||||
|
||||
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
if (ship.State != ShipState.Warping)
|
||||
var spoolDurationSeconds = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
|
||||
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;
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
ship.State = ShipState.Warping;
|
||||
ship.Position = Vector3.Zero;
|
||||
ship.TargetPosition = Vector3.Zero;
|
||||
ship.SpatialState.SystemPosition = originPosition;
|
||||
transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f);
|
||||
subTask.Progress = transit.Progress;
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
|
||||
? ship.Position.DistanceTo(targetPosition)
|
||||
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
|
||||
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
|
||||
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
|
||||
ship.State = ShipState.Warping;
|
||||
var warpTravelDuration = MathF.Max(0.001f, totalDuration - spoolDurationSeconds);
|
||||
var travelElapsed = Math.Clamp(elapsedSeconds - spoolDurationSeconds, 0f, warpTravelDuration);
|
||||
var travelProgress = Math.Clamp(travelElapsed / warpTravelDuration, 0f, 1f);
|
||||
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;
|
||||
if (ship.Position.DistanceTo(targetPosition) > 18f)
|
||||
if (elapsedSeconds < totalDuration - 0.001f)
|
||||
{
|
||||
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(
|
||||
@@ -692,20 +753,24 @@ public sealed partial class ShipAiService
|
||||
float deltaSeconds,
|
||||
string targetSystemId,
|
||||
Vector3 entryPosition,
|
||||
CelestialRuntime? targetCelestial,
|
||||
AnchorRuntime? entryAnchor,
|
||||
bool completeOnArrival,
|
||||
Vector3 finalTargetPosition)
|
||||
Vector3 finalTargetPosition,
|
||||
AnchorRuntime? finalTargetAnchor)
|
||||
{
|
||||
var destinationNodeId = targetCelestial?.Id;
|
||||
var destinationAnchorId = entryAnchor?.Id;
|
||||
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
|
||||
{
|
||||
Regime = MovementRegimeKind.FtlTransit,
|
||||
OriginNodeId = ship.SpatialState.CurrentCelestialId,
|
||||
DestinationNodeId = destinationNodeId,
|
||||
OriginAnchorId = ship.SpatialState.CurrentAnchorId,
|
||||
DestinationAnchorId = destinationAnchorId,
|
||||
StartedAtUtc = world.GeneratedAtUtc,
|
||||
ArrivalDueAtUtc = world.GeneratedAtUtc.AddSeconds(initialSpoolDuration + initialTravelDuration),
|
||||
};
|
||||
ship.SpatialState.Transit = transit;
|
||||
subTask.ElapsedSeconds = 0f;
|
||||
@@ -713,39 +778,32 @@ public sealed partial class ShipAiService
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit;
|
||||
ship.SpatialState.CurrentCelestialId = null;
|
||||
ship.SpatialState.DestinationNodeId = destinationNodeId;
|
||||
ship.SpatialState.CurrentAnchorId = null;
|
||||
ship.SpatialState.DestinationAnchorId = destinationAnchorId;
|
||||
|
||||
if (ship.State != ShipState.Ftl)
|
||||
{
|
||||
ship.State = ShipState.SpoolingFtl;
|
||||
if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f)))
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
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));
|
||||
var spoolDurationSeconds = MathF.Max(ship.Definition.SpoolTime, 0.1f);
|
||||
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);
|
||||
ship.State = elapsedSeconds < spoolDurationSeconds ? ShipState.SpoolingFtl : ShipState.Ftl;
|
||||
transit.Progress = Math.Clamp(elapsedSeconds / totalDuration, 0f, 1f);
|
||||
subTask.Progress = transit.Progress;
|
||||
if (transit.Progress < 0.999f)
|
||||
if (elapsedSeconds < totalDuration - 0.001f)
|
||||
{
|
||||
return SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
ship.Position = entryPosition;
|
||||
ship.Position = Vector3.Zero;
|
||||
ship.TargetPosition = finalTargetPosition;
|
||||
ship.SystemId = targetSystemId;
|
||||
ship.SpatialState.CurrentSystemId = targetSystemId;
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
ship.SpatialState.CurrentAnchorId = entryAnchor?.Id;
|
||||
ship.SpatialState.DestinationAnchorId = finalTargetAnchor?.Id ?? entryAnchor?.Id;
|
||||
ship.SpatialState.SystemPosition = entryPosition;
|
||||
ship.State = ShipState.Arriving;
|
||||
|
||||
// Cross-system travel is only complete once the ship finishes the
|
||||
@@ -753,7 +811,7 @@ public sealed partial class ShipAiService
|
||||
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.TargetPosition = targetPosition;
|
||||
@@ -762,8 +820,14 @@ public sealed partial class ShipAiService
|
||||
ship.SpatialState.Transit = null;
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
||||
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
|
||||
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
|
||||
ship.SpatialState.CurrentAnchorId = targetAnchor?.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;
|
||||
return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ public sealed partial class ShipAiService
|
||||
{
|
||||
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))
|
||||
{
|
||||
var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId);
|
||||
@@ -44,15 +49,20 @@ public sealed partial class ShipAiService
|
||||
if (!string.IsNullOrWhiteSpace(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);
|
||||
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);
|
||||
@@ -76,25 +86,145 @@ public sealed partial class ShipAiService
|
||||
.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)
|
||||
{
|
||||
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
|
||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ResolveShipSystemPosition(world, ship)))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
|
||||
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) =>
|
||||
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) =>
|
||||
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 preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId;
|
||||
var preferredAnchorId = ship.DefaultBehavior.PreferredAnchorId;
|
||||
var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange);
|
||||
var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination);
|
||||
string? deniedReason = null;
|
||||
@@ -194,6 +325,11 @@ public sealed partial class ShipAiService
|
||||
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))
|
||||
{
|
||||
deniedReason ??= reason;
|
||||
@@ -214,7 +350,7 @@ public sealed partial class ShipAiService
|
||||
+ (effectiveMiningSkill * 10f)
|
||||
- distancePenalty
|
||||
- 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}");
|
||||
})
|
||||
.OrderByDescending(candidate => candidate.Score)
|
||||
@@ -452,7 +588,7 @@ public sealed partial class ShipAiService
|
||||
?? 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);
|
||||
string? deniedReason = null;
|
||||
@@ -467,6 +603,11 @@ public sealed partial class ShipAiService
|
||||
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))
|
||||
{
|
||||
deniedReason ??= reason;
|
||||
@@ -487,6 +628,54 @@ public sealed partial class ShipAiService
|
||||
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)
|
||||
{
|
||||
var policy = ResolvePolicy(world, ship.PolicySetId);
|
||||
@@ -686,9 +875,14 @@ public sealed partial class ShipAiService
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -720,6 +914,16 @@ public sealed partial class ShipAiService
|
||||
private static StationRuntime? ResolveStation(SimulationWorld world, string? 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) =>
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -867,7 +1072,7 @@ public sealed partial class ShipAiService
|
||||
|
||||
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)
|
||||
{
|
||||
site.State = ConstructionSiteStateKinds.Destroyed;
|
||||
@@ -878,13 +1083,13 @@ public sealed partial class ShipAiService
|
||||
{
|
||||
Id = $"station-{world.Stations.Count + 1}",
|
||||
SystemId = site.SystemId,
|
||||
AnchorId = site.AnchorId,
|
||||
Label = BuildFoundedStationLabel(site.TargetDefinitionId),
|
||||
Category = "station",
|
||||
Objective = DetermineFoundationObjective(site.TargetDefinitionId),
|
||||
Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color,
|
||||
Position = anchor.Position,
|
||||
Position = Vector3.Zero,
|
||||
FactionId = site.FactionId,
|
||||
CelestialId = site.CelestialId,
|
||||
Health = 600f,
|
||||
MaxHealth = 600f,
|
||||
};
|
||||
|
||||
@@ -128,11 +128,11 @@ public sealed partial class ShipAiService
|
||||
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-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}",
|
||||
[
|
||||
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,
|
||||
string? itemId = null,
|
||||
string? moduleId = null,
|
||||
string? targetNodeId = null) =>
|
||||
string? targetAnchorId = null,
|
||||
string? targetResourceNodeId = null,
|
||||
string? targetResourceDepositId = null) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
@@ -310,7 +312,9 @@ public sealed partial class ShipAiService
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
TargetEntityId = targetEntityId,
|
||||
TargetNodeId = targetNodeId,
|
||||
TargetAnchorId = targetAnchorId,
|
||||
TargetResourceNodeId = targetResourceNodeId,
|
||||
TargetResourceDepositId = targetResourceDepositId,
|
||||
ItemId = itemId,
|
||||
ModuleId = moduleId,
|
||||
Threshold = threshold,
|
||||
|
||||
@@ -171,7 +171,8 @@ public sealed partial class ShipAiService
|
||||
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 (!string.Equals(node.SystemId, systemId, StringComparison.Ordinal))
|
||||
@@ -188,7 +189,7 @@ public sealed partial class ShipAiService
|
||||
}
|
||||
else
|
||||
{
|
||||
node = SelectLocalMiningNode(world, ship, systemId, itemId);
|
||||
node = SelectLocalMiningNode(world, ship, systemId, itemId, anchor?.Id);
|
||||
}
|
||||
|
||||
if (node is null)
|
||||
@@ -197,24 +198,30 @@ public sealed partial class ShipAiService
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
order.FailureReason = "mine-order-incomplete";
|
||||
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)
|
||||
{
|
||||
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);
|
||||
if (node is null || buyer is null)
|
||||
{
|
||||
@@ -222,7 +229,7 @@ public sealed partial class ShipAiService
|
||||
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)
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
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(
|
||||
ship,
|
||||
sourceKind,
|
||||
@@ -408,8 +416,8 @@ public sealed partial class ShipAiService
|
||||
[
|
||||
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", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity())
|
||||
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, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id)
|
||||
]),
|
||||
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(
|
||||
ship,
|
||||
sourceKind,
|
||||
@@ -433,8 +442,8 @@ public sealed partial class ShipAiService
|
||||
[
|
||||
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", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId)
|
||||
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, targetAnchorId: node.AnchorId, targetResourceNodeId: node.Id, targetResourceDepositId: deposit?.Id)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed record ShipOrderCommandRequest(
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? AnchorId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float? WaitSeconds,
|
||||
@@ -28,7 +28,7 @@ public sealed record ShipOrderTemplateCommandRequest(
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? AnchorId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float? WaitSeconds,
|
||||
@@ -43,7 +43,7 @@ public sealed record ShipDefaultBehaviorCommandRequest(
|
||||
string? AreaSystemId,
|
||||
string? TargetEntityId,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredAnchorId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
Vector3Dto? TargetPosition,
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed record ShipOrderSnapshot(
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? AnchorId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float WaitSeconds,
|
||||
@@ -41,7 +41,7 @@ public sealed record ShipOrderTemplateSnapshot(
|
||||
string? SourceStationId,
|
||||
string? DestinationStationId,
|
||||
string? ItemId,
|
||||
string? NodeId,
|
||||
string? AnchorId,
|
||||
string? ConstructionSiteId,
|
||||
string? ModuleId,
|
||||
float WaitSeconds,
|
||||
@@ -56,7 +56,7 @@ public sealed record DefaultBehaviorSnapshot(
|
||||
string? AreaSystemId,
|
||||
string? TargetEntityId,
|
||||
string? ItemId,
|
||||
string? PreferredNodeId,
|
||||
string? PreferredAnchorId,
|
||||
string? PreferredConstructionSiteId,
|
||||
string? PreferredModuleId,
|
||||
Vector3Dto? TargetPosition,
|
||||
@@ -95,7 +95,9 @@ public sealed record ShipSubTaskSnapshot(
|
||||
string Summary,
|
||||
string? TargetEntityId,
|
||||
string? TargetSystemId,
|
||||
string? TargetNodeId,
|
||||
string? TargetAnchorId,
|
||||
string? TargetResourceNodeId,
|
||||
string? TargetResourceDepositId,
|
||||
Vector3Dto? TargetPosition,
|
||||
string? ItemId,
|
||||
string? ModuleId,
|
||||
@@ -135,6 +137,7 @@ public sealed record ShipSnapshot(
|
||||
string Purpose,
|
||||
string Type,
|
||||
string SystemId,
|
||||
string? AnchorId,
|
||||
Vector3Dto LocalPosition,
|
||||
Vector3Dto LocalVelocity,
|
||||
Vector3Dto TargetLocalPosition,
|
||||
@@ -151,11 +154,11 @@ public sealed record ShipSnapshot(
|
||||
string? ControlReason,
|
||||
string? LastReplanReason,
|
||||
string? LastAccessFailureReason,
|
||||
string? CelestialId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
IReadOnlyList<string> CargoTypes,
|
||||
float TravelSpeed,
|
||||
string TravelSpeedUnit,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
@@ -170,6 +173,7 @@ public sealed record ShipDelta(
|
||||
string Purpose,
|
||||
string Type,
|
||||
string SystemId,
|
||||
string? AnchorId,
|
||||
Vector3Dto LocalPosition,
|
||||
Vector3Dto LocalVelocity,
|
||||
Vector3Dto TargetLocalPosition,
|
||||
@@ -186,11 +190,11 @@ public sealed record ShipDelta(
|
||||
string? ControlReason,
|
||||
string? LastReplanReason,
|
||||
string? LastAccessFailureReason,
|
||||
string? CelestialId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
IReadOnlyList<string> CargoTypes,
|
||||
float TravelSpeed,
|
||||
string TravelSpeedUnit,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
@@ -202,17 +206,17 @@ public sealed record ShipDelta(
|
||||
public sealed record ShipSpatialStateSnapshot(
|
||||
string SpaceLayer,
|
||||
string CurrentSystemId,
|
||||
string? CurrentCelestialId,
|
||||
string? CurrentAnchorId,
|
||||
Vector3Dto? LocalPosition,
|
||||
Vector3Dto? SystemPosition,
|
||||
string MovementRegime,
|
||||
string? DestinationNodeId,
|
||||
string? DestinationAnchorId,
|
||||
ShipTransitSnapshot? Transit);
|
||||
|
||||
public sealed record ShipTransitSnapshot(
|
||||
string Regime,
|
||||
string? OriginNodeId,
|
||||
string? DestinationNodeId,
|
||||
string? OriginAnchorId,
|
||||
string? DestinationAnchorId,
|
||||
DateTimeOffset? StartedAtUtc,
|
||||
DateTimeOffset? ArrivalDueAtUtc,
|
||||
float Progress);
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class ShipOrderRuntime
|
||||
public string? SourceStationId { get; set; }
|
||||
public string? DestinationStationId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? AnchorId { get; set; }
|
||||
public string? ConstructionSiteId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public float WaitSeconds { get; set; }
|
||||
@@ -78,7 +78,7 @@ public sealed class DefaultBehaviorRuntime
|
||||
public string? AreaSystemId { get; set; }
|
||||
public string? TargetEntityId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? PreferredNodeId { get; set; }
|
||||
public string? PreferredAnchorId { get; set; }
|
||||
public string? PreferredConstructionSiteId { get; set; }
|
||||
public string? PreferredModuleId { get; set; }
|
||||
public Vector3? TargetPosition { get; set; }
|
||||
@@ -102,7 +102,7 @@ public sealed class ShipOrderTemplateRuntime
|
||||
public string? SourceStationId { get; set; }
|
||||
public string? DestinationStationId { get; set; }
|
||||
public string? ItemId { get; set; }
|
||||
public string? NodeId { get; set; }
|
||||
public string? AnchorId { get; set; }
|
||||
public string? ConstructionSiteId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
public float WaitSeconds { get; set; }
|
||||
@@ -146,7 +146,9 @@ public sealed class ShipSubTaskRuntime
|
||||
public WorkStatus Status { get; set; } = WorkStatus.Pending;
|
||||
public string? TargetEntityId { 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 string? ItemId { get; set; }
|
||||
public string? ModuleId { get; set; }
|
||||
|
||||
@@ -104,12 +104,12 @@ internal sealed class SimulationEngine
|
||||
CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
|
||||
world.Stations.Remove(station);
|
||||
|
||||
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
|
||||
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.State = ClaimStateKinds.Destroyed;
|
||||
|
||||
@@ -24,6 +24,7 @@ internal sealed class SimulationProjectionService
|
||||
false,
|
||||
events,
|
||||
BuildCelestialDeltas(world),
|
||||
BuildAnchorDeltas(world),
|
||||
BuildNodeDeltas(world),
|
||||
BuildStationDeltas(world),
|
||||
BuildClaimDeltas(world),
|
||||
@@ -87,26 +88,37 @@ internal sealed class SimulationProjectionService
|
||||
c.Kind,
|
||||
c.OrbitalAnchor,
|
||||
c.LocalSpaceRadius,
|
||||
c.ParentNodeId,
|
||||
c.ParentAnchorId,
|
||||
c.OccupyingStructureId,
|
||||
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(
|
||||
node.Id,
|
||||
node.AnchorId,
|
||||
node.SystemId,
|
||||
node.LocalPosition,
|
||||
node.CelestialId,
|
||||
node.LocalSpaceRadius,
|
||||
node.SourceKind,
|
||||
node.OreRemaining,
|
||||
node.MaxOre,
|
||||
node.ItemId)).ToList(),
|
||||
node.ItemId,
|
||||
node.Deposits)).ToList(),
|
||||
world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot(
|
||||
station.Id,
|
||||
station.Label,
|
||||
station.Category,
|
||||
station.Objective,
|
||||
station.SystemId,
|
||||
station.AnchorId,
|
||||
station.LocalPosition,
|
||||
station.CelestialId,
|
||||
station.Color,
|
||||
station.DockedShips,
|
||||
station.DockedShipIds,
|
||||
@@ -127,7 +139,7 @@ internal sealed class SimulationProjectionService
|
||||
claim.Id,
|
||||
claim.FactionId,
|
||||
claim.SystemId,
|
||||
claim.CelestialId,
|
||||
claim.AnchorId,
|
||||
claim.State,
|
||||
claim.Health,
|
||||
claim.PlacedAtUtc,
|
||||
@@ -136,7 +148,7 @@ internal sealed class SimulationProjectionService
|
||||
site.Id,
|
||||
site.FactionId,
|
||||
site.SystemId,
|
||||
site.CelestialId,
|
||||
site.AnchorId,
|
||||
site.TargetKind,
|
||||
site.TargetDefinitionId,
|
||||
site.BlueprintId,
|
||||
@@ -180,6 +192,7 @@ internal sealed class SimulationProjectionService
|
||||
ship.Purpose,
|
||||
ship.Type,
|
||||
ship.SystemId,
|
||||
ship.AnchorId,
|
||||
ship.LocalPosition,
|
||||
ship.LocalVelocity,
|
||||
ship.TargetLocalPosition,
|
||||
@@ -196,11 +209,11 @@ internal sealed class SimulationProjectionService
|
||||
ship.ControlReason,
|
||||
ship.LastReplanReason,
|
||||
ship.LastAccessFailureReason,
|
||||
ship.CelestialId,
|
||||
ship.DockedStationId,
|
||||
ship.CommanderId,
|
||||
ship.PolicySetId,
|
||||
ship.CargoCapacity,
|
||||
ship.CargoTypes,
|
||||
ship.TravelSpeed,
|
||||
ship.TravelSpeedUnit,
|
||||
ship.Inventory,
|
||||
@@ -239,6 +252,11 @@ internal sealed class SimulationProjectionService
|
||||
celestial.LastDeltaSignature = BuildCelestialSignature(celestial);
|
||||
}
|
||||
|
||||
foreach (var anchor in world.Anchors)
|
||||
{
|
||||
anchor.LastDeltaSignature = BuildAnchorSignature(anchor);
|
||||
}
|
||||
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
station.LastDeltaSignature = BuildStationSignature(world, station);
|
||||
@@ -298,6 +316,24 @@ internal sealed class SimulationProjectionService
|
||||
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)
|
||||
{
|
||||
var deltas = new List<CelestialDelta>();
|
||||
@@ -466,17 +502,30 @@ internal sealed class SimulationProjectionService
|
||||
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
|
||||
|
||||
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) =>
|
||||
$"{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)
|
||||
{
|
||||
var processes = ToStationActionProgressSnapshots(world, station);
|
||||
return string.Join("|",
|
||||
station.SystemId,
|
||||
station.CelestialId ?? "none",
|
||||
station.AnchorId ?? "none",
|
||||
station.CommanderId ?? "none",
|
||||
station.PolicySetId ?? "none",
|
||||
BuildInventorySignature(station.Inventory),
|
||||
@@ -495,10 +544,10 @@ internal sealed class SimulationProjectionService
|
||||
}
|
||||
|
||||
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) =>
|
||||
$"{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) =>
|
||||
$"{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(",",
|
||||
ToActiveSubTaskSnapshots(ship).Select(subTask =>
|
||||
$"{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.CommanderId ?? "none",
|
||||
ship.PolicySetId ?? "none",
|
||||
ship.SpatialState.SpaceLayer.ToContractValue(),
|
||||
ship.SpatialState.CurrentCelestialId ?? "none",
|
||||
ship.SpatialState.CurrentAnchorId ?? "none",
|
||||
ship.SpatialState.MovementRegime.ToContractValue(),
|
||||
ship.SpatialState.DestinationNodeId ?? "none",
|
||||
ship.SpatialState.DestinationAnchorId ?? "none",
|
||||
ship.SpatialState.Transit?.Regime.ToContractValue() ?? "none",
|
||||
ship.SpatialState.Transit?.OriginNodeId ?? "none",
|
||||
ship.SpatialState.Transit?.DestinationNodeId ?? "none",
|
||||
ship.SpatialState.Transit?.OriginAnchorId ?? "none",
|
||||
ship.SpatialState.Transit?.DestinationAnchorId ?? "none",
|
||||
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
|
||||
GetShipCargoAmount(ship).ToString("0.###"),
|
||||
ship.Skills.Navigation.ToString(CultureInfo.InvariantCulture),
|
||||
@@ -653,13 +702,33 @@ internal sealed class SimulationProjectionService
|
||||
|
||||
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
||||
node.Id,
|
||||
node.AnchorId,
|
||||
node.SystemId,
|
||||
ToDto(node.Position),
|
||||
node.CelestialId,
|
||||
node.LocalSpaceRadius,
|
||||
node.SourceKind,
|
||||
node.OreRemaining,
|
||||
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(
|
||||
celestial.Id,
|
||||
@@ -667,7 +736,7 @@ internal sealed class SimulationProjectionService
|
||||
celestial.Kind.ToContractValue(),
|
||||
ToDto(celestial.Position),
|
||||
celestial.LocalSpaceRadius,
|
||||
celestial.ParentNodeId,
|
||||
celestial.ParentAnchorId,
|
||||
celestial.OccupyingStructureId,
|
||||
celestial.OrbitReferenceId);
|
||||
|
||||
@@ -677,8 +746,8 @@ internal sealed class SimulationProjectionService
|
||||
station.Category,
|
||||
station.Objective,
|
||||
station.SystemId,
|
||||
station.AnchorId,
|
||||
ToDto(station.Position),
|
||||
station.CelestialId,
|
||||
station.Color,
|
||||
station.DockedShipIds.Count,
|
||||
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
|
||||
@@ -737,7 +806,7 @@ internal sealed class SimulationProjectionService
|
||||
claim.Id,
|
||||
claim.FactionId,
|
||||
claim.SystemId,
|
||||
claim.CelestialId,
|
||||
claim.AnchorId,
|
||||
claim.State,
|
||||
claim.Health,
|
||||
claim.PlacedAtUtc,
|
||||
@@ -747,7 +816,7 @@ internal sealed class SimulationProjectionService
|
||||
site.Id,
|
||||
site.FactionId,
|
||||
site.SystemId,
|
||||
site.CelestialId,
|
||||
site.AnchorId,
|
||||
site.TargetKind,
|
||||
site.TargetDefinitionId,
|
||||
site.BlueprintId,
|
||||
@@ -811,6 +880,7 @@ internal sealed class SimulationProjectionService
|
||||
ship.Definition.Purpose.ToDataValue(),
|
||||
ship.Definition.Type.ToDataValue(),
|
||||
ship.SystemId,
|
||||
ship.SpatialState.CurrentAnchorId,
|
||||
ToDto(ship.Position),
|
||||
ToDto(ship.Velocity),
|
||||
ToDto(ship.TargetPosition),
|
||||
@@ -827,11 +897,16 @@ internal sealed class SimulationProjectionService
|
||||
ship.ControlReason,
|
||||
ship.LastReplanReason,
|
||||
ship.LastAccessFailureReason,
|
||||
ship.SpatialState.CurrentCelestialId,
|
||||
ship.DockedStationId,
|
||||
ship.CommanderId,
|
||||
ship.PolicySetId,
|
||||
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).Unit,
|
||||
@@ -880,7 +955,7 @@ internal sealed class SimulationProjectionService
|
||||
order.SourceStationId,
|
||||
order.DestinationStationId,
|
||||
order.ItemId,
|
||||
order.NodeId,
|
||||
order.AnchorId,
|
||||
order.ConstructionSiteId,
|
||||
order.ModuleId,
|
||||
order.WaitSeconds,
|
||||
@@ -906,7 +981,7 @@ internal sealed class SimulationProjectionService
|
||||
behavior.AreaSystemId,
|
||||
behavior.TargetEntityId,
|
||||
behavior.ItemId,
|
||||
behavior.PreferredNodeId,
|
||||
behavior.PreferredAnchorId,
|
||||
behavior.PreferredConstructionSiteId,
|
||||
behavior.PreferredModuleId,
|
||||
behavior.TargetPosition is null ? null : ToDto(behavior.TargetPosition.Value),
|
||||
@@ -929,7 +1004,7 @@ internal sealed class SimulationProjectionService
|
||||
template.SourceStationId,
|
||||
template.DestinationStationId,
|
||||
template.ItemId,
|
||||
template.NodeId,
|
||||
template.AnchorId,
|
||||
template.ConstructionSiteId,
|
||||
template.ModuleId,
|
||||
template.WaitSeconds,
|
||||
@@ -1002,10 +1077,12 @@ internal sealed class SimulationProjectionService
|
||||
subTask.Kind,
|
||||
subTask.Status.ToContractValue(),
|
||||
subTask.Summary,
|
||||
subTask.TargetEntityId,
|
||||
subTask.TargetSystemId,
|
||||
subTask.TargetNodeId,
|
||||
subTask.TargetPosition is null ? null : ToDto(subTask.TargetPosition.Value),
|
||||
subTask.TargetEntityId,
|
||||
subTask.TargetSystemId,
|
||||
subTask.TargetAnchorId,
|
||||
subTask.TargetResourceNodeId,
|
||||
subTask.TargetResourceDepositId,
|
||||
subTask.TargetPosition is null ? null : ToDto(subTask.TargetPosition.Value),
|
||||
subTask.ItemId,
|
||||
subTask.ModuleId,
|
||||
subTask.Threshold,
|
||||
@@ -1408,7 +1485,7 @@ internal sealed class SimulationProjectionService
|
||||
claim.SourceClaimId,
|
||||
claim.FactionId,
|
||||
claim.SystemId,
|
||||
claim.CelestialId,
|
||||
claim.AnchorId,
|
||||
claim.Status,
|
||||
claim.ClaimKind,
|
||||
claim.ClaimStrength,
|
||||
@@ -1564,15 +1641,15 @@ internal sealed class SimulationProjectionService
|
||||
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
|
||||
state.SpaceLayer.ToContractValue(),
|
||||
state.CurrentSystemId,
|
||||
state.CurrentCelestialId,
|
||||
state.CurrentAnchorId,
|
||||
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
|
||||
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
|
||||
state.MovementRegime.ToContractValue(),
|
||||
state.DestinationNodeId,
|
||||
state.DestinationAnchorId,
|
||||
state.Transit is null ? null : new ShipTransitSnapshot(
|
||||
state.Transit.Regime.ToContractValue(),
|
||||
state.Transit.OriginNodeId,
|
||||
state.Transit.DestinationNodeId,
|
||||
state.Transit.OriginAnchorId,
|
||||
state.Transit.DestinationAnchorId,
|
||||
state.Transit.StartedAtUtc,
|
||||
state.Transit.ArrivalDueAtUtc,
|
||||
state.Transit.Progress));
|
||||
|
||||
@@ -10,8 +10,8 @@ public sealed record StationSnapshot(
|
||||
string Category,
|
||||
string Objective,
|
||||
string SystemId,
|
||||
string? AnchorId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? CelestialId,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
IReadOnlyList<string> DockedShipIds,
|
||||
@@ -35,8 +35,8 @@ public sealed record StationDelta(
|
||||
string Category,
|
||||
string Objective,
|
||||
string SystemId,
|
||||
string? AnchorId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? CelestialId,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
IReadOnlyList<string> DockedShipIds,
|
||||
@@ -74,7 +74,7 @@ public sealed record ClaimSnapshot(
|
||||
string Id,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string State,
|
||||
float Health,
|
||||
DateTimeOffset PlacedAtUtc,
|
||||
@@ -84,7 +84,7 @@ public sealed record ClaimDelta(
|
||||
string Id,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string State,
|
||||
float Health,
|
||||
DateTimeOffset PlacedAtUtc,
|
||||
@@ -94,7 +94,7 @@ public sealed record ConstructionSiteSnapshot(
|
||||
string Id,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string TargetKind,
|
||||
string TargetDefinitionId,
|
||||
string? BlueprintId,
|
||||
@@ -112,7 +112,7 @@ public sealed record ConstructionSiteDelta(
|
||||
string Id,
|
||||
string FactionId,
|
||||
string SystemId,
|
||||
string CelestialId,
|
||||
string AnchorId,
|
||||
string TargetKind,
|
||||
string TargetDefinitionId,
|
||||
string? BlueprintId,
|
||||
|
||||
@@ -5,7 +5,7 @@ public sealed class ClaimRuntime
|
||||
public required string Id { get; init; }
|
||||
public required string FactionId { 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 DateTimeOffset PlacedAtUtc { get; init; }
|
||||
public DateTimeOffset ActivatesAtUtc { get; set; }
|
||||
@@ -19,7 +19,7 @@ public sealed class ConstructionSiteRuntime
|
||||
public required string Id { get; init; }
|
||||
public required string FactionId { 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 TargetDefinitionId { get; init; }
|
||||
public string? BlueprintId { get; set; }
|
||||
|
||||
@@ -7,6 +7,7 @@ public sealed class StationRuntime
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string SystemId { get; init; }
|
||||
public string? AnchorId { get; set; }
|
||||
public required string Label { get; set; }
|
||||
public string Category { get; set; } = "station";
|
||||
public string Objective { get; set; } = "general";
|
||||
@@ -14,7 +15,6 @@ public sealed class StationRuntime
|
||||
public required Vector3 Position { get; set; }
|
||||
public float Radius { get; set; } = 24f;
|
||||
public required string FactionId { get; init; }
|
||||
public string? CelestialId { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public List<StationModuleRuntime> Modules { get; } = [];
|
||||
|
||||
@@ -100,7 +100,7 @@ internal sealed class StationLifecycleService
|
||||
{
|
||||
CurrentSystemId = station.SystemId,
|
||||
SpaceLayer = SpaceLayerKind.LocalSpace,
|
||||
CurrentCelestialId = station.CelestialId,
|
||||
CurrentAnchorId = station.AnchorId,
|
||||
LocalPosition = position,
|
||||
SystemPosition = position,
|
||||
MovementRegime = MovementRegimeKind.LocalFlight,
|
||||
|
||||
@@ -33,11 +33,11 @@ public sealed class StreamWorldHandler(WorldService worldService) : EndpointWith
|
||||
}
|
||||
|
||||
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(
|
||||
scopeKind,
|
||||
string.IsNullOrWhiteSpace(systemId) ? null : systemId,
|
||||
string.IsNullOrWhiteSpace(bubbleId) ? null : bubbleId);
|
||||
string.IsNullOrWhiteSpace(anchorId) ? null : anchorId);
|
||||
var stream = worldService.Subscribe(scope, afterSequence, cancellationToken);
|
||||
|
||||
await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken);
|
||||
|
||||
@@ -42,25 +42,57 @@ public sealed record PlanetSnapshot(
|
||||
string Color,
|
||||
bool HasRing);
|
||||
|
||||
public sealed record ResourceDepositSnapshot(
|
||||
string Id,
|
||||
string NodeId,
|
||||
string AnchorId,
|
||||
Vector3Dto LocalPosition,
|
||||
float OreRemaining,
|
||||
float MaxOre);
|
||||
|
||||
public sealed record ResourceNodeSnapshot(
|
||||
string Id,
|
||||
string AnchorId,
|
||||
string SystemId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? CelestialId,
|
||||
float LocalSpaceRadius,
|
||||
string SourceKind,
|
||||
float OreRemaining,
|
||||
float MaxOre,
|
||||
string ItemId);
|
||||
string ItemId,
|
||||
IReadOnlyList<ResourceDepositSnapshot> Deposits);
|
||||
|
||||
public sealed record ResourceNodeDelta(
|
||||
string Id,
|
||||
string AnchorId,
|
||||
string SystemId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? CelestialId,
|
||||
float LocalSpaceRadius,
|
||||
string SourceKind,
|
||||
float OreRemaining,
|
||||
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(
|
||||
string Id,
|
||||
@@ -68,7 +100,7 @@ public sealed record CelestialSnapshot(
|
||||
string Kind,
|
||||
Vector3Dto OrbitalAnchor,
|
||||
float LocalSpaceRadius,
|
||||
string? ParentNodeId,
|
||||
string? ParentAnchorId,
|
||||
string? OccupyingStructureId,
|
||||
string? OrbitReferenceId);
|
||||
|
||||
@@ -78,6 +110,6 @@ public sealed record CelestialDelta(
|
||||
string Kind,
|
||||
Vector3Dto OrbitalAnchor,
|
||||
float LocalSpaceRadius,
|
||||
string? ParentNodeId,
|
||||
string? ParentAnchorId,
|
||||
string? OccupyingStructureId,
|
||||
string? OrbitReferenceId);
|
||||
|
||||
@@ -10,6 +10,7 @@ public sealed record WorldSnapshot(
|
||||
DateTimeOffset GeneratedAtUtc,
|
||||
IReadOnlyList<SystemSnapshot> Systems,
|
||||
IReadOnlyList<CelestialSnapshot> Celestials,
|
||||
IReadOnlyList<AnchorSnapshot> Anchors,
|
||||
IReadOnlyList<ResourceNodeSnapshot> Nodes,
|
||||
IReadOnlyList<StationSnapshot> Stations,
|
||||
IReadOnlyList<ClaimSnapshot> Claims,
|
||||
@@ -29,6 +30,7 @@ public sealed record WorldDelta(
|
||||
bool RequiresSnapshotRefresh,
|
||||
IReadOnlyList<SimulationEventRecord> Events,
|
||||
IReadOnlyList<CelestialDelta> Celestials,
|
||||
IReadOnlyList<AnchorDelta> Anchors,
|
||||
IReadOnlyList<ResourceNodeDelta> Nodes,
|
||||
IReadOnlyList<StationDelta> Stations,
|
||||
IReadOnlyList<ClaimDelta> Claims,
|
||||
@@ -54,7 +56,7 @@ public sealed record SimulationEventRecord(
|
||||
public sealed record ObserverScope(
|
||||
string ScopeKind,
|
||||
string? SystemId = null,
|
||||
string? CelestialId = null);
|
||||
string? AnchorId = null);
|
||||
|
||||
public sealed record OrbitalSimulationSnapshot(
|
||||
double SimulatedSecondsPerRealSecond);
|
||||
|
||||
@@ -6,6 +6,7 @@ public sealed class SimulationWorld
|
||||
public required string Label { get; init; }
|
||||
public required int Seed { 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<CelestialRuntime> Celestials { get; init; }
|
||||
public required List<WreckRuntime> Wrecks { get; init; }
|
||||
|
||||
@@ -7,22 +7,49 @@ public sealed class SystemRuntime
|
||||
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 required string Id { get; init; }
|
||||
public required string AnchorId { get; init; }
|
||||
public required string SystemId { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required string SourceKind { 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 OrbitPhase { get; init; }
|
||||
public float OrbitInclination { get; init; }
|
||||
public float OreRemaining { get; set; }
|
||||
public float MaxOre { get; init; }
|
||||
public List<ResourceDepositRuntime> Deposits { get; } = [];
|
||||
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 required string Id { get; init; }
|
||||
@@ -30,7 +57,7 @@ public sealed class CelestialRuntime
|
||||
public required SpatialNodeKind Kind { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public float LocalSpaceRadius { get; init; }
|
||||
public string? ParentNodeId { get; set; }
|
||||
public string? ParentAnchorId { get; set; }
|
||||
public string? OccupyingStructureId { get; set; }
|
||||
public string? OrbitReferenceId { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
@@ -52,19 +79,19 @@ public sealed class ShipSpatialStateRuntime
|
||||
{
|
||||
public SpaceLayerKind SpaceLayer { get; set; } = SpaceLayerKind.LocalSpace;
|
||||
public required string CurrentSystemId { get; set; }
|
||||
public string? CurrentCelestialId { get; set; }
|
||||
public string? CurrentAnchorId { get; set; }
|
||||
public Vector3? LocalPosition { get; set; }
|
||||
public Vector3? SystemPosition { get; set; }
|
||||
public MovementRegimeKind MovementRegime { get; set; } = MovementRegimeKind.LocalFlight;
|
||||
public string? DestinationNodeId { get; set; }
|
||||
public string? DestinationAnchorId { get; set; }
|
||||
public ShipTransitRuntime? Transit { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ShipTransitRuntime
|
||||
{
|
||||
public required MovementRegimeKind Regime { get; init; }
|
||||
public string? OriginNodeId { get; init; }
|
||||
public string? DestinationNodeId { get; init; }
|
||||
public string? OriginAnchorId { get; init; }
|
||||
public string? DestinationAnchorId { get; init; }
|
||||
public DateTimeOffset? StartedAtUtc { get; set; }
|
||||
public DateTimeOffset? ArrivalDueAtUtc { get; set; }
|
||||
public float Progress { get; set; }
|
||||
|
||||
@@ -18,13 +18,13 @@ public sealed class ScenarioContentBuilder(
|
||||
scenario,
|
||||
topology.SystemsById,
|
||||
topology.SpatialLayout.SystemGraphs,
|
||||
topology.SpatialLayout.Celestials);
|
||||
topology.SpatialLayout.Anchors);
|
||||
|
||||
var patrolRoutes = BuildPatrolRoutes(scenario, topology.SystemsById);
|
||||
var ships = CreateShips(
|
||||
scenario,
|
||||
topology.SystemsById,
|
||||
topology.SpatialLayout.Celestials,
|
||||
topology.SpatialLayout.Anchors,
|
||||
patrolRoutes,
|
||||
stations);
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class ScenarioContentBuilder(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials)
|
||||
IReadOnlyCollection<AnchorRuntime> anchors)
|
||||
{
|
||||
var stations = new List<StationRuntime>();
|
||||
var stationIdCounter = 0;
|
||||
@@ -47,23 +47,27 @@ public sealed class ScenarioContentBuilder(
|
||||
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
|
||||
{
|
||||
Id = $"station-{++stationIdCounter}",
|
||||
SystemId = system.Definition.Id,
|
||||
AnchorId = placement.Anchor.Id,
|
||||
Label = plan.Label,
|
||||
Color = plan.Color,
|
||||
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
|
||||
Position = placement.Position,
|
||||
Position = Vector3.Zero,
|
||||
FactionId = GetRequiredFactionId(plan.FactionId, $"station '{plan.Label}'"),
|
||||
CelestialId = placement.AnchorCelestial.Id,
|
||||
Health = 600f,
|
||||
MaxHealth = 600f,
|
||||
};
|
||||
|
||||
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);
|
||||
foreach (var moduleId in startingModules)
|
||||
@@ -162,7 +166,7 @@ public sealed class ScenarioContentBuilder(
|
||||
private List<ShipRuntime> CreateShips(
|
||||
ScenarioDefinition scenario,
|
||||
IReadOnlyDictionary<string, SystemRuntime> systemsById,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyCollection<AnchorRuntime> anchors,
|
||||
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
|
||||
IReadOnlyCollection<StationRuntime> stations)
|
||||
{
|
||||
@@ -181,6 +185,8 @@ public sealed class ScenarioContentBuilder(
|
||||
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
|
||||
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
|
||||
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
|
||||
{
|
||||
@@ -188,9 +194,9 @@ public sealed class ScenarioContentBuilder(
|
||||
SystemId = formation.SystemId,
|
||||
Definition = definition,
|
||||
FactionId = factionId,
|
||||
Position = position,
|
||||
TargetPosition = position,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
|
||||
Position = localPosition,
|
||||
TargetPosition = localPosition,
|
||||
SpatialState = spatialState,
|
||||
DefaultBehavior = CreateBehavior(
|
||||
definition,
|
||||
formation.SystemId,
|
||||
|
||||
@@ -2,8 +2,15 @@ using static SpaceGame.Api.Universe.Scenario.LoaderSupport;
|
||||
|
||||
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)
|
||||
{
|
||||
var systemGraphs = systems.ToDictionary(
|
||||
@@ -11,6 +18,19 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
BuildSystemSpatialGraph,
|
||||
StringComparer.Ordinal);
|
||||
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 nodeIdCounter = 0;
|
||||
|
||||
@@ -20,24 +40,43 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
foreach (var node in system.Definition.ResourceNodes)
|
||||
{
|
||||
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
|
||||
{
|
||||
Id = $"node-{++nodeIdCounter}",
|
||||
Id = nodeId,
|
||||
AnchorId = nodeId,
|
||||
SystemId = system.Definition.Id,
|
||||
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
|
||||
Position = localPosition,
|
||||
SourceKind = node.SourceKind,
|
||||
ItemId = node.ItemId,
|
||||
CelestialId = anchorCelestial?.Id,
|
||||
LocalSpaceRadius = LocalSpaceRadius,
|
||||
OrbitRadius = node.RadiusOffset,
|
||||
OrbitPhase = node.Angle,
|
||||
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
|
||||
OreRemaining = 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)
|
||||
@@ -70,7 +109,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
kind: SpatialNodeKind.Planet,
|
||||
position: planetPosition,
|
||||
localSpaceRadius: LocalSpaceRadius,
|
||||
parentNodeId: primaryStarNodeId);
|
||||
parentAnchorId: primaryStarNodeId);
|
||||
|
||||
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
|
||||
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
|
||||
@@ -82,7 +121,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
kind: SpatialNodeKind.LagrangePoint,
|
||||
position: point.Position,
|
||||
localSpaceRadius: LocalSpaceRadius,
|
||||
parentNodeId: planetCelestial.Id,
|
||||
parentAnchorId: planetCelestial.Id,
|
||||
orbitReferenceId: point.Designation);
|
||||
lagrangeNodes[point.Designation] = lagrangeCelestial;
|
||||
}
|
||||
@@ -100,7 +139,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
kind: SpatialNodeKind.Moon,
|
||||
position: moonPosition,
|
||||
localSpaceRadius: LocalSpaceRadius,
|
||||
parentNodeId: planetCelestial.Id);
|
||||
parentAnchorId: planetCelestial.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +153,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
SpatialNodeKind kind,
|
||||
Vector3 position,
|
||||
float localSpaceRadius,
|
||||
string? parentNodeId = null,
|
||||
string? parentAnchorId = null,
|
||||
string? orbitReferenceId = null)
|
||||
{
|
||||
var celestial = new CelestialRuntime
|
||||
@@ -124,7 +163,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
Kind = kind,
|
||||
Position = position,
|
||||
LocalSpaceRadius = localSpaceRadius,
|
||||
ParentNodeId = parentNodeId,
|
||||
ParentAnchorId = parentAnchorId,
|
||||
OrbitReferenceId = orbitReferenceId,
|
||||
};
|
||||
|
||||
@@ -183,7 +222,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
InitialStationDefinition plan,
|
||||
SystemRuntime system,
|
||||
SystemSpatialGraph graph,
|
||||
IReadOnlyCollection<CelestialRuntime> existingCelestials)
|
||||
IReadOnlyCollection<AnchorRuntime> existingAnchors)
|
||||
{
|
||||
if (plan.PlanetIndex is int planetIndex &&
|
||||
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
|
||||
@@ -191,28 +230,32 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
|
||||
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 })
|
||||
{
|
||||
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
|
||||
var preferredCelestial = existingCelestials
|
||||
.Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint)
|
||||
.OrderBy(c => c.Position.DistanceTo(targetPosition))
|
||||
var preferredAnchor = existingAnchors
|
||||
.Where(anchor => anchor.SystemId == system.Definition.Id && anchor.Kind == SpatialNodeKind.LagrangePoint)
|
||||
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
|
||||
.FirstOrDefault()
|
||||
?? existingCelestials
|
||||
.Where(c => c.SystemId == system.Definition.Id)
|
||||
.OrderBy(c => c.Position.DistanceTo(targetPosition))
|
||||
?? existingAnchors
|
||||
.Where(anchor => anchor.SystemId == system.Definition.Id && IsConstructibleAnchorKind(anchor.Kind))
|
||||
.OrderBy(anchor => anchor.Position.DistanceTo(targetPosition))
|
||||
.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
|
||||
.FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
|
||||
?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet);
|
||||
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
|
||||
var fallbackAnchor = existingAnchors
|
||||
.Where(anchor => anchor.SystemId == system.Definition.Id)
|
||||
.FirstOrDefault(anchor => anchor.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(anchor.OccupyingStructureId))
|
||||
?? 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
|
||||
@@ -256,20 +299,80 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
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 offset = new Vector3(
|
||||
return new Vector3(
|
||||
MathF.Cos(definition.Angle) * definition.RadiusOffset,
|
||||
verticalOffset,
|
||||
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)
|
||||
@@ -286,19 +389,22 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
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
|
||||
.Where(c => c.SystemId == systemId)
|
||||
.OrderBy(c => c.Position.DistanceTo(position))
|
||||
var nearestAnchor = anchors
|
||||
.Where(anchor => anchor.SystemId == systemId)
|
||||
.OrderBy(anchor => anchor.Position.DistanceTo(position))
|
||||
.FirstOrDefault();
|
||||
var localPosition = nearestAnchor is null
|
||||
? position
|
||||
: position.Subtract(nearestAnchor.Position);
|
||||
|
||||
return new ShipSpatialStateRuntime
|
||||
{
|
||||
CurrentSystemId = systemId,
|
||||
SpaceLayer = SpaceLayerKind.LocalSpace,
|
||||
CurrentCelestialId = nearestCelestial?.Id,
|
||||
LocalPosition = position,
|
||||
CurrentAnchorId = nearestAnchor?.Id,
|
||||
LocalPosition = localPosition,
|
||||
SystemPosition = position,
|
||||
MovementRegime = MovementRegimeKind.LocalFlight,
|
||||
};
|
||||
@@ -307,6 +413,7 @@ public sealed class SpatialBuilder(IBalanceService balance)
|
||||
|
||||
public sealed record ScenarioSpatialLayout(
|
||||
IReadOnlyDictionary<string, SystemSpatialGraph> SystemGraphs,
|
||||
List<AnchorRuntime> Anchors,
|
||||
List<CelestialRuntime> Celestials,
|
||||
List<ResourceNodeRuntime> Nodes);
|
||||
|
||||
@@ -317,4 +424,4 @@ public sealed record SystemSpatialGraph(
|
||||
|
||||
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);
|
||||
|
||||
@@ -18,13 +18,14 @@ public sealed class WorldRuntimeAssembler(
|
||||
var policies = seedingService.CreatePolicies(factions);
|
||||
var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships);
|
||||
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
|
||||
{
|
||||
Label = "Split Viewer / Simulation World",
|
||||
Seed = worldGenerationOptions.Seed,
|
||||
Systems = topology.SystemRuntimes.ToList(),
|
||||
Anchors = topology.SpatialLayout.Anchors,
|
||||
Celestials = topology.SpatialLayout.Celestials,
|
||||
Nodes = topology.SpatialLayout.Nodes,
|
||||
Wrecks = [],
|
||||
|
||||
@@ -74,27 +74,27 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
|
||||
|
||||
internal List<ClaimRuntime> CreateClaims(
|
||||
IReadOnlyCollection<StationRuntime> stations,
|
||||
IReadOnlyCollection<CelestialRuntime> celestials,
|
||||
IReadOnlyCollection<AnchorRuntime> anchors,
|
||||
DateTimeOffset nowUtc)
|
||||
{
|
||||
var stationsByCelestialId = stations
|
||||
.Where(station => station.CelestialId is not null)
|
||||
.ToDictionary(station => station.CelestialId!, StringComparer.Ordinal);
|
||||
var stationsByAnchorId = stations
|
||||
.Where(station => !string.IsNullOrWhiteSpace(station.AnchorId))
|
||||
.ToDictionary(station => station.AnchorId!, StringComparer.Ordinal);
|
||||
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;
|
||||
}
|
||||
|
||||
claims.Add(new ClaimRuntime
|
||||
{
|
||||
Id = $"claim-{celestial.Id}",
|
||||
Id = $"claim-{anchor.Id}",
|
||||
FactionId = station.FactionId,
|
||||
SystemId = celestial.SystemId,
|
||||
CelestialId = celestial.Id,
|
||||
SystemId = anchor.SystemId,
|
||||
AnchorId = anchor.Id,
|
||||
PlacedAtUtc = nowUtc,
|
||||
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
||||
State = ClaimStateKinds.Activating,
|
||||
@@ -119,12 +119,12 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
|
||||
}
|
||||
|
||||
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
|
||||
if (moduleId is null || station.CelestialId is null)
|
||||
if (moduleId is null || station.AnchorId is null)
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
@@ -135,7 +135,7 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData)
|
||||
Id = $"site-{station.Id}",
|
||||
FactionId = station.FactionId,
|
||||
SystemId = station.SystemId,
|
||||
CelestialId = station.CelestialId,
|
||||
AnchorId = station.AnchorId,
|
||||
TargetKind = "station-module",
|
||||
TargetDefinitionId = "station",
|
||||
BlueprintId = moduleId,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
|
||||
using SpaceGame.Api.Universe.Scenario;
|
||||
|
||||
namespace SpaceGame.Api.Universe.Simulation;
|
||||
|
||||
internal sealed class OrbitalStateUpdater
|
||||
@@ -223,22 +225,47 @@ internal sealed class OrbitalStateUpdater
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
@@ -261,20 +288,29 @@ internal sealed class OrbitalStateUpdater
|
||||
{
|
||||
ship.SpatialState.CurrentSystemId = ship.SystemId;
|
||||
ship.SpatialState.LocalPosition = ship.Position;
|
||||
ship.SpatialState.SystemPosition = ship.Position;
|
||||
if (ship.SpatialState.Transit is not null)
|
||||
{
|
||||
ship.SpatialState.CurrentCelestialId = null;
|
||||
ship.SpatialState.CurrentAnchorId = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace;
|
||||
ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight;
|
||||
var nearestCelestial = world.Celestials
|
||||
.Where(candidate => candidate.SystemId == ship.SystemId)
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
|
||||
.FirstOrDefault();
|
||||
ship.SpatialState.CurrentCelestialId = nearestCelestial?.Id;
|
||||
var currentAnchor = ship.SpatialState.CurrentAnchorId is not null
|
||||
? world.Anchors.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentAnchorId)
|
||||
: null;
|
||||
if (currentAnchor is null || !string.Equals(currentAnchor.SystemId, ship.SystemId, StringComparison.Ordinal))
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -282,9 +318,9 @@ internal sealed class OrbitalStateUpdater
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,8 @@ public sealed class WorldService
|
||||
string.Equals(candidate.FactionId, faction.Id, StringComparison.Ordinal)
|
||||
&& string.Equals(candidate.SystemId, system.Definition.Id, StringComparison.Ordinal));
|
||||
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
|
||||
{
|
||||
@@ -322,9 +324,9 @@ public sealed class WorldService
|
||||
SystemId = system.Definition.Id,
|
||||
Definition = definition,
|
||||
FactionId = faction.Id,
|
||||
Position = spawnPosition,
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials),
|
||||
Position = localPosition,
|
||||
TargetPosition = localPosition,
|
||||
SpatialState = spatialState,
|
||||
DefaultBehavior = defaultBehavior,
|
||||
Skills = ShipBootstrapPolicy.CreateSkills(definition),
|
||||
Health = definition.Hull,
|
||||
@@ -352,15 +354,18 @@ public sealed class WorldService
|
||||
? $"{faction.Label} {ToTitleCaseToken(objective)} {CountFactionStationsInSystem(faction.Id, system.Definition.Id) + 1}"
|
||||
: request.Label.Trim();
|
||||
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
|
||||
{
|
||||
Id = stationId,
|
||||
SystemId = system.Definition.Id,
|
||||
AnchorId = anchor.Id,
|
||||
Label = label,
|
||||
Color = faction.Color,
|
||||
Objective = objective,
|
||||
Position = position,
|
||||
Position = Vector3.Zero,
|
||||
FactionId = faction.Id,
|
||||
PolicySetId = faction.DefaultPolicySetId,
|
||||
Health = 600f,
|
||||
@@ -375,6 +380,7 @@ public sealed class WorldService
|
||||
station.PopulationCapacity = GetStationSupportedPopulation(_world.ModuleDefinitions, station);
|
||||
station.WorkforceRequired = GetStationRequiredWorkforce(_world.ModuleDefinitions, station);
|
||||
_world.Stations.Add(station);
|
||||
anchor.OccupyingStructureId = station.Id;
|
||||
|
||||
new GeopoliticalSimulationService().Update(_world, 0f, []);
|
||||
PublishSnapshotRefreshUnsafe("spawn-station", $"Spawned station {station.Id}", "station", station.Id);
|
||||
@@ -490,6 +496,7 @@ public sealed class WorldService
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null);
|
||||
|
||||
_history.Enqueue(worldDelta);
|
||||
@@ -526,6 +533,7 @@ public sealed class WorldService
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null);
|
||||
|
||||
_history.Enqueue(worldDelta);
|
||||
@@ -608,6 +616,8 @@ public sealed class WorldService
|
||||
var shipId = $"ship-{playerFaction.Id}-{definition.Id}-{Guid.NewGuid():N}".ToLowerInvariant();
|
||||
var spawnPosition = ResolveSpawnPosition(system.Definition.Id);
|
||||
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
|
||||
{
|
||||
@@ -615,9 +625,9 @@ public sealed class WorldService
|
||||
SystemId = system.Definition.Id,
|
||||
Definition = definition,
|
||||
FactionId = playerFaction.Id,
|
||||
Position = spawnPosition,
|
||||
TargetPosition = spawnPosition,
|
||||
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials),
|
||||
Position = localPosition,
|
||||
TargetPosition = localPosition,
|
||||
SpatialState = spatialState,
|
||||
DefaultBehavior = defaultBehavior,
|
||||
Skills = ShipBootstrapPolicy.CreateSkills(definition),
|
||||
Health = definition.Hull,
|
||||
@@ -712,7 +722,7 @@ public sealed class WorldService
|
||||
SourceStationId = request.SourceStationId,
|
||||
DestinationStationId = request.DestinationStationId,
|
||||
ItemId = request.ItemId,
|
||||
NodeId = request.NodeId,
|
||||
AnchorId = request.AnchorId,
|
||||
ConstructionSiteId = request.ConstructionSiteId,
|
||||
ModuleId = request.ModuleId,
|
||||
WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f),
|
||||
@@ -780,7 +790,7 @@ public sealed class WorldService
|
||||
ship.DefaultBehavior.AreaSystemId = request.AreaSystemId;
|
||||
ship.DefaultBehavior.TargetEntityId = request.TargetEntityId;
|
||||
ship.DefaultBehavior.ItemId = request.ItemId;
|
||||
ship.DefaultBehavior.PreferredNodeId = request.PreferredNodeId;
|
||||
ship.DefaultBehavior.PreferredAnchorId = request.PreferredAnchorId;
|
||||
ship.DefaultBehavior.PreferredConstructionSiteId = request.PreferredConstructionSiteId;
|
||||
ship.DefaultBehavior.PreferredModuleId = request.PreferredModuleId;
|
||||
ship.DefaultBehavior.TargetPosition = request.TargetPosition is null
|
||||
@@ -807,7 +817,7 @@ public sealed class WorldService
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
AnchorId = template.AnchorId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
WaitSeconds = template.WaitSeconds ?? 0f,
|
||||
@@ -905,6 +915,16 @@ public sealed class WorldService
|
||||
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)
|
||||
{
|
||||
var modules = new List<string>();
|
||||
@@ -1079,9 +1099,9 @@ public sealed class WorldService
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1091,6 +1111,7 @@ public sealed class WorldService
|
||||
.Where((evt) => IsEventVisibleToScope(evt, scope, 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(),
|
||||
Stations = delta.Stations.Where((station) => systemFilter is null || station.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,
|
||||
};
|
||||
|
||||
private string? ResolveCelestialSystemId(string celestialId) =>
|
||||
_world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId;
|
||||
private string? ResolveAnchorSystemId(string anchorId) =>
|
||||
_world.Anchors.FirstOrDefault((anchor) => anchor.Id == anchorId)?.SystemId;
|
||||
|
||||
private string? ResolveMarketOrderSystemId(string orderId)
|
||||
{
|
||||
@@ -1181,7 +1202,7 @@ public sealed class WorldService
|
||||
{
|
||||
"universe" => true,
|
||||
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
|
||||
"local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
|
||||
"local-anchor" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
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 ViewerHistoryLayer from "./components/ViewerHistoryLayer.vue";
|
||||
import ViewerEntityBrowserPanel from "./components/ViewerEntityBrowserPanel.vue";
|
||||
@@ -31,8 +31,8 @@ const authStore = useAuthStore();
|
||||
const playerFactionStore = usePlayerFactionStore();
|
||||
const automationCatalogStore = useShipAutomationCatalogStore();
|
||||
const selectionStore = useViewerSelectionStore();
|
||||
const { selectedEntityId, selectedEntityLabel } = storeToRefs(selectionStore);
|
||||
const { canAccessGm, effectivePlayerId } = storeToRefs(authStore);
|
||||
const { selectedEntityId } = storeToRefs(selectionStore);
|
||||
const { canAccessGm, effectivePlayerId, isActingAsAlternateIdentity } = storeToRefs(authStore);
|
||||
const { playerFaction } = storeToRefs(playerFactionStore);
|
||||
let viewer: GameViewer | undefined;
|
||||
|
||||
@@ -42,14 +42,27 @@ const gmSettingsOpen = ref(false);
|
||||
const gmMenuOpen = ref(false);
|
||||
const leftSidebarTab = ref<"player" | "entities">("player");
|
||||
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 () => {
|
||||
window.addEventListener("pointermove", onWindowPointerMove);
|
||||
window.addEventListener("pointerup", stopRightSidebarResize);
|
||||
window.addEventListener("pointercancel", stopRightSidebarResize);
|
||||
void automationCatalogStore.load();
|
||||
await refreshPlayerContext();
|
||||
await startViewerIfAuthenticated();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("pointermove", onWindowPointerMove);
|
||||
window.removeEventListener("pointerup", stopRightSidebarResize);
|
||||
window.removeEventListener("pointercancel", stopRightSidebarResize);
|
||||
viewer?.dispose();
|
||||
});
|
||||
|
||||
@@ -71,7 +84,7 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => playerFaction.value?.requiresOnboarding ?? false,
|
||||
() => shouldShowOnboarding.value,
|
||||
async (requiresOnboarding) => {
|
||||
if (requiresOnboarding) {
|
||||
viewer?.dispose();
|
||||
@@ -101,8 +114,31 @@ function onFocusSelection(selection: Selectable, cameraMode?: "follow" | "tactic
|
||||
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() {
|
||||
if (!authStore.isAuthenticated || viewer || !playerContextReady.value || playerFaction.value?.requiresOnboarding) {
|
||||
if (!authStore.isAuthenticated || viewer || !playerContextReady.value || shouldShowOnboarding.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +191,7 @@ async function refreshPlayerContext() {
|
||||
<p>Loading your in-universe identity and ownership state.</p>
|
||||
</div>
|
||||
</div>
|
||||
<PlayerOnboardingPanel v-else-if="playerContextReady && playerFaction?.requiresOnboarding" />
|
||||
<PlayerOnboardingPanel v-else-if="shouldShowOnboarding" />
|
||||
<div v-else class="viewer-app">
|
||||
<div
|
||||
ref="canvasHostEl"
|
||||
@@ -232,27 +268,28 @@ async function refreshPlayerContext() {
|
||||
</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">
|
||||
<ViewerEntityInspectorPanel
|
||||
class="min-h-0 flex-1"
|
||||
:fallback-title="hudState.detailPanel.title"
|
||||
:fallback-html="hudState.detailPanel.bodyHtml"
|
||||
@focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-auto rounded-xl bg-[rgba(255,116,88,0.14)] px-3.5 py-3 text-[#ffd8cf]"
|
||||
:hidden="hudState.error.hidden"
|
||||
>
|
||||
{{ hudState.error.message }}
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedEntityId"
|
||||
type="button"
|
||||
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"
|
||||
@click="selectionStore.clearSelection('ui')"
|
||||
>
|
||||
Clear {{ selectedEntityLabel ?? "Selection" }}
|
||||
</button>
|
||||
<div class="viewer-right-sidebar-dock" :style="{ width: `${rightSidebarWidth}px` }">
|
||||
<section class="viewer-right-sidebar pointer-events-auto">
|
||||
<div
|
||||
class="viewer-right-sidebar__resize-handle"
|
||||
:class="rightSidebarResizing ? 'viewer-right-sidebar__resize-handle--active' : ''"
|
||||
@pointerdown="startRightSidebarResize"
|
||||
/>
|
||||
<div class="viewer-right-sidebar__body">
|
||||
<ViewerEntityInspectorPanel
|
||||
class="viewer-right-sidebar__panel"
|
||||
:fallback-title="hudState.detailPanel.title"
|
||||
:fallback-html="hudState.detailPanel.bodyHtml"
|
||||
@focus="(selection, cameraMode) => onFocusSelection(selection, cameraMode)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="viewer-right-sidebar__error"
|
||||
:hidden="hudState.error.hidden"
|
||||
>
|
||||
{{ hudState.error.message }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div ref="historyLayerHostEl">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updatePanFromKeyboard } from "./viewerCamera";
|
||||
import { setShellReticleOpacity } from "./viewerControls";
|
||||
import { renderFrame, resizeViewer, stepCamera } from "./viewerRenderLoop";
|
||||
import { updateSystemStarPresentation } from "./viewerPresentation";
|
||||
import { resolveFocusedCelestialId } from "./viewerSelection";
|
||||
import { resolveFocusedAnchorId } from "./viewerSelection";
|
||||
import { describeSelectionParent } from "./viewerPanels";
|
||||
import {
|
||||
createInitialNetworkStats,
|
||||
@@ -195,6 +195,7 @@ export class ViewerAppController {
|
||||
return this.sceneDataController.createWorldPresentationContext({
|
||||
world: this.world,
|
||||
activeSystemId: this.activeSystemId,
|
||||
focusedAnchorId: this.resolveFocusedAnchorId(),
|
||||
cameraMode: this.cameraMode,
|
||||
povLevel: this.povLevel,
|
||||
orbitYaw: this.orbitYaw,
|
||||
@@ -284,6 +285,7 @@ export class ViewerAppController {
|
||||
this.sceneStore.setViewContext(this.activeSystemId ?? null, this.povLevel);
|
||||
}
|
||||
this.navigationController.updateActiveSystem();
|
||||
this.navigationController.syncGalaxyAnchorToActiveSystem();
|
||||
|
||||
if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) {
|
||||
// Follow camera directly controls systemLayer.camera in updateFollowCamera.
|
||||
@@ -350,8 +352,8 @@ export class ViewerAppController {
|
||||
this.interactionController.refreshHistoryWindows();
|
||||
}
|
||||
|
||||
private resolveFocusedCelestialId() {
|
||||
return resolveFocusedCelestialId(this.world, this.selectedItems);
|
||||
private resolveFocusedAnchorId() {
|
||||
return resolveFocusedAnchorId(this.world, this.selectedItems);
|
||||
}
|
||||
|
||||
private onResize(width: number, height: number) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
export interface WorldStreamScope {
|
||||
scopeKind?: string;
|
||||
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> {
|
||||
@@ -105,8 +105,8 @@ export function openWorldStream(
|
||||
if (scope?.systemId) {
|
||||
query.set("systemId", scope.systemId);
|
||||
}
|
||||
if (scope?.bubbleId) {
|
||||
query.set("bubbleId", scope.bubbleId);
|
||||
if (scope?.anchorId) {
|
||||
query.set("anchorId", scope.anchorId);
|
||||
}
|
||||
|
||||
const stream = new EventSource(`/api/world/stream?${query.toString()}`);
|
||||
|
||||
@@ -115,12 +115,13 @@ function formatShipLocation(ship: ShipSnapshot) {
|
||||
return `Docked ${dockedStation.label}`;
|
||||
}
|
||||
|
||||
if (ship.spatialState.transit?.destinationNodeId) {
|
||||
return `Transit ${ship.systemId}`;
|
||||
const transitAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
|
||||
if (transitAnchorId) {
|
||||
return `Transit ${titleCase(transitAnchorId)}`;
|
||||
}
|
||||
|
||||
if (ship.celestialId) {
|
||||
return `Orbit ${titleCase(ship.celestialId)}`;
|
||||
if (ship.spatialState.currentAnchorId) {
|
||||
return `Anchor ${compactAnchorId(ship.spatialState.currentAnchorId)}`;
|
||||
}
|
||||
|
||||
const system = systemById.value.get(ship.systemId);
|
||||
@@ -129,13 +130,32 @@ function formatShipLocation(ship: ShipSnapshot) {
|
||||
|
||||
function formatStationLocation(station: StationSnapshot) {
|
||||
const system = systemById.value.get(station.systemId);
|
||||
if (station.celestialId) {
|
||||
return `${system?.label ?? station.systemId} · ${titleCase(station.celestialId)}`;
|
||||
if (station.anchorId) {
|
||||
return `${system?.label ?? station.systemId} · ${compactAnchorId(station.anchorId)}`;
|
||||
}
|
||||
|
||||
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) {
|
||||
const travelToken = ship.spatialState.transit ? "TRV" : "";
|
||||
const dockToken = ship.dockedStationId ? "DCK" : "";
|
||||
|
||||
@@ -80,6 +80,14 @@ function formatPercent(value: number) {
|
||||
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>) {
|
||||
return parts.filter((part): part is string => !!part && part.trim().length > 0).join(" · ");
|
||||
}
|
||||
@@ -88,7 +96,7 @@ function describeOrderTarget(order: {
|
||||
itemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
@@ -97,7 +105,7 @@ function describeOrderTarget(order: {
|
||||
return order.itemId
|
||||
?? order.targetEntityId
|
||||
?? order.targetSystemId
|
||||
?? order.nodeId
|
||||
?? order.anchorId
|
||||
?? order.constructionSiteId
|
||||
?? order.destinationStationId
|
||||
?? order.sourceStationId
|
||||
@@ -109,13 +117,15 @@ function describeSubTaskTarget(subTask: {
|
||||
itemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetNodeId?: string | null;
|
||||
targetAnchorId?: string | null;
|
||||
targetResourceNodeId?: string | null;
|
||||
moduleId?: string | null;
|
||||
}) {
|
||||
return subTask.itemId
|
||||
?? subTask.targetEntityId
|
||||
?? subTask.targetSystemId
|
||||
?? subTask.targetNodeId
|
||||
?? subTask.targetAnchorId
|
||||
?? subTask.targetResourceNodeId
|
||||
?? subTask.moduleId
|
||||
?? "—";
|
||||
}
|
||||
@@ -184,8 +194,14 @@ const shipStatusRows = computed(() => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shipLocation = selectedShip.value.spatialState.currentAnchorId
|
||||
?? selectedShip.value.anchorId
|
||||
?? selectedShip.value.systemId
|
||||
?? "unknown";
|
||||
|
||||
return [
|
||||
{ label: "State", value: titleCase(selectedShip.value.state) },
|
||||
{ label: "Location", value: shipLocation },
|
||||
{ label: "Behavior", value: getShipBehaviorLabel(selectedShip.value.defaultBehavior.kind) },
|
||||
{ label: "Control", value: titleCase(selectedShip.value.controlSourceKind) },
|
||||
{ label: "Assignment", value: selectedShip.value.assignment?.kind ?? "unassigned" },
|
||||
@@ -201,20 +217,21 @@ const shipStatusRows = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const shipCargoSummaryRows = computed(() => {
|
||||
const shipCargoBarRows = computed(() => {
|
||||
if (!selectedShip.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const usedCargo = selectedShip.value.inventory.reduce((sum, entry) => sum + entry.amount, 0);
|
||||
return [
|
||||
{ label: "Used", value: formatAmount(usedCargo) },
|
||||
{ label: "Capacity", value: formatAmount(selectedShip.value.cargoCapacity) },
|
||||
{ label: "Free", value: formatAmount(Math.max(selectedShip.value.cargoCapacity - usedCargo, 0)) },
|
||||
{ label: "Travel", value: `${formatAmount(selectedShip.value.travelSpeed)} ${selectedShip.value.travelSpeedUnit}` },
|
||||
{ label: "Hull", value: formatAmount(selectedShip.value.health) },
|
||||
{ label: "Regime", value: titleCase(selectedShip.value.spatialState.movementRegime) },
|
||||
];
|
||||
return [{
|
||||
key: "cargo",
|
||||
label: `${formatCargoTypeLabel(selectedShip.value.cargoTypes)}`,
|
||||
value: usedCargo,
|
||||
valueLabel: formatAmount(usedCargo),
|
||||
max: selectedShip.value.cargoCapacity,
|
||||
maxLabel: formatAmount(selectedShip.value.cargoCapacity),
|
||||
fillRatio: selectedShip.value.cargoCapacity > 0 ? usedCargo / selectedShip.value.cargoCapacity : 0,
|
||||
}];
|
||||
});
|
||||
|
||||
const shipCargoRows = computed(() =>
|
||||
@@ -309,8 +326,13 @@ const stationStatusRows = computed(() => {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stationLocation = selectedStation.value.anchorId
|
||||
?? selectedStation.value.systemId
|
||||
?? "unknown";
|
||||
|
||||
return [
|
||||
{ label: "Category", value: titleCase(selectedStation.value.category) },
|
||||
{ label: "Location", value: stationLocation },
|
||||
{ label: "Objective", value: titleCase(selectedStation.value.objective) },
|
||||
{ label: "Docked", value: `${selectedStation.value.dockedShips} / ${selectedStation.value.dockingPads}` },
|
||||
{
|
||||
@@ -335,10 +357,12 @@ const stationModuleRows = computed(() =>
|
||||
const stationStorageRows = computed(() =>
|
||||
selectedStation.value?.storageUsage.map((entry) => ({
|
||||
key: entry.storageClass,
|
||||
storageClass: titleCase(entry.storageClass),
|
||||
used: formatAmount(entry.used),
|
||||
capacity: formatAmount(entry.capacity),
|
||||
fill: entry.capacity > 0 ? formatPercent(entry.used / entry.capacity) : "0%",
|
||||
label: titleCase(entry.storageClass),
|
||||
value: entry.used,
|
||||
valueLabel: formatAmount(entry.used),
|
||||
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"
|
||||
? (behaviorForm.itemId.trim() || null)
|
||||
: null,
|
||||
preferredNodeId: null,
|
||||
preferredAnchorId: null,
|
||||
preferredConstructionSiteId: null,
|
||||
preferredModuleId: null,
|
||||
targetPosition: null,
|
||||
@@ -461,7 +485,7 @@ async function queueHoldPositionOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -497,7 +521,7 @@ async function queueMoveOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -540,7 +564,7 @@ async function queueMineResourceOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -609,15 +633,20 @@ async function clearOrders() {
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Cargo</h4>
|
||||
<div class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table entity-inspector-table--kv">
|
||||
<tbody>
|
||||
<tr v-for="row in shipCargoSummaryRows" :key="row.label">
|
||||
<th scope="row">{{ row.label }}</th>
|
||||
<td>{{ row.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="entity-inspector-capacity-list">
|
||||
<div v-for="row in shipCargoBarRows" :key="row.key" class="entity-inspector-capacity">
|
||||
<div class="entity-inspector-capacity__header">
|
||||
<span class="entity-inspector-capacity__label">{{ row.label }}</span>
|
||||
<span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
|
||||
</div>
|
||||
<div class="entity-inspector-capacity__scale">
|
||||
<span>0</span>
|
||||
<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 v-if="shipCargoRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
@@ -635,7 +664,7 @@ async function clearOrders() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="entity-inspector-empty">No cargo.</div>
|
||||
<div v-else class="entity-inspector-empty">No wares loaded.</div>
|
||||
</div>
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
@@ -856,25 +885,20 @@ async function clearOrders() {
|
||||
|
||||
<div class="entity-inspector-section">
|
||||
<h4>Storage</h4>
|
||||
<div v-if="stationStorageRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Class</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Used</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Capacity</th>
|
||||
<th scope="col" class="entity-inspector-table__numeric">Fill</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in stationStorageRows" :key="row.key">
|
||||
<td>{{ row.storageClass }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.used }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.capacity }}</td>
|
||||
<td class="entity-inspector-table__numeric">{{ row.fill }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="stationStorageRows.length > 0" class="entity-inspector-capacity-list">
|
||||
<div v-for="row in stationStorageRows" :key="row.key" class="entity-inspector-capacity">
|
||||
<div class="entity-inspector-capacity__header">
|
||||
<span class="entity-inspector-capacity__label">{{ row.label }}</span>
|
||||
<span class="entity-inspector-capacity__value">{{ row.valueLabel }} / {{ row.maxLabel }}</span>
|
||||
</div>
|
||||
<div class="entity-inspector-capacity__scale">
|
||||
<span>0</span>
|
||||
<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 v-if="stationInventoryRows.length > 0" class="entity-inspector-table-wrap">
|
||||
<table class="entity-inspector-table">
|
||||
|
||||
@@ -157,7 +157,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId,
|
||||
nodeId: target.value.selection.kind === "node" ? target.value.selection.id : null,
|
||||
anchorId: target.value.anchorId ?? null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
@@ -182,7 +182,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 8,
|
||||
@@ -207,7 +207,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 6,
|
||||
@@ -232,7 +232,7 @@ async function runAction(action: MenuAction) {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: null,
|
||||
nodeId: null,
|
||||
anchorId: null,
|
||||
constructionSiteId: null,
|
||||
moduleId: null,
|
||||
waitSeconds: 0,
|
||||
|
||||
@@ -149,13 +149,6 @@ function compactRate(value: number | null | undefined) {
|
||||
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) {
|
||||
if (value == null || Number.isNaN(value)) return "—";
|
||||
return `${Math.round(value * 100)}%`;
|
||||
@@ -281,8 +274,6 @@ type ShipRow = {
|
||||
plan: string;
|
||||
step: string;
|
||||
subtask: string;
|
||||
cargo: number;
|
||||
health: number;
|
||||
};
|
||||
|
||||
const shipRows = computed<ShipRow[]>(() =>
|
||||
@@ -305,8 +296,6 @@ const shipRows = computed<ShipRow[]>(() =>
|
||||
plan: s.activePlan ? `${titleCaseToken(s.activePlan.kind)} · ${titleCaseToken(s.activePlan.status)}` : "—",
|
||||
step: currentStep ? `${titleCaseToken(currentStep.kind)} · ${titleCaseToken(currentStep.status)}` : "—",
|
||||
subtask: currentSubTask ? `${titleCaseToken(currentSubTask.kind)} ${Math.round(currentSubTask.progress * 100)}%` : "—",
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
health: Math.round(s.health),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -329,16 +318,11 @@ const shipColumns = [
|
||||
shipColumnHelper.accessor("plan", { header: "Plan" }),
|
||||
shipColumnHelper.accessor("step", { header: "Current Step" }),
|
||||
shipColumnHelper.accessor("subtask", { header: "SubTask" }),
|
||||
shipColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
shipColumnHelper.accessor("health", { header: "HP" }),
|
||||
];
|
||||
|
||||
const shipFilter = ref("");
|
||||
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({
|
||||
get data() { return shipRows.value; },
|
||||
@@ -373,7 +357,6 @@ type StationRow = {
|
||||
docked: string;
|
||||
orders: number;
|
||||
orderDetails: MarketOrderSnapshot[];
|
||||
cargo: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
@@ -400,7 +383,6 @@ const stationRows = computed<StationRow[]>(() =>
|
||||
const order = marketOrderMap.value.get(id);
|
||||
return order ? [order] : [];
|
||||
}),
|
||||
cargo: s.inventory.reduce((sum, e) => sum + e.amount, 0),
|
||||
modules: s.installedModules.length,
|
||||
})),
|
||||
);
|
||||
@@ -421,16 +403,12 @@ const stationColumns = [
|
||||
stationColumnHelper.accessor("workforce", { header: "Workforce" }),
|
||||
stationColumnHelper.accessor("docked", { header: "Docked" }),
|
||||
stationColumnHelper.accessor("orders", { header: "Orders" }),
|
||||
stationColumnHelper.accessor("cargo", {
|
||||
header: "Cargo",
|
||||
cell: (info) => formatCargoAmount(info.getValue()),
|
||||
}),
|
||||
stationColumnHelper.accessor("modules", { header: "Modules" }),
|
||||
];
|
||||
|
||||
const stationFilter = ref("");
|
||||
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({
|
||||
get data() { return stationRows.value; },
|
||||
|
||||
@@ -251,7 +251,7 @@ const behaviorForm = reactive({
|
||||
areaSystemId: "",
|
||||
targetEntityId: "",
|
||||
itemId: "",
|
||||
preferredNodeId: "",
|
||||
preferredAnchorId: "",
|
||||
preferredConstructionSiteId: "",
|
||||
preferredModuleId: "",
|
||||
waitSeconds: 3,
|
||||
@@ -268,7 +268,7 @@ const orderForm = reactive({
|
||||
targetEntityId: "",
|
||||
targetSystemId: "",
|
||||
itemId: "",
|
||||
nodeId: "",
|
||||
anchorId: "",
|
||||
constructionSiteId: "",
|
||||
moduleId: "",
|
||||
waitSeconds: 3,
|
||||
@@ -344,7 +344,7 @@ watch(selectedShip, (ship) => {
|
||||
behaviorForm.areaSystemId = ship.defaultBehavior.areaSystemId ?? ship.systemId;
|
||||
behaviorForm.targetEntityId = ship.defaultBehavior.targetEntityId ?? "";
|
||||
behaviorForm.itemId = ship.defaultBehavior.itemId ?? "";
|
||||
behaviorForm.preferredNodeId = ship.defaultBehavior.preferredNodeId ?? "";
|
||||
behaviorForm.preferredAnchorId = ship.defaultBehavior.preferredAnchorId ?? "";
|
||||
behaviorForm.preferredConstructionSiteId = ship.defaultBehavior.preferredConstructionSiteId ?? "";
|
||||
behaviorForm.preferredModuleId = ship.defaultBehavior.preferredModuleId ?? "";
|
||||
behaviorForm.waitSeconds = ship.defaultBehavior.waitSeconds;
|
||||
@@ -484,7 +484,7 @@ async function submitDirective() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: directiveForm.itemId || null,
|
||||
preferredNodeId: null,
|
||||
preferredAnchorId: null,
|
||||
preferredConstructionSiteId: null,
|
||||
preferredModuleId: null,
|
||||
priority: directiveForm.priority,
|
||||
@@ -612,7 +612,7 @@ async function submitDirectBehavior() {
|
||||
areaSystemId: behaviorForm.areaSystemId || null,
|
||||
targetEntityId: behaviorForm.targetEntityId || null,
|
||||
itemId: behaviorForm.itemId || null,
|
||||
preferredNodeId: behaviorForm.preferredNodeId || null,
|
||||
preferredAnchorId: behaviorForm.preferredAnchorId || null,
|
||||
preferredConstructionSiteId: behaviorForm.preferredConstructionSiteId || null,
|
||||
preferredModuleId: behaviorForm.preferredModuleId || null,
|
||||
targetPosition: null,
|
||||
@@ -646,7 +646,7 @@ async function submitDirectOrder() {
|
||||
sourceStationId: null,
|
||||
destinationStationId: null,
|
||||
itemId: orderForm.itemId || null,
|
||||
nodeId: orderForm.nodeId || null,
|
||||
anchorId: orderForm.anchorId || null,
|
||||
constructionSiteId: orderForm.constructionSiteId || null,
|
||||
moduleId: orderForm.moduleId || null,
|
||||
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>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>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>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>
|
||||
@@ -723,7 +723,7 @@ async function submitDirectOrder() {
|
||||
<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>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>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>
|
||||
|
||||
@@ -7,10 +7,13 @@ export type {
|
||||
OrbitalSimulationSnapshot,
|
||||
} from "./contractsWorld";
|
||||
export type {
|
||||
AnchorSnapshot,
|
||||
AnchorDelta,
|
||||
StarSnapshot,
|
||||
MoonSnapshot,
|
||||
SystemSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceDepositSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
ResourceNodeDelta,
|
||||
CelestialSnapshot,
|
||||
|
||||
@@ -46,26 +46,50 @@ export interface PlanetSnapshot {
|
||||
hasRing: boolean;
|
||||
}
|
||||
|
||||
export interface ResourceDepositSnapshot {
|
||||
id: string;
|
||||
nodeId: string;
|
||||
anchorId: string;
|
||||
localPosition: Vector3Dto;
|
||||
oreRemaining: number;
|
||||
maxOre: number;
|
||||
}
|
||||
|
||||
export interface ResourceNodeSnapshot {
|
||||
id: string;
|
||||
anchorId: string;
|
||||
systemId: string;
|
||||
localPosition: Vector3Dto;
|
||||
celestialId?: string | null;
|
||||
localSpaceRadius: number;
|
||||
sourceKind: string;
|
||||
oreRemaining: number;
|
||||
maxOre: number;
|
||||
itemId: string;
|
||||
deposits: ResourceDepositSnapshot[];
|
||||
}
|
||||
|
||||
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 {
|
||||
id: string;
|
||||
systemId: string;
|
||||
kind: string;
|
||||
orbitalAnchor: Vector3Dto;
|
||||
localSpaceRadius: number;
|
||||
parentNodeId?: string | null;
|
||||
parentAnchorId?: string | null;
|
||||
occupyingStructureId?: string | null;
|
||||
orbitReferenceId?: string | null;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export interface TerritoryClaimSnapshot {
|
||||
sourceClaimId?: string | null;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId?: string | null;
|
||||
anchorId: string;
|
||||
status: string;
|
||||
claimKind: string;
|
||||
claimStrength: number;
|
||||
|
||||
@@ -27,8 +27,8 @@ export interface StationSnapshot {
|
||||
category: string;
|
||||
objective: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
localPosition: Vector3Dto;
|
||||
celestialId?: string | null;
|
||||
color: string;
|
||||
dockedShips: number;
|
||||
dockedShipIds: string[];
|
||||
@@ -53,7 +53,7 @@ export interface ClaimSnapshot {
|
||||
id: string;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
state: string;
|
||||
health: number;
|
||||
placedAtUtc: string;
|
||||
@@ -66,7 +66,7 @@ export interface ConstructionSiteSnapshot {
|
||||
id: string;
|
||||
factionId: string;
|
||||
systemId: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
targetKind: string;
|
||||
targetDefinitionId: string;
|
||||
blueprintId?: string | null;
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface PlayerDirectiveSnapshot {
|
||||
useOrders: boolean;
|
||||
stagingOrderKind?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface ShipOrderSnapshot {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
@@ -43,7 +43,7 @@ export interface ShipOrderTemplateSnapshot {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds: number;
|
||||
@@ -59,7 +59,7 @@ export interface DefaultBehaviorSnapshot {
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
@@ -100,7 +100,9 @@ export interface ShipSubTaskSnapshot {
|
||||
summary: string;
|
||||
targetEntityId?: string | null;
|
||||
targetSystemId?: string | null;
|
||||
targetNodeId?: string | null;
|
||||
targetAnchorId?: string | null;
|
||||
targetResourceNodeId?: string | null;
|
||||
targetResourceDepositId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
itemId?: string | null;
|
||||
moduleId?: string | null;
|
||||
@@ -143,6 +145,7 @@ export interface ShipSnapshot {
|
||||
purpose: string;
|
||||
type: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
localPosition: Vector3Dto;
|
||||
localVelocity: Vector3Dto;
|
||||
targetLocalPosition: Vector3Dto;
|
||||
@@ -159,11 +162,11 @@ export interface ShipSnapshot {
|
||||
controlReason?: string | null;
|
||||
lastReplanReason?: string | null;
|
||||
lastAccessFailureReason?: string | null;
|
||||
celestialId?: string | null;
|
||||
dockedStationId?: string | null;
|
||||
commanderId?: string | null;
|
||||
policySetId?: string | null;
|
||||
cargoCapacity: number;
|
||||
cargoTypes: string[];
|
||||
travelSpeed: number;
|
||||
travelSpeedUnit: string;
|
||||
inventory: InventoryEntry[];
|
||||
@@ -178,18 +181,18 @@ export interface ShipDelta extends ShipSnapshot {}
|
||||
export interface ShipSpatialStateSnapshot {
|
||||
spaceLayer: string;
|
||||
currentSystemId: string;
|
||||
currentCelestialId?: string | null;
|
||||
currentAnchorId?: string | null;
|
||||
localPosition?: Vector3Dto | null;
|
||||
systemPosition?: Vector3Dto | null;
|
||||
movementRegime: string;
|
||||
destinationNodeId?: string | null;
|
||||
destinationAnchorId?: string | null;
|
||||
transit?: ShipTransitSnapshot | null;
|
||||
}
|
||||
|
||||
export interface ShipTransitSnapshot {
|
||||
regime: string;
|
||||
originNodeId?: string | null;
|
||||
destinationNodeId?: string | null;
|
||||
originAnchorId?: string | null;
|
||||
destinationAnchorId?: string | null;
|
||||
startedAtUtc?: string | null;
|
||||
arrivalDueAtUtc?: string | null;
|
||||
progress: number;
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
FactionSnapshot,
|
||||
} from "./contractsFactions";
|
||||
import type {
|
||||
AnchorDelta,
|
||||
AnchorSnapshot,
|
||||
CelestialDelta,
|
||||
CelestialSnapshot,
|
||||
ResourceNodeDelta,
|
||||
@@ -37,6 +39,7 @@ export interface WorldSnapshot {
|
||||
generatedAtUtc: string;
|
||||
systems: SystemSnapshot[];
|
||||
celestials: CelestialSnapshot[];
|
||||
anchors: AnchorSnapshot[];
|
||||
nodes: ResourceNodeSnapshot[];
|
||||
stations: import("./contractsInfrastructure").StationSnapshot[];
|
||||
claims: ClaimSnapshot[];
|
||||
@@ -57,6 +60,7 @@ export interface WorldDelta {
|
||||
requiresSnapshotRefresh: boolean;
|
||||
events: SimulationEventRecord[];
|
||||
celestials: CelestialDelta[];
|
||||
anchors: AnchorDelta[];
|
||||
nodes: ResourceNodeDelta[];
|
||||
stations: import("./contractsInfrastructure").StationDelta[];
|
||||
claims: ClaimDelta[];
|
||||
@@ -84,7 +88,7 @@ export interface SimulationEventRecord {
|
||||
export interface ObserverScope {
|
||||
scopeKind: string;
|
||||
systemId?: string | null;
|
||||
celestialId?: string | null;
|
||||
anchorId?: string | null;
|
||||
}
|
||||
|
||||
export interface OrbitalSimulationSnapshot {
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface PlayerDirectiveCommandRequest {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
priority: number;
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ShipOrderCommandRequest {
|
||||
sourceStationId?: string | null;
|
||||
destinationStationId?: string | null;
|
||||
itemId?: string | null;
|
||||
nodeId?: string | null;
|
||||
anchorId?: string | null;
|
||||
constructionSiteId?: string | null;
|
||||
moduleId?: string | null;
|
||||
waitSeconds?: number | null;
|
||||
@@ -28,7 +28,7 @@ export interface ShipDefaultBehaviorCommandRequest {
|
||||
areaSystemId?: string | null;
|
||||
targetEntityId?: string | null;
|
||||
itemId?: string | null;
|
||||
preferredNodeId?: string | null;
|
||||
preferredAnchorId?: string | null;
|
||||
preferredConstructionSiteId?: string | null;
|
||||
preferredModuleId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
|
||||
@@ -366,6 +366,74 @@ canvas {
|
||||
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 {
|
||||
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||
font-size: 0.8rem;
|
||||
@@ -1705,6 +1773,62 @@ canvas {
|
||||
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 {
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.83rem;
|
||||
@@ -1910,6 +2034,23 @@ canvas {
|
||||
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 {
|
||||
top: 96px;
|
||||
left: 20px;
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ViewerOrderContextMenuTarget {
|
||||
selection: Selectable;
|
||||
label: string;
|
||||
systemId?: string | null;
|
||||
anchorId?: string | null;
|
||||
itemId?: string | null;
|
||||
targetPosition?: Vector3Dto | null;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ResolveSelectionPositionParams {
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
planetVisuals: PlanetVisual[];
|
||||
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 {
|
||||
@@ -47,7 +47,7 @@ interface SeedSystemFocusParams {
|
||||
nodeVisuals: Map<string, NodeVisual>;
|
||||
planetVisuals: PlanetVisual[];
|
||||
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 {
|
||||
@@ -107,6 +107,7 @@ export function applyPanFromScreenDelta(
|
||||
delta: THREE.Vector2,
|
||||
orbitYaw: number,
|
||||
currentDistance: number,
|
||||
cameraFovDegrees: number,
|
||||
povLevel: PovLevel,
|
||||
activeSystemId: string | undefined,
|
||||
systemAnchor: THREE.Vector3,
|
||||
@@ -125,18 +126,19 @@ export function applyPanFromScreenDelta(
|
||||
|
||||
const forward = new THREE.Vector3(Math.cos(orbitYaw), 0, Math.sin(orbitYaw));
|
||||
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) {
|
||||
const scale = povLevel === "system"
|
||||
? THREE.MathUtils.mapLinear(currentDistance, 80, 4000, KILOMETERS_PER_AU * 0.35, KILOMETERS_PER_AU * 6.5)
|
||||
: THREE.MathUtils.mapLinear(currentDistance, minimumDistance, 120, 1200, 180000);
|
||||
systemAnchor.addScaledVector(pan, scale);
|
||||
const systemDisplayToKilometers = 1 / (DISPLAY_UNITS_PER_KILOMETER * ACTIVE_SYSTEM_DETAIL_SCALE);
|
||||
systemAnchor.addScaledVector(pan, systemDisplayToKilometers);
|
||||
return;
|
||||
}
|
||||
|
||||
const galaxyScale = THREE.MathUtils.mapLinear(currentDistance, minimumDistance, maximumDistance, 1800, 22000);
|
||||
galaxyAnchor.addScaledVector(pan, galaxyScale);
|
||||
galaxyAnchor.add(pan);
|
||||
}
|
||||
|
||||
export function determineActiveSystemId(params: DetermineActiveSystemParams): string | undefined {
|
||||
@@ -235,11 +237,11 @@ export function resolveSelectionPosition(params: ResolveSelectionPositionParams)
|
||||
}
|
||||
if (selection.kind === "claim") {
|
||||
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") {
|
||||
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") {
|
||||
const system = world.systems.get(selection.systemId);
|
||||
|
||||
@@ -30,8 +30,14 @@ export function createViewerControllers(host: any) {
|
||||
claimGroup: host.systemLayer.claimGroup,
|
||||
constructionSiteGroup: host.systemLayer.constructionSiteGroup,
|
||||
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,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
localSelectableTargets: host.localLayer.selectableTargets,
|
||||
systemVisuals: host.galaxyLayer.systemVisuals,
|
||||
planetVisuals: host.systemLayer.planetVisuals,
|
||||
celestialVisuals: host.systemLayer.celestialVisuals,
|
||||
@@ -40,6 +46,11 @@ export function createViewerControllers(host: any) {
|
||||
claimVisuals: host.systemLayer.claimVisuals,
|
||||
constructionSiteVisuals: host.systemLayer.constructionSiteVisuals,
|
||||
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({
|
||||
@@ -152,8 +163,9 @@ export function createViewerControllers(host: any) {
|
||||
applyClaimDeltas: (claims) => sceneDataController.applyClaimDeltas(claims),
|
||||
applyConstructionSiteDeltas: (sites) => sceneDataController.applyConstructionSiteDeltas(sites),
|
||||
applyShipDeltas: (ships, tickIntervalMs) => sceneDataController.applyShipDeltas(ships, tickIntervalMs),
|
||||
refreshLocalLayer: () => sceneDataController.refreshLocalLayer(host.world, host.resolveFocusedAnchorId()),
|
||||
refreshHistoryWindows: () => host.refreshHistoryWindows(),
|
||||
resolveFocusedCelestialId: () => host.resolveFocusedCelestialId(),
|
||||
resolveFocusedAnchorId: () => host.resolveFocusedAnchorId(),
|
||||
updateSystemSummaries: () => host.updateSystemSummaries(),
|
||||
applyZoomPresentation: () => presentationController.applyZoomPresentation(),
|
||||
updateNetworkPanel: () => presentationController.updateNetworkPanel(),
|
||||
@@ -191,8 +203,10 @@ export function createViewerControllers(host: any) {
|
||||
mouse: host.mouse,
|
||||
galaxyCamera: host.galaxyLayer.camera,
|
||||
systemCamera: host.systemLayer.camera,
|
||||
localCamera: host.localLayer.camera,
|
||||
galaxySelectableTargets: host.galaxyLayer.selectableTargets,
|
||||
systemSelectableTargets: host.systemLayer.selectableTargets,
|
||||
localSelectableTargets: host.localLayer.selectableTargets,
|
||||
hoverLabelEl: host.hoverLabelEl,
|
||||
hoverConnectorLineEl: host.hoverConnectorLineEl,
|
||||
marqueeEl: host.marqueeEl,
|
||||
@@ -244,6 +258,9 @@ export function createViewerControllers(host: any) {
|
||||
delta,
|
||||
host.orbitYaw,
|
||||
host.currentDistance,
|
||||
host.activeSystemId
|
||||
? (host.povLevel === "local" ? host.localLayer.camera.fov : host.systemLayer.camera.fov)
|
||||
: host.galaxyLayer.camera.fov,
|
||||
host.povLevel,
|
||||
host.activeSystemId,
|
||||
host.systemAnchor,
|
||||
|
||||
@@ -148,9 +148,11 @@ export function updateFollowCamera(params: {
|
||||
|
||||
if (ship.spatialState.movementRegime === "ftl-transit") {
|
||||
systemAnchor.set(0, 0, 0);
|
||||
const destinationNodeId = ship.spatialState.transit?.destinationNodeId;
|
||||
const destinationCelestial = destinationNodeId ? world.celestials.get(destinationNodeId) : undefined;
|
||||
const destinationSystem = destinationCelestial ? world.systems.get(destinationCelestial.systemId) : undefined;
|
||||
const destinationAnchorId = ship.spatialState.transit?.destinationAnchorId ?? ship.spatialState.destinationAnchorId;
|
||||
const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
|
||||
const destinationSystem = destinationAnchor
|
||||
? world.systems.get(destinationAnchor.systemId)
|
||||
: undefined;
|
||||
const originSystem = world.systems.get(ship.systemId);
|
||||
if (originSystem && destinationSystem) {
|
||||
followCameraDesiredDirection
|
||||
|
||||
@@ -36,14 +36,17 @@ export function pickSelectableAtClientPosition(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
raycaster: THREE.Raycaster,
|
||||
mouse: THREE.Vector2,
|
||||
povLevel: PovLevel,
|
||||
galaxyCamera: THREE.Camera,
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
systemCamera: THREE.Camera,
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
localCamera: THREE.Camera,
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
clientX: 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;
|
||||
}
|
||||
|
||||
@@ -51,13 +54,23 @@ export function pickSelectableHitAtClientPosition(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
raycaster: THREE.Raycaster,
|
||||
mouse: THREE.Vector2,
|
||||
povLevel: PovLevel,
|
||||
galaxyCamera: THREE.Camera,
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
systemCamera: THREE.Camera,
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
localCamera: THREE.Camera,
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): 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)
|
||||
const systemHit = pickOneCamera(renderer, raycaster, mouse, systemCamera, systemSelectableTargets, clientX, clientY);
|
||||
if (systemHit) {
|
||||
|
||||
@@ -28,8 +28,10 @@ export interface ViewerInteractionContext {
|
||||
mouse: THREE.Vector2;
|
||||
galaxyCamera: THREE.PerspectiveCamera;
|
||||
systemCamera: THREE.PerspectiveCamera;
|
||||
localCamera: THREE.PerspectiveCamera;
|
||||
galaxySelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
hoverLabelEl: HTMLDivElement;
|
||||
hoverConnectorLineEl: SVGLineElement;
|
||||
marqueeEl: HTMLDivElement;
|
||||
@@ -391,10 +393,13 @@ export class ViewerInteractionController {
|
||||
this.context.renderer,
|
||||
this.context.raycaster,
|
||||
this.context.mouse,
|
||||
this.context.getPovLevel(),
|
||||
this.context.galaxyCamera,
|
||||
this.context.galaxySelectableTargets,
|
||||
this.context.systemCamera,
|
||||
this.context.systemSelectableTargets,
|
||||
this.context.localCamera,
|
||||
this.context.localSelectableTargets,
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
@@ -405,10 +410,13 @@ export class ViewerInteractionController {
|
||||
this.context.renderer,
|
||||
this.context.raycaster,
|
||||
this.context.mouse,
|
||||
this.context.getPovLevel(),
|
||||
this.context.galaxyCamera,
|
||||
this.context.galaxySelectableTargets,
|
||||
this.context.systemCamera,
|
||||
this.context.systemSelectableTargets,
|
||||
this.context.localCamera,
|
||||
this.context.localSelectableTargets,
|
||||
clientX,
|
||||
clientY,
|
||||
);
|
||||
@@ -466,6 +474,7 @@ export class ViewerInteractionController {
|
||||
selection,
|
||||
label: node.itemId,
|
||||
systemId: node.systemId,
|
||||
anchorId: node.anchorId,
|
||||
itemId: node.itemId,
|
||||
targetPosition: node.localPosition,
|
||||
} : null;
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
import * as THREE from "three";
|
||||
import type {
|
||||
ClaimVisual,
|
||||
ConstructionSiteVisual,
|
||||
NodeVisual,
|
||||
Selectable,
|
||||
ShipVisual,
|
||||
StructureVisual,
|
||||
} from "./viewerTypes";
|
||||
|
||||
/**
|
||||
* Local rendering layer.
|
||||
* Scene coordinate unit: reserved for future close-up detail.
|
||||
* Camera far plane covers immediate surroundings.
|
||||
* Currently empty — populated when local-space objects are introduced.
|
||||
*/
|
||||
export class LocalLayer {
|
||||
readonly scene = new THREE.Scene();
|
||||
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);
|
||||
|
||||
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) {
|
||||
this.camera.position.copy(orbitOffset);
|
||||
this.camera.lookAt(LocalLayer.ORIGIN);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as THREE from "three";
|
||||
import { scaleGalaxyVector, toThreeVector } from "./viewerMath";
|
||||
import {
|
||||
determineActiveSystemId,
|
||||
focusOnSelection,
|
||||
@@ -62,6 +63,22 @@ export interface ViewerNavigationContext {
|
||||
export class ViewerNavigationController {
|
||||
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({
|
||||
world: this.context.getWorld(),
|
||||
@@ -70,7 +87,7 @@ export class ViewerNavigationController {
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
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(),
|
||||
galaxyAnchor: this.context.galaxyAnchor,
|
||||
systemAnchor: this.context.systemAnchor,
|
||||
@@ -85,7 +102,7 @@ export class ViewerNavigationController {
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
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,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const moduleProductionById = new Map<string, {
|
||||
const itemTransportById = new Map<string, string>(
|
||||
(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 {
|
||||
CameraMode,
|
||||
NodeVisual,
|
||||
@@ -461,7 +461,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `${celestial.kind} celestial`,
|
||||
bodyHtml: `
|
||||
<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>Local space radius ${celestial.localSpaceRadius.toFixed(0)} km</p>
|
||||
`,
|
||||
@@ -477,7 +477,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `Claim ${claim.id}`,
|
||||
bodyHtml: `
|
||||
<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>Activates ${new Date(claim.activatesAtUtc).toLocaleTimeString()}</p>
|
||||
`,
|
||||
@@ -494,7 +494,7 @@ export function buildDetailPanelState(params: DetailPanelParams) {
|
||||
title: `Construction ${site.id}`,
|
||||
bodyHtml: `
|
||||
<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>State ${site.state}<br>Progress ${(site.progress * 100).toFixed(0)}%</p>
|
||||
<p>Orders ${orderCount}<br>Assigned constructors ${site.assignedConstructorShipIds.length}</p>
|
||||
@@ -608,24 +608,33 @@ export function describeSelectionParent(
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
return station.celestialId
|
||||
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId) ?? `${station.systemId} network`
|
||||
return station.anchorId
|
||||
? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId) ?? `${station.systemId} network`
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "node") {
|
||||
const node = world.nodes.get(selection.id);
|
||||
const visual = node ? nodeVisuals.get(selection.id) : undefined;
|
||||
return describeOrbitalParent(world, node?.systemId, visual?.anchor);
|
||||
return node
|
||||
? describeAnchorPathWithinSystem(world, node.systemId, node.anchorId) ?? node.anchorId
|
||||
: "unknown";
|
||||
}
|
||||
if (selection.kind === "celestial") {
|
||||
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") {
|
||||
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") {
|
||||
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";
|
||||
|
||||
@@ -14,6 +14,16 @@ import {
|
||||
syncShips as syncShipScene,
|
||||
syncStations as syncStationScene,
|
||||
} from "./viewerSceneSync";
|
||||
import {
|
||||
createClaimMesh,
|
||||
createConstructionSiteMesh,
|
||||
createNodeMesh,
|
||||
createResourceDepositMesh,
|
||||
createShipMesh,
|
||||
createShipTacticalIcon,
|
||||
createStationMesh,
|
||||
createTacticalIcon,
|
||||
} from "./viewerSceneFactory";
|
||||
import {
|
||||
deriveNodeOrbital,
|
||||
deriveOrbitalFromLocalPosition,
|
||||
@@ -43,7 +53,7 @@ import type {
|
||||
SystemSnapshot,
|
||||
} from "./contracts";
|
||||
import type { OrbitalAnchor, Selectable } from "./viewerTypes";
|
||||
import { rawObject } from "./viewerScenePrimitives";
|
||||
import { rawObject, registerSelectableTarget } from "./viewerScenePrimitives";
|
||||
|
||||
export interface ViewerSceneDataContext {
|
||||
documentRef: Document;
|
||||
@@ -61,8 +71,14 @@ export interface ViewerSceneDataContext {
|
||||
claimGroup: THREE.Group;
|
||||
constructionSiteGroup: 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>;
|
||||
systemSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
localSelectableTargets: Map<THREE.Object3D, Selectable>;
|
||||
systemVisuals: Map<any, any>;
|
||||
planetVisuals: any[];
|
||||
celestialVisuals: Map<any, any>;
|
||||
@@ -71,6 +87,11 @@ export interface ViewerSceneDataContext {
|
||||
claimVisuals: Map<any, any>;
|
||||
constructionSiteVisuals: 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 {
|
||||
@@ -136,6 +157,162 @@ export class ViewerSceneDataController {
|
||||
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
|
||||
* and updates visibility of all system-filtered objects.
|
||||
@@ -202,6 +379,7 @@ export class ViewerSceneDataController {
|
||||
createWorldPresentationContext(overrides: {
|
||||
world: any;
|
||||
activeSystemId?: string;
|
||||
focusedAnchorId?: string;
|
||||
cameraMode: any;
|
||||
povLevel: any;
|
||||
orbitYaw: number;
|
||||
@@ -215,17 +393,23 @@ export class ViewerSceneDataController {
|
||||
worldTimeSyncMs: this.context.getWorldTimeSyncMs(),
|
||||
worldSeed: this.context.getWorldSeed(),
|
||||
activeSystemId: overrides.activeSystemId,
|
||||
focusedAnchorId: overrides.focusedAnchorId,
|
||||
cameraMode: overrides.cameraMode,
|
||||
povLevel: overrides.povLevel,
|
||||
orbitYaw: overrides.orbitYaw,
|
||||
camera: overrides.systemCamera,
|
||||
systemAnchor: overrides.systemAnchor,
|
||||
shipVisuals: this.context.shipVisuals,
|
||||
localShipVisuals: this.context.localShipVisuals,
|
||||
nodeVisuals: this.context.nodeVisuals,
|
||||
localNodeVisuals: this.context.localNodeVisuals,
|
||||
celestialVisuals: this.context.celestialVisuals,
|
||||
stationVisuals: this.context.stationVisuals,
|
||||
localStationVisuals: this.context.localStationVisuals,
|
||||
claimVisuals: this.context.claimVisuals,
|
||||
localClaimVisuals: this.context.localClaimVisuals,
|
||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||
localConstructionSiteVisuals: this.context.localConstructionSiteVisuals,
|
||||
systemVisuals: this.context.systemVisuals,
|
||||
systemSummaryVisuals: new Map(),
|
||||
toDisplayLocalPosition: overrides.toDisplayLocalPosition,
|
||||
@@ -248,8 +432,14 @@ export class ViewerSceneDataController {
|
||||
claimGroup: this.context.claimGroup,
|
||||
constructionSiteGroup: this.context.constructionSiteGroup,
|
||||
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,
|
||||
systemSelectableTargets: this.context.systemSelectableTargets,
|
||||
localSelectableTargets: this.context.localSelectableTargets,
|
||||
systemVisuals: this.context.systemVisuals,
|
||||
planetVisuals: this.context.planetVisuals,
|
||||
celestialVisuals: this.context.celestialVisuals,
|
||||
@@ -258,12 +448,17 @@ export class ViewerSceneDataController {
|
||||
claimVisuals: this.context.claimVisuals,
|
||||
constructionSiteVisuals: this.context.constructionSiteVisuals,
|
||||
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,
|
||||
shipLength,
|
||||
shipPresentationColor,
|
||||
celestialColor,
|
||||
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),
|
||||
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),
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ConstructionSiteSnapshot,
|
||||
MoonSnapshot,
|
||||
PlanetSnapshot,
|
||||
ResourceDepositSnapshot,
|
||||
ResourceNodeSnapshot,
|
||||
ShipSnapshot,
|
||||
StationSnapshot,
|
||||
@@ -46,6 +47,23 @@ export function createNodeMesh(node: ResourceNodeSnapshot): SceneNode {
|
||||
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 {
|
||||
const color = celestialColor(node.kind);
|
||||
return createSceneNode(new THREE.Mesh(
|
||||
|
||||
@@ -70,6 +70,18 @@ function toSystemPos(localPosition: THREE.Vector3): THREE.Vector3 {
|
||||
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 {
|
||||
documentRef: Document;
|
||||
worldOrbitalTimeSeconds?: number;
|
||||
@@ -98,7 +110,7 @@ interface SceneSyncContext {
|
||||
shipPresentationColor: (ship: ShipSnapshot) => string;
|
||||
celestialColor: (kind: string) => string;
|
||||
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"];
|
||||
deriveNodeOrbital: (node: ResourceNodeSnapshot | ResourceNodeDelta, anchor: NodeVisual["anchor"]) => {
|
||||
radius: number;
|
||||
@@ -250,7 +262,8 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
||||
const mesh = createNodeMesh(node);
|
||||
const icon = createTacticalIcon(context.documentRef, node.sourceKind === "gas-cloud" ? "#7fd6ff" : "#d2b07a", 100);
|
||||
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);
|
||||
icon.setPosition(displayPos);
|
||||
const isActive = node.systemId === activeSystemId;
|
||||
@@ -260,6 +273,7 @@ export function syncNodes(context: SceneSyncContext, nodes: ResourceNodeSnapshot
|
||||
const orbital = context.deriveNodeOrbital(node, anchor);
|
||||
context.nodeVisuals.set(node.id, {
|
||||
systemId: node.systemId,
|
||||
anchorId: node.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
sourceKind: node.sourceKind,
|
||||
@@ -283,7 +297,8 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
||||
const mesh = createStationMesh(station);
|
||||
const icon = createTacticalIcon(context.documentRef, station.color, 130);
|
||||
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);
|
||||
icon.setPosition(displayPos);
|
||||
const isActive = station.systemId === activeSystemId;
|
||||
@@ -294,6 +309,7 @@ export function syncStations(context: SceneSyncContext, stations: StationSnapsho
|
||||
context.stationVisuals.set(station.id, {
|
||||
id: station.id,
|
||||
systemId: station.systemId,
|
||||
anchorId: station.anchorId,
|
||||
mesh,
|
||||
icon,
|
||||
anchor,
|
||||
@@ -313,7 +329,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
|
||||
context.claimVisuals.clear();
|
||||
|
||||
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 mesh = createClaimMesh(claim);
|
||||
const icon = createTacticalIcon(context.documentRef, "#ff5b5b", 90);
|
||||
@@ -324,7 +340,7 @@ export function syncClaims(context: SceneSyncContext, claims: ClaimSnapshot[], a
|
||||
icon.setVisible(isActive);
|
||||
context.claimVisuals.set(claim.id, {
|
||||
id: claim.id,
|
||||
celestialId: claim.celestialId,
|
||||
anchorId: claim.anchorId,
|
||||
systemId: claim.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
@@ -341,7 +357,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
|
||||
context.constructionSiteVisuals.clear();
|
||||
|
||||
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 mesh = createConstructionSiteMesh(site);
|
||||
const icon = createTacticalIcon(context.documentRef, "#9df29c", 90);
|
||||
@@ -352,7 +368,7 @@ export function syncConstructionSites(context: SceneSyncContext, sites: Construc
|
||||
icon.setVisible(isActive);
|
||||
context.constructionSiteVisuals.set(site.id, {
|
||||
id: site.id,
|
||||
celestialId: site.celestialId,
|
||||
anchorId: site.anchorId,
|
||||
systemId: site.systemId,
|
||||
mesh,
|
||||
icon,
|
||||
@@ -374,7 +390,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
const iconBaseScale = 78;
|
||||
const icon = createShipTacticalIcon(context.documentRef, shipColor, iconBaseScale);
|
||||
const localPosition = toThreeVector(ship.localPosition);
|
||||
const displayPos = toSystemPos(localPosition);
|
||||
const displayPos = toSystemPos(resolveShipSystemPosition(ship, context));
|
||||
mesh.setPosition(displayPos);
|
||||
icon.setPosition(displayPos);
|
||||
icon.setColor(shipColor);
|
||||
@@ -386,6 +402,7 @@ export function syncShips(context: SceneSyncContext, ships: ShipSnapshot[], tick
|
||||
registerSelectableTarget(context.systemSelectableTargets, icon, { kind: "ship", id: ship.id });
|
||||
context.shipVisuals.set(ship.id, {
|
||||
systemId: ship.systemId,
|
||||
anchorId: ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined,
|
||||
mesh,
|
||||
icon,
|
||||
iconBaseScale,
|
||||
@@ -430,6 +447,7 @@ export function applyNodeDeltas(context: SceneSyncContext, nodes: ResourceNodeDe
|
||||
}
|
||||
|
||||
visual.systemId = node.systemId;
|
||||
visual.anchorId = node.anchorId;
|
||||
visual.sourceKind = node.sourceKind;
|
||||
visual.localPosition.copy(toThreeVector(node.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.anchorId = station.anchorId;
|
||||
visual.localPosition.copy(toThreeVector(station.localPosition));
|
||||
visual.anchor = context.resolveOrbitalAnchor(station.systemId, visual.localPosition);
|
||||
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.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);
|
||||
visual.mesh.setPosition(displayPos);
|
||||
visual.icon.setPosition(displayPos);
|
||||
@@ -494,7 +514,8 @@ export function applyConstructionSiteDeltas(context: SceneSyncContext, sites: Co
|
||||
}
|
||||
|
||||
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);
|
||||
visual.mesh.setPosition(displayPos);
|
||||
visual.icon.setPosition(displayPos);
|
||||
@@ -514,6 +535,7 @@ export function applyShipDeltas(context: SceneSyncContext, ships: ShipDelta[], t
|
||||
}
|
||||
|
||||
visual.systemId = ship.systemId;
|
||||
visual.anchorId = ship.anchorId ?? ship.spatialState.currentAnchorId ?? undefined;
|
||||
visual.startPosition.copy(getAnimatedShipLocalPosition(visual));
|
||||
visual.authoritativePosition.copy(toThreeVector(ship.localPosition));
|
||||
visual.targetPosition.copy(toThreeVector(ship.targetLocalPosition));
|
||||
|
||||
@@ -20,10 +20,17 @@ export function describeSelectable(world: WorldState | undefined, item: Selectab
|
||||
return world.stations.get(item.id)?.label ?? item.id;
|
||||
}
|
||||
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") {
|
||||
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") {
|
||||
return `claim ${item.id}`;
|
||||
@@ -113,9 +120,7 @@ export function describeHoverLabel(world: WorldState | undefined, item: Selectab
|
||||
return item.id;
|
||||
}
|
||||
|
||||
const anchorPath = node.celestialId
|
||||
? describeCelestialPathWithinSystem(world, node.systemId, node.celestialId)
|
||||
: undefined;
|
||||
const anchorPath = describeAnchorPathWithinSystem(world, node.systemId, node.anchorId);
|
||||
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") {
|
||||
const claim = world.claims.get(item.id);
|
||||
const anchorPath = claim?.celestialId
|
||||
? describeCelestialPathWithinSystem(world, claim.systemId, claim.celestialId)
|
||||
const anchorPath = claim
|
||||
? describeAnchorPathWithinSystem(world, claim.systemId, claim.anchorId)
|
||||
: undefined;
|
||||
return anchorPath ? `${anchorPath} claim` : `Claim ${item.id}`;
|
||||
}
|
||||
|
||||
if (item.kind === "construction-site") {
|
||||
const site = world.constructionSites.get(item.id);
|
||||
const anchorPath = site?.celestialId
|
||||
? describeCelestialPathWithinSystem(world, site.systemId, site.celestialId)
|
||||
const anchorPath = site
|
||||
? describeAnchorPathWithinSystem(world, site.systemId, site.anchorId)
|
||||
: undefined;
|
||||
const siteLabel = site ? (site.blueprintId ?? site.targetDefinitionId) : item.id;
|
||||
return anchorPath ? `${anchorPath} / ${siteLabel}` : `Construction ${siteLabel}`;
|
||||
@@ -210,20 +215,74 @@ export function resolveFocusedCelestialId(world: WorldState | undefined, selecte
|
||||
return selected.id;
|
||||
}
|
||||
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") {
|
||||
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") {
|
||||
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") {
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!world || !systemId) {
|
||||
return "unknown";
|
||||
@@ -330,23 +389,31 @@ export function describeShipState(world: WorldState | undefined, ship: ShipSnaps
|
||||
return baseState;
|
||||
}
|
||||
|
||||
const destinationNodeId = ship.spatialState.destinationNodeId ?? ship.spatialState.transit?.destinationNodeId;
|
||||
if (!destinationNodeId) {
|
||||
const destinationAnchorId = ship.spatialState.destinationAnchorId ?? ship.spatialState.transit?.destinationAnchorId;
|
||||
if (!destinationAnchorId) {
|
||||
return baseState;
|
||||
}
|
||||
|
||||
const destinationCelestial = world.celestials.get(destinationNodeId);
|
||||
if (!destinationCelestial) {
|
||||
return `${baseState} -> ${destinationNodeId}`;
|
||||
}
|
||||
|
||||
const destinationAnchor = destinationAnchorId ? world.anchors.get(destinationAnchorId) : undefined;
|
||||
if (baseState === "warping" || baseState === "spooling-warp") {
|
||||
const destinationPath = describeCelestialPathWithinSystem(world, destinationCelestial.systemId, destinationNodeId);
|
||||
return `${baseState} -> ${destinationPath ?? destinationNodeId}`;
|
||||
const destinationSystemId = destinationAnchor?.systemId ?? ship.spatialState.currentSystemId ?? ship.systemId;
|
||||
const destinationPath = describeAnchorPathWithinSystem(
|
||||
world,
|
||||
destinationSystemId,
|
||||
destinationAnchorId,
|
||||
);
|
||||
return `${baseState} -> ${destinationPath ?? destinationAnchorId}`;
|
||||
}
|
||||
|
||||
const destinationSystem = world.systems.get(destinationCelestial.systemId);
|
||||
return `${baseState} -> ${destinationSystem?.label ?? destinationCelestial.systemId}`;
|
||||
const destinationSystemId = destinationAnchor?.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 {
|
||||
@@ -406,8 +473,8 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
||||
if (ship.dockedStationId) {
|
||||
const station = world.stations.get(ship.dockedStationId);
|
||||
if (station) {
|
||||
const anchorPath = station.celestialId
|
||||
? describeCelestialPathWithinSystem(world, station.systemId, station.celestialId)
|
||||
const anchorPath = station.anchorId
|
||||
? describeAnchorPathWithinSystem(world, station.systemId, station.anchorId)
|
||||
: undefined;
|
||||
return {
|
||||
system: systemLabel,
|
||||
@@ -416,11 +483,11 @@ export function describeShipLocation(world: WorldState | undefined, ship: ShipSn
|
||||
}
|
||||
}
|
||||
|
||||
const currentCelestialId = ship.spatialState.currentCelestialId ?? ship.celestialId;
|
||||
if (currentCelestialId) {
|
||||
const celestialPath = describeCelestialPathWithinSystem(world, systemId, currentCelestialId);
|
||||
if (celestialPath) {
|
||||
return { system: systemLabel, local: celestialPath };
|
||||
const currentAnchorId = ship.spatialState.currentAnchorId ?? ship.anchorId;
|
||||
if (currentAnchorId) {
|
||||
const anchorPath = describeAnchorPathWithinSystem(world, systemId, currentAnchorId);
|
||||
if (anchorPath) {
|
||||
return { system: systemLabel, local: anchorPath };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,9 +513,9 @@ export function describeActiveSpace(
|
||||
return activeSystem.label;
|
||||
}
|
||||
|
||||
const celestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||
if (celestialId) {
|
||||
const localPath = describeCelestialPathWithinSystem(world, activeSystem.id, celestialId);
|
||||
const anchorId = resolveFocusedAnchorId(world, selectedItems);
|
||||
if (anchorId) {
|
||||
const localPath = describeAnchorPathWithinSystem(world, activeSystem.id, anchorId);
|
||||
return localPath
|
||||
? `${activeSystem.label} / ${localPath}`
|
||||
: activeSystem.label;
|
||||
@@ -472,10 +539,9 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (celestial.parentNodeId) {
|
||||
const parentPath = describeCelestialPathWithinSystem(world, systemId, celestial.parentNodeId);
|
||||
const segment = describeCelestialSegment(system, celestial);
|
||||
return parentPath ? `${parentPath}/${segment}` : segment;
|
||||
const anchorId = resolveAnchorIdForCelestial(world, celestialId);
|
||||
if (anchorId) {
|
||||
return describeAnchorPathWithinSystem(world, systemId, anchorId);
|
||||
}
|
||||
|
||||
if (celestial.kind === "star") {
|
||||
@@ -485,6 +551,60 @@ export function describeCelestialPathWithinSystem(world: WorldState, systemId: s
|
||||
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 {
|
||||
const moonMatch = celestial.id.match(/-planet-(\d+)-moon-(\d+)$/);
|
||||
if (moonMatch) {
|
||||
|
||||
@@ -41,6 +41,7 @@ export function createWorldState(snapshot: WorldSnapshot): WorldState {
|
||||
generatedAtUtc: snapshot.generatedAtUtc,
|
||||
systems: new Map(snapshot.systems.map((system) => [system.id, system])),
|
||||
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])),
|
||||
stations: new Map(snapshot.stations.map((station) => [station.id, station])),
|
||||
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) {
|
||||
world.celestials.set(celestial.id, celestial);
|
||||
}
|
||||
for (const anchor of delta.anchors) {
|
||||
world.anchors.set(anchor.id, anchor);
|
||||
}
|
||||
for (const node of delta.nodes) {
|
||||
world.nodes.set(node.id, node);
|
||||
}
|
||||
@@ -101,6 +105,7 @@ export function recordDeltaStats(networkStats: NetworkStats, delta: WorldDelta,
|
||||
+ delta.stations.length
|
||||
+ delta.nodes.length
|
||||
+ delta.celestials.length
|
||||
+ delta.anchors.length
|
||||
+ delta.claims.length
|
||||
+ delta.constructionSites.length
|
||||
+ delta.marketOrders.length
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as THREE from "three";
|
||||
import type { SceneNode } from "./viewerScenePrimitives";
|
||||
import type {
|
||||
AnchorSnapshot,
|
||||
CelestialSnapshot,
|
||||
ClaimSnapshot,
|
||||
ConstructionSiteSnapshot,
|
||||
@@ -35,6 +36,7 @@ export type Selectable =
|
||||
|
||||
export interface ShipVisual {
|
||||
systemId: string;
|
||||
anchorId?: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
iconBaseScale: number;
|
||||
@@ -74,6 +76,7 @@ export type OrbitalAnchor =
|
||||
|
||||
export interface NodeVisual {
|
||||
systemId: string;
|
||||
anchorId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
sourceKind: string;
|
||||
@@ -96,7 +99,8 @@ export interface CelestialVisual {
|
||||
|
||||
export interface ClaimVisual {
|
||||
id: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
celestialId?: string | null;
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
@@ -105,7 +109,8 @@ export interface ClaimVisual {
|
||||
|
||||
export interface ConstructionSiteVisual {
|
||||
id: string;
|
||||
celestialId: string;
|
||||
anchorId: string;
|
||||
celestialId?: string | null;
|
||||
systemId: string;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
@@ -115,6 +120,7 @@ export interface ConstructionSiteVisual {
|
||||
export interface StructureVisual {
|
||||
id: string;
|
||||
systemId: string;
|
||||
anchorId?: string | null;
|
||||
mesh: SceneNode;
|
||||
icon: SceneNode;
|
||||
anchor: OrbitalAnchor;
|
||||
@@ -145,6 +151,7 @@ export interface WorldState {
|
||||
generatedAtUtc: string;
|
||||
systems: Map<string, SystemSnapshot>;
|
||||
celestials: Map<string, CelestialSnapshot>;
|
||||
anchors: Map<string, AnchorSnapshot>;
|
||||
nodes: Map<string, ResourceNodeSnapshot>;
|
||||
stations: Map<string, StationSnapshot>;
|
||||
claims: Map<string, ClaimSnapshot>;
|
||||
|
||||
@@ -65,8 +65,9 @@ export interface ViewerWorldLifecycleContext {
|
||||
applyClaimDeltas: (claims: ClaimDelta[]) => void;
|
||||
applyConstructionSiteDeltas: (sites: ConstructionSiteDelta[]) => void;
|
||||
applyShipDeltas: (ships: ShipDelta[], tickIntervalMs: number) => void;
|
||||
refreshLocalLayer: () => void;
|
||||
refreshHistoryWindows: () => void;
|
||||
resolveFocusedCelestialId: () => string | undefined;
|
||||
resolveFocusedAnchorId: () => string | undefined;
|
||||
updateSystemSummaries: () => void;
|
||||
applyZoomPresentation: () => void;
|
||||
updateNetworkPanel: () => void;
|
||||
@@ -165,6 +166,7 @@ export class ViewerWorldLifecycle {
|
||||
this.context.syncClaims(snapshot.claims);
|
||||
this.context.syncConstructionSites(snapshot.constructionSites);
|
||||
this.context.syncShips(snapshot.ships, snapshot.tickIntervalMs);
|
||||
this.context.refreshLocalLayer();
|
||||
this.rebuildFactions(snapshot.factions);
|
||||
this.context.updateSystemSummaries();
|
||||
this.context.applyZoomPresentation();
|
||||
@@ -185,6 +187,7 @@ export class ViewerWorldLifecycle {
|
||||
this.context.applyClaimDeltas(delta.claims);
|
||||
this.context.applyConstructionSiteDeltas(delta.constructionSites);
|
||||
this.context.applyShipDeltas(delta.ships, delta.tickIntervalMs);
|
||||
this.context.refreshLocalLayer();
|
||||
this.rebuildFactions(cloneFactions(world));
|
||||
this.context.updateSystemSummaries();
|
||||
}
|
||||
@@ -219,6 +222,7 @@ export class ViewerWorldLifecycle {
|
||||
}
|
||||
|
||||
this.context.refreshHistoryWindows();
|
||||
this.context.refreshLocalLayer();
|
||||
this.context.updateSystemPanel();
|
||||
this.refreshStreamScopeIfNeeded();
|
||||
const detailState = buildDetailPanelState({
|
||||
@@ -241,12 +245,12 @@ export class ViewerWorldLifecycle {
|
||||
return { scopeKind: "universe" as const };
|
||||
}
|
||||
|
||||
const celestialId = this.context.resolveFocusedCelestialId();
|
||||
if (this.context.getPovLevel() === "local" && celestialId) {
|
||||
const anchorId = this.context.resolveFocusedAnchorId();
|
||||
if (this.context.getPovLevel() === "local" && anchorId) {
|
||||
return {
|
||||
scopeKind: "local-celestial" as const,
|
||||
scopeKind: "local-anchor" as const,
|
||||
systemId: activeSystemId,
|
||||
celestialId,
|
||||
anchorId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
toThreeVector,
|
||||
} from "./viewerMath";
|
||||
import { ACTIVE_SYSTEM_DETAIL_SCALE } from "./viewerConstants";
|
||||
import { describeActiveSpace, resolveFocusedCelestialId } from "./viewerSelection";
|
||||
import { describeActiveSpace, resolveFocusedAnchorId } from "./viewerSelection";
|
||||
import {
|
||||
resolveShipHeading,
|
||||
updateSystemStarPresentation,
|
||||
@@ -58,15 +58,21 @@ export interface WorldOrbitalContext {
|
||||
|
||||
export interface WorldPresentationContext extends WorldOrbitalContext {
|
||||
activeSystemId?: string;
|
||||
focusedAnchorId?: string;
|
||||
cameraMode: CameraMode;
|
||||
povLevel: PovLevel;
|
||||
orbitYaw: number;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
systemAnchor: THREE.Vector3;
|
||||
shipVisuals: Map<string, ShipVisual>;
|
||||
localShipVisuals: Map<string, ShipVisual>;
|
||||
claimVisuals: Map<string, ClaimVisual>;
|
||||
localClaimVisuals: Map<string, ClaimVisual>;
|
||||
constructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||
localConstructionSiteVisuals: Map<string, ConstructionSiteVisual>;
|
||||
systemVisuals: Map<string, SystemVisual>;
|
||||
localNodeVisuals: Map<string, NodeVisual>;
|
||||
localStationVisuals: Map<string, StructureVisual>;
|
||||
systemSummaryVisuals: Map<string, any>;
|
||||
toDisplayLocalPosition: (localPosition: THREE.Vector3) => THREE.Vector3;
|
||||
updateSystemDetailVisibility: () => void;
|
||||
@@ -95,7 +101,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const worldPosition = getAnimatedShipLocalPosition(visual, now);
|
||||
const worldPosition = resolveShipRenderPosition(context, ship, visual, now, renderMode);
|
||||
const displayPosition = context.toDisplayLocalPosition(worldPosition);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
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()) {
|
||||
const animatedLocalPosition = computeNodeLocalPosition(context, visual, worldTimeSeconds);
|
||||
const animatedLocalPosition = resolveNodeRenderPosition(context, visual, worldTimeSeconds, renderMode);
|
||||
visual.mesh.setPosition(context.toDisplayLocalPosition(animatedLocalPosition));
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
@@ -148,7 +168,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
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);
|
||||
visual.mesh.setPosition(displayPosition);
|
||||
visual.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
@@ -165,7 +185,7 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
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.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.setVisible(visual.systemId === context.activeSystemId);
|
||||
@@ -173,12 +193,41 @@ export function updateWorldPresentation(context: WorldPresentationContext) {
|
||||
}
|
||||
|
||||
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.icon.setPosition(rawObject(visual.mesh).position.clone());
|
||||
visual.mesh.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";
|
||||
@@ -213,6 +262,60 @@ export function resolveShipWorldPosition(
|
||||
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>) {
|
||||
if (!world) {
|
||||
return;
|
||||
@@ -310,9 +413,9 @@ export function describeGameStatus(params: GameStatusParams) {
|
||||
? `sys pos: ${fmtVec(systemAnchor.clone().divideScalar(KILOMETERS_PER_AU), 3)} AU`
|
||||
: "";
|
||||
// Local space: position relative to the focused celestial's orbital anchor in km
|
||||
const focusedCelestialId = resolveFocusedCelestialId(world, selectedItems);
|
||||
const celestialAnchor = focusedCelestialId
|
||||
? world?.celestials.get(focusedCelestialId)?.orbitalAnchor
|
||||
const focusedAnchorId = resolveFocusedAnchorId(world, selectedItems);
|
||||
const celestialAnchor = focusedAnchorId
|
||||
? (world?.anchors.get(focusedAnchorId)?.systemPosition ?? world?.celestials.get(focusedAnchorId)?.orbitalAnchor)
|
||||
: undefined;
|
||||
const locPos = systemAnchor && celestialAnchor
|
||||
? `loc pos: ${fmtVec(systemAnchor.clone().sub(toThreeVector(celestialAnchor)), 0)} km`
|
||||
@@ -415,7 +518,14 @@ export function resolveOrbitalAnchor(context: WorldOrbitalContext, systemId: str
|
||||
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) {
|
||||
const celestial = context.world?.celestials.get(celestialId);
|
||||
if (celestial) {
|
||||
@@ -446,17 +556,17 @@ export function computeCelestialLocalPositionById(
|
||||
}
|
||||
|
||||
const basePosition = toThreeVector(celestial.orbitalAnchor);
|
||||
if (!celestial.parentNodeId) {
|
||||
if (!celestial.parentAnchorId) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
const parentCelestial = context.world.celestials.get(celestial.parentNodeId);
|
||||
const parentCelestial = context.world.celestials.get(celestial.parentAnchorId);
|
||||
if (!parentCelestial) {
|
||||
return basePosition;
|
||||
}
|
||||
|
||||
visiting.add(celestialId);
|
||||
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentNodeId, timeSeconds, visiting);
|
||||
const parentCurrentPosition = computeCelestialLocalPositionById(context, celestial.parentAnchorId, timeSeconds, visiting);
|
||||
visiting.delete(celestialId);
|
||||
if (!parentCurrentPosition) {
|
||||
return basePosition;
|
||||
@@ -548,9 +658,16 @@ function resolveStructureAnimatedLocalPosition(context: WorldOrbitalContext, vis
|
||||
}
|
||||
|
||||
const station = context.world.stations.get(visual.id);
|
||||
if (!station?.celestialId) {
|
||||
if (!station) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
allowedHosts: ["sobina.local"],
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:5080",
|
||||
"/api": "http://127.0.0.1:5079",
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -40,7 +40,6 @@ Recommended global ID families:
|
||||
- `commanderId`
|
||||
- `systemId`
|
||||
- `anchorId`
|
||||
- `localspaceId`
|
||||
- `stationId`
|
||||
- `shipId`
|
||||
- `moduleId`
|
||||
@@ -59,15 +58,14 @@ The intended core entities are:
|
||||
2. `Commander`
|
||||
3. `System`
|
||||
4. `Anchor`
|
||||
5. `Localspace`
|
||||
6. `Station`
|
||||
7. `Ship`
|
||||
8. `ModuleInstance`
|
||||
9. `Claim`
|
||||
10. `ConstructionSite`
|
||||
11. `MarketOrder`
|
||||
12. `Recipe`
|
||||
13. `PolicySet`
|
||||
5. `Station`
|
||||
6. `Ship`
|
||||
7. `ModuleInstance`
|
||||
8. `Claim`
|
||||
9. `ConstructionSite`
|
||||
10. `MarketOrder`
|
||||
11. `Recipe`
|
||||
12. `PolicySet`
|
||||
|
||||
## Faction
|
||||
|
||||
@@ -120,12 +118,12 @@ Suggested fields:
|
||||
- `label`
|
||||
- `galaxyPosition`
|
||||
- `star definition`
|
||||
- `nodeIds`
|
||||
- `anchorIds`
|
||||
- `faction influence later`
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -133,7 +131,7 @@ Suggested fields:
|
||||
- `systemId`
|
||||
- `kind`
|
||||
- `systemPosition`
|
||||
- `localspaceId`
|
||||
- `localspaceRadius`
|
||||
- `parentAnchorId?`
|
||||
- `orbital metadata?`
|
||||
- `constructionIds?`
|
||||
@@ -146,25 +144,9 @@ Recommended anchor kinds:
|
||||
- `lagrange-point`
|
||||
- `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
|
||||
|
||||
A station is a constructed structure that lives inside one localspace.
|
||||
A station is a constructed structure that lives at one anchor.
|
||||
|
||||
Suggested fields:
|
||||
|
||||
@@ -173,7 +155,6 @@ Suggested fields:
|
||||
- `commanderId?`
|
||||
- `anchorId`
|
||||
- `systemId`
|
||||
- `localspaceId`
|
||||
- `moduleIds`
|
||||
- `inventory`
|
||||
- `population`
|
||||
@@ -236,7 +217,6 @@ Suggested fields:
|
||||
- `commanderId?`
|
||||
- `systemId`
|
||||
- `anchorId`
|
||||
- `localspaceId`
|
||||
- `placedAt`
|
||||
- `activatesAt`
|
||||
- `state`
|
||||
@@ -258,7 +238,6 @@ Suggested fields:
|
||||
- `constructionSiteId`
|
||||
- `ownerFactionId`
|
||||
- `anchorId`
|
||||
- `localspaceId`
|
||||
- `targetKind`
|
||||
- `targetDefinitionId`
|
||||
- `requiredItems`
|
||||
@@ -351,7 +330,6 @@ Recommended ship spatial state fields:
|
||||
- `spaceLayer`
|
||||
- `currentSystemId`
|
||||
- `currentAnchorId?`
|
||||
- `currentLocalspaceId?`
|
||||
- `localPosition?`
|
||||
- `systemPosition?`
|
||||
- `movementRegime`
|
||||
|
||||
@@ -34,7 +34,6 @@ Every event should conceptually have:
|
||||
- `kind`
|
||||
- `spaceLayer`
|
||||
- `systemId?`
|
||||
- `localspaceId?`
|
||||
- `anchorId?`
|
||||
- `primaryEntityKind`
|
||||
- `primaryEntityId`
|
||||
|
||||
@@ -73,7 +73,7 @@ Current state:
|
||||
|
||||
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.
|
||||
- [`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:
|
||||
- `AnchorRuntime`
|
||||
- `LocalspaceRuntime`
|
||||
- `CommanderRuntime`
|
||||
- `ClaimRuntime`
|
||||
- `ConstructionSiteRuntime`
|
||||
@@ -262,7 +261,6 @@ Work:
|
||||
- add structured ship spatial state:
|
||||
- current space layer
|
||||
- current anchor
|
||||
- current localspace
|
||||
- current transit
|
||||
|
||||
Why first:
|
||||
|
||||
@@ -8,7 +8,7 @@ It is the canonical reference for:
|
||||
- solar systems
|
||||
- celestials
|
||||
- anchors
|
||||
- localspaces
|
||||
- localspaces as the tactical space around anchors
|
||||
- ship and station placement
|
||||
- intra-system travel
|
||||
- inter-system travel
|
||||
@@ -27,14 +27,13 @@ The structure can be understood as a tree:
|
||||
- the galaxy contains solar systems
|
||||
- each solar system contains celestials and other derived locations
|
||||
- each meaningful location is an anchor
|
||||
- each anchor owns one localspace
|
||||
- each anchor has a localspace around it
|
||||
|
||||
The intended structure is:
|
||||
|
||||
1. `galaxy`
|
||||
2. `solar system`
|
||||
3. `anchor`
|
||||
4. `localspace`
|
||||
|
||||
Ships and stations do not live in arbitrary free-floating "system local space".
|
||||
|
||||
@@ -99,7 +98,7 @@ Systems remain important for:
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -120,7 +119,7 @@ Each anchor should have:
|
||||
- an anchor type
|
||||
- a position in system space
|
||||
- optional orbital metadata
|
||||
- an associated localspace definition
|
||||
- a localspace around it
|
||||
- optional parent/child relationships
|
||||
|
||||
Examples:
|
||||
@@ -131,7 +130,7 @@ Examples:
|
||||
- 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
|
||||
|
||||
Every anchor has exactly one localspace.
|
||||
Every anchor has exactly one localspace around it.
|
||||
|
||||
## Celestials
|
||||
|
||||
@@ -147,9 +146,9 @@ Celestials exist for three reasons:
|
||||
|
||||
1. they structure the solar system visually and strategically
|
||||
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.
|
||||
|
||||
@@ -165,7 +164,7 @@ Initial assumptions:
|
||||
|
||||
- major orbitals can expose `L1` through `L5`
|
||||
- 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
|
||||
|
||||
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 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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` is the tactical simulation term and should be the only term used for this concept.
|
||||
@@ -212,7 +227,7 @@ Use:
|
||||
|
||||
- `localspace`
|
||||
|
||||
Each localspace belongs to exactly one anchor.
|
||||
Each localspace is the tactical space around exactly one anchor.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -233,16 +248,26 @@ Localspace is where close simulation happens:
|
||||
- local logistics
|
||||
- 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.
|
||||
|
||||
## 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:
|
||||
|
||||
- in a localspace
|
||||
- traveling between localspaces in the same system
|
||||
- at an anchor
|
||||
- traveling between anchors in the same system
|
||||
- traveling between systems
|
||||
|
||||
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.
|
||||
|
||||
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 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:
|
||||
|
||||
- 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
|
||||
|
||||
## Simulation Implications
|
||||
@@ -466,7 +567,7 @@ This model supports:
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- `system`: the solar system container
|
||||
- `celestial`: star, planet, or moon
|
||||
- `anchor`: anything that owns a localspace
|
||||
- `localspace`: the tactical simulation bubble attached to one anchor
|
||||
- `anchor`: a meaningful location in a system and the primary tactical simulation container
|
||||
- `localspace`: the tactical simulation space around an anchor, not a separately identified entity
|
||||
- `intra-system warp`: movement between anchors in the same system
|
||||
- `inter-system FTL`: movement between systems
|
||||
|
||||
|
||||
Reference in New Issue
Block a user