Restore daemon cockpit thermal medicine

This commit is contained in:
Meta Vens 2026-07-12 06:10:33 +01:00
commit b50c8a9053
12 changed files with 361 additions and 11 deletions

View file

@ -32,6 +32,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
RadiatorPumpsHeatBeforeReactorSettlement();
EquipmentThermalPerformanceOwnsShutdownAndWear();
ThermalTopologyComesFromHullAndEquipment();
ThermalMedicalExposureUsesCockpitTemperature();
ThermalMedicalDeathUsesOrdinaryDestructionPath();
MultipleActorsUseTheSameMovementLever();
AgentClaimsAndCompletesExploreTaskThroughCommands();
SchedulerAssignsHighestPriorityCompatibleTask();
@ -2110,6 +2112,106 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"authored hull topology must initialize each occupied cell at the fossil temperature");
}
private static void ThermalMedicalExposureUsesCockpitTemperature()
{
var entity = Entity(0, 0, "player");
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
{
Item = new AetheriaRuntimeLoadoutItemCommit
{ ItemKey = "medical-cockpit", Quality = 1, Durability = 1, Enabled = true }
}];
entity.StatGrids =
[
Grid("hull", 100), Grid("temperature", 340),
Grid("thermal-mass", 1), Grid("conductivity", 1)
];
var cockpit = CatalogItem("medical-cockpit",
new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, []));
cockpit.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
var catalog = new AetheriaRuntimeCatalogSnapshot([cockpit], [], []);
var settings = AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
var heat = AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 300, settings);
RequireNear(0.3, entity.Heatstroke, 0.000001,
"cockpit heat must accumulate nonlinear heatstroke at the fossil rate");
Require(heat.HeatstrokeRiskCrossed,
"heatstroke must emit one upward severe-risk crossing");
RequireNear(340, Stat(entity, "cockpit-temperature"), 0.000001,
"cockpit cell temperature must become an authoritative Eve-readable fact");
entity.StatGrids.Single(value => value.Name == "temperature").Values = [280];
AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 1, settings);
RequireNear(0.1, entity.Heatstroke, 0.000001,
"heatstroke must recover linearly below the authored threshold");
entity.StatGrids.Single(value => value.Name == "temperature").Values = [263];
var cold = AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 300, settings);
RequireNear(0.3, entity.Hypothermia, 0.000001,
"cockpit cold must accumulate nonlinear hypothermia at the fossil rate");
Require(cold.HypothermiaRiskCrossed,
"hypothermia must use its own exposure for the intended severe-risk crossing");
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "thermal-medical-surface", CurrentZoneIndex = 0,
CurrentEntityKey = "zone.0.entity.0",
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
};
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
new AetheriaRuntimeDaemonHealthDocument(),
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
var worldEntity = Flatten(surface.Surface.Root).Single(node =>
node.Id == "aetheria.daemon.game.world.entity.0");
RequireEqual("263", worldEntity.Props["cockpitTemperature"],
"Eve must publish canonical cockpit temperature rather than deriving it from effects");
RequireEqual("true", worldEntity.Props["hypothermiaRisk"],
"Eve must publish the current severe cold-risk state");
Require(worldEntity.Props.ContainsKey("heatstrokePostWeight") &&
worldEntity.Props.ContainsKey("severeHeatstrokeWeight"),
"Eve must publish the exact source weights used by the original heatstroke presentation");
}
private static void ThermalMedicalDeathUsesOrdinaryDestructionPath()
{
var entity = Entity(0, 0, "player");
entity.EntityId = "thermal-pilot";
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
{
Item = new AetheriaRuntimeLoadoutItemCommit
{ ItemKey = "lethal-cockpit", Quality = 1, Durability = 1, Enabled = true }
}];
entity.StatGrids =
[
Grid("hull", 100), Grid("temperature", 700),
Grid("thermal-mass", 1), Grid("conductivity", 1)
];
var cockpit = CatalogItem("lethal-cockpit",
new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, []));
cockpit.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
var catalog = new AetheriaRuntimeCatalogSnapshot([cockpit], [], []);
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] };
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "thermal-death", CurrentZoneIndex = 0,
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
};
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 1,
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
new AetheriaYmirPhysicalPayloadPhysics(), new AetheriaYmirWorldPhysics(), catalog, frameId: 17);
RequireEqual("heatstroke", entity.CauseOfDeath,
"lethal cockpit heat must persist the typed cause of death");
Require(!entity.IsActive && !string.IsNullOrWhiteSpace(entity.DestructionId),
"thermal death must enter the ordinary exactly-once destruction path");
var death = run.GameEvents.Single(value => value.Kind == "entity.destroyed");
RequireEqual("heatstroke", death.Reason,
"Eve feedback must carry the thermal cause needed for the original death transition");
Require(run.GameEvents.Any(value => value.Kind == "pilot.heatstroke.risk"),
"a lethal step crossing the severe threshold must publish risk before death");
}
private static void ThermalCellsUseFossilConductionAndRadiation()
{
var entity = Entity(7, 0, "player");

View file

@ -16333,6 +16333,8 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
var runtime = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime");
var energyPath = Path.Combine(runtime, "AetheriaRuntimeEnergySimulation.cs");
var thermalPath = Path.Combine(runtime, "AetheriaRuntimeThermalSimulation.cs");
var medicalPath = Path.Combine(runtime, "AetheriaRuntimeThermalMedicalSimulation.cs");
var settingsPath = Path.Combine(runtime, "AetheriaRuntimeDaemonSimulationSettings.cs");
var simulationPath = Path.Combine(runtime, "AetheriaRuntimeDaemonSimulation.cs");
var snapshotPath = Path.Combine(runtime, "AetheriaRuntimeSnapshotDocuments.cs");
var surfacePath = Path.Combine(runtime, "AetheriaRuntimeDaemonGameSurfaceBuilder.cs");
@ -16342,6 +16344,8 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
{
[energyPath] = File.Exists(energyPath) ? File.ReadAllText(energyPath) : "",
[thermalPath] = File.Exists(thermalPath) ? File.ReadAllText(thermalPath) : "",
[medicalPath] = File.Exists(medicalPath) ? File.ReadAllText(medicalPath) : "",
[settingsPath] = File.Exists(settingsPath) ? File.ReadAllText(settingsPath) : "",
[simulationPath] = File.Exists(simulationPath) ? File.ReadAllText(simulationPath) : "",
[snapshotPath] = File.Exists(snapshotPath) ? File.ReadAllText(snapshotPath) : "",
[surfacePath] = File.Exists(surfacePath) ? File.ReadAllText(surfacePath) : ""
@ -16363,6 +16367,24 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
"public static void AddHeatToEquipment(",
"public static void ApplyWear("
},
[medicalPath] = new[]
{
"public static class AetheriaRuntimeThermalMedicalSimulation",
"temperature > settings.HeatstrokeTemperature",
"settings.HeatstrokeRecoveryPerSecond * deltaSeconds",
"temperature < settings.HypothermiaTemperature",
"settings.HypothermiaRecoveryPerSecond * deltaSeconds",
"entity.Heatstroke > 0.99",
"entity.Hypothermia > 0.99"
},
[settingsPath] = new[]
{
"public double SevereThermalRiskThreshold",
"public double HeatstrokeTemperature",
"public double HeatstrokeMultiplier",
"public double HypothermiaTemperature",
"public double HypothermiaMultiplier"
},
[simulationPath] = new[]
{
"AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog)",
@ -16371,6 +16393,10 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
"AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds)",
"AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds)",
"AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog)",
"AetheriaRuntimeThermalMedicalSimulation.Step(",
"PublishThermalMedicalEvents(",
"CommitDestruction(run, zone, entity, -1,",
"Reason = target.CauseOfDeath",
"AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand)"
},
[snapshotPath] = new[]
@ -16379,7 +16405,8 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
"public sealed class AetheriaRuntimeEquipmentStateCommit",
"public double ThermalPerformance",
"public double Wear",
"public bool Online"
"public bool Online",
"public string CauseOfDeath"
},
[surfacePath] = new[]
{
@ -16387,7 +16414,11 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
"[\"reactorDraw\"]",
"[\"radiatorTemperature\"]",
"[\"minimumEquipmentThermalPerformance\"]",
"[\"offlineEquipmentCount\"]"
"[\"offlineEquipmentCount\"]",
"[\"cockpitTemperature\"]",
"[\"heatstrokeRisk\"]",
"[\"hypothermiaRisk\"]",
"[\"causeOfDeath\"]"
}
};
var missing = required.SelectMany(pair => pair.Value
@ -16405,7 +16436,10 @@ static void RequireDaemonEnergyAndThermalAuthority(string root)
"ApplyCommitEnergy(",
"ApplySetHeat(",
"ApplySetEquipmentOnline(",
"ApplyEquipmentWear("
"ApplyEquipmentWear(",
"ApplySetHeatstroke(",
"ApplySetHypothermia(",
"ApplySetCauseOfDeath("
};
var surviving = forbiddenOperations.Where(symbol => operations.Contains(symbol, StringComparison.Ordinal)).ToArray();
if (surviving.Length > 0)

