using Aetheria.State.Documents; using GameCult.Aetheria.State.Verse; public sealed record AetheriaDaemonMaterializedTutorialZone( AetheriaDaemonTutorialZoneTopology Topology, AetheriaDaemonGeneratedZonePlan CelestialPlan, string NearestFactionKey, int FactionPresence, AetheriaOrbitSnapshot[] Orbits, AetheriaEntitySnapshot[] Entities, AetheriaAgentTaskState[] AgentTasks, int PlayerEntityIndex); public sealed record AetheriaDaemonMaterializedTutorialWorld( AetheriaDaemonTutorialTopology Topology, IReadOnlyDictionary Zones, int PlayerZoneIndex, int PlayerEntityIndex); /// /// Daemon-owned port of the fossil ZoneGenerator population pass. Celestial /// generation hands its exact remaining random state to this organ, so adding /// presentation work cannot perturb station, turret, or enemy placement. /// public static class AetheriaDaemonTutorialWorldMaterializer { public static AetheriaDaemonMaterializedTutorialWorld Materialize( AetheriaDaemonTutorialWorldPlan world, IReadOnlyList factions, AetheriaRuntimeCatalogSnapshot catalog, bool isPrelude = true, string identityPrefix = "tutorial") { if (world == null) throw new ArgumentNullException(nameof(world)); if (catalog == null) throw new ArgumentNullException(nameof(catalog)); var homes = world.Topology.HomeZoneByFactionKey; factions = homes.Keys .Select(key => factions.First(faction => string.Equals(faction.CorporationKey, key, StringComparison.Ordinal))) .ToArray(); var adjacency = world.Topology.Zones.ToDictionary( zone => zone.ZoneIndex, zone => zone.AdjacentZoneIndices, EqualityComparer.Default); var distances = factions.ToDictionary( faction => faction.CorporationKey, faction => DistancesFrom(homes[faction.CorporationKey], adjacency), StringComparer.Ordinal); var zones = new Dictionary(); var playerEntityIndex = -1; foreach (var topology in world.Topology.Zones.OrderBy(zone => zone.ZoneIndex)) { var celestial = world.Zones[topology.ZoneIndex]; var random = new CultMath.Random(celestial.PostBodyRandomState); var spawnRandom = new CultMath.Random(celestial.PostBodyRandomState ^ 0x51A7_10C5u); var nearest = factions .OrderBy(faction => distances[faction.CorporationKey][topology.ZoneIndex]) .First(); var homeDistance = distances[nearest.CorporationKey][topology.ZoneIndex]; var influence = isPrelude ? Math.Max(0, (nearest.InfluenceDistance + 1) / 2) : Math.Max(0, nearest.InfluenceDistance); var factionPresence = influence - homeDistance + 1; var stationCount = (int)(random.NextFloat() * (factionPresence + 1)); var orbits = celestial.Orbits.ToList(); var entities = new List(); var agentTasks = new List(); var generators = new Dictionary(StringComparer.Ordinal); AetheriaDaemonLoadoutGenerator Generator(string factionKey) { if (!generators.TryGetValue(factionKey, out var generator)) { generator = new AetheriaDaemonLoadoutGenerator( catalog, (uint)random.NextInt(1, int.MaxValue), topology.ZoneIndex, homes, adjacency, isPrelude: isPrelude); generators.Add(factionKey, generator); } return generator; } var potential = celestial.Orbits .Where(orbit => !string.IsNullOrWhiteSpace(orbit.ParentOrbitKey)) .Where(orbit => !celestial.Orbits.Any(sibling => !ReferenceEquals(sibling, orbit) && string.Equals(sibling.ParentOrbitKey, orbit.ParentOrbitKey, StringComparison.Ordinal) && Math.Abs(sibling.Distance - orbit.Distance) < 0.1)) .OrderBy(orbit => orbit.Distance) .ToArray(); var selected = potential.Skip(potential.Length / 2).Take(Math.Max(0, stationCount)).ToArray(); for (var stationIndex = 0; stationIndex < selected.Length; stationIndex++) { var baseOrbit = selected[stationIndex]; var stationOrbit = new AetheriaOrbitSnapshot { OrbitKey = $"{identityPrefix}.zone.{topology.ZoneIndex}.station.{stationIndex}.orbit", ParentOrbitKey = baseOrbit.ParentOrbitKey, Distance = baseOrbit.Distance, Phase = baseOrbit.Phase + MathF.PI / 3 * Sign(random.NextFloat() - 0.5f), Period = baseOrbit.Distance, FixedPosition = new AetheriaVector2() }; orbits.Add(stationOrbit); var security = (int)((1f - MathF.Pow(random.NextFloat(), factionPresence / 2f)) * 3f); entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( $"{topology.Name} Station {stationIndex + 1}", "station", nearest.CorporationKey, Generator(nearest.CorporationKey).Build("station", nearest.CorporationKey), orbitKey: stationOrbit.OrbitKey, securityLevel: security, securityRadius: celestial.Radius)); for (var turretIndex = 0; turretIndex < 2; turretIndex++) { var distanceMultiplier = turretIndex / 2; if (turretIndex % 2 == 0) distanceMultiplier = -distanceMultiplier; var turretOrbit = new AetheriaOrbitSnapshot { OrbitKey = $"{identityPrefix}.zone.{topology.ZoneIndex}.station.{stationIndex}.turret.{turretIndex}.orbit", ParentOrbitKey = stationOrbit.ParentOrbitKey, Distance = stationOrbit.Distance, Phase = stationOrbit.Phase + 20f * distanceMultiplier / stationOrbit.Distance, Period = stationOrbit.Distance, FixedPosition = new AetheriaVector2() }; orbits.Add(turretOrbit); entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( $"{topology.Name} Turret {stationIndex + 1}-{turretIndex + 1}", "turret", nearest.CorporationKey, Generator(nearest.CorporationKey).Build("turret", nearest.CorporationKey), orbitKey: turretOrbit.OrbitKey, securityLevel: security, securityRadius: celestial.Radius)); } } var enemyCount = (int)(random.NextFloat() * factionPresence * 2) + stationCount; for (var enemyIndex = 0; enemyIndex < Math.Max(0, enemyCount); enemyIndex++) { var halfRadius = celestial.Radius * 0.5; var enemy = AetheriaDaemonGeneratedEntityFactory.Create( $"{nearest.ShortName} Ship {enemyIndex + 1}", "ship", nearest.CorporationKey, Generator(nearest.CorporationKey).Build("ship", nearest.CorporationKey), spawnRandom.NextFloat((float)-halfRadius, (float)halfRadius), spawnRandom.NextFloat((float)-halfRadius, (float)halfRadius)); var entityIndex = entities.Count; var taskId = $"{identityPrefix}.zone.{topology.ZoneIndex}.entity.{entityIndex}.patrol"; enemy.AgentTaskCapabilities = [ AetheriaRuntimeAgentTaskTypes.Attack, AetheriaRuntimeAgentTaskTypes.Defend, AetheriaRuntimeAgentTaskTypes.Explore, AetheriaRuntimeAgentTaskTypes.Patrol ]; enemy.AssignedAgentTaskId = taskId; agentTasks.Add(new AetheriaAgentTaskState { TaskId = taskId, CorporationKey = nearest.CorporationKey, TaskType = AetheriaRuntimeAgentTaskTypes.Patrol, Priority = 0, ZoneIndex = topology.ZoneIndex, Status = AetheriaRuntimeAgentTaskStatuses.Assigned, AssignedEntityIndex = entityIndex, CompletionRadius = 10, TargetOrbitKeys = orbits .OrderBy(_ => spawnRandom.NextFloat()) .Take(4) .Select(orbit => orbit.OrbitKey) .ToArray() }); entities.Add(enemy); } var localPlayerIndex = -1; if (topology.ZoneIndex == world.Topology.EntranceZoneIndex) { var protagonist = factions[0]; var starter = new AetheriaDaemonLoadoutGenerator( catalog, world.Topology.GenerationSeed ^ 0x50A7_EA11u, topology.ZoneIndex, homes, adjacency, isPrelude: isPrelude).Build("ship", protagonist.CorporationKey); localPlayerIndex = entities.Count; entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( "Pilot", "ship", protagonist.CorporationKey, starter)); playerEntityIndex = localPlayerIndex; } ApplyOrbitalPositions(orbits, entities); zones.Add(topology.ZoneIndex, new AetheriaDaemonMaterializedTutorialZone( topology, celestial, nearest.CorporationKey, factionPresence, orbits.ToArray(), entities.ToArray(), agentTasks.ToArray(), localPlayerIndex)); } if (playerEntityIndex < 0) throw new InvalidOperationException("Tutorial materialization produced no player entry ship."); return new AetheriaDaemonMaterializedTutorialWorld( world.Topology, zones, world.Topology.EntranceZoneIndex, playerEntityIndex); } private static void ApplyOrbitalPositions( IReadOnlyList orbits, IReadOnlyList entities) { var runtimeZone = new AetheriaRuntimeZoneSnapshotCommit { Orbits = orbits.Select(orbit => new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = orbit.OrbitKey, ParentOrbitKey = orbit.ParentOrbitKey, Distance = orbit.Distance, Phase = orbit.Phase, Period = orbit.Period, FixedPositionX = orbit.FixedPosition?.X ?? 0, FixedPositionY = orbit.FixedPosition?.Y ?? 0 }).ToArray() }; var positions = AetheriaRuntimeOrbitQueries.BuildPositions(runtimeZone); foreach (var entity in entities.Where(entity => !string.IsNullOrWhiteSpace(entity.OrbitKey))) { if (!positions.TryGetValue(entity.OrbitKey, out var position)) throw new InvalidDataException( $"Generated orbital entity '{entity.Name}' references missing orbit '{entity.OrbitKey}'."); entity.Position.X = position.x; entity.Position.Z = position.z; } } private static IReadOnlyDictionary DistancesFrom( int start, IReadOnlyDictionary> adjacency) { var result = adjacency.Keys.ToDictionary(index => index, _ => int.MaxValue); var queue = new Queue(); result[start] = 0; queue.Enqueue(start); while (queue.Count > 0) { var current = queue.Dequeue(); foreach (var neighbor in adjacency[current]) { if (result[neighbor] <= result[current] + 1) continue; result[neighbor] = result[current] + 1; queue.Enqueue(neighbor); } } return result; } private static int Sign(float value) => value < 0 ? -1 : value > 0 ? 1 : 0; }