Splits temperature damage processing into its own component (#30515)

* Creates TemperatureDamageThresholdsComponent

* Obsolete TemperatureComponent fields

* Use TemperatureDamageThresholdsComponent
Moves all the uses of the relocated TemperatureComponent fields to use the TDTC versions

* Removes the obsolete TemperatureComponent fields

* Update YAML definitions

* Update doc comments

* Split TemperatureSystem
Creates TemperatureDamageSystem and moves the damage handling from TemperatureSystem

* Cull unused using statements

* Use component-based damage tick scheduling

* Fix temperature damage processing
Check was inverted resulting in things never starting to take temperature damage

* Poke tests

* Add TemperatureDamageThresholds to new prototypes

* Move TemperatureDamageThresholdsComponent to Shared
Parity with TemperatureComponent

* While I'm here
Fixes warning regarding obsolete ProtoId validator attribute

* Fix YAML errors

* Fix merge errors

* Rename TemperatureDamageThresholdsComponent -> TemperatureDamageComponent

* Use ContentHelpers.RoundToLevels for temperature alerts

* Fix YML

* A fuckton of cleanup

* working cleanup

* fix

* misc additions

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
TemporalOroboros
2025-12-23 22:37:11 -08:00
committed by GitHub
parent c6a4d3f7d8
commit 6f38eed9d9
27 changed files with 461 additions and 382 deletions

View File

@@ -0,0 +1,11 @@
namespace Content.Server.Temperature.Components;
[RegisterComponent]
public sealed partial class ContainerTemperatureComponent : Component
{
[DataField]
public float? HeatDamageThreshold;
[DataField]
public float? ColdDamageThreshold;
}

View File

@@ -1,11 +0,0 @@
namespace Content.Server.Temperature.Components;
[RegisterComponent]
public sealed partial class ContainerTemperatureDamageThresholdsComponent: Component
{
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? HeatDamageThreshold;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? ColdDamageThreshold;
}

View File

@@ -0,0 +1,279 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Alert;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Rounding;
using Content.Shared.Temperature;
using Content.Shared.Temperature.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Temperature.Systems;
/// <summary>
/// Handles entities taking damage from being too hot or too cold.
/// Also handles alerts relevant to the same.
/// </summary>
public sealed partial class TemperatureSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private EntityQuery<TemperatureDamageComponent> _tempDamageQuery;
private EntityQuery<ContainerTemperatureComponent> _containerTemperatureQuery;
private EntityQuery<ThermalRegulatorComponent> _thermalRegulatorQuery;
/// <summary>
/// All the components that will have their damage updated at the end of the tick.
/// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
/// that we need some mechanism to ensure it doesn't double-dip on damage for both calls.
/// </summary>
public HashSet<Entity<TemperatureDamageComponent>> ShouldUpdateDamage = new();
/// <summary>
/// Alert prototype for Temperature.
/// </summary>
public static readonly ProtoId<AlertCategoryPrototype> TemperatureAlertCategory = "Temperature";
/// <summary>
/// The maximum severity applicable to temperature alerts.
/// </summary>
public static readonly short MaxTemperatureAlertSeverity = 3;
/// <summary>
/// On a scale of 0. to 1. where 0. is the ideal temperature and 1. is a temperature damage threshold this is the point where the component starts raising temperature alerts.
/// </summary>
public static readonly float MinAlertTemperatureScale = 0.33f;
private void InitializeDamage()
{
SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
SubscribeLocalEvent<TemperatureDamageComponent, OnTemperatureChangeEvent>(EnqueueDamage);
SubscribeLocalEvent<TemperatureDamageComponent, EntityUnpausedEvent>(OnUnpaused);
// Allows overriding thresholds based on the parent's thresholds.
SubscribeLocalEvent<TemperatureDamageComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ContainerTemperatureComponent, ComponentStartup>(OnParentThresholdStartup);
SubscribeLocalEvent<ContainerTemperatureComponent, ComponentShutdown>(OnParentThresholdShutdown);
_tempDamageQuery = GetEntityQuery<TemperatureDamageComponent>();
_containerTemperatureQuery = GetEntityQuery<ContainerTemperatureComponent>();
_thermalRegulatorQuery = GetEntityQuery<ThermalRegulatorComponent>();
}
private void UpdateDamage()
{
foreach (var entity in ShouldUpdateDamage)
{
if (Deleted(entity) || Paused(entity))
continue;
var deltaTime = _gameTiming.CurTime - entity.Comp.LastUpdate;
if (entity.Comp.TakingDamage && deltaTime < entity.Comp.UpdateInterval)
continue;
ChangeDamage(entity, deltaTime);
}
ShouldUpdateDamage.Clear();
}
private void ChangeDamage(Entity<TemperatureDamageComponent> entity, TimeSpan deltaTime)
{
entity.Comp.LastUpdate = _gameTiming.CurTime;
if (!HasComp<DamageableComponent>(entity) || !TemperatureQuery.TryComp(entity, out var temperature))
return;
// See this link for where the scaling func comes from:
// https://www.desmos.com/calculator/0vknqtdvq9
// Based on a logistic curve, which caps out at MaxDamage
var heatK = 0.005;
var a = 1;
var y = entity.Comp.DamageCap;
var c = y * 2;
var heatDamageThreshold = entity.Comp.ParentHeatDamageThreshold ?? entity.Comp.HeatDamageThreshold;
var coldDamageThreshold = entity.Comp.ParentColdDamageThreshold ?? entity.Comp.ColdDamageThreshold;
if (temperature.CurrentTemperature >= heatDamageThreshold)
{
if (!entity.Comp.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking high temperature damage");
entity.Comp.TakingDamage = true;
}
var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
_damageable.TryChangeDamage(entity.Owner, entity.Comp.HeatDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false);
}
else if (temperature.CurrentTemperature <= coldDamageThreshold)
{
if (!entity.Comp.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} started taking low temperature damage");
entity.Comp.TakingDamage = true;
}
var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
var tempDamage =
Math.Sqrt(diff * (Math.Pow(entity.Comp.DamageCap.Double(), 2) / coldDamageThreshold));
_damageable.TryChangeDamage(entity.Owner, entity.Comp.ColdDamage * tempDamage * deltaTime.TotalSeconds, ignoreResistances: true, interruptsDoAfters: false);
}
else if (entity.Comp.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(entity):entity} stopped taking temperature damage");
entity.Comp.TakingDamage = false;
}
}
private void ServerAlert(Entity<AlertsComponent> entity, ref OnTemperatureChangeEvent args)
{
ProtoId<AlertPrototype> type;
float threshold;
float idealTemp;
if (!_tempDamageQuery.TryComp(entity, out var thresholds))
{
_alerts.ClearAlertCategory(entity.Owner, TemperatureAlertCategory);
return;
}
if (_thermalRegulatorQuery.TryComp(entity, out var regulator) &&
regulator.NormalBodyTemperature > thresholds.ColdDamageThreshold &&
regulator.NormalBodyTemperature < thresholds.HeatDamageThreshold)
{
idealTemp = regulator.NormalBodyTemperature;
}
else
{
idealTemp = (thresholds.ColdDamageThreshold + thresholds.HeatDamageThreshold) / 2;
}
if (args.CurrentTemperature <= idealTemp)
{
type = thresholds.ColdAlert;
threshold = thresholds.ColdDamageThreshold;
}
else
{
type = thresholds.HotAlert;
threshold = thresholds.HeatDamageThreshold;
}
// Calculates a scale where 0.0 is the ideal temperature and 1.0 is where temperature damage begins
// The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
var tempScale = (args.CurrentTemperature - idealTemp) / (threshold - idealTemp);
var alertLevel = (short)ContentHelpers.RoundToLevels(tempScale - MinAlertTemperatureScale, 1.00f - MinAlertTemperatureScale, MaxTemperatureAlertSeverity + 1);
if (alertLevel > 0)
_alerts.ShowAlert(entity.AsNullable(), type, alertLevel);
else
_alerts.ClearAlertCategory(entity.AsNullable(), TemperatureAlertCategory);
}
private void EnqueueDamage(Entity<TemperatureDamageComponent> ent, ref OnTemperatureChangeEvent args)
{
if (ShouldUpdateDamage.Add(ent) && !ent.Comp.TakingDamage)
ent.Comp.LastUpdate = _gameTiming.CurTime;
}
private void OnUnpaused(Entity<TemperatureDamageComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.LastUpdate += args.PausedTime;
}
private void OnParentChange(Entity<TemperatureDamageComponent> entity, ref EntParentChangedMessage args)
{
// We only need to update thresholds if the thresholds changed for the entity's ancestors.
var oldThresholds = args.OldParent != null
? RecalculateParentThresholds(args.OldParent.Value)
: (null, null);
var xform = Transform(entity.Owner);
var newThresholds = RecalculateParentThresholds(xform.ParentUid);
if (oldThresholds != newThresholds)
RecursiveThresholdUpdate((entity, entity.Comp, xform));
}
private void OnParentThresholdStartup(Entity<ContainerTemperatureComponent> entity, ref ComponentStartup args)
{
RecursiveThresholdUpdate(entity.Owner);
}
private void OnParentThresholdShutdown(Entity<ContainerTemperatureComponent> entity, ref ComponentShutdown args)
{
RecursiveThresholdUpdate(entity.Owner);
}
/// <summary>
/// Recalculate and apply parent thresholds for the root entity and all its children.
/// </summary>
/// <param name="root">The root entity we're currently updating</param>
private void RecursiveThresholdUpdate(Entity<TemperatureDamageComponent?, TransformComponent?> root)
{
RecalculateAndApplyParentThresholds(root);
var xform = root.Comp2 ?? Transform(root);
var enumerator = xform.ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
RecursiveThresholdUpdate(child);
}
}
/// <summary>
/// Recalculate parent thresholds and apply them on the uid temperature component.
/// </summary>
/// <param name="entity">The entity whose temperature damage thresholds we're updating</param>
private void RecalculateAndApplyParentThresholds(Entity<TemperatureDamageComponent?> entity)
{
if (!_tempDamageQuery.Resolve(entity, ref entity.Comp, logMissing: false))
return;
var newThresholds = RecalculateParentThresholds(Transform(entity).ParentUid);
entity.Comp.ParentHeatDamageThreshold = newThresholds.Item1;
entity.Comp.ParentColdDamageThreshold = newThresholds.Item2;
}
/// <summary>
/// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
/// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
/// </summary>
/// <param name="initialParentUid">parent we start with</param>
private (float?, float?) RecalculateParentThresholds(EntityUid initialParentUid)
{
// Recursively check parents for the best threshold available
var parentUid = initialParentUid;
float? newHeatThreshold = null;
float? newColdThreshold = null;
while (parentUid.IsValid())
{
if (_containerTemperatureQuery.TryComp(parentUid, out var newThresholds))
{
if (newThresholds.HeatDamageThreshold != null)
{
newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
newHeatThreshold ?? 0);
}
if (newThresholds.ColdDamageThreshold != null)
{
newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
newColdThreshold ?? float.MaxValue);
}
}
parentUid = Transform(parentUid).ParentUid;
}
return (newHeatThreshold, newColdThreshold);
}
}

