From f44cdbaccadc326ed875ca2dbc8e2ed4a898d29b Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 6 Aug 2026 09:38:16 -0500 Subject: [PATCH] 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) --- docs/DAMAGE-MODEL.md | 172 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 docs/DAMAGE-MODEL.md diff --git a/docs/DAMAGE-MODEL.md b/docs/DAMAGE-MODEL.md new file mode 100644 index 0000000..5c4aee6 --- /dev/null +++ b/docs/DAMAGE-MODEL.md @@ -0,0 +1,172 @@ +# 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.0` → `VTV::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 `DamageScoreLossPointType` — **damage 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 |