View file

@ -670,6 +670,13 @@ namespace GameCult.Aetheria.State.Verse
var maximumWear = equipmentStates.Where(value => value != null)
.Select(value => value.Wear).DefaultIfEmpty(0).Max();
var offlineEquipmentCount = equipmentStates.Count(value => value != null && !value.Online);
var cockpitTemperature = EntityStat(entity, "cockpit-temperature");
var thermalRiskThreshold = simulationSettings.SevereThermalRiskThreshold;
var heatstrokePostWeight = thermalRiskThreshold <= 0 ? 1 :
Math.Max(0, Math.Min(1, entity.Heatstroke / thermalRiskThreshold));
var severeHeatstrokeWeight = thermalRiskThreshold >= 1 ? 0 :
Math.Max(0, Math.Min(1,
(entity.Heatstroke - thermalRiskThreshold) / (1 - thermalRiskThreshold)));
var weaponCooldown = (entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
.Select(state => state?.CooldownProgress ?? 0)
.DefaultIfEmpty(0)
@ -715,8 +722,14 @@ namespace GameCult.Aetheria.State.Verse
["minimumTemperature"] = FormatNumber(minimumTemperature),
["maximumTemperature"] = FormatNumber(maximumTemperature),
["thermalVisibility"] = FormatNumber(thermalVisibility),
["cockpitTemperature"] = FormatNumber(cockpitTemperature),
["heatstroke"] = FormatRatio(entity.Heatstroke, 1),
["hypothermia"] = FormatRatio(entity.Hypothermia, 1),
["heatstrokeRisk"] = entity.Heatstroke > thermalRiskThreshold ? "true" : "false",
["hypothermiaRisk"] = entity.Hypothermia > thermalRiskThreshold ? "true" : "false",
["heatstrokePostWeight"] = FormatNumber(heatstrokePostWeight),
["severeHeatstrokeWeight"] = FormatNumber(severeHeatstrokeWeight),
["causeOfDeath"] = entity.CauseOfDeath ?? "",
["visibility"] = FormatNumber(entity.Visibility),
["targetEntityId"] = targetEntityId,
["targetedByPlayer"] = player != null && player.TargetEntityIndex == entity.EntityIndex ? "true" : "false",

View file

@ -376,6 +376,12 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds);
AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds);
AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog);
var medical = AetheriaRuntimeThermalMedicalSimulation.Step(
entity, catalog, deltaSeconds, settings);
PublishThermalMedicalEvents(run, zone, entity, medical, frameId);
if (medical.Died)
CommitDestruction(run, zone, entity, -1,
$"thermal:{medical.DeathCause}", "", frameId, settings, medical.DeathCause);
entity.IsActive = IsAlive(entity);
if (!entity.IsActive)
{
@ -450,6 +456,29 @@ namespace GameCult.Aetheria.State.Verse
}
}
private static void PublishThermalMedicalEvents(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeThermalMedicalResult result,
long frameId)
{
void Append(string kind, double exposure)
{
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
{
EventId = $"frame:{frameId}:zone:{zone.ZoneIndex}:entity:{entity.EntityIndex}:{kind}",
Kind = kind, FrameId = frameId, ZoneIndex = zone.ZoneIndex,
TargetEntityIndex = entity.EntityIndex, SubjectKey = entity.EntityId,
ScalarValue = exposure, AuxiliaryValue = result.CockpitTemperature,
PositionX = entity.PositionX, PositionZ = entity.PositionZ
});
}
if (result.HeatstrokeRiskCrossed) Append("pilot.heatstroke.risk", entity.Heatstroke);
if (result.HypothermiaRiskCrossed) Append("pilot.hypothermia.risk", entity.Hypothermia);
}
private static void ResolveDeployableDetonations(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
@ -1236,13 +1265,15 @@ namespace GameCult.Aetheria.State.Verse
string subjectKey,
string weaponItemKey,
long frameId,
AetheriaRuntimeDaemonSimulationSettings settings)
AetheriaRuntimeDaemonSimulationSettings settings,
string causeOfDeath = "hull-destroyed")
{
if (!string.IsNullOrWhiteSpace(target.DestructionId)) return;
var destructionId = $"destruction:{run.RunId}:{zone.ZoneIndex}:{target.EntityIndex}:{frameId}";
target.DestructionId = destructionId;
target.DestroyedFrameId = frameId;
target.CauseOfDeath = causeOfDeath ?? "";
target.IsActive = false;
target.TargetEntityIndex = -1;
target.TractorPower = 0;
@ -1332,7 +1363,8 @@ namespace GameCult.Aetheria.State.Verse
EventId = destructionId, Kind = "entity.destroyed", FrameId = frameId,
ZoneIndex = zone.ZoneIndex, SourceEntityIndex = sourceEntityIndex,
TargetEntityIndex = target.EntityIndex, SubjectKey = subjectKey,
ItemKey = weaponItemKey, PositionX = target.PositionX, PositionZ = target.PositionZ
ItemKey = weaponItemKey, Reason = target.CauseOfDeath,
PositionX = target.PositionX, PositionZ = target.PositionZ
});
}

View file

@ -37,7 +37,16 @@ namespace GameCult.Aetheria.State.Verse
weaponLockDecayPerSecond: 1.0,
lootDropProbability: 0.25,
lootDropVelocity: 25.0,
pickupLifetimeSeconds: 30.0);
pickupLifetimeSeconds: 30.0,
severeThermalRiskThreshold: 0.25,
heatstrokeTemperature: 330.0,
heatstrokeMultiplier: 0.00001,
heatstrokeExponent: 2.0,
heatstrokeRecoveryPerSecond: 0.2,
hypothermiaTemperature: 273.0,
hypothermiaMultiplier: 0.00001,
hypothermiaExponent: 2.0,
hypothermiaRecoveryPerSecond: 0.2);
public AetheriaRuntimeDaemonSimulationSettings(
double pawnSpeed,
@ -68,7 +77,16 @@ namespace GameCult.Aetheria.State.Verse
double weaponLockDecayPerSecond = AetheriaDefaultRaw.WeaponLockDecayPerSecond,
double lootDropProbability = AetheriaDefaultRaw.LootDropProbability,
double lootDropVelocity = AetheriaDefaultRaw.LootDropVelocity,
double pickupLifetimeSeconds = AetheriaDefaultRaw.PickupLifetimeSeconds)
double pickupLifetimeSeconds = AetheriaDefaultRaw.PickupLifetimeSeconds,
double severeThermalRiskThreshold = AetheriaDefaultRaw.SevereThermalRiskThreshold,
double heatstrokeTemperature = AetheriaDefaultRaw.HeatstrokeTemperature,
double heatstrokeMultiplier = AetheriaDefaultRaw.HeatstrokeMultiplier,
double heatstrokeExponent = AetheriaDefaultRaw.HeatstrokeExponent,
double heatstrokeRecoveryPerSecond = AetheriaDefaultRaw.HeatstrokeRecoveryPerSecond,
double hypothermiaTemperature = AetheriaDefaultRaw.HypothermiaTemperature,
double hypothermiaMultiplier = AetheriaDefaultRaw.HypothermiaMultiplier,
double hypothermiaExponent = AetheriaDefaultRaw.HypothermiaExponent,
double hypothermiaRecoveryPerSecond = AetheriaDefaultRaw.HypothermiaRecoveryPerSecond)
{
PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed);
RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed);
@ -99,6 +117,15 @@ namespace GameCult.Aetheria.State.Verse
LootDropProbability = Clamp01Or(lootDropProbability, AetheriaDefaultRaw.LootDropProbability);
LootDropVelocity = NonNegativeOr(lootDropVelocity, AetheriaDefaultRaw.LootDropVelocity);
PickupLifetimeSeconds = PositiveOr(pickupLifetimeSeconds, AetheriaDefaultRaw.PickupLifetimeSeconds);
SevereThermalRiskThreshold = Clamp01Or(severeThermalRiskThreshold, AetheriaDefaultRaw.SevereThermalRiskThreshold);
HeatstrokeTemperature = PositiveOr(heatstrokeTemperature, AetheriaDefaultRaw.HeatstrokeTemperature);
HeatstrokeMultiplier = NonNegativeOr(heatstrokeMultiplier, AetheriaDefaultRaw.HeatstrokeMultiplier);
HeatstrokeExponent = PositiveOr(heatstrokeExponent, AetheriaDefaultRaw.HeatstrokeExponent);
HeatstrokeRecoveryPerSecond = NonNegativeOr(heatstrokeRecoveryPerSecond, AetheriaDefaultRaw.HeatstrokeRecoveryPerSecond);
HypothermiaTemperature = PositiveOr(hypothermiaTemperature, AetheriaDefaultRaw.HypothermiaTemperature);
HypothermiaMultiplier = NonNegativeOr(hypothermiaMultiplier, AetheriaDefaultRaw.HypothermiaMultiplier);
HypothermiaExponent = PositiveOr(hypothermiaExponent, AetheriaDefaultRaw.HypothermiaExponent);
HypothermiaRecoveryPerSecond = NonNegativeOr(hypothermiaRecoveryPerSecond, AetheriaDefaultRaw.HypothermiaRecoveryPerSecond);
}
public double PawnSpeed { get; }
@ -130,6 +157,15 @@ namespace GameCult.Aetheria.State.Verse
public double LootDropProbability { get; }
public double LootDropVelocity { get; }
public double PickupLifetimeSeconds { get; }
public double SevereThermalRiskThreshold { get; }
public double HeatstrokeTemperature { get; }
public double HeatstrokeMultiplier { get; }
public double HeatstrokeExponent { get; }
public double HeatstrokeRecoveryPerSecond { get; }
public double HypothermiaTemperature { get; }
public double HypothermiaMultiplier { get; }
public double HypothermiaExponent { get; }
public double HypothermiaRecoveryPerSecond { get; }
private static double PositiveOr(double value, double fallback)
{
@ -177,6 +213,15 @@ namespace GameCult.Aetheria.State.Verse
public const double LootDropProbability = 0.25;
public const double LootDropVelocity = 25.0;
public const double PickupLifetimeSeconds = 30.0;
public const double SevereThermalRiskThreshold = 0.25;
public const double HeatstrokeTemperature = 330.0;
public const double HeatstrokeMultiplier = 0.00001;
public const double HeatstrokeExponent = 2.0;
public const double HeatstrokeRecoveryPerSecond = 0.2;
public const double HypothermiaTemperature = 273.0;
public const double HypothermiaMultiplier = 0.00001;
public const double HypothermiaExponent = 2.0;
public const double HypothermiaRecoveryPerSecond = 0.2;
}
}
}

