Files
BT411/game
Joe DiPrimaandClaude Opus 5 e6103d02b2 #97: leak gauge reaches all THREE levels -- the normalisation was dropped with an x87 expression
Players: "you can get up to three traingles which drains really quickly" ...
"only seeing one level (lowest) right now" (Oracle), corroborated by Draco
("3 triple delta being the top level").

The display was never the problem: BitMapInverseWipe is already built with
frames=3 and stacks three segments at two levels each (0..6).  The LEVEL
computation was wrong.

Our Execute rounded the raw leak rate:
    level = round(value)
The binary's @004c5d08 does NOT.  Ghidra renders the round as a bare
`FUN_004dcd94()` because it drops the x87 expression feeding __ftol -- exactly
KB gotcha 19, which we already had written down.  The real prologue, disassembled:

    fild  dword ptr [ebp-0x14]    ; ST0 = (float)fullWidth   (frames*2 = 6)
    fmul  dword ptr [ebx+0xb4]    ; ST0 *= value             (CoolantMassLeakRate)
    fdiv  dword ptr [ebx+0xb0]    ; ST0 /= third             (full-scale divisor)
    call  0x4dcd94                ; level = round(ST0)

i.e. level = round(fullWidth * leakRate / fullScale).

Without the normalisation the gauge rounded a value that never exceeds ~1.0
(coolantDraw = zoneDamage * heatLoad, and heatLoad is clamp(0.002*T, 0, 1) --
constants re-verified from the image, including the 80-bit extended 0.002).  So
level could only ever be 0, or 1 via the >0.0025 floor: ONE triangle, always, no
matter how bad the leak.  Three of the six condensers being mis-lamped (#98) hid
how systematic this was.

Also corrected on the way:
  * `third` was declared int but the binary FDIVs it -- it is a float.  The two
    call sites passed *(int *)(subsystem+0x150), i.e. the BIT PATTERN of a float
    read from a RAW OFFSET into our own layout (the databinding trap): at +0x150
    our layout has filterDecay, not the binary's field.  Now resolved through a
    complete-type bridge, BTHeatSinkLeakFullScale.
  * filterDecay was initialised 0.4f; @004b8fec writes 0.15f (param_1[0x54] =
    0x3e19999a).  It is written-once-never-read elsewhere in the port, so the
    correction is behaviourally safe and it IS the gauge's full scale.

Verified live: Condenser6 at 10% zone damage -> draw 0.0358 -> level 1 (one
triangle); the same formula reaches all three at heavier damage, and the mapping
spans draw 0.0026 (half of one) to 0.15 (three full).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:54:13 -05:00
..