1
0

ExplosionSystem.Airtight.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. using Content.Server.Atmos.Components;
  2. using Content.Server.Destructible;
  3. using Content.Shared.Atmos;
  4. using Content.Shared.Damage;
  5. using Content.Shared.Explosion;
  6. using Content.Shared.Explosion.EntitySystems;
  7. using Content.Shared.FixedPoint;
  8. using Robust.Shared.Map.Components;
  9. namespace Content.Server.Explosion.EntitySystems;
  10. public sealed partial class ExplosionSystem
  11. {
  12. [Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
  13. private readonly Dictionary<string, int> _explosionTypes = new();
  14. private void InitAirtightMap()
  15. {
  16. // Currently explosion prototype hot-reload isn't supported, as it would involve completely re-computing the
  17. // airtight map. Could be done, just not yet implemented.
  18. // for storing airtight entity damage thresholds for all anchored airtight entities, we will use integers in
  19. // place of id-strings. This initializes the string <--> id association.
  20. // This allows us to replace a Dictionary<string, float> with just a float[].
  21. int index = 0;
  22. foreach (var prototype in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
  23. {
  24. _explosionTypes.Add(prototype.ID, index);
  25. index++;
  26. }
  27. }
  28. // The explosion intensity required to break an entity depends on the explosion type. So it is stored in a
  29. // Dictionary<string, float>
  30. //
  31. // Hence, each tile has a tuple (Dictionary<string, float>, AtmosDirection). This specifies what directions are
  32. // blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that
  33. // tile. This is the TileData struct.
  34. //
  35. // We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid
  36. // indices to this tile-data struct.
  37. private Dictionary<EntityUid, Dictionary<Vector2i, TileData>> _airtightMap = new();
  38. public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? grid = null, EntityQuery<AirtightComponent>? query = null)
  39. {
  40. if (Resolve(gridId, ref grid, false))
  41. UpdateAirtightMap(gridId, grid, tile, query);
  42. }
  43. /// <summary>
  44. /// Update the map of explosion blockers.
  45. /// </summary>
  46. /// <remarks>
  47. /// Gets a list of all airtight entities on a tile. Assembles a <see cref="AtmosDirection"/> that specifies
  48. /// what directions are blocked, along with the largest explosion tolerance. Note that as we only keep track
  49. /// of the largest tolerance, this means that the explosion map will actually be inaccurate if you have
  50. /// something like a normal and a reinforced windoor on the same tile. But given that this is a pretty rare
  51. /// occurrence, I am fine with this.
  52. /// </remarks>
  53. public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile, EntityQuery<AirtightComponent>? query = null)
  54. {
  55. var tolerance = new float[_explosionTypes.Count];
  56. var blockedDirections = AtmosDirection.Invalid;
  57. if (!_airtightMap.ContainsKey(gridId))
  58. _airtightMap[gridId] = new();
  59. query ??= EntityManager.GetEntityQuery<AirtightComponent>();
  60. var damageQuery = EntityManager.GetEntityQuery<DamageableComponent>();
  61. var destructibleQuery = EntityManager.GetEntityQuery<DestructibleComponent>();
  62. var anchoredEnumerator = grid.GetAnchoredEntitiesEnumerator(tile);
  63. while (anchoredEnumerator.MoveNext(out var uid))
  64. {
  65. if (!query.Value.TryGetComponent(uid, out var airtight) || !airtight.AirBlocked)
  66. continue;
  67. blockedDirections |= airtight.AirBlockedDirection;
  68. var entityTolerances = GetExplosionTolerance(uid.Value, damageQuery, destructibleQuery);
  69. for (var i = 0; i < tolerance.Length; i++)
  70. {
  71. tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]);
  72. }
  73. }
  74. if (blockedDirections != AtmosDirection.Invalid)
  75. _airtightMap[gridId][tile] = new(tolerance, blockedDirections);
  76. else
  77. _airtightMap[gridId].Remove(tile);
  78. }
  79. /// <summary>
  80. /// On receiving damage, re-evaluate how much explosion damage is needed to destroy an airtight entity.
  81. /// </summary>
  82. private void OnAirtightDamaged(EntityUid uid, AirtightComponent airtight, DamageChangedEvent args)
  83. {
  84. // do we need to update our explosion blocking map?
  85. if (!airtight.AirBlocked)
  86. return;
  87. if (!EntityManager.TryGetComponent(uid, out TransformComponent? transform) || !transform.Anchored)
  88. return;
  89. if (!TryComp<MapGridComponent>(transform.GridUid, out var grid))
  90. return;
  91. UpdateAirtightMap(transform.GridUid.Value, grid, grid.CoordinatesToTile(transform.Coordinates));
  92. }
  93. /// <summary>
  94. /// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
  95. /// </summary>
  96. public float[] GetExplosionTolerance(
  97. EntityUid uid,
  98. EntityQuery<DamageableComponent> damageQuery,
  99. EntityQuery<DestructibleComponent> destructibleQuery)
  100. {
  101. // How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES
  102. // that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
  103. // are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously.
  104. var totalDamageTarget = FixedPoint2.MaxValue;
  105. if (destructibleQuery.TryGetComponent(uid, out var destructible))
  106. {
  107. totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
  108. }
  109. var explosionTolerance = new float[_explosionTypes.Count];
  110. if (totalDamageTarget == FixedPoint2.MaxValue || !damageQuery.TryGetComponent(uid, out var damageable))
  111. {
  112. for (var i = 0; i < explosionTolerance.Length; i++)
  113. {
  114. explosionTolerance[i] = float.MaxValue;
  115. }
  116. return explosionTolerance;
  117. }
  118. // What multiple of each explosion type damage set will result in the damage exceeding the required amount? This
  119. // does not support entities dynamically changing explosive resistances (e.g. via clothing). But these probably
  120. // shouldn't be airtight structures anyways....
  121. foreach (var (id, index) in _explosionTypes)
  122. {
  123. if (!_prototypeManager.TryIndex<ExplosionPrototype>(id, out var explosionType))
  124. continue;
  125. // evaluate the damage that this damage type would do to this entity
  126. var damagePerIntensity = FixedPoint2.Zero;
  127. foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
  128. {
  129. if (!damageable.Damage.DamageDict.ContainsKey(type))
  130. continue;
  131. var ev = new GetExplosionResistanceEvent(explosionType.ID);
  132. RaiseLocalEvent(uid, ref ev);
  133. damagePerIntensity += value * Math.Max(0, ev.DamageCoefficient);
  134. }
  135. explosionTolerance[index] = damagePerIntensity > 0
  136. ? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity)
  137. : float.MaxValue;
  138. }
  139. return explosionTolerance;
  140. }
  141. /// <summary>
  142. /// Data struct that describes the explosion-blocking airtight entities on a tile.
  143. /// </summary>
  144. public struct TileData
  145. {
  146. public TileData(float[] explosionTolerance, AtmosDirection blockedDirections)
  147. {
  148. ExplosionTolerance = explosionTolerance;
  149. BlockedDirections = blockedDirections;
  150. }
  151. public float[] ExplosionTolerance;
  152. public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
  153. }
  154. }