Complete universe model migration
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user