Files
RP412/docs/DAMAGE-MODEL.md
T
CydandClaude Opus 5 f44cdbacca How a hit becomes lost armor and lost score
The damage model, end to end, read off the authentic surviving RP source
(VTV.cpp, WEAPSYS.cpp, RIVET.cpp, DEMOPACK.cpp, RPPLAYER.cpp) rather than
inferred. Companion to the BT doc of the same name; the shared engine
layers are the same and only summarised here.

The finding it is built around: RP's damage model is a physics and score
economy, not a subsystem-failure simulation. One armor pool at zone 0, no
criticals, no per-zone cascade. Collision armor is not authored at all -
it is calibrated from the vehicle's own mass and MaxImpactSpeed so that a
full-speed hit spends exactly the whole budget - and deathConstant
converts damage to score at that same exchange rate. Every point of
damage is simultaneously a transaction between two players, through a
two-second revenge window. Martian football, not a mech duel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 09:38:16 -05:00

8.7 KiB
Raw Blame History

Red Planet — the damage model, end to end

How a hit becomes lost armor, lost SCORE, and a burning VTV. Unlike BT, this analysis reads the AUTHENTIC surviving game source (RP/ in this tree — VTV.cpp, WEAPSYS.cpp, RIVET.cpp, DEMOPACK.cpp, RPPLAYER.cpp) on top of the shared MUNGA engine. The BT companion doc is TeslaRel410/restoration/source410/BT/DAMAGE-MODEL.md; the engine layers (Damage record, DamageZone, Entity message routing) are identical and only summarized here.

The headline difference: RP's damage model is a physics + score economy, not a subsystem-failure simulation. There are no critical tables, no per-zone cascade, no equipment destruction. Damage is dominated by KINETIC events (ramming, walls, crushing doors), armor is calibrated from the vehicle's physics constants, and every point of damage is simultaneously a score transaction between players — Martian football, not a mech duel.

laser raycast / rivet impact / demo-pack blast / collision bounce
        │
        ▼
 TakeDamageMessage (zone 0 — the hull)
        │
        ▼
 VTV::TakeDamageMessageHandler
   ├─ collision?  recompute damage locally via DynamicBounce (+ knockback)
   ├─ otherwise   apply damageForce/mass to velocity (knockback impulse)
   └─ chain to Entity → damageZones[0]->TakeDamage
        │
        ▼
 VTV__DamageZone::TakeDamage
   ├─ engine armor model: level += amount × scale[type], 1.0 = dead
   ├─ level ≥ 1.0 → VTV::BurningState (+ VehicleDead to the player)
   └─ EVERY hit forwarded to the linked RPPlayer
        │
        ▼
 RPPlayer::TakeDamageMessageHandler
   ├─ victim loses damageAmount as SCORE (collision × deathConstant)
   └─ top attacker in the 2-second revenge window GAINS the same

1. Shared engine layers (identical to BT)

The Damage record (MUNGA/DAMAGE.HPP): 5 types (Collision, Ballistic, Explosive, Laser, Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount. DamageZone::TakeDamage integrates damageLevel += amount × damageScale[type], clamps [0,1], 1.0 = Burning + Destroyed graphic. Entity::TakeDamageMessageHandler routes damageZones[zone]->TakeDamage and DROPS zone 1.

RP never needs the 1 path in combat: every producer passes zone 0. The VTV streams its zone array from the type-20 resource like any entity (multiple zones can exist for graphics), but combat, collision, suicide and out-of-world damage all land on zone 0 — the hull is one armor pool. There is no RP analog of BT's cylinder hit-location table, no aimed-zone fire, and no criticals: VTVSubsystem adds only controls plumbing (compare BT's MechSubsystem, which owns a private armored zone per equipment piece).

2. Producers — where Damage records are born

LaserGun (WEAPSYS.cpp, front + rear lasers): a RAYCAST along the vehicle ±Z axis (FindBoxedSolidHitBy), explosion entity spawned at the beam end, TakeDamageMessage (zone 0) at the struck entity. Authored DamageAmount is per-second; the ctor pre-multiplies by the firing cycle (laserDuration + pulseDuration) so each completed cycle delivers one quantum. Charge drains per cycle; type Laser.

RivetGun (RIVET.cpp): a real projectile entity. On striking a MOVER the authored amount is scaled kinetically: amount ×= (|v_closing|/350)² — rear-ending a fleeing VTV tickles, a head-on rivet hurts. Static world hits use the authored amount. Explosion spawned at impact; zone 0.

DemoPack (DEMOPACK.cpp): deployed charge. On detonation every entity within blastRadius takes the FULL authored amount (no falloff), type Explosive, damageForce = normalized blast direction × concussiveForce (the knockback shove). Compare the engine's generic Explosion splash (EXPLODE.CPP), which decays burstCount/r^1.2 — the demo pack is a flat binary yes/no blast.

Collisions — the star of the show (VTV.cpp MoveVTV + MUNGA/MOVER.CPP): the per-frame integrator collides the hull volume (ray-cast ahead when moving faster than one hull diameter per frame).

  • Static world (walls/terrain): StaticBounce computes the energy loss = the damage amount; a keel-angle grace applies — surface normals within 45° of the keel (collisionNormal.y > 0.7) take bottomArmorScale (authored, default 0.1×) — belly scrapes are cheap, wall slams are not.
  • VTV-on-VTV ram: the RAMMER computes DynamicBounce damage locally, applies it to itself, and dispatches the same record at the victim. The victim's handler does NOT trust the amount: it re-runs DynamicBounce from its own frame (relative velocity vs the rammer, its own elasticity), ignores the message if the relative motion doesn't oppose the passed normal, and applies its own bounce physics. Both sides also enter each other's revenge queue.
  • Doors: velocity is adjusted by the door's motion; caught between both doors = CRUSH (the closing-door instant-kill).
  • Out of world: leaving the arena volume = instant full Explosive budget (1/scale) + a random tumble.
  • Suicide: the Kavorkian button (authentic name) applies exactly the full Explosive budget to zone 0.

Knockback: non-collision damage applies damageForce / moverMass to the victim's velocity — lasers, rivets and demo packs physically shove the target. While BURNING, collision forces convert to torque (the death tumble, clamped ±20).

3. Armor calibration — physics, not points

The authored .dmg per-type points stream in as in BT, but the VTV zone ctor overwrites the Collision scale from the vehicle's physics:

scale[Collision] = 2000 / (mass × (maxImpactSpeed/3.6)² × (1  e²))

i.e. the hull is calibrated so a full-speed impact (authored MaxImpactSpeed, km/h → m/s) spends exactly the whole budget — armor is defined in impact-speed terms, matched to what StaticBounce/ DynamicBounce return (kinetic energy with the elastic fraction restituted). The companion constant

deathConstant = 2000 × deathScoreLoss / ((1  e²) × deathSpeed² × mass)

converts collision damage to SCORE at the same exchange rate: dying at DeathSpeed costs exactly DeathScoreLoss points.

4. The victim — VTV::TakeDamageMessageHandler + zone override

Handler order: (1) look up the inflicting entity's player and push an OffensivePlayer{player, damageType, timestamp} onto OUR revenge queue; (2) Collision → local DynamicBounce recompute (see above), else → knockback impulse; (3) chain to the Entity base → zone 0.

VTV__DamageZone::TakeDamage then:

  1. Runs the engine armor model.
  2. damageLevel ≥ 1.0VTV::BurningState — the vehicle is DEAD but keeps tumbling physically (burn + torque) until reset.
  3. On the ALIVE→BURNING transition, sends VehicleDeadMessage to the linked player.
  4. Forwards a COPY of every damage record to the linked RPPlayer — the score pipe. Damage and score are the same event in RP.

5. The score economy — RPPlayer::TakeDamageMessageHandler

  • Victim: score_loss = damageAmount (Collision × deathConstant), posted as DamageScoreLossPointTypedamage is score loss.
  • The revenge queue (offensivePlayerList, entries expire after 2.0 s) is consulted: the TOP attacker — most recent player to damage us — gains the same amount as DamageBonusPointType (sign-flipped by the team/deathBonus rules; negatives become drop-zone-hit penalties), and the arcade console is notified (ConsolePlayerVTVDamagedMessage).
  • On vehicle death the same queue decides KILL credit (VehicleDeadMessageHandler), then the VTV runs DeathShutdown (FootballReset for Crusher/Blocker roles, RegularReset otherwise) and respawns via the DropZone machinery.
  • Role twists (Martian football): the Runner carries a scoreMultiplier that DOUBLES per score-zone pass (reset to 1 on death) — damage taken during the drop-zone penalty window is scored differently; Crusher and Blocker are RPPlayer roles, not weapons — crushing is done with the hull, priced by the collision physics above.

6. RP vs BT — the design fork from one engine

Red Planet BattleTech 4.10
hull zones in combat one pool (zone 0) many, vital/leg flags, LOD shells
unaimed hit resolve none needed cylinder height×angle table
criticals / equipment none per-zone crit tables → subsystem kill
collision armor derived from physics constants authored points (halved scales for legs)
ram damage victim recomputes locally inflictor-authoritative message
knockback damageForce/mass on every hit none (gyro shake only)
death zone 0 full → Burning + tumble vital/leg zone full → mech kill
meaning of damage score transaction (2 s revenge window) armor attrition + equipment failure