247 lines
10 KiB
C#
247 lines
10 KiB
C#
using Aetheria.State.Daemon;
|
|
using GameCult.Aetheria.State.Verse;
|
|
|
|
var checks = new AetheriaDaemonYmirSmokeChecks();
|
|
checks.Run();
|
|
Console.WriteLine("Daemon Ymir projectile smoke passed.");
|
|
|
|
internal sealed class AetheriaDaemonYmirSmokeChecks
|
|
{
|
|
public void Run()
|
|
{
|
|
YmirMovesProjectileAndReportsStableContact();
|
|
DaemonSimulationAppliesYmirHit();
|
|
MissingPhysicsOwnerCannotAdvanceProjectiles();
|
|
ThermalCellsUseFossilConductionAndRadiation();
|
|
MultipleActorsUseTheSameMovementLever();
|
|
}
|
|
|
|
private static void MultipleActorsUseTheSameMovementLever()
|
|
{
|
|
var player = Entity(0, 0, "player");
|
|
var agent = Entity(1, 0, "worker");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "shared-lever-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player, agent] }]
|
|
};
|
|
var commands = new[]
|
|
{
|
|
MovementCommand("player-move", "zone.0.entity.0", 1, 0),
|
|
MovementCommand("agent-move", "zone.0.entity.1", 0, 1)
|
|
};
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(run, commands);
|
|
|
|
RequireEqual(2, operation.Intents.Movements.Count, "movement intent must retain one lever position per actor");
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
operation.Intents,
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
AetheriaRuntimeProjectilePhysicsUnavailable.Instance);
|
|
Require(player.VelocityX > 0 && Math.Abs(player.VelocityY) < 0.001,
|
|
"player command must drive its actor through the shared movement lever");
|
|
Require(agent.VelocityY > 0 && Math.Abs(agent.VelocityX) < 0.001,
|
|
"agent command must drive its actor through the shared movement lever");
|
|
}
|
|
|
|
private static AetheriaRuntimeDaemonCommandDocument MovementCommand(
|
|
string commandId,
|
|
string actor,
|
|
double x,
|
|
double y)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetMoveVector,
|
|
"shared-control-smoke",
|
|
"shared-control-session",
|
|
0,
|
|
actor);
|
|
command.CommandId = commandId;
|
|
command.DirectionX = x;
|
|
command.DirectionY = y;
|
|
command.ScalarValue = 1;
|
|
return command;
|
|
}
|
|
|
|
private static void ThermalCellsUseFossilConductionAndRadiation()
|
|
{
|
|
var entity = Entity(7, 0, "player");
|
|
entity.StatGrids =
|
|
[
|
|
Grid(AetheriaRuntimeThermalSimulation.TemperatureGrid, 2, 1, 300, 280),
|
|
Grid(AetheriaRuntimeThermalSimulation.ThermalMassGrid, 2, 1, 1, 1),
|
|
Grid(AetheriaRuntimeThermalSimulation.ConductivityGrid, 2, 1, 1, 1)
|
|
];
|
|
|
|
AetheriaRuntimeThermalSimulation.AddHeat(entity, 20);
|
|
RequireNear(310, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), 0.000001,
|
|
"heat energy must be divided across cells and thermal mass");
|
|
RequireNear(290, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 1), 0.000001,
|
|
"heat energy must be divided across cells and thermal mass");
|
|
|
|
AetheriaRuntimeThermalSimulation.Step(entity, 0.1);
|
|
var expectedHot = (310 / 0.01 + 290) / 101;
|
|
expectedHot -= Math.Pow(expectedHot, 3) * 0.00000001 * 0.1;
|
|
var expectedCool = (290 / 0.01 + 310) / 101;
|
|
expectedCool -= Math.Pow(expectedCool, 3) * 0.00000001 * 0.1;
|
|
RequireNear(expectedHot, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), 0.000001,
|
|
"hot cell must follow fossil conduction and radiation");
|
|
RequireNear(expectedCool, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 1), 0.000001,
|
|
"cool cell must follow fossil conduction and radiation");
|
|
RequireNear((expectedHot + expectedCool) / 2, Stat(entity, "heat"), 0.000001,
|
|
"legacy heat scalar must be derived from cell temperature");
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "thermal-projection-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.7",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
var document = AetheriaRuntimeGameDocuments.CurrentEntity(new AetheriaRuntimeDaemonFrameDocument { Run = run });
|
|
RequireNear(Stat(entity, "heat"), document.Hud.MeanTemperature, 0.000001,
|
|
"Eve current-entity state must publish mean temperature");
|
|
RequireNear(GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), document.Hud.MaximumTemperature, 0.000001,
|
|
"Eve current-entity state must publish maximum temperature");
|
|
Require(document.Hud.ThermalVisibility > 0, "Eve current-entity state must publish thermal visibility");
|
|
}
|
|
|
|
private static void YmirMovesProjectileAndReportsStableContact()
|
|
{
|
|
var (run, zone, target) = Scenario();
|
|
var step = new AetheriaYmirProjectilePhysics().Step(zone, zone.Entities, 0.1);
|
|
|
|
RequireEqual("ymir.core", new AetheriaYmirProjectilePhysics().AuthorityId, "adapter must identify its owner");
|
|
RequireEqual(0, step.Projectiles.Count, "contacted projectile must not survive");
|
|
RequireEqual(1, step.Hits.Count, "Ymir must report one projectile contact");
|
|
RequireEqual(target.EntityIndex, step.Hits[0].TargetEntityIndex, "contact must resolve the daemon entity");
|
|
RequireEqual("aetheria.projectile.smoke-projectile", step.Hits[0].ProjectileBodyId, "projectile body id must be stable");
|
|
RequireEqual("aetheria.daemon.entity.2", step.Hits[0].TargetBodyId, "entity body id must be stable");
|
|
}
|
|
|
|
private static void DaemonSimulationAppliesYmirHit()
|
|
{
|
|
var (run, _, target) = Scenario();
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
new AetheriaYmirProjectilePhysics());
|
|
|
|
RequireEqual(88.0, Stat(target, "hull"), "Aetheria must interpret the Ymir contact as damage");
|
|
RequireEqual(0, run.Zones[0].Projectiles.Count, "spent projectile must leave daemon state");
|
|
}
|
|
|
|
private static void MissingPhysicsOwnerCannotAdvanceProjectiles()
|
|
{
|
|
var (run, _, _) = Scenario();
|
|
try
|
|
{
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
AetheriaRuntimeProjectilePhysicsUnavailable.Instance);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("daemon advanced a projectile without an authoritative physics owner");
|
|
}
|
|
|
|
private static (AetheriaRuntimeRunCheckpointCommit Run, AetheriaRuntimeZoneSnapshotCommit Zone, AetheriaRuntimeEntitySnapshotCommit Target) Scenario()
|
|
{
|
|
var source = Entity(1, 0, "player");
|
|
var target = Entity(2, 30, "raider");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source, target],
|
|
Projectiles =
|
|
[
|
|
new AetheriaRuntimeProjectileCommit
|
|
{
|
|
ProjectileId = "smoke-projectile",
|
|
SourceEntityIndex = 1,
|
|
TargetEntityIndex = 2,
|
|
PositionX = 0,
|
|
PositionZ = 0,
|
|
VelocityX = 100,
|
|
VelocityY = 0,
|
|
Radius = 1,
|
|
Damage = 12,
|
|
LifetimeSeconds = 5,
|
|
Active = true
|
|
}
|
|
]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "ymir-projectile-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Zones = [zone]
|
|
};
|
|
return (run, zone, target);
|
|
}
|
|
|
|
private static AetheriaRuntimeEntitySnapshotCommit Entity(int index, double x, string faction) => new()
|
|
{
|
|
EntityIndex = index,
|
|
Kind = "ship",
|
|
FactionKey = faction,
|
|
PositionX = x,
|
|
PositionZ = 0,
|
|
TargetEntityIndex = -1,
|
|
IsActive = true,
|
|
StatGrids = [Grid("hull", 100), Grid("shield", 0), Grid("heat", 0)]
|
|
};
|
|
|
|
private static AetheriaRuntimeEntityStatGridCommit Grid(string name, double value) => new()
|
|
{
|
|
Name = name,
|
|
Width = 1,
|
|
Height = 1,
|
|
Values = [value]
|
|
};
|
|
|
|
private static AetheriaRuntimeEntityStatGridCommit Grid(string name, int width, int height, params double[] values) => new()
|
|
{
|
|
Name = name,
|
|
Width = width,
|
|
Height = height,
|
|
Values = values
|
|
};
|
|
|
|
private static double GridValue(AetheriaRuntimeEntitySnapshotCommit entity, string name, int index) =>
|
|
entity.StatGrids.Single(grid => string.Equals(grid.Name, name, StringComparison.OrdinalIgnoreCase)).Values[index];
|
|
|
|
private static double Stat(AetheriaRuntimeEntitySnapshotCommit entity, string name) =>
|
|
entity.StatGrids.Single(grid => string.Equals(grid.Name, name, StringComparison.OrdinalIgnoreCase)).Values[0];
|
|
|
|
private static void RequireEqual<T>(T expected, T actual, string message)
|
|
{
|
|
if (!EqualityComparer<T>.Default.Equals(expected, actual))
|
|
throw new InvalidOperationException($"{message}. Expected {expected}; actual {actual}.");
|
|
}
|
|
|
|
private static void Require(bool condition, string message)
|
|
{
|
|
if (!condition)
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
|
|
private static void RequireNear(double expected, double actual, double tolerance, string message)
|
|
{
|
|
if (Math.Abs(expected - actual) > tolerance)
|
|
throw new InvalidOperationException($"{message}. Expected {expected}; actual {actual}.");
|
|
}
|
|
}
|