[MICROBALANCE PR] Explosives no longer pierce armor (#40090)

* Explosiv nerf

* Minor changes

* Whoops

* whoopsie doodles.

* Address half of the review.

* forgot this too (flat reductions are applied before the modifier so we need to account for that :P)

* fix merge conflicts

* it's time

* whoops

* me when I logmissing true

* they call me the forgor cause I forgor

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2026-06-01 19:50:53 -07:00
committed by GitHub
parent b2e9308ca0
commit b5a8acdf9f
7 changed files with 111 additions and 61 deletions
@@ -1,4 +1,3 @@
using System.Linq;
using System.Runtime.InteropServices;
using Content.Server.Atmos.Components;
using Content.Server.Explosion.Components;
@@ -214,12 +213,16 @@ public sealed partial class ExplosionSystem
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
// are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously.
var totalDamageTarget = FixedPoint2.MaxValue;
if (_destructibleQuery.TryGetComponent(uid, out var destructible))
if (_destructibleQuery.TryComp(uid, out var destructible))
{
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
}
if (totalDamageTarget == FixedPoint2.MaxValue || !_injurableQuery.TryGetComponent(uid, out var injurable))
// We are assuming airtight entities don't need to relay since they shouldn't have inventories.
var modifiers = _damageableSystem.GetDamageModifierSet(uid);
var explosionComp = _explosionResistanceQuery.CompOrNull(uid);
if (totalDamageTarget == FixedPoint2.MaxValue || !_injurableQuery.TryComp(uid, out var injurable))
{
for (var i = 0; i < explosionTolerance.Length; i++)
{
@@ -238,35 +241,94 @@ public sealed partial class ExplosionSystem
{
// TODO EXPLOSION SYSTEM
// cache explosion type damage.
if (!_prototypeManager.Resolve(id, out ExplosionPrototype? explosionType))
if (!_prototypeManager.Resolve(id, out var explosionType))
continue;
// evaluate the damage that this damage type would do to this entity
var damagePerIntensity = FixedPoint2.Zero;
// Create a dictionary of intensity thresholds which dictates when damagePerIntensity increases!
var damageThresholds = new SortedDictionary<FixedPoint2, FixedPoint2>();
foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
{
if (!_damageableSystem.CanBeDamagedBy((uid, injurable), type))
continue;
// TODO EXPLOSION SYSTEM
// add a variant of the event that gets raised once, instead of once per prototype.
// Or better yet, just calculate this manually w/o the event.
// The event mainly exists for indirect resistances via things like inventory & clothing
// But this shouldn't matter for airtight entities.
var ev = new GetExplosionResistanceEvent(explosionType.ID);
RaiseLocalEvent(uid, ref ev);
var modifier = mod;
if (explosionComp != null)
{
modifier *= explosionComp.DamageCoefficient;
if (explosionComp.Modifiers.TryGetValue(explosionType.ID, out var typeMod))
modifier *= typeMod;
}
damagePerIntensity += value * mod * Math.Max(0, ev.DamageCoefficient);
if (modifiers != null)
{
if (modifiers.Coefficients.TryGetValue(type, out var armorMod))
modifier *= armorMod;
if (modifiers.FlatReduction.TryGetValue(type, out var flat))
{
if (flat > 0)
{
// If the flat modifier is reducing damage, we cache the extra damage per intensity for later!
var intensity = flat / value;
var damage = damageThresholds.GetValueOrDefault(intensity);
damageThresholds[intensity] = value * Math.Max(0, modifier) + damage;
continue;
}
}
}
damagePerIntensity += value * Math.Max(0, modifier);
}
var toleranceValue = damagePerIntensity > 0
? (float) ((totalDamageTarget - _damageableSystem.GetTotalDamage(uid)) / damagePerIntensity)
: ToleranceValues.Invulnerable;
explosionTolerance[index] = toleranceValue;
explosionTolerance[index] = GetExplosionTolerance(uid, totalDamageTarget, damagePerIntensity, damageThresholds);
}
}
private FixedPoint2 GetExplosionTolerance(EntityUid uid,
FixedPoint2 totalDamageTarget,
FixedPoint2 damagePerIntensity,
SortedDictionary<FixedPoint2, FixedPoint2> damageThresholds)
{
return GetExplosionTolerance(totalDamageTarget - _damageableSystem.GetTotalDamage(uid),
damagePerIntensity,
damageThresholds);
}
private FixedPoint2 GetExplosionTolerance(FixedPoint2 damageTarget,
FixedPoint2 damagePerIntensity,
SortedDictionary<FixedPoint2, FixedPoint2> damageThresholds)
{
var tolerance = damagePerIntensity > 0 ? damageTarget / damagePerIntensity : ToleranceValues.Invulnerable;
var prevIntensity = FixedPoint2.Zero;
/*
* Calculated through a pretty simple equation which relies on this dictionary being sorted.
* We precalculate the intensity at which an explosion's damage type exceeds the flat reduction of an entity's armor
* That is done above and stored in our `damageThresholds` SortedDictionary. If you can find a more mem efficient way to do this be my guest,
* but these values *have* to be sorted.
*/
foreach (var (intensity, damage) in damageThresholds)
{
// Check if the object would break before hitting this threshold, if so, return the current tolerance value
if (intensity > tolerance)
return tolerance;
/*
* If the object breaks after this threshold, reduce the HP left by the amount of HP lost between the last flat reduction and this one
* Then adjust our damagePerIntensity and new tolerance values accordingly.
* Lastly store this intensity value so we can calculate the delta next loop.
*/
damageTarget -= (intensity - prevIntensity) * damagePerIntensity;
damagePerIntensity += damage;
tolerance = intensity + damageTarget / damagePerIntensity;
prevIntensity = intensity;
}
return tolerance;
}
private void OnAirtightGridRemoved(EntityUid entity)
{
if (!TryComp(entity, out ExplosionAirtightGridComponent? airtightGrid))
@@ -1,24 +1,20 @@
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
using Content.Shared.Damage.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
@@ -228,34 +224,25 @@ public sealed partial class ExplosionSystem
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
// process anchored entities
var tileBlocked = false;
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
processed.Add(entity);
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks, cause);
}
// heat the atmosphere
if (temperature != null)
{
_atmosphere.HotspotExpose(grid.Owner, tile, temperature.Value, currentIntensity, cause, true);
}
// We process anchored entities last, these should've been caught by the lookups earlier.
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
// information about the entities that were spawned as a result, but without that information we just have to
// re-check for new anchored entities. Compared to entity spawning & deleting, this should still be relatively minor.
var tileBlocked = false;
_map.GetAnchoredEntities(grid, tile, _anchored);
if (_anchored.Count > 0)
{
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
tileBlocked |= IsBlockingTurf(entity);
}
_anchored.Clear();
}
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
@@ -441,7 +428,7 @@ public sealed partial class ExplosionSystem
DamageSpecifier? originalDamage,
float throwForce,
string id,
TransformComponent? xform,
TransformComponent xform,
float? fireStacksOnIgnite,
EntityUid? cause)
{
@@ -454,7 +441,7 @@ public sealed partial class ExplosionSystem
continue;
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage((entity, damageable), damage, ignoreResistances: true, ignoreGlobalModifiers: true);
_damageableSystem.ChangeDamage((entity, damageable), damage);
if (_actorQuery.HasComp(entity))
{
@@ -478,8 +465,7 @@ public sealed partial class ExplosionSystem
}
// throw
if (xform != null // null implies anchored or in a container
&& !xform.Anchored
if (!xform.Anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
&& _physicsQuery.TryGetComponent(uid, out var physics)
@@ -704,7 +690,7 @@ sealed class Explosion
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
private readonly Shared.Damage.Systems.DamageableSystem _damageable;
private readonly DamageableSystem _damageable;
public readonly EntityUid VisualEnt;
@@ -6,6 +6,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Destructible;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Armor;
using Content.Shared.Atmos.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
@@ -64,6 +65,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private EntityQuery<ActorComponent> _actorQuery = default!;
[Dependency] private EntityQuery<DestructibleComponent> _destructibleQuery = default!;
[Dependency] private EntityQuery<DamageableComponent> _damageableQuery = default!;
[Dependency] private EntityQuery<ExplosionResistanceComponent> _explosionResistanceQuery = default!;
[Dependency] private EntityQuery<InjurableComponent> _injurableQuery = default!;
[Dependency] private EntityQuery<AirtightComponent> _airtightQuery = default!;
[Dependency] private EntityQuery<TileHistoryComponent> _tileHistoryQuery = default!;
@@ -17,6 +17,17 @@ public sealed partial class DamageableSystem
return _supportedTypesByContainer[container.Value].Contains(type);
}
public DamageModifierSet? GetDamageModifierSet(Entity<DamageableComponent?> entity)
{
if (!_damageableQuery.Resolve(entity, ref entity.Comp, false)
|| entity.Comp.DamageModifierSetId is not { } proto
|| !_prototypeManager.Resolve(proto, out var modifierSet)
)
return null;
return modifierSet;
}
/// <summary>
/// Directly sets the damage in a damageable component.
/// </summary>
@@ -132,10 +143,7 @@ public sealed partial class DamageableSystem
// Apply resistances
if (!ignoreResistances)
{
if (
ent.Comp.DamageModifierSetId != null &&
_prototypeManager.Resolve(ent.Comp.DamageModifierSetId, out var modifierSet)
)
if (GetDamageModifierSet(ent) is { } modifierSet)
damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet);
// TODO DAMAGE
@@ -1,2 +1,2 @@
explosion-resistance-coefficient-value = - [color=orange]Explosion[/color] damage reduced by [color=lightblue]{$value}%[/color].
explosion-resistance-coefficient-value = - [color=orange]Explosion[/color] damage reduced by an additional [color=lightblue]{$value}%[/color].
explosion-resistance-contents-coefficient-value = - [color=orange]Explosion[/color] damage [color=white]to contents[/color] reduced by [color=lightblue]{$value}%[/color].
@@ -114,8 +114,6 @@
Piercing: 0.6
Heat: 0.5
Caustic: 0.9
- type: ExplosionResistance
damageCoefficient: 0.65
- type: GroupExamine
- type: entity
@@ -187,8 +185,6 @@
Heat: 0.3
Radiation: 0.5
Caustic: 0.5
- type: ExplosionResistance
damageCoefficient: 0.5
- type: FireProtection
reduction: 0.85
- type: StaticPrice
@@ -238,8 +234,6 @@
Piercing: 0.35
Heat: 0.35
Caustic: 0.5
- type: ExplosionResistance
damageCoefficient: 0.35
- type: StaminaResistance # do not add these to other equipment or mobs, we don't want to microbalance these everywhere
damageCoefficient: 0.45
- type: ClothingSpeedModifier
@@ -425,7 +419,7 @@
sprintModifier: 0.65
- type: HeldSpeedModifier
- type: ExplosionResistance
damageCoefficient: 0.5
damageCoefficient: 0.75
- type: GroupExamine
- type: ProtectedFromStepTriggers
slots: WITHOUT_POCKET
@@ -451,7 +445,7 @@
sprintModifier: 0.8
- type: HeldSpeedModifier
- type: ExplosionResistance
damageCoefficient: 0.4
damageCoefficient: 0.6
- type: GroupExamine
- type: Construction
graph: BoneArmor
@@ -162,7 +162,7 @@
highPressureMultiplier: 0.5
lowPressureMultiplier: 1000
- type: ExplosionResistance
damageCoefficient: 0.3
damageCoefficient: 0.5
- type: Armor
modifiers:
coefficients:
@@ -234,7 +234,7 @@
highPressureMultiplier: 0.5
lowPressureMultiplier: 1000
- type: ExplosionResistance
damageCoefficient: 0.4
damageCoefficient: 0.5
- type: Armor
modifiers:
coefficients:
@@ -296,7 +296,7 @@
highPressureMultiplier: 0.5
lowPressureMultiplier: 1000
- type: ExplosionResistance
damageCoefficient: 0.4
damageCoefficient: 0.5
- type: Armor
modifiers:
coefficients:
@@ -327,7 +327,7 @@
highPressureMultiplier: 0.02
lowPressureMultiplier: 1000
- type: ExplosionResistance
damageCoefficient: 0.5
damageCoefficient: 0.7
- type: Armor
modifiers:
coefficients:
@@ -608,7 +608,7 @@
heatingCoefficient: 0.001
coolingCoefficient: 0.001
- type: ExplosionResistance
damageCoefficient: 0.2
damageCoefficient: 0.3
- type: FireProtection
reduction: 0.8
- type: StaminaResistance # do not add these to other equipment or mobs, we don't want to microbalance these everywhere
@@ -688,8 +688,6 @@
- type: PressureProtection
highPressureMultiplier: 0.2
lowPressureMultiplier: 1000
- type: ExplosionResistance
damageCoefficient: 0.3
# - type: StaminaResistance # Should not have stamina resistance, this is purely so people know it was not forgotten.
- type: Armor
modifiers: