RIM-116 & Phalanx
Damage model
A mount that only knows “intact” or “scrap” is dull: it either sweeps the sky or it is gone. Assemblies that fail one at a time give a middle ground – and the hit position decides which one.
Only the structure has a hit zone
The structure is the mount's default zone and catches every round. Which assembly suffers is decided by the
hit position: BaseDamageContext carries hitPosition, and the mount knows which of
its moving parts sits there. The zones therefore rotate with the turret, and it costs not one polygon on the
model.
MyAD_ESubsystem MyAD_Mount_AssemblyAt(vector hit)
{
if (Contains(m_Launcher, hit))
return MyAD_ESubsystem.LAUNCHER;
if (Contains(m_Arms, hit))
return MyAD_ESubsystem.ELEVATION; // the arms tilt the weapon
if (Contains(m_Base, hit))
return MyAD_ESubsystem.TRAVERSE; // the slew ring sits in the base plate
return MyAD_ESubsystem.STRUCTURE;
}
Blast finds the nearest part, not the part underneath
A round hits a point; a charge works across an area. A rocket that went off two metres beside a mount did nothing, because no part sat at the impact point and the concrete base was invulnerable. Blast and fragments do not stop at a box boundary. So for explosive damage, ask which part is nearest, not which part sits there. On a direct hit the distance is zero and it reduces to the point-accurate mapping.
A second approach that failed
Written down so nobody tries it twice. The idea: give every moving part its own damage manager, so a round on the
barrel cluster hits the launcher directly. It fails because SCR_DamageManagerComponent requires an
RplComponent on the same entity. Plain GenericEntity parts have no replication of their
own, all prefabs stopped loading, and the editor showed nothing but the base:
ENTITY (E): GenericEntity is missing component RplComponent
as required by component SCR_DamageManagerComponent
Armour as a threshold
A mount that intercepts cruise missiles must not fall to small arms. Override
ComputeEffectiveDamage in a hit zone subclass – an event, and therefore overridable,
unlike proto external methods which are only callable. It filters before the damage is applied; a
filter in OnDamage would be too late, the hit is already counted there.
class MyAD_ArmouredHitZone : SCR_HitZone
{
[Attribute("150")] protected float m_fMinDamage; // example value
[Attribute("0.25")] protected float m_fPassthrough; // example value
override float ComputeEffectiveDamage(notnull BaseDamageContext ctx, bool isDOT)
{
if (ctx.damageType == EDamageType.TRUE) // the editor's "destroy" must really destroy
return super.ComputeEffectiveDamage(ctx, isDOT);
if (ctx.damageValue < m_fMinDamage)
return 0;
return super.ComputeEffectiveDamage(ctx, isDOT) * m_fPassthrough;
}
}
Fragments are the main damage, not the blast
Measured with an anti-tank rocket on the mount: one hit produces two damage events, and they are nothing like each other – a blast event below one point and a fragmentation event of more than a thousand. A filter that only lets explosive and kinetic damage through discards the main share of every explosion. The report was “I fired several times at the gun and the rounds went straight through”.
EDamageType.TRUE must pass unfiltered – the enum says so itself: “ignores damage multipliers, damage reduction and threshold”. A custom armour routine does not honour that by itself. Here it turned 3,000 into about a third, so the Game Master's destroy tool worked at a fraction of its effect.ComputeEffectiveDamage is called MULTIPLE times per damage event – it is a calculation, not an event. Anything with a side effect belongs in OnDamage, which fires exactly once. One hit with three calls destroyed three assemblies at once, which read in game as “the mount is either fine or gone”.Repair needs a remainder
The engine locks a destroyed default hit zone against any healing – SetHealth and
SetHealthScaled fail there silently, with no error. Measured in one run:
launcher box destroyed=1 | health 0 -> 800 state after: 0 (worked)
base structure destroyed=1 | health 0 -> 0 state after: 2 (did not)
Both zones were destroyed; the only difference is which one is the default. So clamp the damage to always leave a
tiny remainder, which keeps the zone out of DESTROYED. It changes nothing about play – a remainder
that small is far below any damage grade you display, so the assembly still reads as failed. It is merely
repairable.
float MyAD_Zone_Clamp(float damage)
{
float room = GetHealth() - GetMaxHealth() * 0.001; // always leave a per-mille
return Math.Clamp(damage, 0, Math.Max(room, 0));
}