BT410 5.3.89: the hit-location cylinder MEASURED -- 18 tables, 8 distinct, and two chassis that never twist
The operator recalled the damage model as "a pie wedged cylinder" and asked how it maps across the mechs. It is exactly that, and the shipped data is now extracted rather than described. dmgscan.py brute-forces every offset in BTL4.RES and accepts a candidate only if the ENTIRE nested type-29 structure parses -- thresholds strictly ascending and terminating at exactly 1.0, zone indices in range, names NUL-terminated. A wrong format guess cannot survive that, so finding exactly 18 tables -- the count DAMAGE-MODEL.md already claimed from an independent reversal -- is a confirmation of the format, not a coincidence. MEASURED: 18 tables, every one 7 bands x 8 wedges = 56 cells. Only EIGHT are distinct by content; the other ten are duplicates. 22 zones 4 twisting x3 Avatar / Mad Cat class -- table A 22 zones 4 twisting x2 Avatar / Mad Cat class -- table B 21 zones 4 twisting x3 Loki 22 zones 4 twisting x2 Thor 21 zones 4 twisting x2 SND2 24 zones 4 twisting x2 Battlemaster / Vulture 20 zones 0 twisting x2 Black Hawk 17 zones 0 twisting x2 Owens BLACK HAWK AND OWENS ROTATE NO BAND WITH THE TORSO. Every other chassis rotates its upper four. That is a real behavioural difference in the shipped data, not an absence of it. The zone COUNTS match the per-chassis .SKL dz_ sets exactly, which is what lets a table be fingerprinted back to a chassis. It is not always unique -- Avatar and Mad Cat share a zone set but have two DIFFERENT tables, and no chassis name sits near the stream, so they are recorded A/B rather than guessed. Stated as undetermined in both the notes and the visual. THE GEOMETRY, now named: 18 wedge names in six anatomical rings (Foot, Leg, Hip, Waist, Chest, Top). Slot 0 starts at angle 0 spanning 45 degrees, so under atan2(z,x) the mech's +X is right and +Z is front. Each named face covers TWO adjacent wedges (Right = 7,0 / Front = 1,2 / Left = 3,4 / Rear = 5,6) -- so dead ahead is the SEAM between two Front cells, never the centre of one. Bands 0-2 are chassis-fixed; the live torso twist is added to the impact angle before the wedge pick on the rest. And the scatter is generous in a way worth knowing at the controls: a clean foot-wedge hit is only 50% that foot, 30% the lower leg, and 20% of the time the OTHER foot entirely. ALSO: an interactive plate of all of it -- every cell of all 8 tables, plan and elevation, and a twist slider that rotates the upper bands live -- published for the playtesters. Its dataset regenerates from dmgscan.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
#
|
||||||
|
# dmgscan -- extract the type-29 cylinder hit-location tables from BTL4.RES.
|
||||||
|
#
|
||||||
|
# The unaimed-damage model is a cylinder around the mech: 7 height BANDS x 8
|
||||||
|
# angular WEDGES, and each of the 56 cells carries its own cumulative
|
||||||
|
# distribution over that chassis's armor zones. DMGTABLE.CPP resolves a hit
|
||||||
|
# by height -> band, atan2(z,x) -> wedge, then one uniform roll down the
|
||||||
|
# cell's table.
|
||||||
|
#
|
||||||
|
# The stream format is documented in source410/BT/DMGTABLE.CPP and this
|
||||||
|
# scanner is the check on it: it brute-forces every offset in the resource and
|
||||||
|
# only accepts a candidate whose ENTIRE nested structure parses -- thresholds
|
||||||
|
# strictly ascending and terminating at exactly 1.0, zone indices in range,
|
||||||
|
# names NUL-terminated. A wrong format guess cannot survive that, which is
|
||||||
|
# why finding exactly 18 tables (the count the notes already claimed, from an
|
||||||
|
# independent reversal) is a real confirmation rather than a coincidence.
|
||||||
|
#
|
||||||
|
# Findings, 2026-07-30:
|
||||||
|
# * 18 tables, every one 7 rows x 8 cells.
|
||||||
|
# * Only 8 are DISTINCT by content; the rest are duplicates.
|
||||||
|
# * Bands 0-2 are chassis-fixed, bands 3-6 rotateWithTorso -- except on
|
||||||
|
# Black Hawk and Owens, where NO band rotates.
|
||||||
|
# * 18 wedge names in 6 anatomical rings: Foot, Leg, Hip, Waist, Chest, Top.
|
||||||
|
# * Zone counts (17/20/21/22/24) match the per-chassis .SKL dz_ sets exactly,
|
||||||
|
# which is what lets a table be fingerprinted back to a chassis family.
|
||||||
|
#
|
||||||
|
# Usage: python3 dmgscan.py [path/to/BTL4.RES]
|
||||||
|
#
|
||||||
|
# Format (DMGTABLE.CPP, byte-verified against the real streams):
|
||||||
|
# Table { i32 rowCount; Row[rowCount] }
|
||||||
|
# Row { i32 rotateWithTorso; i32 cellCount; Cell[cellCount] }
|
||||||
|
# Cell { i32 nameLen; char name[nameLen]; u8 0; i32 entryCount; Entry[] }
|
||||||
|
# Entry { f32 cumulativeThreshold; i32 zoneIndex }
|
||||||
|
#
|
||||||
|
import struct, sys, json
|
||||||
|
|
||||||
|
d = open(sys.argv[1] if len(sys.argv) > 1 else r'../ALPHA_1/REL410/BT/BTL4.RES', 'rb').read()
|
||||||
|
N = len(d)
|
||||||
|
u32 = lambda o: struct.unpack_from('<I', d, o)[0]
|
||||||
|
i32 = lambda o: struct.unpack_from('<i', d, o)[0]
|
||||||
|
f32 = lambda o: struct.unpack_from('<f', d, o)[0]
|
||||||
|
|
||||||
|
def cell(o):
|
||||||
|
if o + 4 > N: return None
|
||||||
|
ln = i32(o)
|
||||||
|
if not (0 <= ln <= 64): return None
|
||||||
|
o2 = o + 4
|
||||||
|
if o2 + ln + 1 > N: return None
|
||||||
|
nm = d[o2:o2+ln]
|
||||||
|
if ln and not all(32 <= c < 127 for c in nm): return None
|
||||||
|
if d[o2+ln] != 0: return None
|
||||||
|
o2 += ln + 1
|
||||||
|
if o2 + 4 > N: return None
|
||||||
|
ec = i32(o2); o2 += 4
|
||||||
|
if not (1 <= ec <= 40): return None
|
||||||
|
if o2 + 8*ec > N: return None
|
||||||
|
ents, prev = [], -1.0
|
||||||
|
for k in range(ec):
|
||||||
|
t = f32(o2); z = i32(o2+4); o2 += 8
|
||||||
|
if not (0.0 < t <= 1.0001) or t < prev - 1e-6: return None
|
||||||
|
if not (0 <= z < 64): return None
|
||||||
|
ents.append((t, z)); prev = t
|
||||||
|
if abs(ents[-1][0] - 1.0) > 1e-3: return None
|
||||||
|
return nm.decode('ascii', 'ignore'), ents, o2
|
||||||
|
|
||||||
|
def row(o):
|
||||||
|
if o + 8 > N: return None
|
||||||
|
rt = i32(o); cc = i32(o+4)
|
||||||
|
if rt not in (0, 1): return None
|
||||||
|
if not (1 <= cc <= 32): return None
|
||||||
|
o2 = o + 8; cells = []
|
||||||
|
for _ in range(cc):
|
||||||
|
c = cell(o2)
|
||||||
|
if c is None: return None
|
||||||
|
cells.append((c[0], c[1])); o2 = c[2]
|
||||||
|
return rt, cells, o2
|
||||||
|
|
||||||
|
def table(o):
|
||||||
|
if o + 4 > N: return None
|
||||||
|
rc = i32(o)
|
||||||
|
if not (2 <= rc <= 32): return None
|
||||||
|
o2 = o + 4; rows = []
|
||||||
|
for _ in range(rc):
|
||||||
|
r = row(o2)
|
||||||
|
if r is None: return None
|
||||||
|
rows.append((r[0], r[1])); o2 = r[2]
|
||||||
|
return rows, o2
|
||||||
|
|
||||||
|
out, o = [], 0
|
||||||
|
while o < N - 8:
|
||||||
|
t = table(o)
|
||||||
|
if t and len(t[0]) >= 3 and sum(len(r[1]) for r in t[0]) >= 12:
|
||||||
|
out.append((o, t[0])); o = t[1]
|
||||||
|
else:
|
||||||
|
o += 1
|
||||||
|
print("tables found: %d" % len(out))
|
||||||
|
for off, rows in out:
|
||||||
|
cells = sum(len(r[1]) for r in rows)
|
||||||
|
rot = sum(1 for r in rows if r[0])
|
||||||
|
zs = sorted({z for r in rows for _, es in r[1] for _, z in es})
|
||||||
|
print(" @%#08x %d rows x %d cells (%d) torso-rotating rows: %d zones used: %d"
|
||||||
|
% (off, len(rows), len(rows[0][1]), cells, rot, len(zs)))
|
||||||
|
json.dump([{'off': o, 'rows': [{'rot': r[0], 'cells': [{'name': c[0], 'entries': c[1]}
|
||||||
|
for c in r[1]]} for r in rows]} for o, rows in out],
|
||||||
|
open('dmgtables.json', 'w'))
|
||||||
@@ -1,197 +1,261 @@
|
|||||||
# BT 4.10 — the damage model, end to end
|
# BT 4.10 — the damage model, end to end
|
||||||
|
|
||||||
How a shot becomes lost armor, dead equipment, and a mech kill. Synthesized
|
How a shot becomes lost armor, dead equipment, and a mech kill. Synthesized
|
||||||
from the surviving 1995 engine source (`CODE/RP/MUNGA/`), the reconstructed
|
from the surviving 1995 engine source (`CODE/RP/MUNGA/`), the reconstructed
|
||||||
BT-side TUs (`restoration/source410/BT/`), and the BT411 binary reversal.
|
BT-side TUs (`restoration/source410/BT/`), and the BT411 binary reversal.
|
||||||
Per-TU depth lives in the sibling `*.NOTES.md` files; this doc is the
|
Per-TU depth lives in the sibling `*.NOTES.md` files; this doc is the
|
||||||
cross-cutting flow.
|
cross-cutting flow.
|
||||||
|
|
||||||
```
|
```
|
||||||
weapon fire ──► Damage record ──► TakeDamageMessage ──► victim handler
|
weapon fire ──► Damage record ──► TakeDamageMessage ──► victim handler
|
||||||
│ (unaimed? cylinder table)
|
│ (unaimed? cylinder table)
|
||||||
▼
|
▼
|
||||||
damageZones[zone]->TakeDamage
|
damageZones[zone]->TakeDamage
|
||||||
│
|
│
|
||||||
armor economy: level += amount × scale[type]
|
armor economy: level += amount × scale[type]
|
||||||
│
|
│
|
||||||
┌──────────────────┬────────────────────────┤
|
┌──────────────────┬────────────────────────┤
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
level ≥ 1.0 Energy special level < 1.0
|
level ≥ 1.0 Energy special level < 1.0
|
||||||
zone destroyed (generator short) (leg ≥ 0.5 → limp)
|
zone destroyed (generator short) (leg ≥ 0.5 → limp)
|
||||||
│
|
│
|
||||||
┌──────────┼──────────────┐
|
┌──────────┼──────────────┐
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
VITAL LEG other zone
|
VITAL LEG other zone
|
||||||
mech kill mech down SendSubsystemDamage (crit allotments → equipment)
|
mech kill mech down SendSubsystemDamage (crit allotments → equipment)
|
||||||
+ RecurseSegmentTable (SIBS / DESCEND)
|
+ RecurseSegmentTable (SIBS / DESCEND)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 1. The Damage record (engine: `MUNGA/DAMAGE.HPP`)
|
## 1. The Damage record (engine: `MUNGA/DAMAGE.HPP`)
|
||||||
|
|
||||||
Every hit travels as one `Damage` struct:
|
Every hit travels as one `Damage` struct:
|
||||||
|
|
||||||
| field | meaning |
|
| field | meaning |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `damageType` | 0 Collision · 1 Ballistic · 2 Explosive · 3 Laser · 4 Energy |
|
| `damageType` | 0 Collision · 1 Ballistic · 2 Explosive · 3 Laser · 4 Energy |
|
||||||
| `damageAmount` | points (the only field in the armor formula) |
|
| `damageAmount` | points (the only field in the armor formula) |
|
||||||
| `damageForce` | impulse vector (physics/feel, not armor) |
|
| `damageForce` | impulse vector (physics/feel, not armor) |
|
||||||
| `surfaceNormal`, `impactPoint` | world-space impact geometry |
|
| `surfaceNormal`, `impactPoint` | world-space impact geometry |
|
||||||
| `burstCount` | "times to apply" — **NOT in the armor formula**; one message = one application. Feeds splash falloff and the gyro bounce only. |
|
| `burstCount` | "times to apply" — **NOT in the armor formula**; one message = one application. Feeds splash falloff and the gyro bounce only. |
|
||||||
|
|
||||||
## 2. Producers — where Damage records are born
|
## 2. Producers — where Damage records are born
|
||||||
|
|
||||||
**Beam weapons** (`EMITTER.CPP`, PPC/lasers): instant-hit at the owner's
|
**Beam weapons** (`EMITTER.CPP`, PPC/lasers): instant-hit at the owner's
|
||||||
current target when `rangeToTarget <= effectiveRange`. The discharge energy
|
current target when `rangeToTarget <= effectiveRange`. The discharge energy
|
||||||
splits by the authored ratio `damageFraction = dmg/(dmg+heat)`; the delivered
|
splits by the authored ratio `damageFraction = dmg/(dmg+heat)`; the delivered
|
||||||
amount scales with charge: `damagePortion = authored × chargeRatio²` (an
|
amount scales with charge: `damagePortion = authored × chargeRatio²` (an
|
||||||
undercharged PPC hits soft). The heat portion goes into the FIRER's own heat
|
undercharged PPC hits soft). The heat portion goes into the FIRER's own heat
|
||||||
sinks. Delivery = `MechWeapon::SendDamage`.
|
sinks. Delivery = `MechWeapon::SendDamage`.
|
||||||
|
|
||||||
**Ballistic / missile weapons** (`PROJWEAP.CPP`, `MISLANCH.CPP`,
|
**Ballistic / missile weapons** (`PROJWEAP.CPP`, `MISLANCH.CPP`,
|
||||||
`MISSILE.CPP`): the weapon FSM's Loaded case pulls ammo (`FeedAmmo`), checks
|
`MISSILE.CPP`): the weapon FSM's Loaded case pulls ammo (`FeedAmmo`), checks
|
||||||
jam, then `FireWeapon` spawns ONE cluster `Missile` entity per salvo
|
jam, then `FireWeapon` spawns ONE cluster `Missile` entity per salvo
|
||||||
(salvo-split `damageAmount`, `burstCount` = missile count). The round flies
|
(salvo-split `damageAmount`, `burstCount` = missile count). The round flies
|
||||||
guided (seeker + thruster); the proximity fuse delivers the whole record
|
guided (seeker + thruster); the proximity fuse delivers the whole record
|
||||||
once, `impactPoint` = the round's world position, zone = −1 (unaimed).
|
once, `impactPoint` = the round's world position, zone = −1 (unaimed).
|
||||||
|
|
||||||
**Explosions / splash** (`MUNGA/EXPLODE.CPP`): a boxed splash volume gathers
|
**Explosions / splash** (`MUNGA/EXPLODE.CPP`): a boxed splash volume gathers
|
||||||
movers + cultural objects sorted by distance; each target gets
|
movers + cultural objects sorted by distance; each target gets
|
||||||
`burstCount = original / r^1.2` (min 1), force along the radius vector,
|
`burstCount = original / r^1.2` (min 1), force along the radius vector,
|
||||||
zone = −1.
|
zone = −1.
|
||||||
|
|
||||||
**Collisions** (`MUNGA/MOVER.CPP` `ProcessCollisionList`): type Collision,
|
**Collisions** (`MUNGA/MOVER.CPP` `ProcessCollisionList`): type Collision,
|
||||||
amount computed by the bounce resolver (`StaticBounce` from velocity,
|
amount computed by the bounce resolver (`StaticBounce` from velocity,
|
||||||
elasticity, friction), impact point from the collision slice. Multiple
|
elasticity, friction), impact point from the collision slice. Multiple
|
||||||
same-frame collisions are averaged, damage summed.
|
same-frame collisions are averaged, damage summed.
|
||||||
|
|
||||||
## 3. Delivery — `Entity::TakeDamageMessage` (`ENTITY3.HPP`)
|
## 3. Delivery — `Entity::TakeDamageMessage` (`ENTITY3.HPP`)
|
||||||
|
|
||||||
Fields: `inflictingEntity`, `damageZone` (+`invalidDamageZone` = zone < 0),
|
Fields: `inflictingEntity`, `damageZone` (+`invalidDamageZone` = zone < 0),
|
||||||
`damageData`, `inflictingSubsystemID` (so the BT message manager can bundle
|
`damageData`, `inflictingSubsystemID` (so the BT message manager can bundle
|
||||||
explosion resource IDs). Dispatched AT the victim entity.
|
explosion resource IDs). Dispatched AT the victim entity.
|
||||||
|
|
||||||
The authored contract (ENTITY3.HPP warning): **only reticle-based (aimed)
|
The authored contract (ENTITY3.HPP warning): **only reticle-based (aimed)
|
||||||
weapons carry a valid zone**. Everything else — missiles, splash, rams —
|
weapons carry a valid zone**. Everything else — missiles, splash, rams —
|
||||||
arrives zone = −1 and must be resolved by the victim.
|
arrives zone = −1 and must be resolved by the victim.
|
||||||
|
|
||||||
The attacker also posts a `ScoreInflicted` message to its own player per
|
The attacker also posts a `ScoreInflicted` message to its own player per
|
||||||
delivered hit (the damage score).
|
delivered hit (the damage score).
|
||||||
|
|
||||||
## 4. Victim routing — `Mech::TakeDamageMessageHandler` (`MECH.CPP`)
|
## 4. Victim routing — `Mech::TakeDamageMessageHandler` (`MECH.CPP`)
|
||||||
|
|
||||||
Binary hub @004a0230, in order:
|
Binary hub @004a0230, in order:
|
||||||
|
|
||||||
1. Feed the RAW record to the gyro (cockpit bounce — even an invalid-zone
|
1. Feed the RAW record to the gyro (cockpit bounce — even an invalid-zone
|
||||||
hit shakes the pilot). *(staged: feel wave)*
|
hit shakes the pilot). *(staged: feel wave)*
|
||||||
2. Latch `lastInflictingID` — keys the LOD damage-clustering below.
|
2. Latch `lastInflictingID` — keys the LOD damage-clustering below.
|
||||||
3. **Unaimed resolve**: if `invalidDamageZone`, map `impactPoint` through the
|
3. **Unaimed resolve**: if `invalidDamageZone`, map `impactPoint` through the
|
||||||
cylinder hit-location table (`DMGTABLE.CPP`, type-29 resource, cached
|
cylinder hit-location table (`DMGTABLE.CPP`, type-29 resource, cached
|
||||||
mech+0x444): world → mech-local; local height picks a ROW (feet rows are
|
mech+0x444): world → mech-local; local height picks a ROW (feet rows are
|
||||||
chassis-fixed, upper rows add the LIVE torso twist to the impact angle);
|
chassis-fixed, upper rows add the LIVE torso twist to the impact angle);
|
||||||
`atan2(z,x)` picks the angular CELL; a uniform roll walks the cell's
|
`atan2(z,x)` picks the angular CELL; a uniform roll walks the cell's
|
||||||
cumulative distribution (the BattleTech dice scatter) → hull zone.
|
cumulative distribution (the BattleTech dice scatter) → hull zone.
|
||||||
4. Chain to `Entity::TakeDamageMessageHandler`: zone −1 is DROPPED (base
|
4. Chain to `Entity::TakeDamageMessageHandler`: zone −1 is DROPPED (base
|
||||||
contract), otherwise `damageZones[zone]->TakeDamage(damageData)`.
|
contract), otherwise `damageZones[zone]->TakeDamage(damageData)`.
|
||||||
|
|
||||||
## 5. The hull zone — `Mech__DamageZone::TakeDamage` (`MECHDMG.CPP`)
|
## 5. The hull zone — `Mech__DamageZone::TakeDamage` (`MECHDMG.CPP`)
|
||||||
|
|
||||||
**Artifact (LOD) zones** — a zone with a non-empty redirect table is a
|
**Artifact (LOD) zones** — a zone with a non-empty redirect table is a
|
||||||
low-LOD hull shell: it never takes damage itself, it routes to a real child
|
low-LOD hull shell: it never takes damage itself, it routes to a real child
|
||||||
zone. Same-attacker clustering: within 0.25 s the SAME child is hit again;
|
zone. Same-attacker clustering: within 0.25 s the SAME child is hit again;
|
||||||
past 10 s re-roll; in between, 33 % reuse. The artifact's displayed level =
|
past 10 s re-roll; in between, 33 % reuse. The artifact's displayed level =
|
||||||
mean of its children.
|
mean of its children.
|
||||||
|
|
||||||
**Real zones — the armor economy**:
|
**Real zones — the armor economy**:
|
||||||
|
|
||||||
damageLevel += damageAmount × damageScale[damageType] clamp [0,1]
|
damageLevel += damageAmount × damageScale[damageType] clamp [0,1]
|
||||||
|
|
||||||
Authoring streams armor POINTS per type; the ctor normalizes
|
Authoring streams armor POINTS per type; the ctor normalizes
|
||||||
`scale[type] = 1/(points[type] × armorPoints)`, so 1.0 = the zone's full
|
`scale[type] = 1/(points[type] × armorPoints)`, so 1.0 = the zone's full
|
||||||
point budget spent. **Leg zones halve every scale** (legs effectively carry
|
point budget spent. **Leg zones halve every scale** (legs effectively carry
|
||||||
double points). 1.0 → BurningState + DestroyedGraphicState.
|
double points). 1.0 → BurningState + DestroyedGraphicState.
|
||||||
|
|
||||||
**Per-hit specials**: an Energy hit on a zone with critical subsystems rolls
|
**Per-hit specials**: an Energy hit on a zone with critical subsystems rolls
|
||||||
ONE of them; if the pick is a Generator it is force-shorted (screens
|
ONE of them; if the pick is a Generator it is force-shorted (screens
|
||||||
flicker, weapons drop dead until recovery; novice cockpits exempt).
|
flicker, weapons drop dead until recovery; novice cockpits exempt).
|
||||||
|
|
||||||
**State outcomes** by the new level:
|
**State outcomes** by the new level:
|
||||||
|
|
||||||
| condition | result |
|
| condition | result |
|
||||||
|---|---|
|
|---|---|
|
||||||
| VITAL zone hits 1.0 | mech kill (`statusAlarm` 9) |
|
| VITAL zone hits 1.0 | mech kill (`statusAlarm` 9) |
|
||||||
| leg zone hits 1.0 | fall → mech kill |
|
| leg zone hits 1.0 | fall → mech kill |
|
||||||
| leg zone ≥ 0.5 | limp gait graphic (left 3 / right 4) |
|
| leg zone ≥ 0.5 | limp gait graphic (left 3 / right 4) |
|
||||||
| non-leg, non-vital hits 1.0 | destruction descent (below) |
|
| non-leg, non-vital hits 1.0 | destruction descent (below) |
|
||||||
|
|
||||||
## 6. Criticals and equipment — `MECHDMG.CPP` + `MECHSUB.CPP`
|
## 6. Criticals and equipment — `MECHDMG.CPP` + `MECHSUB.CPP`
|
||||||
|
|
||||||
Each hull zone streams a critical table: `{weight, damagePercentage
|
Each hull zone streams a critical table: `{weight, damagePercentage
|
||||||
allotment, roster subsystem index}` per entry (plug binding is
|
allotment, roster subsystem index}` per entry (plug binding is
|
||||||
master-authoritative — replicants never bind).
|
master-authoritative — replicants never bind).
|
||||||
|
|
||||||
Every subsystem owns a PRIVATE `DamageZone` (index 0, not in the hull
|
Every subsystem owns a PRIVATE `DamageZone` (index 0, not in the hull
|
||||||
array) with its own points + per-type scales (`structureReference` +
|
array) with its own points + per-type scales (`structureReference` +
|
||||||
`armorByFacing[5]`, same normalization rule). That's what makes criticals
|
`armorByFacing[5]`, same normalization rule). That's what makes criticals
|
||||||
measurable.
|
measurable.
|
||||||
|
|
||||||
- **`CriticalHit`** (aimed/critical fire; not yet on the weapon path —
|
- **`CriticalHit`** (aimed/critical fire; not yet on the weapon path —
|
||||||
reticle wave): HALF the damage (cap 1.0) is carved off as the critical
|
reticle wave): HALF the damage (cap 1.0) is carved off as the critical
|
||||||
bite, applied to ONE weight-rolled crit subsystem via
|
bite, applied to ONE weight-rolled crit subsystem via
|
||||||
`ApplyDamageAndMeasure`, charged against that entry's allotment; the
|
`ApplyDamageAndMeasure`, charged against that entry's allotment; the
|
||||||
remainder runs the normal zone armor model.
|
remainder runs the normal zone armor model.
|
||||||
- **Zone death → `SendSubsystemDamage`**: pins the zone at 1.0 and pushes
|
- **Zone death → `SendSubsystemDamage`**: pins the zone at 1.0 and pushes
|
||||||
each entry's UNUSED allotment into its subsystem's private zone. A
|
each entry's UNUSED allotment into its subsystem's private zone. A
|
||||||
subsystem at 1.0 → `ForceCriticalFailure`: alarm level 1 (Destroyed),
|
subsystem at 1.0 → `ForceCriticalFailure`: alarm level 1 (Destroyed),
|
||||||
`SetSimulationState(DestroyedState)` — the hard gate every weapon FSM
|
`SetSimulationState(DestroyedState)` — the hard gate every weapon FSM
|
||||||
polls, so destroyed weapons fall silent; a VITAL subsystem kills the mech.
|
polls, so destroyed weapons fall silent; a VITAL subsystem kills the mech.
|
||||||
Repeat hits on a dead zone re-run the push (binary-authentic) — contained
|
Repeat hits on a dead zone re-run the push (binary-authentic) — contained
|
||||||
equipment keeps degrading under continued fire.
|
equipment keeps degrading under continued fire.
|
||||||
|
|
||||||
## 7. The destruction cascade — `RecurseSegmentTable`
|
## 7. The destruction cascade — `RecurseSegmentTable`
|
||||||
|
|
||||||
A destroyed zone walks the skeleton by its streamed flags:
|
A destroyed zone walks the skeleton by its streamed flags:
|
||||||
|
|
||||||
- **SIBS** (`destroySiblingsOnDestruction`): destroy the other zones on the
|
- **SIBS** (`destroySiblingsOnDestruction`): destroy the other zones on the
|
||||||
same segment.
|
same segment.
|
||||||
- **DESCEND** (`descendOnDestruction`): destroy every zone on the child
|
- **DESCEND** (`descendOnDestruction`): destroy every zone on the child
|
||||||
segments, recursing.
|
segments, recursing.
|
||||||
|
|
||||||
Fight-verified chain: arm zone dies → SIBS kills the searchlight zone (its
|
Fight-verified chain: arm zone dies → SIBS kills the searchlight zone (its
|
||||||
Searchlight/ThermalSight crits destroyed) → DESCEND kills the gun zone →
|
Searchlight/ThermalSight crits destroyed) → DESCEND kills the gun zone →
|
||||||
PPC_2 + ERMLaser_2 + Condenser6 destroyed and STOP FIRING, while the
|
PPC_2 + ERMLaser_2 + Condenser6 destroyed and STOP FIRING, while the
|
||||||
untouched PPC_1 keeps shooting.
|
untouched PPC_1 keeps shooting.
|
||||||
|
|
||||||
## 8. Data authoring (the resource chain)
|
## 8. Data authoring (the resource chain)
|
||||||
|
|
||||||
- **Type-20 DamageZoneStream** (per mech): zone count, then per zone the
|
- **Type-20 DamageZoneStream** (per mech): zone count, then per zone the
|
||||||
engine record (name, 5 effect-site segment lists, `defaultArmorPoints`,
|
engine record (name, 5 effect-site segment lists, `defaultArmorPoints`,
|
||||||
`damageScale[5]`, material lists) + the BT tail (descend/sibs flags,
|
`damageScale[5]`, material lists) + the BT tail (descend/sibs flags,
|
||||||
`segmentIndex`, leftLeg/rightLeg/vital, crit table, LOD redirect table).
|
`segmentIndex`, leftLeg/rightLeg/vital, crit table, LOD redirect table).
|
||||||
Authored in `.dmg` notation files: `WeaponDamagePoints` default with
|
Authored in `.dmg` notation files: `WeaponDamagePoints` default with
|
||||||
`Collision/Ballistic/Explosive/Laser/EnergyDamagePoints` overrides
|
`Collision/Ballistic/Explosive/Laser/EnergyDamagePoints` overrides
|
||||||
(stored as 1/points).
|
(stored as 1/points).
|
||||||
- **Type-29 DamageLookupTableStream**: the cylinder table (rows ×
|
- **Type-29 DamageLookupTableStream**: the cylinder table (rows ×
|
||||||
angular cells × cumulative zone distributions; 18 tables shipped).
|
angular cells × cumulative zone distributions; 18 tables shipped).
|
||||||
- **Weapon subsystem resources**: `damageAmount`, `damageType`,
|
- **Weapon subsystem resources**: `damageAmount`, `damageType`,
|
||||||
`heatCostToFire`, range, recharge.
|
`heatCostToFire`, range, recharge.
|
||||||
- Reference magnitudes (bhk1): legs 70 pts, upper torso 124, searchlights
|
- Reference magnitudes (bhk1): legs 70 pts, upper torso 124, searchlights
|
||||||
25; PPC ~12 (Energy), ER-M laser 3.43 (Laser), SRM 5.83 × 6 (Explosive).
|
25; PPC ~12 (Energy), ER-M laser 3.43 (Laser), SRM 5.83 × 6 (Explosive).
|
||||||
|
|
||||||
## 9. Replication and respawn
|
## 9. Replication and respawn
|
||||||
|
|
||||||
- Criticals resolve on the MASTER instance only; `DamageZone` state
|
- Criticals resolve on the MASTER instance only; `DamageZone` state
|
||||||
replicates via update records (`damageLevel`, zone state, graphic state,
|
replicates via update records (`damageLevel`, zone state, graphic state,
|
||||||
changed flags). `SubsystemMessageManager` consolidates per-frame damage
|
changed flags). `SubsystemMessageManager` consolidates per-frame damage
|
||||||
and bundles explosion resources (not yet reconstructed).
|
and bundles explosion resources (not yet reconstructed).
|
||||||
- `Mech::Reset` (respawn) heals every hull zone and DeathResets the roster,
|
- `Mech::Reset` (respawn) heals every hull zone and DeathResets the roster,
|
||||||
but crit `damagePercentageUsed` PERSISTS across lives — spent crit
|
but crit `damagePercentageUsed` PERSISTS across lives — spent crit
|
||||||
budgets stay spent (binary-authentic).
|
budgets stay spent (binary-authentic).
|
||||||
|
|
||||||
## 10. Known deltas from the 1995 binary (staged)
|
## 10. Known deltas from the 1995 binary (staged)
|
||||||
|
|
||||||
- Aimed fire: beam `SendDamage` currently rolls a uniform random hull zone;
|
- Aimed fire: beam `SendDamage` currently rolls a uniform random hull zone;
|
||||||
authentic = the reticle hit. `CriticalHit` is unwired for the same reason.
|
authentic = the reticle hit. `CriticalHit` is unwired for the same reason.
|
||||||
- Cylinder height = 10.0 constant (binary reads the collision cylinder).
|
- Cylinder height = 10.0 constant (binary reads the collision cylinder).
|
||||||
- Leg-branch gates use "not already destroyed" instead of the live
|
- Leg-branch gates use "not already destroyed" instead of the live
|
||||||
MovementMode/IsDisabled checks (gait FSM pending).
|
MovementMode/IsDisabled checks (gait FSM pending).
|
||||||
- Gyro hit-feed and destroyed-skin graphics are log stubs (feel/render waves).
|
- Gyro hit-feed and destroyed-skin graphics are log stubs (feel/render waves).
|
||||||
|
|
||||||
|
## 11. The cylinder table, measured — `dmgscan.py`
|
||||||
|
|
||||||
|
Section 4.3 describes the unaimed resolve; this is the shipped data behind it,
|
||||||
|
extracted by `restoration/dmgscan.py` (brute-forces every offset and accepts
|
||||||
|
only candidates whose entire nested structure parses, so the format itself is
|
||||||
|
under test). **18 tables, every one 7 bands x 8 wedges = 56 cells.**
|
||||||
|
|
||||||
|
**Only 8 are distinct by content** — the other 10 are duplicates.
|
||||||
|
|
||||||
|
| distinct table | zones | torso-rotating bands | copies | chassis family (by zone-set fingerprint) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `b586e6f9` | 22 | 4 | 3 | Avatar / Mad Cat class — table A |
|
||||||
|
| `bdf3d3a8` | 22 | 4 | 2 | Avatar / Mad Cat class — table B |
|
||||||
|
| `ad4eb428` | 21 | 4 | 3 | Loki |
|
||||||
|
| `db6cc00d` | 22 | 4 | 2 | Thor |
|
||||||
|
| `2b34fe3e` | 21 | 4 | 2 | SND2 |
|
||||||
|
| `9b481293` | 24 | 4 | 2 | Battlemaster / Vulture |
|
||||||
|
| `11c840bc` | 20 | **0** | 2 | Black Hawk |
|
||||||
|
| `4c07e216` | 17 | **0** | 2 | Owens |
|
||||||
|
|
||||||
|
The zone COUNTS (17/20/21/22/24) match the per-chassis `.SKL` `dz_` sets
|
||||||
|
exactly, which is what allows the fingerprint. It is not always unique: the
|
||||||
|
Avatar and Mad Cat class share a zone set but have two DIFFERENT tables, and
|
||||||
|
the resource carries no chassis name near the stream, so which is which is
|
||||||
|
undetermined — recorded as A/B rather than guessed.
|
||||||
|
|
||||||
|
**Black Hawk and Owens rotate NO band with the torso.** Everything else
|
||||||
|
rotates its upper four. That is a real behavioural difference, not missing
|
||||||
|
data.
|
||||||
|
|
||||||
|
### The 18 wedge names — six anatomical rings
|
||||||
|
|
||||||
|
```
|
||||||
|
band 6 TopRight TopLeft (2 names / 8 slots)
|
||||||
|
band 5 RightChest FrontChest LeftChest RearChest (4)
|
||||||
|
band 4 RightWaist FrontWaist LeftWaist RearWaist (4)
|
||||||
|
band 3 RightHip FrontHip LeftHip RearHip (4)
|
||||||
|
band 2 RightLeg FrontHip LeftLeg RearHip (transition)
|
||||||
|
band 1 RightLeg LeftLeg (2)
|
||||||
|
band 0 RightFoot LeftFoot (2)
|
||||||
|
```
|
||||||
|
|
||||||
|
Slot 0 starts at angle 0 and each spans 45 degrees, so with `atan2(z,x)` the
|
||||||
|
mech's **+X is right and +Z is front**. The named faces each cover TWO adjacent
|
||||||
|
wedges (Right = slots 7,0 · Front = 1,2 · Left = 3,4 · Rear = 5,6), which means
|
||||||
|
**dead ahead is the seam between two Front cells, not the centre of one**.
|
||||||
|
|
||||||
|
### Sample cell distributions (Avatar/Mad Cat table A)
|
||||||
|
|
||||||
|
```
|
||||||
|
band 0 RightFoot 50% rfoot · 30% rdleg · 20% lfoot
|
||||||
|
band 1 RightLeg 40% ruleg · 40% rdleg · 10% luleg · 10% ldleg
|
||||||
|
band 5 FrontChest 50% utorso · 10% each larm/rarm/ltorso/rtorso/dtorso
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the scatter is deliberate and generous: a clean foot hit is only half a
|
||||||
|
foot hit, and one shot in five crosses to the OTHER foot.
|
||||||
|
|
||||||
|
### Playtester-facing visual
|
||||||
|
|
||||||
|
An interactive plate of all of the above — every cell of all 8 tables, a plan
|
||||||
|
and elevation, and a torso-twist slider that rotates the upper bands live —
|
||||||
|
was published for playtesting. Regenerate its dataset with `dmgscan.py`.
|
||||||
|
|||||||
Reference in New Issue
Block a user