Add player onboarding and tactical viewer updates

This commit is contained in:
2026-04-06 17:12:44 -04:00
parent 706e1cda8f
commit 63a9f808bb
52 changed files with 2699 additions and 577 deletions

View File

@@ -12,7 +12,10 @@ public sealed class PlayerFactionProjectionService
return new PlayerFactionSnapshot(
player.Id,
player.Label,
player.PersonaName,
player.RaceId,
player.SovereignFactionId,
player.RequiresOnboarding,
player.Status,
player.CreatedAtUtc,
player.UpdatedAtUtc,

View File

@@ -20,14 +20,12 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime EnsureDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId)
{
var sovereignFaction = world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault()
?? throw new InvalidOperationException("Cannot create a player faction domain without any factions in the world.");
var player = playerStateStore.GetOrAddPlayerFaction(playerId, () => new PlayerFactionRuntime
{
Id = PlayerFactionDomainId,
Label = $"{sovereignFaction.Label} Command",
SovereignFactionId = sovereignFaction.Id,
Label = "Pending Pilot",
SovereignFactionId = string.Empty,
RequiresOnboarding = true,
CreatedAtUtc = world.GeneratedAtUtc,
UpdatedAtUtc = world.GeneratedAtUtc,
});
@@ -37,6 +35,58 @@ internal sealed class PlayerFactionService
return player;
}
internal PlayerFactionRuntime CompleteOnboarding(
SimulationWorld world,
IPlayerStateStore playerStateStore,
string playerId,
CompletePlayerOnboardingRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
if (!player.RequiresOnboarding)
{
throw new InvalidOperationException("Player onboarding has already been completed.");
}
var personaName = request.Name.Trim();
if (personaName.Length < 2)
{
throw new InvalidOperationException("Player name must contain at least 2 characters.");
}
if (personaName.Length > 48)
{
throw new InvalidOperationException("Player name must contain at most 48 characters.");
}
var ownedFactionId = BuildOwnedFactionId(playerId);
if (world.Factions.Any(faction => string.Equals(faction.Id, ownedFactionId, StringComparison.Ordinal)))
{
throw new InvalidOperationException($"Player faction '{ownedFactionId}' already exists in the current world.");
}
player.Label = personaName;
player.PersonaName = personaName;
player.RaceId = request.RaceId.Trim();
player.SovereignFactionId = ownedFactionId;
player.RequiresOnboarding = false;
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
return player;
}
internal PlayerFactionRuntime EnsureInitializedDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId)
{
var player = EnsureDomain(world, playerStateStore, playerId);
if (player.RequiresOnboarding || string.IsNullOrWhiteSpace(player.SovereignFactionId))
{
throw new InvalidOperationException("Player onboarding must be completed before issuing gameplay commands.");
}
return player;
}
internal static string BuildOwnedFactionId(string playerId) =>
$"player-{playerId.Replace("-", string.Empty, StringComparison.Ordinal).ToLowerInvariant()}";
internal void Update(SimulationWorld world, IPlayerStateStore playerStateStore, float _deltaSeconds, ICollection<SimulationEventRecord> events)
{
if (playerStateStore.GetPlayerFactions().Count == 0)
@@ -63,7 +113,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerOrganizationCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var id = CreateDomainId(request.Kind, request.Label, ExistingOrganizationIds(player));
var nowUtc = DateTimeOffset.UtcNow;
@@ -180,7 +230,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
RemoveOrganization(player, organizationId);
player.Assignments.RemoveAll(assignment =>
assignment.FleetId == organizationId ||
@@ -198,7 +248,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var kind = ResolveOrganizationKind(player, organizationId);
switch (kind)
{
@@ -249,7 +299,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? directiveId, PlayerDirectiveCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var directive = directiveId is null
? null
: player.Directives.FirstOrDefault(candidate => string.Equals(candidate.Id, directiveId, StringComparison.Ordinal));
@@ -326,7 +376,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string directiveId)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
player.Directives.RemoveAll(directive => directive.Id == directiveId);
foreach (var assignment in player.Assignments.Where(assignment => assignment.DirectiveId == directiveId))
{
@@ -340,7 +390,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? policyId, PlayerPolicyCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var policy = policyId is null
? null
: player.Policies.FirstOrDefault(candidate => string.Equals(candidate.Id, policyId, StringComparison.Ordinal));
@@ -411,7 +461,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var policy = automationPolicyId is null
? null
: player.AutomationPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, automationPolicyId, StringComparison.Ordinal));
@@ -469,7 +519,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var policy = reinforcementPolicyId is null
? null
: player.ReinforcementPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, reinforcementPolicyId, StringComparison.Ordinal));
@@ -503,7 +553,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var program = productionProgramId is null
? null
: player.ProductionPrograms.FirstOrDefault(candidate => string.Equals(candidate.Id, productionProgramId, StringComparison.Ordinal));
@@ -535,7 +585,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string assetId, PlayerAssetAssignmentCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
var assignment = player.Assignments.FirstOrDefault(candidate =>
string.Equals(candidate.AssetId, assetId, StringComparison.Ordinal) &&
string.Equals(candidate.AssetKind, request.AssetKind, StringComparison.Ordinal));
@@ -594,7 +644,7 @@ internal sealed class PlayerFactionService
internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerStrategicIntentCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
player.StrategicIntent.StrategicPosture = request.StrategicPosture;
player.StrategicIntent.EconomicPosture = request.EconomicPosture;
player.StrategicIntent.MilitaryPosture = request.MilitaryPosture;
@@ -610,7 +660,7 @@ internal sealed class PlayerFactionService
internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipOrderCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -669,7 +719,7 @@ internal sealed class PlayerFactionService
internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, string orderId)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -712,7 +762,7 @@ internal sealed class PlayerFactionService
internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipDefaultBehaviorCommandRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
if (!player.AssetRegistry.ShipIds.Contains(shipId))
{
return null;
@@ -852,6 +902,24 @@ internal sealed class PlayerFactionService
private static void SyncRegistry(SimulationWorld world, PlayerFactionRuntime player)
{
if (string.IsNullOrWhiteSpace(player.SovereignFactionId))
{
SyncSet(player.AssetRegistry.ShipIds, []);
SyncSet(player.AssetRegistry.StationIds, []);
SyncSet(player.AssetRegistry.CommanderIds, []);
SyncSet(player.AssetRegistry.ClaimIds, []);
SyncSet(player.AssetRegistry.ConstructionSiteIds, []);
SyncSet(player.AssetRegistry.PolicySetIds, player.Policies.Where(entry => entry.PolicySetId is not null).Select(entry => entry.PolicySetId!));
SyncSet(player.AssetRegistry.MarketOrderIds, []);
SyncSet(player.AssetRegistry.FleetIds, player.Fleets.Select(fleet => fleet.Id));
SyncSet(player.AssetRegistry.TaskForceIds, player.TaskForces.Select(taskForce => taskForce.Id));
SyncSet(player.AssetRegistry.StationGroupIds, player.StationGroups.Select(group => group.Id));
SyncSet(player.AssetRegistry.EconomicRegionIds, player.EconomicRegions.Select(region => region.Id));
SyncSet(player.AssetRegistry.FrontIds, player.Fronts.Select(front => front.Id));
SyncSet(player.AssetRegistry.ReserveIds, player.Reserves.Select(reserve => reserve.Id));
return;
}
SyncSet(player.AssetRegistry.ShipIds, world.Ships.Where(ship => ship.FactionId == player.SovereignFactionId).Select(ship => ship.Id));
SyncSet(player.AssetRegistry.StationIds, world.Stations.Where(station => station.FactionId == player.SovereignFactionId).Select(station => station.Id));
SyncSet(player.AssetRegistry.CommanderIds, world.Commanders.Where(commander => commander.FactionId == player.SovereignFactionId).Select(commander => commander.Id));
@@ -1224,8 +1292,7 @@ internal sealed class PlayerFactionService
return player.AutomationPolicies.FirstOrDefault(policy => policy.Id == automationId);
}
return SelectScopedAutomationPolicy(player, assignment, assetKind, assetId)
?? player.AutomationPolicies.FirstOrDefault(policy => policy.Id == "player-core-automation");
return SelectScopedAutomationPolicy(player, assignment, assetKind, assetId);
}
private static PlayerFactionPolicyRuntime? ResolvePolicy(PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, PlayerDirectiveRuntime? directive, string assetKind, string assetId)