View File

@@ -1,64 +1,32 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Inventory;
using Content.Shared.Rejuvenate;
using Content.Shared.Temperature;
using Robust.Shared.Prototypes;
using Content.Shared.Projectiles;
using Content.Shared.Temperature.Components;
using Content.Shared.Temperature.Systems;
namespace Content.Server.Temperature.Systems;
public sealed class TemperatureSystem : SharedTemperatureSystem
public sealed partial class TemperatureSystem : SharedTemperatureSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
/// <summary>
/// All the components that will have their damage updated at the end of the tick.
/// This is done because both AtmosExposed and Flammable call ChangeHeat in the same tick, meaning
/// that we need some mechanism to ensure it doesn't double dip on damage for both calls.
/// </summary>
public HashSet<Entity<TemperatureComponent>> ShouldUpdateDamage = new();
public float UpdateInterval = 1.0f;
private float _accumulatedFrametime;
public static readonly ProtoId<AlertCategoryPrototype> TemperatureAlertCategory = "Temperature";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
SubscribeLocalEvent<TemperatureComponent, AtmosExposedUpdateEvent>(OnAtmosExposedUpdate);
SubscribeLocalEvent<TemperatureComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
Subs.SubscribeWithRelay<TemperatureProtectionComponent, ModifyChangedTemperatureEvent>(OnTemperatureChangeAttempt, held: false);
SubscribeLocalEvent<InternalTemperatureComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ChangeTemperatureOnCollideComponent, ProjectileHitEvent>(ChangeTemperatureOnCollide);
// Allows overriding thresholds based on the parent's thresholds.
SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
OnParentThresholdStartup);
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentShutdown>(
OnParentThresholdShutdown);
InitializeDamage();
}
public override void Update(float frameTime)
@@ -88,49 +56,23 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
ForceChangeTemperature(uid, temp.CurrentTemperature - degrees, temp);
}
UpdateDamage(frameTime);
}
private void UpdateDamage(float frameTime)
{
_accumulatedFrametime += frameTime;
if (_accumulatedFrametime < UpdateInterval)
return;
_accumulatedFrametime -= UpdateInterval;
if (!ShouldUpdateDamage.Any())
return;
foreach (var comp in ShouldUpdateDamage)
{
MetaDataComponent? metaData = null;
var uid = comp.Owner;
if (Deleted(uid, metaData) || Paused(uid, metaData))
continue;
ChangeDamage(uid, comp);
}
ShouldUpdateDamage.Clear();
UpdateDamage();
}
public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null)
{
if (!Resolve(uid, ref temperature))
if (!TemperatureQuery.Resolve(uid, ref temperature))
return;
float lastTemp = temperature.CurrentTemperature;
float delta = temperature.CurrentTemperature - temp;
var lastTemp = temperature.CurrentTemperature;
var delta = temperature.CurrentTemperature - temp;
temperature.CurrentTemperature = temp;
RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta),
true);
RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true);
}
public override void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null)
{
if (!Resolve(uid, ref temperature, false))
if (!TemperatureQuery.Resolve(uid, ref temperature, false))
return;
if (!ignoreHeatResistance)
@@ -144,11 +86,10 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature);
float delta = temperature.CurrentTemperature - lastTemp;
RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true);
RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), broadcast: true);
}
private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature,
ref AtmosExposedUpdateEvent args)
private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, ref AtmosExposedUpdateEvent args)
{
var transform = args.Transform;
@@ -158,17 +99,18 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature;
var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false);
var heatCapacity = GetHeatCapacity(uid, temperature);
// TODO ATMOS: This heat transfer formula is really really wrong, it needs to be pulled out. Pending on HeatContainers.
var heat = temperatureDelta * (airHeatCapacity * heatCapacity /
(airHeatCapacity + heatCapacity));
ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
}
private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
private void OnInit(Entity<InternalTemperatureComponent> entity, ref MapInitEvent args)
{
if (!TryComp<TemperatureComponent>(uid, out var temp))
if (!TemperatureQuery.TryComp(entity, out var temp))
return;
comp.Temperature = temp.CurrentTemperature;
entity.Comp.Temperature = temp.CurrentTemperature;
}
private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args)
@@ -176,116 +118,6 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
ForceChangeTemperature(uid, Atmospherics.T20C, comp);
}
private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
{
ProtoId<AlertPrototype> type;
float threshold;
float idealTemp;
if (!TryComp<TemperatureComponent>(uid, out var temperature))
{
_alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
return;
}
if (TryComp<ThermalRegulatorComponent>(uid, out var regulator) &&
regulator.NormalBodyTemperature > temperature.ColdDamageThreshold &&
regulator.NormalBodyTemperature < temperature.HeatDamageThreshold)
{
idealTemp = regulator.NormalBodyTemperature;
}
else
{
idealTemp = (temperature.ColdDamageThreshold + temperature.HeatDamageThreshold) / 2;
}
if (args.CurrentTemperature <= idealTemp)
{
type = temperature.ColdAlert;
threshold = temperature.ColdDamageThreshold;
}
else
{
type = temperature.HotAlert;
threshold = temperature.HeatDamageThreshold;
}
// Calculates a scale where 1.0 is the ideal temperature and 0.0 is where temperature damage begins
// The cold and hot scales will differ in their range if the ideal temperature is not exactly halfway between the thresholds
var tempScale = (args.CurrentTemperature - threshold) / (idealTemp - threshold);
switch (tempScale)
{
case <= 0f:
_alerts.ShowAlert(uid, type, 3);
break;
case <= 0.4f:
_alerts.ShowAlert(uid, type, 2);
break;
case <= 0.66f:
_alerts.ShowAlert(uid, type, 1);
break;
case > 0.66f:
_alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
break;
}
}
private void EnqueueDamage(Entity<TemperatureComponent> temperature, ref OnTemperatureChangeEvent args)
{
ShouldUpdateDamage.Add(temperature);
}
private void ChangeDamage(EntityUid uid, TemperatureComponent temperature)
{
if (!HasComp<DamageableComponent>(uid))
return;
// See this link for where the scaling func comes from:
// https://www.desmos.com/calculator/0vknqtdvq9
// Based on a logistic curve, which caps out at MaxDamage
var heatK = 0.005;
var a = 1;
var y = temperature.DamageCap;
var c = y * 2;
var heatDamageThreshold = temperature.ParentHeatDamageThreshold ?? temperature.HeatDamageThreshold;
var coldDamageThreshold = temperature.ParentColdDamageThreshold ?? temperature.ColdDamageThreshold;
if (temperature.CurrentTemperature >= heatDamageThreshold)
{
if (!temperature.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking high temperature damage");
temperature.TakingDamage = true;
}
var diff = Math.Abs(temperature.CurrentTemperature - heatDamageThreshold);
var tempDamage = c / (1 + a * Math.Pow(Math.E, -heatK * diff)) - y;
_damageable.TryChangeDamage(uid, temperature.HeatDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
}
else if (temperature.CurrentTemperature <= coldDamageThreshold)
{
if (!temperature.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} started taking low temperature damage");
temperature.TakingDamage = true;
}
var diff = Math.Abs(temperature.CurrentTemperature - coldDamageThreshold);
var tempDamage =
Math.Sqrt(diff * (Math.Pow(temperature.DamageCap.Double(), 2) / coldDamageThreshold));
_damageable.TryChangeDamage(uid, temperature.ColdDamage * tempDamage, ignoreResistances: true, interruptsDoAfters: false);
}
else if (temperature.TakingDamage)
{
_adminLogger.Add(LogType.Temperature, $"{ToPrettyString(uid):entity} stopped taking temperature damage");
temperature.TakingDamage = false;
}
}
private void OnTemperatureChangeAttempt(EntityUid uid, TemperatureProtectionComponent component, ModifyChangedTemperatureEvent args)
{
var coefficient = args.TemperatureDelta < 0
@@ -300,118 +132,6 @@ public sealed class TemperatureSystem : SharedTemperatureSystem
private void ChangeTemperatureOnCollide(Entity<ChangeTemperatureOnCollideComponent> ent, ref ProjectileHitEvent args)
{
_temperature.ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
private void OnParentChange(EntityUid uid, TemperatureComponent component,
ref EntParentChangedMessage args)
{
var temperatureQuery = GetEntityQuery<TemperatureComponent>();
var transformQuery = GetEntityQuery<TransformComponent>();
var thresholdsQuery = GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>();
// We only need to update thresholds if the thresholds changed for the entity's ancestors.
var oldThresholds = args.OldParent != null
? RecalculateParentThresholds(args.OldParent.Value, transformQuery, thresholdsQuery)
: (null, null);
var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, thresholdsQuery);
if (oldThresholds != newThresholds)
{
RecursiveThresholdUpdate(uid, temperatureQuery, transformQuery, thresholdsQuery);
}
}
private void OnParentThresholdStartup(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
ComponentStartup args)
{
RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
}
private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component,
ComponentShutdown args)
{
RecursiveThresholdUpdate(uid, GetEntityQuery<TemperatureComponent>(), GetEntityQuery<TransformComponent>(),
GetEntityQuery<ContainerTemperatureDamageThresholdsComponent>());
}
/// <summary>
/// Recalculate and apply parent thresholds for the root entity and all its descendant.
/// </summary>
/// <param name="root"></param>
/// <param name="temperatureQuery"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private void RecursiveThresholdUpdate(EntityUid root, EntityQuery<TemperatureComponent> temperatureQuery,
EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
{
RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery);
var enumerator = Transform(root).ChildEnumerator;
while (enumerator.MoveNext(out var child))
{
RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery);
}
}
/// <summary>
/// Recalculate parent thresholds and apply them on the uid temperature component.
/// </summary>
/// <param name="uid"></param>
/// <param name="temperatureQuery"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private void RecalculateAndApplyParentThresholds(EntityUid uid,
EntityQuery<TemperatureComponent> temperatureQuery, EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
{
if (!temperatureQuery.TryGetComponent(uid, out var temperature))
{
return;
}
var newThresholds = RecalculateParentThresholds(transformQuery.GetComponent(uid).ParentUid, transformQuery, tempThresholdsQuery);
temperature.ParentHeatDamageThreshold = newThresholds.Item1;
temperature.ParentColdDamageThreshold = newThresholds.Item2;
}
/// <summary>
/// Recalculate Parent Heat/Cold DamageThreshold by recursively checking each ancestor and fetching the
/// maximum HeatDamageThreshold and the minimum ColdDamageThreshold if any exists (aka the best value for each).
/// </summary>
/// <param name="initialParentUid"></param>
/// <param name="transformQuery"></param>
/// <param name="tempThresholdsQuery"></param>
private (float?, float?) RecalculateParentThresholds(
EntityUid initialParentUid,
EntityQuery<TransformComponent> transformQuery,
EntityQuery<ContainerTemperatureDamageThresholdsComponent> tempThresholdsQuery)
{
// Recursively check parents for the best threshold available
var parentUid = initialParentUid;
float? newHeatThreshold = null;
float? newColdThreshold = null;
while (parentUid.IsValid())
{
if (tempThresholdsQuery.TryGetComponent(parentUid, out var newThresholds))
{
if (newThresholds.HeatDamageThreshold != null)
{
newHeatThreshold = Math.Max(newThresholds.HeatDamageThreshold.Value,
newHeatThreshold ?? 0);
}
if (newThresholds.ColdDamageThreshold != null)
{
newColdThreshold = Math.Min(newThresholds.ColdDamageThreshold.Value,
newColdThreshold ?? float.MaxValue);
}
}
parentUid = transformQuery.GetComponent(parentUid).ParentUid;
}
return (newHeatThreshold, newColdThreshold);
ChangeHeat(args.Target, ent.Comp.Heat, ent.Comp.IgnoreHeatResistance);// adjust the temperature
}
}