View file

@ -1049,6 +1049,9 @@ namespace GameCult.Aetheria.State.Verse
[Key(47)]
public IReadOnlyList<AetheriaRuntimeEquipmentStateCommit> EquipmentStates { get; set; } =
Array.Empty<AetheriaRuntimeEquipmentStateCommit>();
[Key(48)]
public string CauseOfDeath { get; set; } = "";
}
[MessagePackObject]

View file

@ -0,0 +1,90 @@
using System;
using System.Linq;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaRuntimeThermalMedicalSimulation
{
public static AetheriaRuntimeThermalMedicalResult Step(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeCatalogSnapshot? catalog,
double deltaSeconds,
AetheriaRuntimeDaemonSimulationSettings settings)
{
if (entity == null || catalog == null || deltaSeconds <= 0 || !entity.IsActive)
return default;
var cockpit = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Cockpit")
.OrderBy(value => value.EquipmentIndex)
.FirstOrDefault();
if (cockpit == null)
return default;
var temperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(
entity, catalog, cockpit.EquipmentIndex);
SetScalar(entity, "cockpit-temperature", temperature);
var previousHeatstroke = Clamp01(entity.Heatstroke);
var previousHypothermia = Clamp01(entity.Hypothermia);
entity.Heatstroke = temperature > settings.HeatstrokeTemperature
? Clamp01(previousHeatstroke + Math.Pow(
temperature - settings.HeatstrokeTemperature,
settings.HeatstrokeExponent) * settings.HeatstrokeMultiplier * deltaSeconds)
: Clamp01(previousHeatstroke - settings.HeatstrokeRecoveryPerSecond * deltaSeconds);
entity.Hypothermia = temperature < settings.HypothermiaTemperature
? Clamp01(previousHypothermia + Math.Pow(
settings.HypothermiaTemperature - temperature,
settings.HypothermiaExponent) * settings.HypothermiaMultiplier * deltaSeconds)
: Clamp01(previousHypothermia - settings.HypothermiaRecoveryPerSecond * deltaSeconds);
var heatstrokeRisk = Crossed(previousHeatstroke, entity.Heatstroke,
settings.SevereThermalRiskThreshold);
var hypothermiaRisk = Crossed(previousHypothermia, entity.Hypothermia,
settings.SevereThermalRiskThreshold);
var deathCause = entity.Heatstroke > 0.99 ? "heatstroke" :
entity.Hypothermia > 0.99 ? "hypothermia" : "";
return new AetheriaRuntimeThermalMedicalResult(
cockpit.EquipmentIndex, temperature, heatstrokeRisk, hypothermiaRisk, deathCause);
}
private static bool Crossed(double previous, double current, double threshold) =>
previous < threshold && current > threshold;
private static void SetScalar(AetheriaRuntimeEntitySnapshotCommit entity, string name, double value)
{
var grids = (entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>()).ToList();
var grid = grids.FirstOrDefault(candidate => string.Equals(candidate.Name, name, StringComparison.Ordinal));
if (grid == null)
{
grid = new AetheriaRuntimeEntityStatGridCommit { Name = name, Width = 1, Height = 1 };
grids.Add(grid);
}
grid.Values = new[] { value };
entity.StatGrids = grids;
}
private static double Clamp01(double value) => value < 0 ? 0 : value > 1 ? 1 : value;
}
public readonly struct AetheriaRuntimeThermalMedicalResult
{
public AetheriaRuntimeThermalMedicalResult(int cockpitEquipmentIndex, double cockpitTemperature,
bool heatstrokeRiskCrossed, bool hypothermiaRiskCrossed, string deathCause)
{
CockpitEquipmentIndex = cockpitEquipmentIndex;
CockpitTemperature = cockpitTemperature;
HeatstrokeRiskCrossed = heatstrokeRiskCrossed;
HypothermiaRiskCrossed = hypothermiaRiskCrossed;
DeathCause = deathCause ?? "";
}
public int CockpitEquipmentIndex { get; }
public double CockpitTemperature { get; }
public bool HeatstrokeRiskCrossed { get; }
public bool HypothermiaRiskCrossed { get; }
public string DeathCause { get; }
public bool Died => !string.IsNullOrWhiteSpace(DeathCause);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 741e91657d8b412ebf96032d567ecfc7

View file

@ -1942,9 +1942,13 @@ First Aetheria surfaces to publish:
equipment-local temperature/performance, shutdown, wear, conduction, and
radiation. The daemon orders radiator pumping before reactor settlement and
thermal conduction; client operations have no direct energy, heat, wear, or
equipment-online writer. The Eve game surface publishes the resulting power,
radiator, temperature, wear, and offline-equipment facts. Cockpit medical
consequences, rotated equipment footprints, and parent/child radiation remain
equipment-online writer. `AetheriaRuntimeThermalMedicalSimulation` derives
cockpit exposure, recovery, severe-risk crossings, and thermal death from the
cockpit's canonical cells; all death causes use the same destruction commit.
The Eve game surface publishes the resulting power, radiator, temperature,
wear, offline-equipment, cockpit, risk, render-weight, and typed death facts.
Rotated equipment footprints, parent/child radiation, schematic composition,
native thermal post effects, and the cause-specific death fade remain
migration work rather than presentation authority.
- `dotnet list package --vulnerable --include-transitive` is clean for active
maintained projects.

View file

@ -23,7 +23,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; daemon weapon lock now acquires and decays from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock | target-switch reset, angular decay, visibility/nearest/explicit cases, and runtime lock feedback |
| Projectile flight | weapon/projectile classes | Eve presentation for ordinary tracers; Ymir for persistent physical payloads | receipt trajectory/effect facts or persistent payload state | native projectile visual | ordinary instant, charged, and constant execution creates no projectile body; each weapon state owns a stable shot sequence and receipts carry origin, endpoint, nominal duration, and effect identity for Eve presentation; `SpawnProjectile` and its alternate writer are deleted; Ymir projectile contacts are feedback-only for explicitly persisted payload rows and cannot write damage; Ymir.Core remains embedded for world integration, radial fields, circle contacts, pickups, and genuinely persistent/interceptable payloads | split/rename the remaining physical-payload interface and remove ordinary projectile terminology from runtime contracts |
| Shield/armor/equipment/hull damage | energy-funded shield, schematic armor cells, equipment and scalar hull | daemon canonical damage transaction | aggregate/exact armor grid, entity status, and layer-aware shot/damage receipts | schematic, meters, impacts, damage VFX | owner-exists; wired; exclusive; projected: daemon initializes armor/maximum-armor grids from hull plus hardpoint catalog topology; direct and deployable damage share one `shield -> armor cell -> equipment -> scalar hull` transaction; direct impacts select a cell or complete hardpoint footprint before orthogonal spread and 0.5-cell penetration, while splash payloads select the source-facing hull half; the fossil penetration infinite-loop defect is fixed; damage type is typed/pass-through without resistances because the fossil never applied them; Eve exposes aggregate and exact armor state and receipts identify reached/applied layers; fake Ymir contacts and presentation cannot decide damage | fossil parity fixtures for topology, edge impacts, orthogonal spread, multi-cell penetration, equipment interception, and generic shield/equipment effect lifecycle |
| Thermal/energy | cell conduction and capacitor/reactor/radiator network | daemon | ship/equipment state, power/temperature facts and warnings | schematic/post effects | owner-exists; wired; exclusive; projected: catalog hull and installed-item shapes derive cell mass/conductivity/topology; local temperature derives thermal performance, wear potential and online state; all consumers share capacitor-first/reactor-residual transactions; powered radiators pump before reactor settlement; reactor heat then enters the same cell network before conduction/radiation; Eve exposes capacitor, reactor, radiator, temperature, wear and offline-equipment facts; cockpit medical thresholds, rotated equipment topology and parent/child radiation remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, cockpit injury/death timelines and generic schematic/post-effect lowering |
| Thermal/energy | cell conduction, capacitor/reactor/radiator network, and cockpit thermal exposure | daemon | ship/equipment power, temperature, medical state, risk and death feedback | schematic/post effects | owner-exists; wired; exclusive; projected: catalog hull and installed-item shapes derive cell mass/conductivity/topology; local temperature derives thermal performance, wear potential and online state; all consumers share capacitor-first/reactor-residual transactions; powered radiators pump before reactor settlement; reactor heat then enters the same cell network before conduction/radiation; cockpit-cell temperature drives the fossil nonlinear heatstroke/hypothermia accumulation, linear recovery, severe-risk crossings, and ordinary exactly-once destruction with a typed cause; Eve exposes power, temperature, wear, offline-equipment, cockpit, risk, heatstroke render-weight, and death-cause facts; rotated equipment topology, parent/child radiation, schematic composition and native post/death lowering remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, reconnect across risk/death, generic schematic meters, heatstroke phasing and cause-specific death fade |
| Weapons | ServerShared behaviors + Unity effects | daemon plus Ymir geometry/kinematics | action capability, weapon state, shot/damage receipts | effects/audio/HUD | instant and constant resource/timing lifecycles are daemon-owned; charged weapons accept one semantic request, spool without requiring a solution, hold at full charge, commit automatically when a solution appears, and perform deterministic once-per-second malfunction hazard checks after the authored safe hold interval with increasing persisted risk; generic Eve `weapon.state` exposes charge hold/risk and feedback exposes readiness, commit, refusal, and malfunction chronology; committed direct damage now enters the shared daemon armor-cell transaction | guided persistent payloads, visibility/wear, remaining impact-distribution parity, and Unity effect reconciliation |
| Destruction/drops | hull death, 25% equipment rolls, unconditional cargo drops, 25-unit launch, 30-second pickups | daemon | destruction identity, death/drop events, typed pickups | provider-advertised destruction effect and pickups | owner-exists; wired; exclusive; projected: direct and deployable lethal damage commit one deterministic destruction transaction, clear live references, retain only a non-rendered identity tombstone, and emit exact pickups/events; the reserved client `DestroyEntity` command is rejected; Aetheria advertises its original big explosion as `effect.feedback.entity.destroyed`, lowered generically by EveUnity | equipment-destruction-before-hull parity and multiplayer reconnect chronology |
| Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; shield contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; 30s daemon lifetime; instant cargo teleport deleted | prove generic runtime beam, contact, success and rejection feedback |

View file

@ -97,6 +97,22 @@ Death unbinds gameplay input, presents cause, and transitions to appropriate
menu/restart flow. The client cannot clear or save authoritative world state as
a side effect of playing the animation.
The fossil's live heatstroke presentation uses two volume weights. Base weight
is `saturate(heatstroke / severeRiskThreshold)`. Severe weight is the normalized
amount above that threshold, with an authored sinusoidal pulse applied by the
lowerer. Heatstroke and hypothermia each have schematic exposure meters, while
cockpit temperature drives the shared thermal-limit meter. The scene contains
hypothermia volumes, but the inspected live update never drove them before
death; do not invent continuous hypothermia post processing as historical
parity. Cause-specific death fades cross the applicable thermal volumes into
the death volume over one authored second.
The current fossil settings still declare heatstroke and hypothermia control
limits, but the implemented heatstroke cockpit lockout was deliberately removed
and no hypothermia lockout was connected. Thermal exposure therefore does not
silently attenuate or reject semantic controls. The daemon owns medical state
and death; the runtime owns only meter, volume, and transition lowering.
## Effects And Audio
Weapon and impact effects consume semantic event facts and provider asset refs.

View file

@ -42,6 +42,15 @@ performance and online state derive from local temperature.
Cockpit temperature advances heatstroke/hypothermia accumulation and recovery.
Threshold crossings and death are authoritative events.
Above 330 K, heatstroke advances by `(temperature - 330)^2 * 0.00001 * dt`;
below the threshold it recovers by `0.2 * dt`. Below 273 K, hypothermia advances
by `(273 - temperature)^2 * 0.00001 * dt`; above the threshold it also recovers
by `0.2 * dt`. Values saturate to `[0,1]`, severe risk crosses at `0.25`, and a
value above `0.99` kills through the ordinary entity death path. The fossil's
hypothermia risk comparison accidentally read heatstroke; the daemon implements
the intended symmetric threshold crossing while retaining the exact exposure
and death formulas.
The daemon derives the grid from the equipped catalog, rather than accepting a
renderer-authored heat scalar. Hull mass and specific heat are distributed over
the hull shape; installed equipment contributes thermal mass and replaces the