Class DamageMitigation
Reads a defender's block and deflect buffs and answers the two questions combat asks of them: how much of this hit is taken off, and does this object get turned away.
public static class DamageMitigation
- Inheritance
-
DamageMitigation
- Inherited Members
Remarks
One place, because the two halves must not drift. Negation is applied by
CharacterDamageController.Damage and deflection by AbilityObject.ApplyHit —
different subsystems, different ticks in the frame — but both walk the same buff container,
both need the same "is this coming at my front" test, and both have to spend charges the
same way or a shield ends at a different moment depending on which one used it last.
Deterministic order, for free. IBuffController.Buffs is a
SortedDictionary<TKey, TValue> keyed by template id, so every peer walks a
defender's shields in the same order without this file having to sort anything. That matters
because two absorb shields spend in sequence, and which one empties first is observable.
Live positions, no rewind, deliberately. The facing test asks where the DEFENDER was looking, and the defender is not the peer whose view is being reconstructed — a rewind reproduces what the ATTACKER saw. Rewinding the defender's own facing to the attacker's view would let a player be hit through a shield they had already turned towards the attacker, and there is nothing to gain: the shield is the defender's decision about the defender's own character, taken on the tick the server resolves the hit.
Mutation is gated by the caller, not here. Negate(ICharacter, ICharacter, int, bool) and
TryDeflect(ICharacter, Vector3, Vector3, bool, out Vector3) both take a mutate flag: a peer that does not simulate this
defender's buffs may still want the NUMBER (so the caster's client can predict a blocked hit
honestly) but must never spend a pool it does not own. See
IBuffController.SimulatesBuffEffects.
Methods
CollectShieldVolumes(ICharacter, List<ShieldVolume>)
Every shield volume defender currently has raised.
public static void CollectShieldVolumes(ICharacter defender, List<ShieldVolume> into)
Parameters
defenderICharacterThe character whose shields are wanted.
intoList<ShieldVolume>Receives the active volumes. Not cleared first.
Remarks
For the outward-looking half — ShieldInterceptAction sweeps these for objects in
flight. Appended rather than returned, so the caller owns the list and a per-tick query
allocates nothing.
Negate(ICharacter, ICharacter, int, bool)
Damage left after every qualifying negation buff on defender has taken
its share.
public static int Negate(ICharacter defender, ICharacter attacker, int amount, bool mutate)
Parameters
defenderICharacterThe character taking the hit. Null returns
amount.attackerICharacterWho is hitting them, for the facing test. Null skips facing-gated buffs.
amountintDamage remaining after resistances.
mutateboolTrue to spend absorb pools and remove emptied buffs. False to answer the question without touching state — for a peer that does not simulate this defender.
Returns
- int
The damage that survives, never below zero and never above
amount.
Remarks
Modes are applied in the order that makes each one mean what it says: Immune first, because an immunity that ran after a shield would let the shield spend charges on damage that was never going to land; then Reduce, so a percentage is taken off the full hit rather than off whatever a pool happened to leave; then Absorb pools, which spend what actually remains.
A pool that empties is REMOVED, which is the "disappears when the remaining damage
negation amount hits 0" behaviour — and it is removed through
IBuffController.Remove rather than by dropping the entry, so the strip, the FX and
the observer push all hear about it exactly as they would for an expiry.
TryBlockAtVolume(ICharacter, Vector3, bool)
True when an ability object striking localImpactPoint met one of
defender's raised shields rather than the defender.
public static bool TryBlockAtVolume(ICharacter defender, Vector3 localImpactPoint, bool mutate)
Parameters
defenderICharacterThe character the object struck.
localImpactPointVector3Impact point in the defender's local space.
mutateboolTrue to spend the block cost and remove a shield it empties.
Returns
- bool
True when a shield took the hit and the object must not be treated as having landed.
Remarks
Interception, not mitigation. A hit that lands inside a shield volume never reached the character, so there is no damage to reduce and the buff's DamageNegationMode does not enter into it. The caller stops the object outright.
Local space, and that is what makes it correct. The point comes from LocalPoint or LocalPoint, captured inside the rewind scope and expressed in the defender's own frame; the volume is authored in that same frame. Neither has to be transformed, so neither can be read out of the wrong world — the trap that a world-space volume compared against a rewound impact point falls straight into, since hits are dispatched only after the scope has closed.
The FIRST shield in template order that covers the point wins and is the one charged, so two overlapping shields do not both pay for one projectile.
TryDeflect(ICharacter, Vector3, Vector3, bool, out Vector3)
True when defender turns away an object arriving along
incomingHeading, and if so what heading it leaves on.
public static bool TryDeflect(ICharacter defender, Vector3 incomingHeading, Vector3 impactNormal, bool mutate, out Vector3 deflectedHeading)
Parameters
defenderICharacterThe character the object is about to hit.
incomingHeadingVector3The direction the object is travelling.
impactNormalVector3Surface normal the server measured at the impact.
mutateboolTrue to spend a deflection charge and remove an emptied buff.
deflectedHeadingVector3The heading to redirect along. Meaningless when this returns false.
Returns
- bool
True when the object was deflected and must not be treated as a hit.
Remarks
The FIRST qualifying buff in template order wins and is the one charged, so two overlapping guards do not both spend a charge on one projectile.