Aetheria/Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs
2026-07-11 20:38:48 +01:00

381 lines
16 KiB
C#

using Aetheria.State;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
using GameCult.Caching;
internal static class AetheriaDaemonZoneGenerator
{
public const string RunId = "local-terminus";
public const uint GenerationSeed = 0xA37E_2026u;
public static async Task WritePlayableRunAsync(
AetheriaStateNode node,
AetheriaRuntimeCatalogSnapshot catalog,
string now)
{
var runKey = new CultRecordKey($"global:aetheria.run_state.{RunId}.v1");
var zoneKey = new CultRecordKey($"global:aetheria.zone_state.{RunId}.0.v1");
var corporationKeys = (catalog.Corporations ?? Array.Empty<AetheriaRuntimeCorporation>())
.Select(value => value.CorporationKey)
.Where(value => !string.IsNullOrWhiteSpace(value))
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
var homeZones = corporationKeys.ToDictionary(value => value, _ => 0, StringComparer.Ordinal);
var availabilityFactions = new Dictionary<string, string>(StringComparer.Ordinal)
{
["player"] = corporationKeys.ElementAtOrDefault(0) ?? "",
["raider"] = corporationKeys.ElementAtOrDefault(1) ?? corporationKeys.ElementAtOrDefault(0) ?? "",
["neutral"] = corporationKeys.ElementAtOrDefault(2) ?? corporationKeys.ElementAtOrDefault(0) ?? ""
};
var rootRandom = new CultMath.Random(GenerationSeed);
var adjacency = new Dictionary<int, IReadOnlyList<int>> { [0] = Array.Empty<int>() };
var loadouts = availabilityFactions.Values
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.OrderBy(value => value, StringComparer.Ordinal)
.ToDictionary(
value => value,
_ => new AetheriaDaemonLoadoutGenerator(
catalog,
(uint)rootRandom.NextInt(1, int.MaxValue),
0,
homeZones,
adjacency),
StringComparer.Ordinal);
var entities = GenerateEntities(loadouts, availabilityFactions);
var entityKeys = Enumerable.Range(0, entities.Length)
.Select(index => EntityKey(0, index))
.ToArray();
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReadAsync()
.ConfigureAwait(false) ?? new AetheriaPlayerSettings();
settings.ActiveRunKey = runKey.ToString();
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Terminus Pilot" : settings.PlayerName;
settings.TutorialPassed = true;
settings.LastUpdatedAtUtc = now;
await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
.ReplaceAsync(settings)
.ConfigureAwait(false);
await node.MutableDocument<AetheriaRunState>(runKey).ReplaceAsync(new AetheriaRunState
{
RunId = RunId,
EntranceZoneIndex = 0,
ExitZoneIndex = 0,
CurrentZoneIndex = 0,
DiscoveredZoneIndices = [0],
ZoneKeys = [zoneKey.ToString()],
GenerationSeed = GenerationSeed,
CurrentEntityKey = entityKeys[1],
UpdatedAtUtc = now
}).ConfigureAwait(false);
await node.MutableDocument<AetheriaZoneState>(zoneKey).ReplaceAsync(new AetheriaZoneState
{
Name = "Daemon Generated Terminus",
Position = Vec2(0, 0),
EntityKeys = entityKeys,
FactionIndices = [0, 1, 2],
OwnerFactionIndex = 0,
Orbits = GenerateOrbits(),
Bodies = GenerateBodies(),
GravityTerrainRadius = 1900,
GravityTerrainDepth = -7,
GravityTerrainDepthExponent = 1.18,
GravityTerrainBoundaryFog = 0.25,
GravityTerrainWaveFrequency = 0.55
}).ConfigureAwait(false);
for (var i = 0; i < entities.Length; i++)
{
await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[i]))
.ReplaceAsync(entities[i])
.ConfigureAwait(false);
}
await node.FlushAsync().ConfigureAwait(false);
}
public static string EntityKey(int zoneIndex, int entityIndex)
{
return $"global:aetheria.run_state.{RunId}.zone.{zoneIndex}.entity.{entityIndex}.v1";
}
private static AetheriaOrbitSnapshot[] GenerateOrbits()
{
var bodies = BodyPlans();
return bodies.Select(body => new AetheriaOrbitSnapshot
{
OrbitKey = $"{body.Key}.orbit",
ParentOrbitKey = body.ParentKey.Length == 0 ? "" : $"{body.ParentKey}.orbit",
Distance = body.Distance,
Phase = body.Phase,
FixedPosition = Vec2(body.X, body.Z)
}).ToArray();
}
private static AetheriaBodySnapshot[] GenerateBodies()
{
return BodyPlans().Select(body => new AetheriaBodySnapshot
{
BodyKey = body.Key,
Kind = body.Kind,
Name = body.Name,
OrbitKey = $"{body.Key}.orbit",
Mass = body.Mass,
BodyRadiusMultiplier = body.BodyRadius,
GravityRadiusMultiplier = 1,
GravityDepthMultiplier = 1,
GravityDepthExponent = body.DepthExponent,
GravityInfluenceCenterX = body.X,
GravityInfluenceCenterZ = body.Z,
GravityInfluenceRadius = body.GravityRadius,
GravityWellDepth = body.GravityDepth,
GravityWaveRadius = body.WaveRadius,
GravityWaveDepth = body.WaveDepth,
GravityWaveSpeed = body.WaveSpeed,
Asteroids = body.Kind == "asteroid_belt" ? Asteroids(body.Distance) : [],
GasGiantVisual = GasGiantVisual(body),
SunVisual = SunVisual(body)
}).ToArray();
}
private static BodyPlan[] BodyPlans()
{
return
[
Body("local.sun", "", "sun", "Terminus", 0, 0, 1600, 2.8, 980, -92, 3.0, 480, 11, 1.8),
Body("local.inner", "local.sun", "planet", "Cairn", 250, 0.45, 110, 0.78, 230, -20, 2.2, 92, 3, 1.0),
Body("local.blue", "local.sun", "planet", "Blueglass", 440, 2.45, 260, 1.18, 360, -36, 2.55, 150, 5, 1.1),
Body("local.blue.moon", "local.blue", "moon", "Latch", 520, 2.78, 70, 0.58, 170, -15, 2.8, 70, 2.2, 1.7),
Body("local.ember", "local.sun", "planet", "Emberhook", 640, 3.78, 220, 1.05, 330, -31, 2.25, 128, 4.2, 1.3),
Body("local.ember.moon", "local.ember", "moon", "Ashwake", 735, 3.45, 86, 0.62, 185, -16, 2.7, 72, 2.4, 1.45),
Body("local.belt", "local.sun", "asteroid_belt", "Cinder Belt", 760, -0.28, 140, 1.0, 310, -24, 2.0, 122, 4, 0.8),
Body("local.green", "local.sun", "planet", "Marrow", 930, 1.28, 180, 0.96, 300, -26, 2.35, 112, 3.6, 1.0),
Body("local.outer", "local.sun", "gas_giant", "Vesper", 1120, 0.58, 420, 1.55, 500, -48, 2.4, 230, 7, 0.72),
Body("local.outer.moon.a", "local.outer", "moon", "Iris", 1240, 0.88, 92, 0.66, 195, -17, 2.75, 78, 2.5, 1.2),
Body("local.outer.moon.b", "local.outer", "moon", "Hush", 1025, 0.28, 76, 0.55, 180, -15, 2.9, 70, 2.1, 1.5),
Body("local.deep", "local.sun", "gas_giant", "Nacre", 1420, 5.72, 340, 1.36, 440, -41, 2.55, 200, 5.8, 0.95)
];
}
private static BodyPlan Body(
string key,
string parentKey,
string kind,
string name,
double distance,
double phase,
double mass,
double bodyRadius,
double gravityRadius,
double gravityDepth,
double depthExponent,
double waveRadius,
double waveDepth,
double waveSpeed)
{
return new BodyPlan(
key,
parentKey,
kind,
name,
distance,
phase,
Math.Cos(phase) * distance,
Math.Sin(phase) * distance,
mass,
bodyRadius,
gravityRadius,
gravityDepth,
depthExponent,
waveRadius,
waveDepth,
waveSpeed);
}
private static AetheriaEntitySnapshot[] GenerateEntities(
IReadOnlyDictionary<string, AetheriaDaemonLoadoutGenerator> loadouts,
IReadOnlyDictionary<string, string> availabilityFactions)
{
var keys = Enumerable.Range(0, 12)
.Select(index => EntityKey(0, index))
.ToArray();
var entities = new[]
{
Entity(loadouts, availabilityFactions, "Anchor Station", "station", -170, -80, 0, 0, "player", 760, keys[6], [keys[1], keys[2], keys[3], keys[4], keys[6], keys[7], keys[11]], ["repair-parts", "reactor-fuel", "drone-core"]),
Entity(loadouts, availabilityFactions, "Vanguard One", "ship", -40, -30, 10, 4, "player", 540, keys[6], [keys[0], keys[2], keys[4], keys[6], keys[7]], ["coilgun-ammo", "field-rations"]),
Entity(loadouts, availabilityFactions, "Wing Two", "ship", 145, 125, -5, 7, "player", 450, keys[6], [keys[0], keys[1], keys[6], keys[8]], ["sensor-buoy", "shield-cell"]),
Entity(loadouts, availabilityFactions, "Torch Three", "ship", -235, 210, 8, -4, "player", 500, keys[7], [keys[0], keys[1], keys[7]], ["micro-missile", "coolant-pack"]),
Entity(loadouts, availabilityFactions, "Foundry Tug", "ship", 330, -155, -2, 3, "player", 390, keys[6], [keys[0], keys[1], keys[6], keys[10]], ["ore-canister", "field-rations"]),
Entity(loadouts, availabilityFactions, "Derelict Relay", "station", -390, 270, 0, 0, "neutral", 160, "", [keys[0], keys[3]], ["ancient-transponder"]),
Entity(loadouts, availabilityFactions, "Ash Raider", "ship", 455, 180, -5, -2, "raider", 320, keys[1], [keys[0], keys[1], keys[2], keys[4]], ["scrap-metal", "stolen-capacitor"]),
Entity(loadouts, availabilityFactions, "Cinder Knife", "ship", 585, -115, -7, 1, "raider", 280, keys[3], [keys[0], keys[3]], ["volatile-fuel"]),
Entity(loadouts, availabilityFactions, "Blackwake", "ship", 690, 300, -4, -3, "raider", 230, keys[2], [keys[2]], ["ore-cache", "burned-relay-core"]),
Entity(loadouts, availabilityFactions, "Vesper Sloop", "ship", 960, 620, -3, -5, "raider", 250, keys[4], [keys[4]], ["stolen-map-fragment"]),
Entity(loadouts, availabilityFactions, "Survey Skiff", "ship", -610, -365, 3, 5, "neutral", 210, "", [keys[4], keys[5]], ["soil-sample", "deep-scan"]),
Entity(loadouts, availabilityFactions, "Lagrange Beacon", "station", 105, 510, 0, 0, "neutral", 190, "", [keys[0], keys[1], keys[6]], ["navigation-key"])
};
foreach (var index in new[] { 1, 2, 3, 4 })
entities[index].HomeEntityKey = keys[0];
entities[2].AgentTaskCapabilities = ["attack", "defend", "explore"];
entities[3].AgentTaskCapabilities = ["attack", "defend"];
entities[4].AgentTaskCapabilities = ["mine", "haul", "tow", "explore"];
return entities;
}
private static AetheriaEntitySnapshot Entity(
IReadOnlyDictionary<string, AetheriaDaemonLoadoutGenerator> loadouts,
IReadOnlyDictionary<string, string> availabilityFactions,
string name,
string kind,
double x,
double z,
double vx,
double vy,
string faction,
double visibility,
string target,
string[] contactKeys,
string[] cargo)
{
var availabilityFaction = availabilityFactions[faction];
var loadout = loadouts[availabilityFaction].Build(kind, availabilityFaction, cargo);
return new AetheriaEntitySnapshot
{
Name = name,
Kind = kind,
Position = Vec3(x, 0, z),
Direction = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy),
Velocity = Vec2(vx, vy),
FactionKey = faction,
HullItemKey = loadout.HullItemKey,
LoadoutGeneration = loadout.Receipt,
IsActive = true,
HeatsinksEnabled = true,
TractorPower = 0,
Visibility = visibility,
VisibilitySourceCount = 1,
TargetEntityKey = target,
Contacts = contactKeys.Select(key => new AetheriaEntityContactSnapshot
{
TargetEntityKey = key,
InfoGathered = 1,
Hostile = string.Equals(faction, "player", StringComparison.OrdinalIgnoreCase) && key.Contains(".entity.6.", StringComparison.Ordinal)
}).ToArray(),
StatGrids =
[
StatGrid("hull", string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) ? 420 : string.Equals(faction, "raider", StringComparison.OrdinalIgnoreCase) ? 85 : 130),
StatGrid("shield", string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) ? 130 : 50),
StatGrid("heat", 0)
],
Equipment = loadout.Equipment,
CargoContents =
[
new AetheriaCargoBayLoadout
{
Items = loadout.Cargo
}
]
};
}
private static AetheriaGasGiantVisualState GasGiantVisual(BodyPlan body)
{
if (body.Kind != "gas_giant" && body.Kind != "sun")
return new AetheriaGasGiantVisualState();
return new AetheriaGasGiantVisualState
{
FirstOffsetDomainRotationSpeed = body.Kind == "sun" ? 5 : 0,
FirstOffsetRotationSpeed = 5,
SecondOffsetRotationSpeed = 10,
SecondOffsetDomainRotationSpeed = -25,
AlbedoRotationSpeed = -3,
WaveRadiusMultiplier = 1,
WaveDepthMultiplier = 1,
Colors =
[
Color(0.85, 0.32, 0.18, 0),
Color(0.18, 0.54, 0.92, 0.5),
Color(0.92, 0.78, 0.38, 1)
]
};
}
private static AetheriaSunVisualState SunVisual(BodyPlan body)
{
return body.Kind == "sun"
? new AetheriaSunVisualState
{
LightColor = Vec3(1, 0.86, 0.58),
FogTintColor = Vec3(0.45, 0.28, 0.16),
LightRadiusMultiplier = 1.8
}
: new AetheriaSunVisualState();
}
private static AetheriaAsteroidSnapshot[] Asteroids(double orbitDistance)
{
return Enumerable.Range(0, 18)
.Select(index =>
{
var phase = index * Math.Tau / 18.0;
return new AetheriaAsteroidSnapshot
{
Distance = orbitDistance + Math.Sin(phase * 3.0) * 42.0,
Phase = phase,
Size = 0.65 + (index % 5) * 0.24,
RotationSpeed = (index % 2 == 0 ? 1 : -1) * (0.04 + index * 0.006)
};
})
.ToArray();
}
private static AetheriaEntityStatGrid StatGrid(string name, double value)
{
return new AetheriaEntityStatGrid
{
Name = name,
Width = 1,
Height = 1,
Values = [value]
};
}
private static AetheriaColor Color(double x, double y, double z, double w)
{
return new AetheriaColor { X = x, Y = y, Z = z, W = w };
}
private static AetheriaVector2 Vec2(double x, double y)
{
return new AetheriaVector2 { X = x, Y = y };
}
private static AetheriaVector3 Vec3(double x, double y, double z)
{
return new AetheriaVector3 { X = x, Y = y, Z = z };
}
private sealed record BodyPlan(
string Key,
string ParentKey,
string Kind,
string Name,
double Distance,
double Phase,
double X,
double Z,
double Mass,
double BodyRadius,
double GravityRadius,
double GravityDepth,
double DepthExponent,
double WaveRadius,
double WaveDepth,
double WaveSpeed);
}