the crit system, complete: two gap functions recovered, the dead sink revived, and a wrong verdict reversed (#80)

The whole critical-hit pipeline was dark, three layers deep, and one of those
layers had fooled us into a false conclusion about the 1995 binary itself.

LAYER 1 -- the trigger, recovered from the un-exported gap. The Mech MESSAGE
TABLE at 0x50bdf8 ({id, name, handler} rows) names the real
Mech::TakeDamageMessageHandler at 0x4a0230 -- message 0x12 "TakeDamage" --
plus seven sibling handlers (PlayerLink, RealMaxSpeed, BalanceCoolant,
Set/ClearBurningState, EjectPilot, DuckRequest). Inside it, the crit chance
at 0x4a0164: p = clamp(0.7 * damageLevel^2 + 0.01, 0..1), gated on the
player's simLive flag (+0x25c -- novice never crits), rolled PER BURST on the
current zone, skipping a zone already burning. Chance is ~1% on fresh armour,
~18% at half-stripped, ~58% at 90% -- crits arrive exactly as armour fails.

The handler's application loop replaces the engine base's single call, which
ignored burstCount entirely (multi-burst damage under-applied (burst-1)x).
Faithful shape: per burst, crit-roll -> CriticalHit @0049ccc4 (which routes
half the amount through the armour internally and picks ONE critical
subsystem by criticalWeight) else zone->TakeDamage -- then RE-RUN the
cylinder lottery from the impact point for the next burst, stopping early
once the mech is disabled. Multi-burst damage sprays across zones by design.

LAYER 2 -- the sink. MechSubsystem::TakeDamage was an empty btstubs stand-in;
the real body is at 0x4ac0bc (CLASSMAP had that address mislabeled
"HandleMessage"): zone damage, then on level >= 1.0 the Destroyed alarm, the
PrintState gate, the 1.0 pin, and -- for a vital subsystem -- the owner
mech's graphicAlarm to level 9, the same fall/death level the leg path
raises. That is the #28 vital-subsystem kill machinery, now real.

LAYER 3 -- the one that rewrites yesterday. The subsystem ctor DID copy
armour points + per-type scales into the private zone -- through the
ReconDamageZone PROXY, whose fields sit at struct offsets +4/+8, not the
binary's +0x140/+0x144. The floats landed on the engine object's header and
the real damageScale[] stayed zero. The 2026-07-28 experiment that "proved"
subsystem zones cannot be damaged -- and that the Myomers un-powered
self-repair was dead code in the original -- was measuring exactly this port
bug. Both verdicts reversed: the binary ctor (0x4ac7bb) initializes the zone
from the resource keys WeaponDamagePoints (required) + CriticalHitScoreBonus
(required) + Collision/Ballistic/Explosive/Laser/EnergyDamagePoints, none of
which the CSS parsed. Now parsed (with the binary's own error strings), and
the ctor writes the engine's NAMED members -- layout-parity holds, so they
land on +0x140/+0x144 faithfully. The Myomers repair branch is LIVE, in 1995
and here. KB corrected and swept (combat-damage, subsystems WAVE 6,
myomers.cpp, CLASSMAP).

Live-verified twice: [subarmor] prints real parsed scales for every subsystem
at spawn (HeatSink pts=10 scale=0.1x5, Condensers pts=5 scale=0.2x5, ...);
[critroll] landed full-chain crits in both runs (zone -> weighted pick ->
subsystem's own zone driven to 1.0 -> Destroyed); mech death/respawn and the
ammo gates un-regressed; zero crashes/asserts. Honest gaps: burst>1 spraying
is transcribed but not yet exercised live (self-damage fires burst=1), the
damageType==0 COLLISION divert (@0x49ffcc) is documented-not-reconstructed,
and the id-0x16 damage/kill report messages to the players (the authentic
stats plumbing, decoded to field level in the KB) are deferred to the #45
work.

Diags: BT_CRIT_LOG ([subarmor] + [critroll]), the existing BT_DMG_LOG.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-29 10:08:33 -05:00
co-authored by Claude Fable 5
parent f7cf9850b1
commit a5fb96ae96
10 changed files with 352 additions and 74 deletions
+10 -17
View File
@@ -496,23 +496,16 @@ void Myomers::MyomersSimulation(Scalar time_slice)
// so the READING is "a myomer you power down slowly heals, and the `< 1.0`
// test stops a destroyed zone being resurrected".
//
// ⚠ BUT IT IS INERT -- IN THE ORIGINAL TOO. [T0, measured 2026-07-28]
// A subsystem's private zone is built by the 2-arg trivial ctor
// `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes ALL FIVE
// damageScale[] entries; DamageZone::Reset never touches them, and the only
// other writer in the codebase is Mech__DamageZone (the MECH's streamed
// zones, a different class -- mechdmg.cpp:246-253). So damageScale stays
// {0,0,0,0,0} for the life of the zone and the sum above is always
// `damageLevel += amount * 0.0f` == no change. Confirmed live: seeded to
// 0.6, held at NoVoltage for ~1500 ticks, damageLevel never moved off 0.6
// (BT_MYOMERS_REPAIR_TEST below). The crit path reaches subsystem damage a
// different way -- it writes damageLevel DIRECTLY (mechsub.cpp
// DistributeCriticalHit pins *(this[0x38]+0x158) = 1.0f) -- which is why
// subsystems can still be destroyed even though this route cannot.
//
// Reconstructed anyway, and deliberately NOT "fixed": this is what the 1995
// binary does, and inventing a working repair here would be a behavior we
// made up. Kept so the dead branch is visible rather than silently missing.
// ⚠ CORRECTION (2026-07-29, #80): the earlier "inert in the original too"
// note here was WRONG about the original. The 2026-07-28 experiment that
// froze at 0.6 was measuring a PORT bug: the subsystem ctor wrote the
// zone's armour/scales through the ReconDamageZone PROXY (struct offsets
// +4/+8) instead of the engine members at +0x140/+0x144, so the real
// damageScale[] stayed zero. The binary's ctor (@0x4ac7bb) initializes
// them from the resource ("WeaponDamagePoints" + the five per-type
// "...DamagePoints" keys), and the port now does the same -- so this
// branch is LIVE, in 1995 and here: an un-powered, not-yet-destroyed
// myomer heals at 0.011 * damageScale[Explosive] per tick.
// (impactPoint/burstCount are set for fidelity; the callee ignores both.)
if (electricalStateAlarm.GetLevel() == PoweredSubsystem::NoVoltage // this+0x278 == 1
&& DamageStructureLevel() < 1.0f) // zone+0x158 < _DAT_004b8d10