View File

@@ -242,7 +242,7 @@ public sealed partial class ZombieSystem
_mind.MakeSentient(target);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))
if (TryComp<TemperatureDamageComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0);
//Heals the zombie from all the damage it took while human

View File

@@ -1,15 +1,10 @@
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.Temperature.Components;
/// <summary>
/// Handles changing temperature,
/// informing others of the current temperature,
/// and taking fire damage from high temperature.
/// informing others of the current temperature.
/// </summary>
[RegisterComponent]
public sealed partial class TemperatureComponent : Component
@@ -20,24 +15,6 @@ public sealed partial class TemperatureComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float CurrentTemperature = Atmospherics.T20C;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float HeatDamageThreshold = 360f;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float ColdDamageThreshold = 260f;
/// <summary>
/// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? ParentHeatDamageThreshold;
/// <summary>
/// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? ParentColdDamageThreshold;
/// <summary>
/// Heat capacity per kg of mass.
/// </summary>
@@ -49,31 +26,4 @@ public sealed partial class TemperatureComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float AtmosTemperatureTransferEfficiency = 0.1f;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier ColdDamage = new();
[DataField, ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier HeatDamage = new();
/// <summary>
/// Temperature won't do more than this amount of damage per second.
/// </summary>
/// <remarks>
/// Okay it genuinely reaches this basically immediately for a plasma fire.
/// </remarks>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 DamageCap = FixedPoint2.New(8);
/// <summary>
/// Used to keep track of when damage starts/stops. Useful for logs.
/// </summary>
[DataField]
public bool TakingDamage;
[DataField]
public ProtoId<AlertPrototype> HotAlert = "Hot";
[DataField]
public ProtoId<AlertPrototype> ColdAlert = "Cold";
}

