diff --git a/context/combat-damage.md b/context/combat-damage.md index 8370754..337998a 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -122,9 +122,12 @@ offsets). [T2] ## The RAM economy — CLOSED (2026-07-12) [T1 evidence, T3 normalization] Three-layer story, measured live + decomp-verified: -1. **The armor economy is POINTS**: every zone streams `damageScale[5]` EVEN = `1/armorPoints` - (armor 50–140; `damageLevel += amount/armor`, 1.0 = destroyed — the `[zone-armor]` dump, - BT_DMG_LOG, mechdmg.cpp). Weapons author point-scale amounts (laser 11.77 ≈ 10 torso hits). ✓ +1. **The armor economy is POINTS**: every zone streams `damageScale[5]` — 5 cells indexed by + `damageType` DIRECTLY (Collision/Ballistic/Explosive/Laser/Energy; task #60 correction — NOT an + "even/odd slot" parity), each destructive type's cell ≈ `1/armorPoints` (armor 50–140). + `damageLevel += amount × damageScale[type]`, 1.0 = destroyed — engine `DAMAGE.cpp:379`, called + from `mechdmg.cpp:427`; the `[zone-armor]` dump, BT_DMG_LOG. Weapons author point-scale amounts + (laser 11.77 ≈ 10 torso hits). ✓ 2. **StaticBounce prices rams with the AUTHORED moverMass ≈ 1.3e6 units** → ~600×v² ≈ 59,000 points for a 10 m/s ram (measured: 59221@9.94, 398@0.81; `[collide-tx]` now logs mass/e). The binary DISPATCHES it raw (@part_012:15324-15358) — but in pod MP it landed on the local @@ -418,6 +421,23 @@ is the death `movementMode`: `IsDestroyed == (movementMode 2 || 9)` (`FUN_0049fb the death modes never revert. Fix: `IsMechDestroyed()` = `movementMode 2||9` (latch) **OR** `graphicAlarm >= 9` (the vital-kill TRIGGER that first enters the transition). Lesson: **a gauge/status alarm is never a state latch** — later writers rewrite it; latch on the state machine's own mode. +**ROOT-CAUSE now fixed AT SOURCE (task #60):** the illegitimate wreck rewrite to 4/3 came from +`mechdmg.cpp:458` gating the leg partial-failure graphic on the always-0 `IsAirborne()` stub instead +of `IsDisabled()` (`@0049fb54` = movementMode 2||9). On a wreck the binary SUPPRESSES that write; the +stub let it fire and corrupt `graphicAlarm`. Now `mech->IsDisabled()` — the latch above remains as +defence-in-depth. Sibling fix: `mechdmg.cpp:451` read the phantom `stance` (perma-0) instead of +`MovementMode()`, so the leg-shot-out → `graphicAlarm=9` fall/death branch was entirely DEAD; now live. + +## Kill-score damage (task #60) +`BTPostKillScore` (btplayer.cpp:1491) feeds the ScoreMessage `damageAmount` into the kill award +(`@0x4c02e4` → `(damageAmount + scoreAward) × roleScalar × teamMult × tonnageRatio`, `@0x4c052c`). +The port passed a flat `kShotDamage=12` (mech4.cpp:1551), so every kill scored identically regardless +of the finishing weapon. Now it passes `lastInflictingDamage` — the real killing-blow magnitude +latched in `TakeDamageMessageHandler` (mech.cpp:624), mirroring the working per-hit score path +(mech4.cpp:1207). `scoreAward=0` (the authentic kill BONUS) stays an open bring-up gap — it lives in +the un-exported master-perf writer (0x4a9770-0x4ab188 [T4]); the exact authentic `damageAmount` that +writer emitted (msg+0x24) is likewise unrecovered, so the killing-blow magnitude is the faithful +stand-in until it is. [T1 mechanism / T4 exact authentic value] ## Key Relationships - Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. diff --git a/context/open-questions.md b/context/open-questions.md index 46bcdb1..8655a5e 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -257,7 +257,11 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR in the task #8 synthesis) → the pod's 4-8-hit heavy-weapon pacing. The weapon-side SendDamageMessage (@004b9728) is LIVE from Emitter::FireWeapon (damageData filled incl. **damageForce = target−muzzle → the gyro's directional hit-bounce feed**); the mech4 - bring-up damage block + flat kShotDamage are retired to diag hooks. **ROOT-CAUSE find: the + bring-up damage block + flat kShotDamage are retired to diag hooks **(CORRECTED, task #60: + `kShotDamage=12` was only HALF-retired — still LIVE at the kill-score `mech4.cpp:1551` until + task #60 replaced it with `lastInflictingDamage`, the real killing-blow magnitude; only the + cross-pod force hook `mech4.cpp:3255` was env-gated. See the task #60 reconciliation below).** + **ROOT-CAUSE find: the CheckFireEdge NaN latch** -- TriggerState carries ControlsButton INTS (release −65 = a negative NaN); the binary's x87 unordered-compare treated it as "released" but an IEEE-correct float compare latched the detector shut after the first release (why the @@ -267,6 +271,38 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR LODReuseHysteresis corrected 0.82→0.33 (a double); the MP beam-kill live-verify awaits a clear-sightline spawn (mechanism identical to solo; force-dmg cross-pod cycles verified). +- **✅ DAMAGE-ECONOMY RECONCILIATION (task #60, 2026-07-13) [T1/T2].** Full 5-path audit vs the + decomp (5-finder + adversarial-verify workflow). **Clean as-is:** energy beam (`emitter.cpp`, + closed form byte-exact; `_DAT_004bafbc` dumped = x87 1e-7), autocannon (`projweap.cpp`, full + authored DamageAmount from resource +0x19C; the 0.0625 at :667 is the shooter's own gyro recoil, + not the round), zone-armor BASE model (`damageLevel += amount·damageScale[type]`, engine + DAMAGE.cpp:379, legs ×0.5, 1.0=destroyed). **3 stand-ins FIXED:** (A) `mechdmg.cpp:451` + `mech->stance` (phantom, perma-0 → the leg-shot-out fall/death branch was DEAD) → `MovementMode()` + (mech+0x40, @part_012.c:6910); (B) `mechdmg.cpp:458` `IsAirborne()` (always-0 stub) → `IsDisabled()` + (@0049fb54) — the task-#52 wreck-graphic corruption fixed AT SOURCE (was only masked by the + IsMechDestroyed latch); (C) `mech4.cpp:1551` flat `kShotDamage=12` kill-score → `lastInflictingDamage` + (real killing-blow, latched in TakeDamageMessageHandler; the score handler @0x4c02e4 derives the + whole kill award from it, so a flat 12 scored every kill identically). Phantom `int stance` slot + reused for `lastInflictingDamage`. **KB corrected:** the "damageScale[5] EVEN=1/armorPoints" + prose is a mild inaccuracy — the array is 5 cells indexed by damageType directly + (Collision/Ballistic/Explosive/Laser/Energy); the per-type value IS ≈1/armorPoints for the + destructive types (net model unchanged). See [[combat-damage]]. + - **DEFERRED (task #60-D): the missile CLUSTER model.** The port fires N flying rounds each + carrying `authored/N` damage (net salvo total = authored, so per-armor damage is correct), but + the binary (@004bcc60) fires ONE missile whose `burstCount` is a RANDOM cluster-hit roll + (`min(rand(0..N-1)+N/4, N)`, FUN_004bef78) applied to ONE cylinder-resolved zone. The port thus + loses (i) cluster-hit VARIANCE (always full N) and (ii) single-zone CONCENTRATION (N rounds + scatter across zones). Net-neutral on total armor, so LOW priority. **OPEN semantic gating the + severity:** does `burstCount` multiply ARMOR damage or only the gyro kick? DAMAGE.h:49-54 [T0] + says "times to apply the damage" (→ apply-count → port total authentic); settle by reading the + consumer chain FUN_004bef78 → FUN_004be078 → EXPLODE.cpp:209-210 (does it loop burstCount over + DamageZone::TakeDamage or apply once?). Fix only if made byte-faithful (roll clusterHits, thread + `burst` through BTPushProjectile, one damage-bearing round + rest visual-only). + - Also open (bring-up, not stand-ins): `scoreAward=0` (btplayer.cpp:1526 — the authentic kill + bonus lives in the un-exported master-perf writer 0x4a9770-0x4ab188, T4); the exact authentic + kill `damageAmount` from that same writer (msg+0x24) is unrecovered — the killing-blow magnitude + is the faithful stand-in until it is. + - **HEAT (task #9, 2026-07-11): authentic 1e7-unit heat LIVE; conduction ROUTING defect remains [T2/T3].** Landed + byte-anchored: emitters emit `heatCostToFire x 1e7 x (charge/seekV)^2` (closed form; PPC = 1.1e8 -> +632 K on its 174000-mass sink); the missing diff --git a/docs/RECONCILE.md b/docs/RECONCILE.md index 69a8298..0da88d5 100644 --- a/docs/RECONCILE.md +++ b/docs/RECONCILE.md @@ -1173,9 +1173,13 @@ flight entity. - `SetPerformance(&ProjectileWeapon::ProjectileWeaponSimulation)` — wired (ctor). - **MissileLauncher** (`mislanch.cpp/.hpp`) — `FireWeapon` (override @004bcc60) - reconstructed: `ConsumeRound()`, recoil cooldown, then spawn `missileCount` - guided `Missile::New` entities per salvo (damage already pre-split per-missile - in the ctor). Ticks via the inherited `ProjectileWeaponSimulation`; its vtable + reconstructed: `ConsumeRound()`, recoil cooldown, then spawn ONE guided + `Missile::New` entity per salvo (task #60 correction — NOT `missileCount` + entities; the single missile carries `burstCount` = the random cluster-hit + roll, and per-missile damage = authored/missileCount pre-split in the ctor). + The PORT re-expresses this as N flying rounds (net armor total identical; + loses cluster variance + single-zone concentration — see [[open-questions]] + task #60-D). Ticks via the inherited `ProjectileWeaponSimulation`; its vtable `FireWeapon` override is dispatched from there. - **Missile** (`missile.cpp/.hpp`) — flight-entity tick wiring ADDED. diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index c25b95f..7c66f2b 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -622,6 +622,10 @@ void // WRITTEN -- a reconstruction gap; the natural authentic write site is // this handler (every damage message carries the inflictor). [T3] lastInflictingID = message->inflictingEntity; + // task #60: latch the killing-blow magnitude alongside the attacker id so the + // kill score carries the REAL finishing-hit damage (mirrors the per-hit path + // mech4.cpp:1207 that already passes p.damage), not the flat kShotDamage=12. + lastInflictingDamage = message->damageData.damageAmount; // THREAT feed (task #37, the recovered reticle Execute's ThreatVector // attr 0xC): when the PLAYER takes a hit, push the attack direction -- @@ -1419,6 +1423,7 @@ Mech::Mech( frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f); // collision-damage guard snapshot ramLastVictim = 0; // ram contact-edge state ramContactLinger = 0.0f; + lastInflictingDamage = 0.0f; // task #60: killing-blow magnitude (set on hit) if (GroundReal() && GetCollisionVolumeCount() > 0 && collisionTemplate != 0 && collisionVolume != 0) { diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index f658141..3437cb2 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -1009,7 +1009,8 @@ protected: // Defined in mech4.cpp (Mech__DamageZone::vitalDamageZone is protected; Mech has access). int FirstVitalZone() const; int flags; // entity flags word (this+0x28 region) - int stance; // posture state + Scalar lastInflictingDamage; // task #60: killing-blow magnitude (port-only; reuses + // the retired phantom `stance` slot -- feeds the kill score) int ammoState; // @0x44c 0 none / 1 leaking / 2 dry AlarmIndicator graphicAlarm; // body graphic-state alarm EntityID lastInflictingID; // @0x43c last attacker (read by DamageZone routing) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 980447e..2d50c82 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1547,8 +1547,12 @@ void // boresight pick skips a dead mech, so that block never ran against // it again: internally dead, smoking, standing, invulnerable. { - // KILL score (once; the ownerless dummy yields no death for us) - BTPostKillScore((Entity *)this, kShotDamage); + // KILL score (once; the ownerless dummy yields no death for us). + // task #60: pass the REAL killing-blow magnitude (latched in the damage + // handler), not the flat bring-up kShotDamage -- the score handler + // @0x4c02e4 derives the whole kill award from this damageAmount, so a + // flat 12 made every kill score identically regardless of weapon. + BTPostKillScore((Entity *)this, lastInflictingDamage); if ((Entity *)this == gEnemyMech) gEnemyDestroyed = 1; // latches off damage score diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index 6ec0513..99da2e1 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -51,7 +51,7 @@ // FUN_00408050 (Scalar)Random -> [0,1) FUN_0040807c(rng,n) Random(n) // FUN_004ac07c Subsystem::ApplyDamage(damage) -> Scalar // FUN_004b11bc Generator::ForceShortRecovery (class derivations @0050f4bc) -// FUN_0049fb54 Mech:: +// FUN_0049fb54 Mech::IsDisabled() (movementMode 2||9; NOT a torso/leg query) // FUN_0040485c/00404720/00404088/00404190/004040d8 NotationFile readers // FUN_00402298/004022b0/004022d0 operator new / array new / delete // FUN_004dbb24/004db92c/004d9c38/004dbd4c DebugStream << / endl @@ -448,18 +448,26 @@ void if (damageLevel >= StructureMax) RecurseSegmentTable(mech); // FUN_0049cad4 } - else if ((unsigned)(mech->stance - 3) < 2) // standing/walking + else if ((unsigned)(mech->MovementMode() - 3) < 2) // live sim state 3||4 (mech+0x40) { + // task #60: the gate reads the LIVE MovementMode (mech+0x40), + // @part_012.c:6910 `*(int*)(iVar1+0x40) - 3U < 2` -- NOT the phantom + // `stance` member (no binary offset, zero writers -> perma-0, so + // this whole leg-shot-out -> fall/death branch was DEAD). if (rightLeg != 0 && damageLevel >= StructureMax) mech->graphicAlarm.SetLevel(9); // fall / leg gone else if (leftLeg != 0 && damageLevel >= StructureMax) mech->graphicAlarm.SetLevel(9); } - else if (mech->IsAirborne() == 0) // FUN_0049fb54 + else if (mech->IsDisabled() == 0) // FUN_0049fb54 (NOT disabled) { - // Non-standing stance, still grounded: a half-destroyed leg zone - // trips the partial-failure graphic (right -> 4, left -> 3). The - // oracle compares against _DAT_0049c9a0 (0.5), NOT StructureMax. + // A live (non-disabled) mech: a half-destroyed leg zone trips the + // partial-failure graphic (right -> 4, left -> 3). The oracle + // compares against _DAT_0049c9a0 (0.5), NOT StructureMax. task #60: + // the guard is IsDisabled (@0049fb54 = movementMode 2||9), NOT the + // always-0 IsAirborne stub -- on a WRECK the binary SUPPRESSES this + // write; the stub let it corrupt graphicAlarm 9->4/3 (the task-#52 + // wreck-graphic bug, now fixed at source not just masked). if (rightLeg != 0 && damageLevel >= LegHalfStructure) mech->graphicAlarm.SetLevel(4); else if (leftLeg != 0 && damageLevel >= LegHalfStructure) diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index efdb8ed..7bfe790 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -610,11 +610,12 @@ void // -- the weapon-fire damage submission into the owning mech's // SubsystemMessageManager (mech+0x434 -> AddDamageMessage @0049b6d8), NOT a // HUD pip. The "HUD element record" reading below was the same 0x434 -// misattribution the mapper suffered. The authentic weapon-side submission -// (per-weapon damageData + inflictingSubsystemID) is pending the -// damage-economy reconciliation (authored 0.25-scale amounts vs the -// bring-up kShotDamage=12); the interim submitter is the mech4 fire block -// routing through AddDamageMessage. This body stays dormant meanwhile. +// misattribution the mapper suffered. This body is the LIVE per-shot +// beam-damage path (called from Emitter::FireWeapon, emitter.cpp:289; decomp +// part_013.c:7758-7764) carrying the authentic per-weapon damageData + +// inflictingSubsystemID -- the damage-economy reconciliation (task #60) +// CONFIRMED the authored-DamageAmount economy; kShotDamage is retired to the +// kill-score and env-gated diag hooks (task #60). // void MechWeapon::SendDamageMessage(Entity *target) diff --git a/game/reconstructed/mislanch.hpp b/game/reconstructed/mislanch.hpp index 807437a..e22b53d 100644 --- a/game/reconstructed/mislanch.hpp +++ b/game/reconstructed/mislanch.hpp @@ -169,9 +169,12 @@ class Missile; protected: // @004bcc60 (ProjectileWeapon spawn path) -- builds the streamed-entity - // spawn descriptor for a Missile (entity ClassID 0xBD0), seeds it with - // muzzleVelocity + the per-missile Damage, and hands it to the entity - // manager. Invoked missileCount times per trigger pull. + // spawn descriptor for ONE Missile (entity ClassID 0xBD0), seeds it with + // muzzleVelocity + the per-missile Damage (authored/missileCount) and the + // burstCount cluster-hit roll, and hands it to the entity manager. + // (task #60 correction: ONE spawn per trigger pull, NOT missileCount + // spawns -- burstCount carries the cluster count to the single missile; + // the PORT re-expresses this as N flying rounds, see mislanch.cpp.) void FireWeapon();