Aetheria/Assets/Scripts/ServerShared/Zone.cs
2026-07-06 02:00:46 +01:00

656 lines
24 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UniRx;
using cfloat2 = CultMath.float2;
using cfloat3 = CultMath.float3;
using float2 = Unity.Mathematics.float2;
using float3 = Unity.Mathematics.float3;
using float4 = Unity.Mathematics.float4;
using Random = Unity.Mathematics.Random;
public class Zone
{
public const string OrbitKeyPrefix = "aetheria.orbit:";
public const string BodyKeyPrefix = "aetheria.body:";
public Action<string> Log;
public ReactiveCollection<Entity> Entities = new ReactiveCollection<Entity>();
public Dictionary<string, Planet> PlanetInstances = new Dictionary<string, Planet>(StringComparer.Ordinal);
public Dictionary<string, Orbit> Orbits = new Dictionary<string, Orbit>(StringComparer.Ordinal);
public Dictionary<string, AsteroidBelt> AsteroidBelts = new Dictionary<string, AsteroidBelt>(StringComparer.Ordinal);
public PlanetSettings Settings;
private HashSet<string> _updatedOrbits = new HashSet<string>(StringComparer.Ordinal);
private ItemManager _itemManager;
private double _time;
private Random _random;
public List<Agent> Agents = new List<Agent>();
private List<Task> BeltUpdates = new List<Task>();
public float Time
{
get => (float) _time;
}
public float Radius { get; }
public float Mass { get; }
public GalaxyZone GalaxyZone { get; }
public Galaxy Galaxy { get; }
public Zone(ItemManager itemManager, PlanetSettings settings, ZoneConstructionBlueprint blueprint, GalaxyZone galaxyZone, Galaxy galaxy)
{
_time = blueprint.Time;
GalaxyZone = galaxyZone;
Galaxy = galaxy;
Radius = blueprint.Radius;
Mass = blueprint.Mass;
_itemManager = itemManager;
Settings = settings;
_random = new Random(Convert.ToUInt32(Math.Abs(galaxyZone?.Name.GetHashCode() ?? 1337)));
foreach (var orbit in blueprint.Orbits)
{
var runtimeOrbit = new Orbit(Settings, orbit);
Orbits[runtimeOrbit.OrbitKey] = runtimeOrbit;
}
foreach (var planet in blueprint.Bodies)
{
switch (planet)
{
case AsteroidBeltConstructionData belt:
var runtimeBelt = new AsteroidBelt(belt, Orbits[belt.OrbitKey]);
AsteroidBelts[runtimeBelt.BodyKey] = runtimeBelt;
break;
case SunConstructionData sun:
var runtimeSun = new Sun(settings, sun, Orbits[planet.OrbitKey]);
PlanetInstances[runtimeSun.BodyKey] = runtimeSun;
break;
case GasGiantConstructionData gas:
var runtimeGas = new GasGiant(settings, gas, Orbits[planet.OrbitKey]);
PlanetInstances[runtimeGas.BodyKey] = runtimeGas;
break;
default:
var runtimePlanet = new Planet(settings, planet, Orbits[planet.OrbitKey]);
PlanetInstances[runtimePlanet.BodyKey] = runtimePlanet;
break;
}
}
foreach (var entityBlueprint in blueprint.Entities)
{
var entity = EntityConstructionBlueprintMaterializer.InstantiateAuthoritativeFromBlueprint(_itemManager, this, entityBlueprint);
Entities.Add(entity);
entity.Activate();
if (entity is Ship {IsPlayerShip: false} ship)
{
Agents.Add(CreateAgent(ship));
if (CultMath.math.lengthsq(ship.CultPosition) < 1)
{
var halfRadius = blueprint.Radius * .5f;
ship.CultPosition = _itemManager.Random.NextFloat3(
new cfloat3(-halfRadius, -halfRadius, -halfRadius),
new cfloat3(halfRadius, halfRadius, halfRadius));
}
}
}
// TODO: Associate planets with stored entities for planetary colonies
}
private Agent CreateAgent(Ship ship)
{
var agent = new Minion(ship);
var task = new PatrolOrbitsTask();
task.Circuit = Orbits.Values
.OrderBy(_ => _itemManager.Random.NextFloat())
.Take(4)
.Select(orbit => orbit.OrbitKey)
.ToArray();
agent.Task = task;
return agent;
}
public static string OrbitKey(Guid id)
{
return id == Guid.Empty ? "" : $"{OrbitKeyPrefix}{id:D}";
}
public static string BodyKey(Guid id)
{
return id == Guid.Empty ? "" : $"{BodyKeyPrefix}{id:D}";
}
public bool TryGetPlanet(string bodyKey, out Planet planet)
{
return PlanetInstances.TryGetValue(bodyKey ?? "", out planet);
}
public bool TryGetAsteroidBelt(string bodyKey, out AsteroidBelt belt)
{
return AsteroidBelts.TryGetValue(bodyKey ?? "", out belt);
}
public bool AsteroidExists(string asteroidBeltKey, int asteroid)
{
return TryGetAsteroidBelt(asteroidBeltKey, out var belt) && belt.ContainsAsteroid(asteroid);
}
public bool TryGetOrbit(string orbitKey, out Orbit orbit)
{
return Orbits.TryGetValue(orbitKey ?? "", out orbit);
}
public void Update(float deltaTime)
{
_time += deltaTime;
_updatedOrbits.Clear();
foreach (var t in BeltUpdates)
t.Wait();
BeltUpdates.Clear();
foreach (var orbit in Orbits.Values)
{
orbit.PreviousPosition = orbit.Position;
orbit.Position = GetOrbitPosition(orbit.OrbitKey);
orbit.Velocity = (orbit.Position - orbit.PreviousPosition) / deltaTime;
}
foreach (var belt in AsteroidBelts.Values)
{
Array.Copy(belt.NewTransforms, belt.Transforms, belt.Transforms.Length);
belt.OrbitPosition = belt.NewOrbitPosition;
BeltUpdates.Add(Task.Run(() => UpdateAsteroidTransforms(belt)));
}
foreach(var agent in Agents)
agent.Update(deltaTime);
foreach (var entity in Entities.ToArray()) entity.Update(deltaTime);
}
// Determine orbital position recursively, caching parent positions to avoid repeated calculations
public float2 GetOrbitPosition(string orbitKey)
{
// Root orbit is fixed at origin
if (string.IsNullOrWhiteSpace(orbitKey))
return new float2(0, 0);
if (!Orbits.TryGetValue(orbitKey, out var orbit))
{
Log?.Invoke("Requested orbit is not part of this zone!");
return new float2(0, 0);
}
if (!_updatedOrbits.Contains(orbitKey))
{
float2 pos = new float2(0, 0);
if (orbit.Period > .01f)
{
var phase = (float) Fraction(_time / orbit.Period);
pos = Orbit.Evaluate(Fraction(phase + orbit.Phase)) * orbit.Distance;
if (float.IsNaN(pos.x))
{
//_context.Log("Orbit position is NaN, something went very wrong!");
pos = new float2(0, 0);
}
}
var parentPosition = string.IsNullOrWhiteSpace(orbit.ParentOrbitKey)
? orbit.FixedPosition
: GetOrbitPosition(orbit.ParentOrbitKey);
orbit.Position = parentPosition + pos;
_updatedOrbits.Add(orbitKey);
}
return orbit.Position;
}
public float2 GetOrbitVelocity(string orbitKey)
{
return TryGetOrbit(orbitKey, out var orbit) ? orbit.Velocity : float2.zero;
}
public cfloat2 GetCultOrbitPosition(string orbitKey) => AetheriaMath.ToCult(GetOrbitPosition(orbitKey));
public cfloat2 GetCultOrbitVelocity(string orbitKey) => AetheriaMath.ToCult(GetOrbitVelocity(orbitKey));
public int NearestAsteroid(string asteroidBeltKey, float2 position)
{
if (!TryGetAsteroidBelt(asteroidBeltKey, out var belt))
return -1;
var asteroidPositions = belt.Transforms;
int nearest = 0;
float nearestDistance = Single.MaxValue;
for (int i = 0; i < belt.AsteroidCount; i++)
{
var dist = LengthSquared(asteroidPositions[i].xz - position);
if (belt.ContainsAsteroid(i) && dist < nearestDistance)
{
nearest = i;
nearestDistance = dist;
}
}
return nearest;
}
public int NearestAsteroid(string asteroidBeltKey, cfloat2 position) =>
NearestAsteroid(asteroidBeltKey, AetheriaMath.ToUnity(position));
public bool TryGetCultAsteroidTransform(
string asteroidBeltKey,
int asteroid,
out cfloat2 position,
out float radius)
{
position = cfloat2.zero;
radius = 0;
if (!TryGetAsteroidBelt(asteroidBeltKey, out var belt) || !belt.ContainsAsteroid(asteroid))
return false;
var transform = belt.Transforms[asteroid];
position = AetheriaMath.ToCult(transform.xy);
radius = transform.w;
return true;
}
private void UpdateAsteroidTransforms(AsteroidBelt belt)
{
belt.NewOrbitPosition = GetOrbitPosition(belt.Orbit.ParentOrbitKey);
for (var i = 0; i < belt.AsteroidCount; i++)
{
var asteroid = belt.GetAsteroid(i);
float size;
if(belt.RespawnTimers.ContainsKey(i)) size = 0;
else if (belt.Damage.ContainsKey(i))
{
var asteroidHitpoints = Settings.AsteroidHitpoints.Evaluate(asteroid.Size);
var damage = (asteroidHitpoints - belt.Damage[i]) / asteroidHitpoints;
size = Settings.AsteroidSize.Evaluate(damage * asteroid.Size);
}
else size = Settings.AsteroidSize.Evaluate(asteroid.Size);
var rot = (float) (_time * asteroid.RotationSpeed % (MathF.PI * 2));
var pos = Orbit.Evaluate((float) Fraction(_time / Settings.OrbitPeriod.Evaluate(asteroid.Distance) +
asteroid.Phase)) * asteroid.Distance + belt.NewOrbitPosition;
//belt.NewPositions[i] = float3(pos.x, GetHeight(pos) + Settings.AsteroidVerticalOffset, pos.y);
belt.NewTransforms[i] = new float4(pos.x, pos.y, rot, size);
}
}
public void MineAsteroid(Entity miner, string asteroidBeltKey, int asteroid, float damage, float efficiency, float penetration)
{
if (!TryGetAsteroidBelt(asteroidBeltKey, out var belt))
return;
//var asteroidTransform = belt.Transforms[asteroid];
var size = belt.GetAsteroid(asteroid).Size;
var asteroidHitpoints = Settings.AsteroidHitpoints.Evaluate(size);
if (!belt.Damage.ContainsKey(asteroid))
belt.Damage[asteroid] = 0;
belt.Damage[asteroid] = belt.Damage[asteroid] + damage;
if (!belt.MiningAccumulator.ContainsKey((miner, asteroid)))
belt.MiningAccumulator[(miner, asteroid)] = 0;
belt.MiningAccumulator[(miner, asteroid)] = belt.MiningAccumulator[(miner, asteroid)] + damage;
if (belt.Damage[asteroid] > asteroidHitpoints)
{
belt.RespawnTimers[asteroid] = Settings.AsteroidRespawnTime.Evaluate(size);
belt.Damage.Remove(asteroid);
belt.MiningAccumulator.Remove((miner, asteroid));
return;
}
var resourceCount = belt.Resources.Sum(x => x.Value);
var resource = belt.Resources.MaxBy(x => MathF.Pow(x.Value, 1f / penetration) * _random.NextFloat());
if (efficiency * _random.NextFloat() * belt.MiningAccumulator[(miner, asteroid)] * resourceCount / Settings.MiningDifficulty > 1)
{
belt.MiningAccumulator.Remove((miner, asteroid));
// var newSimpleCommodity = new SimpleCommodity
// {
// Data = resource.Key,
// Quantity = 1
// };
// TODO: Drop item onto the Grid
//miner.AddCargo(newSimpleCommodity);
}
}
public SecurityLevel GetSecurityLevel(float2 pos) => GetSecurityLevel(AetheriaMath.ToCult(pos));
public SecurityLevel GetSecurityLevel(cfloat2 pos)
{
if (GalaxyZone.Owner==null) return SecurityLevel.Open;
var security = SecurityLevel.Open;
foreach (var entity in Entities)
{
if (entity is OrbitalEntity orbitalEntity && orbitalEntity.SecurityRadius > 1 && entity.Faction.HasSameKey(GalaxyZone.Owner))
{
if (orbitalEntity.SecurityLevel > security && CultMath.math.length(orbitalEntity.CultPositionXZ - pos) < orbitalEntity.SecurityRadius * Settings.SecureAreaRadiusMultiplier)
security = orbitalEntity.SecurityLevel;
}
}
return security;
}
public float GetHeight(float2 position)
{
var result = -PowerPulse(Length(position)/(Radius*2), Settings.ZoneDepthExponent) * Settings.ZoneDepth;
foreach (var body in PlanetInstances.Values)
{
var p = position - body.Orbit.Position;
var distSqr = LengthSquared(p);
var gravityRadius = body.GravityWellRadius;
if (distSqr < gravityRadius*gravityRadius)
{
var depth = body.GravityWellDepth;
result -= PowerPulse(MathF.Sqrt(distSqr) / gravityRadius, body.GravityDepthExponent) * depth;
}
if (body is GasGiant gas)
{
var waveRadius = gas.GravityWavesRadius;
if(distSqr < waveRadius*waveRadius)
{
var depth = gas.GravityWavesDepth;
var frequency = Settings.WaveFrequency.Evaluate(body.Mass);
var speed = gas.GravityWavesSpeed;
result -= RadialWaves(MathF.Sqrt(distSqr) / waveRadius, 8, 1.25f, frequency, (float) (_time * speed)) * depth;
}
}
}
return result;
}
public float GetHeight(cfloat2 position) => GetHeight(AetheriaMath.ToUnity(position));
public float GetLight(float2 position)
{
var light = 0f;
foreach (var body in PlanetInstances.Values)
{
if (body is Sun sun)
{
var p = position - body.Orbit.Position;
var distSqr = LengthSquared(p);
var lightRadius = sun.LightRadius;
if (distSqr < lightRadius * lightRadius)
{
light += PowerPulse(MathF.Sqrt(distSqr) / lightRadius, 8);
}
}
}
return light;
}
public float GetLight(cfloat2 position) => GetLight(AetheriaMath.ToUnity(position));
public float2 GetForce(float2 position)
{
var normal = GetNormal(position);
var f = new float2(normal.x, normal.z);
return f * Settings.GravityStrength * LengthSquared(f);
}
public cfloat2 GetForce(cfloat2 position) => AetheriaMath.ToCult(GetForce(AetheriaMath.ToUnity(position)));
public static float PowerPulse(float x, float exponent)
{
x *= 2;
x = Clamp(x, -1, 1);
return MathF.Pow((x + 1) * (1 - x), exponent);
}
public static float RadialWaves(float x, float maskExponent, float sineExponent, float frequency, float phase)
{
//x *= 2;
return PowerPulse(x, maskExponent) * MathF.Cos(MathF.Pow(x*2, sineExponent) * frequency + phase);
}
public float3 GetNormal(float2 pos, float step = .1f, float mul = 1)
{
float hL = GetHeight(new float2(pos.x - step, pos.y)) * mul;
float hR = GetHeight(new float2(pos.x + step, pos.y)) * mul;
float hD = GetHeight(new float2(pos.x, pos.y - step)) * mul;
float hU = GetHeight(new float2(pos.x, pos.y + step)) * mul;
// Deduce terrain normal
float3 normal = new float3((hL - hR), (hD - hU), step*2);
return Normalize(normal).xzy;
}
public cfloat3 GetNormal(cfloat2 pos, float step = .1f, float mul = 1) =>
AetheriaMath.ToCult(GetNormal(AetheriaMath.ToUnity(pos), step, mul));
private static float Clamp(float value, float min, float max)
{
return value < min ? min : value > max ? max : value;
}
private static double Fraction(double value)
{
return value - Math.Floor(value);
}
private static float Fraction(float value)
{
return value - MathF.Floor(value);
}
private static float Length(float2 value)
{
return MathF.Sqrt(LengthSquared(value));
}
private static float LengthSquared(float2 value)
{
return value.x * value.x + value.y * value.y;
}
private static float3 Normalize(float3 value)
{
var length = MathF.Sqrt(value.x * value.x + value.y * value.y + value.z * value.z);
return length <= 0 ? new float3(0, 0, 0) : value / length;
}
}
public class Planet
{
public Orbit Orbit;
protected readonly PlanetSettings Settings;
public string BodyKey { get; }
public string Name { get; }
public string OrbitKey { get; }
public float Mass { get; }
public IReadOnlyDictionary<string, float> Resources { get; }
public float BodyRadiusMultiplier { get; }
public float GravityRadiusMultiplier { get; }
public float GravityDepthMultiplier { get; }
public float GravityDepthExponent { get; }
public float GravityWellDepth;
public float GravityWellRadius;
public float BodyRadius;
public Planet(PlanetSettings settings, BodyConstructionData data, Orbit orbit)
{
Settings = settings;
Orbit = orbit;
BodyKey = data.BodyKey ?? "";
Name = data.Name ?? "";
OrbitKey = data.OrbitKey ?? "";
Mass = data.Mass;
Resources = data.Resources;
BodyRadiusMultiplier = data.BodyRadiusMultiplier;
GravityRadiusMultiplier = data.GravityRadiusMultiplier;
GravityDepthMultiplier = data.GravityDepthMultiplier;
GravityDepthExponent = data.GravityDepthExponent;
CalculateProperties();
}
public void CalculateProperties()
{
BodyRadius = Settings.BodyRadius.Evaluate(Mass) * BodyRadiusMultiplier;
GravityWellRadius = Settings.GravityRadius.Evaluate(Mass) * GravityRadiusMultiplier;
GravityWellDepth = Settings.GravityDepth.Evaluate(Mass) * GravityDepthMultiplier;
}
}
public class GasGiant : Planet
{
public float FirstOffsetDomainRotationSpeed { get; }
public float FirstOffsetRotationSpeed { get; }
public float SecondOffsetDomainRotationSpeed { get; }
public float SecondOffsetRotationSpeed { get; }
public float AlbedoRotationSpeed { get; }
public float WaveRadiusMultiplier { get; }
public float WaveDepthMultiplier { get; }
public float WaveDepthExponent { get; }
public float WaveSpeedMultiplier { get; }
public IReadOnlyList<string> MaterialOverrides { get; }
public float4[] Colors { get; }
public float GravityWavesDepth;
public float GravityWavesRadius;
public float GravityWavesSpeed;
public GasGiant(PlanetSettings settings, GasGiantConstructionData data, Orbit orbit) : base(settings, data, orbit)
{
FirstOffsetDomainRotationSpeed = data.FirstOffsetDomainRotationSpeed;
FirstOffsetRotationSpeed = data.FirstOffsetRotationSpeed;
SecondOffsetDomainRotationSpeed = data.SecondOffsetDomainRotationSpeed;
SecondOffsetRotationSpeed = data.SecondOffsetRotationSpeed;
AlbedoRotationSpeed = data.AlbedoRotationSpeed;
WaveRadiusMultiplier = data.WaveRadiusMultiplier;
WaveDepthMultiplier = data.WaveDepthMultiplier;
WaveDepthExponent = data.WaveDepthExponent;
WaveSpeedMultiplier = data.WaveSpeedMultiplier;
MaterialOverrides = data.MaterialOverrides;
Colors = data.Colors;
CalculateProperties();
}
public new void CalculateProperties()
{
base.CalculateProperties();
GravityWavesDepth = Settings.WaveDepth.Evaluate(Mass) * WaveDepthMultiplier;
GravityWavesRadius = Settings.WaveRadius.Evaluate(Mass) * WaveRadiusMultiplier;
GravityWavesSpeed = Settings.WaveSpeed.Evaluate(Mass) * WaveSpeedMultiplier;
}
}
public class Sun : GasGiant
{
public cfloat3 LightColor { get; }
public cfloat3 FogTintColor { get; }
public float LightRadiusMultiplier { get; }
public float LightRadius;
public Sun(PlanetSettings settings, SunConstructionData data, Orbit orbit) : base(settings, data, orbit)
{
LightColor = data.LightColor;
FogTintColor = data.FogTintColor;
LightRadiusMultiplier = data.LightRadiusMultiplier;
CalculateProperties();
}
public new void CalculateProperties()
{
base.CalculateProperties();
LightRadius = Settings.LightRadius.Evaluate(Mass) * LightRadiusMultiplier;
}
}
public class AsteroidBelt
{
private readonly Asteroid[] _asteroids;
public Orbit Orbit { get; }
public string BodyKey { get; }
public string Name { get; }
public string OrbitKey { get; }
public float Mass { get; }
public IReadOnlyDictionary<string, float> Resources { get; }
public float BodyRadiusMultiplier { get; }
public float GravityRadiusMultiplier { get; }
public float GravityDepthMultiplier { get; }
public float GravityDepthExponent { get; }
public float4[] Transforms; // x, y, rotation, scale
public float4[] NewTransforms; // x, y, rotation, scale
public float Radius { get; }
public float2 OrbitPosition;
public float2 NewOrbitPosition;
public Dictionary<int, float> RespawnTimers = new Dictionary<int, float>();
public Dictionary<int, float> Damage = new Dictionary<int, float>();
public Dictionary<(Entity, int), float> MiningAccumulator = new Dictionary<(Entity, int), float>();
public AsteroidBelt(AsteroidBeltConstructionData data, Orbit orbit)
{
_asteroids = data.Asteroids;
Orbit = orbit;
BodyKey = data.BodyKey ?? "";
Name = data.Name ?? "";
OrbitKey = data.OrbitKey ?? "";
Mass = data.Mass;
Resources = data.Resources;
BodyRadiusMultiplier = data.BodyRadiusMultiplier;
GravityRadiusMultiplier = data.GravityRadiusMultiplier;
GravityDepthMultiplier = data.GravityDepthMultiplier;
GravityDepthExponent = data.GravityDepthExponent;
Transforms = new float4[_asteroids.Length];
NewTransforms = new float4[_asteroids.Length];
Radius = _asteroids.Max(a => a.Distance);
}
public int AsteroidCount => _asteroids.Length;
public bool ContainsAsteroid(int asteroid) => asteroid >= 0 && asteroid < _asteroids.Length;
public Asteroid GetAsteroid(int asteroid) => _asteroids[asteroid];
}
public class Orbit
{
public string OrbitKey { get; }
public string ParentOrbitKey { get; }
public float Distance { get; }
public float Phase { get; }
public float2 FixedPosition { get; }
public float2 Velocity = new float2(0, 0);
public float2 Position = new float2(0, 0);
public float2 PreviousPosition = new float2(0, 0);
public float Period;
public static float2 Evaluate(float phase)
{
phase *= MathF.PI * 2;
return new float2(MathF.Cos(phase), MathF.Sin(phase));
}
public Orbit(PlanetSettings settings, OrbitConstructionData data)
{
OrbitKey = data.OrbitKey ?? "";
ParentOrbitKey = data.ParentOrbitKey ?? "";
Distance = data.Distance;
Phase = data.Phase;
FixedPosition = data.FixedPosition;
Period = settings.OrbitPeriod.Evaluate(data.Distance);
}
}