--- /dev/null
+namespace Content.Server.Temperature.Components;
+
+[RegisterComponent]
+public sealed partial class ContainerTemperatureComponent : Component
+{
+ [DataField]
+ public float? HeatDamageThreshold;
+
+ [DataField]
+ public float? ColdDamageThreshold;
+}
+++ /dev/null
-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;
-}
--- /dev/null
+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);
+ }
+}
-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)
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)
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;
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)
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
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
}
}
_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
-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
[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>
/// </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";
}
--- /dev/null
+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);
+}
[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>
SubscribeLocalEvent<TemperatureSpeedComponent, OnTemperatureChangeEvent>(OnTemperatureChanged);
SubscribeLocalEvent<TemperatureSpeedComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
+
+ TemperatureQuery = GetEntityQuery<TemperatureComponent>();
}
private void OnTemperatureChanged(Entity<TemperatureSpeedComponent> ent, ref OnTemperatureChangeEvent args)
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;
}
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:
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
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
Dead:
Base: spacecat_dead
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 423
coldDamageThreshold: 0
- type: Tag
damage:
types: {}
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 1200
- type: entity
id: MobCarpDungeon
Bloodloss:
-0.8
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 800
coldDamageThreshold: 0
- type: MeleeWeapon
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
Quantity: 300
- type: CombatMode
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 500
coldDamageThreshold: 0
- type: MeleeWeapon
sprite: Mobs/Effects/onfire.rsi
normalState: Mouse_burning
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 500
coldDamageThreshold: 0
- type: Reactive
- type: TypingIndicator
proto: alien
- type: Temperature
+ currentTemperature: 310.15
+ - type: TemperatureDamage
heatDamageThreshold: 360
coldDamageThreshold: -150
- currentTemperature: 310.15
- type: Tag
tags:
- CannotSuicide
- 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
- RadiationProtection
- Adrenaline
- type: Temperature
+ - type: TemperatureDamage
heatDamageThreshold: 2400
- type: Metabolizer
solutionOnBody: false
- 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
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
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
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
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
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
# 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
- type: Temperature
# preserve temperature from the boiling step
currentTemperature: 344
+ - type: TemperatureDamage
- type: AtmosExposed
- type: Temperature
currentTemperature: 290
+ - type: TemperatureDamage
# required for cooking to work
- type: InternalTemperature
thickness: 0.02
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Temperature
+ - type: TemperatureDamage
heatDamage:
types:
Heat: 5
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Temperature
+ - type: TemperatureDamage
heatDamage:
types:
Heat: 5
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
- type: AtmosExposed
- type: Temperature
currentTemperature: 290
+ - type: TemperatureDamage
- type: InternalTemperature
thickness: 0.02
area: 0.02
- type: CryoPodAir
- type: Climbable # so that ejected bodies don't get stuck
vaultable: false
- - type: ContainerTemperatureDamageThresholds
+ - type: ContainerTemperature
coldDamageThreshold: 10
- type: GuideHelp
guides:
- type: Damageable
damageModifierSet: Wood
- type: Temperature
+ - type: TemperatureDamage
heatDamage:
types:
Heat: 5