View File

@@ -0,0 +1,94 @@
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Temperature.Components;
/// <summary>
/// Handles taking damage from being excessively hot/cold.
/// Also handles alerts about being too hot or too cold.
/// </summary>
[RegisterComponent]
public sealed partial class TemperatureDamageComponent : Component
{
/// <summary>
/// The temperature above which the entity will start taking damage from being too hot.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float HeatDamageThreshold = 360f;
/// <summary>
/// The temperature below which the entity will start taking damage from being too cold.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float ColdDamageThreshold = 260f;
/// <summary>
/// Overrides HeatDamageThreshold if the entity's within a parent with the ContainerTemperatureDamageThresholdsComponent component.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? ParentHeatDamageThreshold;
/// <summary>
/// Overrides ColdDamageThreshold if the entity's within a parent with the ContainerTemperatureDamageThresholdsComponent component.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float? ParentColdDamageThreshold;
/// <summary>
/// The base damage that this entity will take if it's too cold.
/// Will be scaled according to how cold it is.
/// The scaling maxes out at <see cref="DamageCap"/> times this damage per second.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier ColdDamage = new();
/// <summary>
/// The base damage that this entity will take per second if it's too hot.
/// Will be scaled according to how hot it is.
/// The scaling maxes out at <see cref="DamageCap"/> times this damage per second.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier HeatDamage = new();
/// <summary>
/// Temperature won't do more than this multiple of the base overheating/overcooling damage per seond.
/// </summary>
/// <remarks>
/// Okay it genuinely reaches this basically immediately for a plasma fire.
/// </remarks>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 DamageCap = FixedPoint2.New(8);
/// <summary>
/// Used to keep track of when damage starts/stops. Useful for logs.
/// </summary>
[DataField]
public bool TakingDamage;
/// <summary>
/// The id of the alert thrown when the entity is too hot.
/// </summary>
[DataField]
public ProtoId<AlertPrototype> HotAlert = "Hot";
/// <summary>
/// The id of the alert thrown when the entity is too cold.
/// </summary>
[DataField]
public ProtoId<AlertPrototype> ColdAlert = "Cold";
/// <summary>
/// The last time this entity processed temperature damage.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastUpdate;
/// <summary>
/// The time interval between temperature damage ticks for this entity.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1.0);
}

