Restore daemon cockpit thermal medicine
This commit is contained in:
parent
7fdfdff99a
commit
b50c8a9053
12 changed files with 361 additions and 11 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 741e91657d8b412ebf96032d567ecfc7
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue