* fix a lot of spaces in shuttle call reason * fix tts fatal without api url * Physics Based Air Throws (#342) I've made it so that when a room is explosively depressurized(or when a body of high pressure air flows into one of lower pressure air), that entities inside are launched by the air pressure with effects according to their mass. An entity's mass is now used as an innate resistance to forced movement by airflow, and more massive entities are both less likely to be launched, and will launch less far than others. While lighter entities are launched far more easily, and will shoot off into space quite quickly! Spacing departments has never been so exciting! This can be made extraordinarily fun if more objects are given the ability to injure people when colliding with them at high speeds. As a note, Humans are very unlikely to be sucked into space at a typical force generated from a 101kpa room venting into 0kpa, unless they happened to be standing right next to the opening to space when it was created. The same cannot be said for "Lighter-Than-Human" species such as Felinids and Harpies. I guess that's just the price to pay for being cute. :) On a plus side, because the math behind this is simplified even further than it was before, this actually runs more efficiently than the previous system. Nothing, this is basically done. I've spent a good 6 hours straight finely tuning the system until I was satisfied that it reflects something close to reality. **Before the Space Wind Rework:** https://github.com/Simple-Station/Einstein-Engines/assets/16548818/0bf56c50-58e6-4aef-97f8-027fbe62331e **With this Rework:** https://github.com/Simple-Station/Einstein-Engines/assets/16548818/6be507a9-e9de-4bb8-9d46-8b7c83ed5f1d 🆑 VMSolidus - add: Atmospheric "Throws" are now calculated using object mass, and behave accordingly. Tiny objects will shoot out of rooms quite fast! --------- Signed-off-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> * fixes --------- Signed-off-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com>
473 lines
19 KiB
C#
473 lines
19 KiB
C#
using System.Linq;
|
|
using Content.Server._White.Wizard.SpellBlade;
|
|
using Content.Server.Administration.Logs;
|
|
using Content.Server.Atmos.Components;
|
|
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.Changeling;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Rejuvenate;
|
|
using Content.Shared.Temperature;
|
|
using Robust.Shared.Physics.Components;
|
|
|
|
namespace Content.Server.Temperature.Systems;
|
|
|
|
public sealed class TemperatureSystem : EntitySystem
|
|
{
|
|
[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 SpellBladeSystem _spellBlade = default!; // WD
|
|
|
|
/// <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 override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
|
|
SubscribeLocalEvent<TemperatureComponent, AtmosExposedUpdateEvent>(OnAtmosExposedUpdate);
|
|
SubscribeLocalEvent<TemperatureComponent, RejuvenateEvent>(OnRejuvenate);
|
|
SubscribeLocalEvent<AlertsComponent, OnTemperatureChangeEvent>(ServerAlert);
|
|
SubscribeLocalEvent<TemperatureProtectionComponent, InventoryRelayedEvent<ModifyChangedTemperatureEvent>>(
|
|
OnTemperatureChangeAttempt);
|
|
|
|
SubscribeLocalEvent<InternalTemperatureComponent, MapInitEvent>(OnInit);
|
|
|
|
// Allows overriding thresholds based on the parent's thresholds.
|
|
SubscribeLocalEvent<TemperatureComponent, EntParentChangedMessage>(OnParentChange);
|
|
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentStartup>(
|
|
OnParentThresholdStartup);
|
|
SubscribeLocalEvent<ContainerTemperatureDamageThresholdsComponent, ComponentShutdown>(
|
|
OnParentThresholdShutdown);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
// conduct heat from the surface to the inside of entities with internal temperatures
|
|
var query = EntityQueryEnumerator<InternalTemperatureComponent, TemperatureComponent>();
|
|
while (query.MoveNext(out var uid, out var comp, out var temp))
|
|
{
|
|
// don't do anything if they equalised
|
|
var diff = Math.Abs(temp.CurrentTemperature - comp.Temperature);
|
|
if (diff < 0.1f)
|
|
continue;
|
|
|
|
// heat flow in W/m^2 as per fourier's law in 1D.
|
|
var q = comp.Conductivity * diff / comp.Thickness;
|
|
|
|
// convert to J then K
|
|
var joules = q * comp.Area * frameTime;
|
|
var degrees = joules / GetHeatCapacity(uid, temp);
|
|
if (temp.CurrentTemperature < comp.Temperature)
|
|
degrees *= -1;
|
|
|
|
// exchange heat between inside and surface
|
|
comp.Temperature += degrees;
|
|
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();
|
|
}
|
|
|
|
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)
|
|
{
|
|
var ev = new ModifyChangedTemperatureEvent(heatAmount);
|
|
RaiseLocalEvent(uid, ev);
|
|
heatAmount = ev.TemperatureDelta;
|
|
}
|
|
|
|
float lastTemp = temperature.CurrentTemperature;
|
|
temperature.CurrentTemperature += heatAmount / GetHeatCapacity(uid, temperature);
|
|
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 temperatureDelta = args.GasMixture.Temperature - temperature.CurrentTemperature;
|
|
var airHeatCapacity = _atmosphere.GetHeatCapacity(args.GasMixture, false);
|
|
var heatCapacity = GetHeatCapacity(uid, temperature);
|
|
var heat = temperatureDelta * (airHeatCapacity * heatCapacity /
|
|
(airHeatCapacity + heatCapacity));
|
|
ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
|
|
|
|
// WD START
|
|
var adjEv = new AdjustTemperatureEvent(temperature.CurrentTemperature);
|
|
RaiseLocalEvent(uid, adjEv);
|
|
if (!MathHelper.CloseTo(adjEv.Temperature, temperature.CurrentTemperature))
|
|
ForceChangeTemperature(uid, adjEv.Temperature, temperature);
|
|
// WD END
|
|
}
|
|
|
|
public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null)
|
|
{
|
|
if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
|
|
{
|
|
return Atmospherics.MinimumHeatCapacity;
|
|
}
|
|
if (physics.Mass < 1)
|
|
return comp.SpecificHeat;
|
|
else return comp.SpecificHeat * physics.FixturesMass;
|
|
}
|
|
|
|
private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
|
|
{
|
|
if (!TryComp<TemperatureComponent>(uid, out var temp))
|
|
return;
|
|
|
|
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)
|
|
{
|
|
AlertType type;
|
|
float threshold;
|
|
float idealTemp;
|
|
|
|
if (!TryComp<TemperatureComponent>(uid, out var temperature))
|
|
{
|
|
_alerts.ClearAlertCategory(uid, AlertCategory.Temperature);
|
|
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 = AlertType.Cold;
|
|
threshold = temperature.ColdDamageThreshold;
|
|
}
|
|
else
|
|
{
|
|
type = AlertType.Hot;
|
|
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, AlertCategory.Temperature);
|
|
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)
|
|
{
|
|
// WD START
|
|
if (_spellBlade.IsHoldingItemWithComponent<FireAspectComponent>(uid))
|
|
{
|
|
if (!temperature.TakingDamage)
|
|
return;
|
|
_adminLogger.Add(LogType.Temperature,
|
|
$"{ToPrettyString(uid):entity} stopped taking temperature damage");
|
|
temperature.TakingDamage = false;
|
|
return;
|
|
}
|
|
// WD END
|
|
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)
|
|
{
|
|
// WD START
|
|
if (TryComp(uid, out VoidAdaptationComponent? voidAdaptation))
|
|
{
|
|
if (temperature.TakingDamage)
|
|
{
|
|
_adminLogger.Add(LogType.Temperature,
|
|
$"{ToPrettyString(uid):entity} stopped taking temperature damage");
|
|
temperature.TakingDamage = false;
|
|
}
|
|
|
|
voidAdaptation.ChemMultiplier = 0.75f;
|
|
return;
|
|
}
|
|
|
|
if (_spellBlade.IsHoldingItemWithComponent<FrostAspectComponent>(uid))
|
|
{
|
|
if (!temperature.TakingDamage)
|
|
return;
|
|
_adminLogger.Add(LogType.Temperature,
|
|
$"{ToPrettyString(uid):entity} stopped taking temperature damage");
|
|
temperature.TakingDamage = false;
|
|
return;
|
|
}
|
|
// WD END
|
|
|
|
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<ModifyChangedTemperatureEvent> args)
|
|
{
|
|
var ev = new GetTemperatureProtectionEvent(component.Coefficient);
|
|
RaiseLocalEvent(uid, ref ev);
|
|
|
|
args.Args.TemperatureDelta *= ev.Coefficient;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|