diff --git a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs index ae8acc49ef..024b8a013b 100644 --- a/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs +++ b/Content.Server/Temperature/Components/ContainerTemperatureDamageThresholdsComponent.cs @@ -3,11 +3,9 @@ [RegisterComponent] public sealed partial class ContainerTemperatureDamageThresholdsComponent: Component { - [DataField("heatDamageThreshold")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField, ViewVariables(VVAccess.ReadWrite)] public float? HeatDamageThreshold; - [DataField("coldDamageThreshold")] - [ViewVariables(VVAccess.ReadWrite)] + [DataField, ViewVariables(VVAccess.ReadWrite)] public float? ColdDamageThreshold; } diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs index 6ace329426..7330ebf9ba 100644 --- a/Content.Server/Temperature/Components/TemperatureComponent.cs +++ b/Content.Server/Temperature/Components/TemperatureComponent.cs @@ -4,84 +4,80 @@ using Content.Shared.FixedPoint; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; -namespace Content.Server.Temperature.Components +namespace Content.Server.Temperature.Components; + +/// +/// Handles changing temperature, +/// informing others of the current temperature, +/// and taking fire damage from high temperature. +/// +[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; + /// - /// Handles changing temperature, - /// informing others of the current temperature, - /// and taking fire damage from high temperature. + /// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. /// - [RegisterComponent] - public sealed partial class TemperatureComponent : Component + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float? ParentHeatDamageThreshold; + + /// + /// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float? ParentColdDamageThreshold; + + /// + /// Heat capacity per kg of mass. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float SpecificHeat = 50f; + + /// + /// How well does the air surrounding you merge into your body temperature? + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float AtmosTemperatureTransferEfficiency = 0.1f; + + [ViewVariables] public float HeatCapacity { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("currentTemperature")] - public float CurrentTemperature { get; set; } = Atmospherics.T20C; - - [DataField("heatDamageThreshold")] - [ViewVariables(VVAccess.ReadWrite)] - public float HeatDamageThreshold = 360f; - - [DataField("coldDamageThreshold")] - [ViewVariables(VVAccess.ReadWrite)] - public float ColdDamageThreshold = 260f; - - /// - /// Overrides HeatDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. - /// - [ViewVariables(VVAccess.ReadWrite)] - public float? ParentHeatDamageThreshold; - - /// - /// Overrides ColdDamageThreshold if the entity's within a parent with the TemperatureDamageThresholdsComponent component. - /// - [ViewVariables(VVAccess.ReadWrite)] - public float? ParentColdDamageThreshold; - - [DataField("specificHeat")] - [ViewVariables(VVAccess.ReadWrite)] - public float SpecificHeat = 50f; - - /// - /// How well does the air surrounding you merge into your body temperature? - /// - [DataField("atmosTemperatureTransferEfficiency")] - [ViewVariables(VVAccess.ReadWrite)] - public float AtmosTemperatureTransferEfficiency = 0.1f; - - [ViewVariables] public float HeatCapacity + get { - get + if (IoCManager.Resolve().TryGetComponent(Owner, out var physics) && physics.FixturesMass != 0) { - if (IoCManager.Resolve().TryGetComponent(Owner, out var physics) && physics.FixturesMass != 0) - { - return SpecificHeat * physics.FixturesMass; - } - - return Atmospherics.MinimumHeatCapacity; + return SpecificHeat * physics.FixturesMass; } + + return Atmospherics.MinimumHeatCapacity; } - - [DataField("coldDamage")] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier ColdDamage = new(); - - [DataField("heatDamage")] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier HeatDamage = new(); - - /// - /// Temperature won't do more than this amount of damage per second. - /// - /// Okay it genuinely reaches this basically immediately for a plasma fire. - /// - [DataField("damageCap")] - [ViewVariables(VVAccess.ReadWrite)] - public FixedPoint2 DamageCap = FixedPoint2.New(8); - - /// - /// Used to keep track of when damage starts/stops. Useful for logs. - /// - public bool TakingDamage = false; } + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier ColdDamage = new(); + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public DamageSpecifier HeatDamage = new(); + + /// + /// Temperature won't do more than this amount of damage per second. + /// + /// + /// Okay it genuinely reaches this basically immediately for a plasma fire. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 DamageCap = FixedPoint2.New(8); + + /// + /// Used to keep track of when damage starts/stops. Useful for logs. + /// + [DataField] + public bool TakingDamage = false; } diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs index 9c56754861..4796704d27 100644 --- a/Content.Server/Temperature/Systems/TemperatureSystem.cs +++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Content.Server.Administration.Logs; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; @@ -13,351 +10,349 @@ using Content.Shared.Inventory; using Content.Shared.Rejuvenate; using Content.Shared.Temperature; using Robust.Server.GameObjects; +using System.Linq; -namespace Content.Server.Temperature.Systems +namespace Content.Server.Temperature.Systems; + +public sealed class TemperatureSystem : EntitySystem { - public sealed class TemperatureSystem : EntitySystem + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + /// + /// 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. + /// + public HashSet ShouldUpdateDamage = new(); + + public float UpdateInterval = 1.0f; + + private float _accumulatedFrametime; + + public override void Initialize() { - [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly AlertsSystem _alertsSystem = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; + SubscribeLocalEvent(EnqueueDamage); + SubscribeLocalEvent(OnAtmosExposedUpdate); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(ServerAlert); + SubscribeLocalEvent>( + OnTemperatureChangeAttempt); - /// - /// 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. - /// - public HashSet ShouldUpdateDamage = new(); + // Allows overriding thresholds based on the parent's thresholds. + SubscribeLocalEvent(OnParentChange); + SubscribeLocalEvent( + OnParentThresholdStartup); + SubscribeLocalEvent( + OnParentThresholdShutdown); + } - public float UpdateInterval = 1.0f; + public override void Update(float frameTime) + { + base.Update(frameTime); - private float _accumulatedFrametime; + _accumulatedFrametime += frameTime; - public override void Initialize() + if (_accumulatedFrametime < UpdateInterval) + return; + _accumulatedFrametime -= UpdateInterval; + + if (!ShouldUpdateDamage.Any()) + return; + + foreach (var comp in ShouldUpdateDamage) { - SubscribeLocalEvent(EnqueueDamage); - SubscribeLocalEvent(OnAtmosExposedUpdate); - SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(ServerAlert); - SubscribeLocalEvent>( - OnTemperatureChangeAttempt); + MetaDataComponent? metaData = null; - // Allows overriding thresholds based on the parent's thresholds. - SubscribeLocalEvent(OnParentChange); - SubscribeLocalEvent( - OnParentThresholdStartup); - SubscribeLocalEvent( - OnParentThresholdShutdown); + var uid = comp.Owner; + if (Deleted(uid, metaData) || Paused(uid, metaData)) + continue; + + ChangeDamage(uid, comp); } - public override void Update(float frameTime) + ShouldUpdateDamage.Clear(); + } + + public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null) + { + if (!Resolve(uid, ref temperature)) + return; + + float lastTemp = temperature.CurrentTemperature; + float delta = temperature.CurrentTemperature - temp; + temperature.CurrentTemperature = temp; + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), + true); + } + + public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, + TemperatureComponent? temperature = null) + { + if (!Resolve(uid, ref temperature)) + return; + + if (!ignoreHeatResistance) { - base.Update(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(); + var ev = new ModifyChangedTemperatureEvent(heatAmount); + RaiseLocalEvent(uid, ev, false); + heatAmount = ev.TemperatureDelta; } - public void ForceChangeTemperature(EntityUid uid, float temp, TemperatureComponent? temperature = null) + float lastTemp = temperature.CurrentTemperature; + temperature.CurrentTemperature += heatAmount / temperature.HeatCapacity; + float delta = temperature.CurrentTemperature - lastTemp; + + RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), true); + } + + private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, + ref AtmosExposedUpdateEvent args) + { + var transform = args.Transform; + if (transform.MapUid == null) + return; + + var position = _transform.GetGridOrMapTilePosition(uid, transform); + + var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature; + var tileHeatCapacity = + _atmosphere.GetTileHeatCapacity(transform.GridUid, transform.MapUid.Value, position); + var heat = temperatureDelta * (tileHeatCapacity * temperature.HeatCapacity / + (tileHeatCapacity + temperature.HeatCapacity)); + ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); + } + + private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args) + { + ForceChangeTemperature(uid, Atmospherics.T20C, comp); + } + + private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args) + { + switch (args.CurrentTemperature) { - if (!Resolve(uid, ref temperature)) - return; + // Cold strong. + case <= 260: + _alerts.ShowAlert(uid, AlertType.Cold, 3); + break; - float lastTemp = temperature.CurrentTemperature; - float delta = temperature.CurrentTemperature - temp; - temperature.CurrentTemperature = temp; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), - true); - } + // Cold mild. + case <= 280 and > 260: + _alerts.ShowAlert(uid, AlertType.Cold, 2); + break; - public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, - TemperatureComponent? temperature = null) - { - if (Resolve(uid, ref temperature)) - { - if (!ignoreHeatResistance) - { - var ev = new ModifyChangedTemperatureEvent(heatAmount); - RaiseLocalEvent(uid, ev, false); - heatAmount = ev.TemperatureDelta; - } + // Cold weak. + case <= 292 and > 280: + _alerts.ShowAlert(uid, AlertType.Cold, 1); + break; - float lastTemp = temperature.CurrentTemperature; - temperature.CurrentTemperature += heatAmount / temperature.HeatCapacity; - float delta = temperature.CurrentTemperature - lastTemp; + // Safe. + case <= 327 and > 292: + _alerts.ClearAlertCategory(uid, AlertCategory.Temperature); + break; - RaiseLocalEvent(uid, new OnTemperatureChangeEvent(temperature.CurrentTemperature, lastTemp, delta), - true); - } - } + // Heat weak. + case <= 335 and > 327: + _alerts.ShowAlert(uid, AlertType.Hot, 1); + break; - private void OnAtmosExposedUpdate(EntityUid uid, TemperatureComponent temperature, - ref AtmosExposedUpdateEvent args) - { - var transform = args.Transform; + // Heat mild. + case <= 360 and > 335: + _alerts.ShowAlert(uid, AlertType.Hot, 2); + break; - if (transform.MapUid == null) - return; - - var position = _transformSystem.GetGridOrMapTilePosition(uid, transform); - - var temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature; - var tileHeatCapacity = - _atmosphereSystem.GetTileHeatCapacity(transform.GridUid, transform.MapUid.Value, position); - var heat = temperatureDelta * (tileHeatCapacity * temperature.HeatCapacity / - (tileHeatCapacity + temperature.HeatCapacity)); - ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); - } - - private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEvent args) - { - ForceChangeTemperature(uid, Atmospherics.T20C, comp); - } - - private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args) - { - switch (args.CurrentTemperature) - { - // Cold strong. - case <= 260: - _alertsSystem.ShowAlert(uid, AlertType.Cold, 3); - break; - - // Cold mild. - case <= 280 and > 260: - _alertsSystem.ShowAlert(uid, AlertType.Cold, 2); - break; - - // Cold weak. - case <= 292 and > 280: - _alertsSystem.ShowAlert(uid, AlertType.Cold, 1); - break; - - // Safe. - case <= 327 and > 292: - _alertsSystem.ClearAlertCategory(uid, AlertCategory.Temperature); - break; - - // Heat weak. - case <= 335 and > 327: - _alertsSystem.ShowAlert(uid, AlertType.Hot, 1); - break; - - // Heat mild. - case <= 360 and > 335: - _alertsSystem.ShowAlert(uid, AlertType.Hot, 2); - break; - - // Heat strong. - case > 360: - _alertsSystem.ShowAlert(uid, AlertType.Hot, 3); - break; - } - } - - private void EnqueueDamage(EntityUid uid, TemperatureComponent component, OnTemperatureChangeEvent args) - { - ShouldUpdateDamage.Add(component); - } - - private void ChangeDamage(EntityUid uid, TemperatureComponent temperature) - { - if (!HasComp(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; - _damageableSystem.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)); - _damageableSystem.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, - InventoryRelayedEvent args) - { - args.Args.TemperatureDelta *= component.Coefficient; - } - - private void OnParentChange(EntityUid uid, TemperatureComponent component, - ref EntParentChangedMessage args) - { - var temperatureQuery = GetEntityQuery(); - var transformQuery = GetEntityQuery(); - var thresholdsQuery = GetEntityQuery(); - // 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(), GetEntityQuery(), - GetEntityQuery()); - } - - private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, - ComponentShutdown args) - { - RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), - GetEntityQuery()); - } - - /// - /// Recalculate and apply parent thresholds for the root entity and all its descendant. - /// - /// - /// - /// - /// - private void RecursiveThresholdUpdate(EntityUid root, EntityQuery temperatureQuery, - EntityQuery transformQuery, - EntityQuery tempThresholdsQuery) - { - RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery); - - foreach (var child in Transform(root).ChildEntities) - { - RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery); - } - } - - /// - /// Recalculate parent thresholds and apply them on the uid temperature component. - /// - /// - /// - /// - /// - private void RecalculateAndApplyParentThresholds(EntityUid uid, - EntityQuery temperatureQuery, EntityQuery transformQuery, - EntityQuery 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; - } - - /// - /// 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). - /// - /// - /// - /// - private (float?, float?) RecalculateParentThresholds( - EntityUid initialParentUid, - EntityQuery transformQuery, - EntityQuery 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); + // Heat strong. + case > 360: + _alerts.ShowAlert(uid, AlertType.Hot, 3); + break; } } - public sealed class OnTemperatureChangeEvent : EntityEventArgs + private void EnqueueDamage(EntityUid uid, TemperatureComponent component, OnTemperatureChangeEvent args) { - public float CurrentTemperature { get; } - public float LastTemperature { get; } - public float TemperatureDelta { get; } + ShouldUpdateDamage.Add(component); + } - public OnTemperatureChangeEvent(float current, float last, float delta) + private void ChangeDamage(EntityUid uid, TemperatureComponent temperature) + { + if (!HasComp(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) { - CurrentTemperature = current; - LastTemperature = last; - TemperatureDelta = delta; + 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, + InventoryRelayedEvent args) + { + args.Args.TemperatureDelta *= component.Coefficient; + } + + private void OnParentChange(EntityUid uid, TemperatureComponent component, + ref EntParentChangedMessage args) + { + var temperatureQuery = GetEntityQuery(); + var transformQuery = GetEntityQuery(); + var thresholdsQuery = GetEntityQuery(); + // 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(), GetEntityQuery(), + GetEntityQuery()); + } + + private void OnParentThresholdShutdown(EntityUid uid, ContainerTemperatureDamageThresholdsComponent component, + ComponentShutdown args) + { + RecursiveThresholdUpdate(uid, GetEntityQuery(), GetEntityQuery(), + GetEntityQuery()); + } + + /// + /// Recalculate and apply parent thresholds for the root entity and all its descendant. + /// + /// + /// + /// + /// + private void RecursiveThresholdUpdate(EntityUid root, EntityQuery temperatureQuery, + EntityQuery transformQuery, + EntityQuery tempThresholdsQuery) + { + RecalculateAndApplyParentThresholds(root, temperatureQuery, transformQuery, tempThresholdsQuery); + + foreach (var child in Transform(root).ChildEntities) + { + RecursiveThresholdUpdate(child, temperatureQuery, transformQuery, tempThresholdsQuery); + } + } + + /// + /// Recalculate parent thresholds and apply them on the uid temperature component. + /// + /// + /// + /// + /// + private void RecalculateAndApplyParentThresholds(EntityUid uid, + EntityQuery temperatureQuery, EntityQuery transformQuery, + EntityQuery 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; + } + + /// + /// 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). + /// + /// + /// + /// + private (float?, float?) RecalculateParentThresholds( + EntityUid initialParentUid, + EntityQuery transformQuery, + EntityQuery 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); + } +} + +public sealed class OnTemperatureChangeEvent : EntityEventArgs +{ + public float CurrentTemperature { get; } + public float LastTemperature { get; } + public float TemperatureDelta { get; } + + public OnTemperatureChangeEvent(float current, float last, float delta) + { + CurrentTemperature = current; + LastTemperature = last; + TemperatureDelta = delta; } }