View File

@@ -16,6 +16,8 @@ public abstract class SharedTemperatureSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
protected EntityQuery<TemperatureComponent> TemperatureQuery;
/// <summary>
/// Band-aid for unpredicted atmos. Delays the application for a short period so that laggy clients can get the replicated temperature.
/// </summary>
@@ -27,6 +29,8 @@ public abstract class SharedTemperatureSystem : EntitySystem
SubscribeLocalEvent<TemperatureSpeedComponent, OnTemperatureChangeEvent>(OnTemperatureChanged);
SubscribeLocalEvent<TemperatureSpeedComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
TemperatureQuery = GetEntityQuery<TemperatureComponent>();
}
private void OnTemperatureChanged(Entity<TemperatureSpeedComponent> ent, ref OnTemperatureChangeEvent args)
@@ -87,7 +91,7 @@ public abstract class SharedTemperatureSystem : EntitySystem
public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
if (!TemperatureQuery.Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
{
return Atmospherics.MinimumHeatCapacity;
}

View File

@@ -1610,10 +1610,11 @@
Slash: 5
Piercing: 4
- type: Temperature
heatDamageThreshold: 360
coldDamageThreshold: 285
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 360
coldDamageThreshold: 285
- type: Sprite
drawdepth: Mobs
layers:
@@ -2368,10 +2369,11 @@
factions:
- Passive
- type: Temperature
heatDamageThreshold: 335
coldDamageThreshold: 230
currentTemperature: 310.15
specificHeat: 46
- type: TemperatureDamage
heatDamageThreshold: 335
coldDamageThreshold: 230
coldDamage:
types:
Cold : 0.05 #per second, scales with temperature & other constants
@@ -3055,13 +3057,14 @@
damageContainer: BiologicalMetaphysical
damageModifierSet: Infernal
- type: Temperature
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 4000 #They come from hell, so..
coldDamageThreshold: 260
currentTemperature: 310.15
coldDamage:
types:
Cold : 1 #per second, scales with temperature & other constants
specificHeat: 42
heatDamage:
types:
Heat : 1 #per second, scales with temperature & other constants
@@ -3256,6 +3259,7 @@
Dead:
Base: spacecat_dead
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 423
coldDamageThreshold: 0
- type: Tag

View File

@@ -185,6 +185,7 @@
damage:
types: {}
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 1200
- type: entity
id: MobCarpDungeon

View File

@@ -833,6 +833,7 @@
Bloodloss:
-0.8
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 800
coldDamageThreshold: 0
- type: MeleeWeapon

View File

@@ -36,10 +36,11 @@
60: Critical
125: Dead
- type: Temperature
heatDamageThreshold: 360
coldDamageThreshold: 285
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 360
coldDamageThreshold: 285
# Good eatin', you monster.
- type: Butcherable
butcheringType: Spike

View File

@@ -45,6 +45,7 @@
Quantity: 300
- type: CombatMode
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 500
coldDamageThreshold: 0
- type: MeleeWeapon
@@ -512,6 +513,7 @@
sprite: Mobs/Effects/onfire.rsi
normalState: Mouse_burning
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 500
coldDamageThreshold: 0
- type: Reactive

View File

@@ -107,9 +107,10 @@
- type: TypingIndicator
proto: alien
- type: Temperature
currentTemperature: 310.15
- type: TemperatureDamage
heatDamageThreshold: 360
coldDamageThreshold: -150
currentTemperature: 310.15
- type: Tag
tags:
- CannotSuicide
@@ -524,9 +525,10 @@
- type: TypingIndicator
proto: alien
- type: Temperature
currentTemperature: 310.15
- type: TemperatureDamage
heatDamageThreshold: 360
coldDamageThreshold: -150
currentTemperature: 310.15
- type: NoSlip
- type: Perishable #Ummmm the acid kills a lot of the bacteria or something
molsPerSecondPerUnitMass: 0.0005

View File

@@ -109,6 +109,7 @@
- RadiationProtection
- Adrenaline
- type: Temperature
- type: TemperatureDamage
heatDamageThreshold: 2400
- type: Metabolizer
solutionOnBody: false

View File

@@ -239,10 +239,11 @@
- type: Blindable
# Other
- type: Temperature
heatDamageThreshold: 325
coldDamageThreshold: 260
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 325
coldDamageThreshold: 260
coldDamage:
types:
Cold: 0.1 #per second, scales with temperature & other constants

View File

@@ -58,11 +58,12 @@
damage:
types:
Heat: 2.5 # moths burn more easily
- type: Temperature # Moths hate the heat and thrive in the cold.
heatDamageThreshold: 320
coldDamageThreshold: 230
- type: Temperature
currentTemperature: 310.15
specificHeat: 46
- type: TemperatureDamage # Moths hate the heat and thrive in the cold.
heatDamageThreshold: 320
coldDamageThreshold: 230
coldDamage:
types:
Cold : 0.05 #per second, scales with temperature & other constants

View File

@@ -54,10 +54,11 @@
types:
Slash: 5
- type: Temperature
heatDamageThreshold: 400
coldDamageThreshold: 285
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 400
coldDamageThreshold: 285
coldDamage:
types:
Cold : 0.1 #per second, scales with temperature & other constants

View File

@@ -34,9 +34,10 @@
Piercing: 2
Slash: 3
- type: Temperature # Same as moth temperatures until below is solved.
specificHeat: 44
- type: TemperatureDamage
heatDamageThreshold: 320 # TODO: 315 when there is a way to make the temperature alert not blink to the side of the screen and disappear when you "sweat" at 39C.
coldDamageThreshold: 230 # TODO: 220 when the above is solved.
specificHeat: 44
coldDamage:
types:
Cold: 0.05 # Per second, scales with temperature & other constants

View File

@@ -169,13 +169,14 @@
components:
- type: AtmosExposed
- type: Temperature
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 325
coldDamageThreshold: 0
currentTemperature: 310.15
coldDamage: #per second, scales with temperature & other constants
types:
Cold : 0.1
specificHeat: 42
heatDamage: #per second, scales with temperature & other constants
types:
Heat : 1.5
@@ -204,10 +205,11 @@
normalBodyTemperature: 310.15
thermalRegulationTemperatureThreshold: 2
- type: Temperature
heatDamageThreshold: 325
coldDamageThreshold: 260
currentTemperature: 310.15
specificHeat: 42
- type: TemperatureDamage
heatDamageThreshold: 325
coldDamageThreshold: 260
coldDamage:
types:
Cold: 1 #per second, scales with temperature & other constants

View File

@@ -61,6 +61,13 @@
# all below are for egg cooking/exploding
- type: AtmosExposed
- type: Temperature
- type: TemperatureDamage
- type: InternalTemperature
# ~1mm shell and ~1cm of albumen
thickness: 0.011
area: 0.04
# conductivity of egg shell based on a paper from Romanoff and Romanoff (1949)
conductivity: 0.456
# Splat
- type: entity
@@ -128,3 +135,4 @@
- type: Temperature
# preserve temperature from the boiling step
currentTemperature: 344
- type: TemperatureDamage

View File

@@ -42,6 +42,7 @@
- type: AtmosExposed
- type: Temperature
currentTemperature: 290
- type: TemperatureDamage
# required for cooking to work
- type: InternalTemperature
thickness: 0.02

View File

@@ -45,6 +45,7 @@
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Temperature
- type: TemperatureDamage
heatDamage:
types:
Heat: 5

View File

@@ -56,6 +56,7 @@
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Temperature
- type: TemperatureDamage
heatDamage:
types:
Heat: 5
@@ -226,6 +227,7 @@
Cold: -1.0
Blunt: -0.5 # Needs to be balanced (approx 3x) with vacuum damage to stall but not kill Kudzu
- type: Temperature
- type: TemperatureDamage
heatDamage:
types:
Heat: 10

View File

@@ -89,6 +89,7 @@
- type: AtmosExposed
- type: Temperature
currentTemperature: 290
- type: TemperatureDamage
- type: InternalTemperature
thickness: 0.02
area: 0.02

View File

@@ -111,7 +111,7 @@
- type: CryoPodAir
- type: Climbable # so that ejected bodies don't get stuck
vaultable: false
- type: ContainerTemperatureDamageThresholds
- type: ContainerTemperature
coldDamageThreshold: 10
- type: GuideHelp
guides:

View File

@@ -24,6 +24,7 @@
- type: Damageable
damageModifierSet: Wood
- type: Temperature
- type: TemperatureDamage
heatDamage:
types:
Heat: 5