#95: REVERT the salvo-damage hack; deliver the cluster the way the binary does
Supersedes the mislanch change inefc3e9f, which multiplied the lead round by missileCount. That produced roughly the right average total but as ONE lump on ONE zone, with no scatter and no variance -- not what the arcade does. WHAT THE BINARY DOES (all byte-verified): * MissileLauncher ctor @004bcff0: burstCount = missileCount; damageAmount /= missileCount * FireWeapon @004bcc60 spawns exactly ONE Missile -- no loop, missileCount is never read there. The salvo IS one cluster round. * Missile::Perform rolls how many of the cluster connect right before it dispatches (part_013.c:10082): b = Random(n) + n/4, clamped to n -- i.e. between a quarter of the salvo and all of it. * It then dispatches DIRECTLY at the struck entity (FUN_004be078: param_2->Dispatch(&msg)) -- NOT through the shooter's message manager. * Mech::TakeDamageMessageHandler @0x4a0439-0x4a04d8 applies the hit burstCount times, RE-ROLLING the struck zone per burst. Our handler already implements that loop faithfully (mech.cpp, task #80) -- I wrongly believed it was missing, because the KB asserts as [T1] that "burstCount is cosmetic for zone damage". That is half right: DamageZone::TakeDamage really does ignore burstCount (verified @0041e4e0), but the HANDLER honours it by calling that function repeatedly. KB corrected separately. The actual defect was that the projectile impact path hardcoded burstCount = 1, and -- worse -- routed damage through the SubsystemMessageManager, whose consolidation rebuilds the record from a stream carrying only {damageType, damageAmount, subsystemID}. DamageInformation has no burstCount field, so the cluster count was DROPPED in transit no matter what the round carried. So: dispatch projectile damage directly, as the binary does, carrying the rolled burst. This also retires the #84 double explosion architecturally -- the second blast came from the manager's bundled explosion, and projectiles no longer go through the manager at all. The MarkRoundDetonated suppression added inefc3e9fis therefore dead and is removed. ALL-WEAPON-CLASS AUDIT (scratchpad/night8/allweap.{sh,py}), one run, all classes firing at once: EXPLOSIVE (missile) 38 zone applications, bursts spread 1x2,2x6,3x9,4x4,5x5, 6x12 across ELEVEN zones -- the [n/4..n] roll plus the per-burst zone re-roll, i.e. the cluster scattering ENERGY (beam) 8 applications, burst 1 -- unchanged, no regression type4 8 applications, burst 1 -- unchanged COLLISION 0 applications reached armour -- divert intact Explosions: 10 projectile impacts produce no bundled blast; direct-fire still queues its own (11 made). NOT yet verified: cross-pod delivery. Dispatch reroutes to a replicant on its own (and the binary relies on exactly that), but the manager routing is gone, so MP damage should get a two-node run before this ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
efc3e9ff1a
commit
3b7c19c232
@@ -0,0 +1,45 @@
|
||||
"""Per-weapon-class damage breakdown from a bench log (#95 burst-loop audit).
|
||||
|
||||
Classifies each [dmghit] by the damage TYPE it carried and the burst count, and
|
||||
reports the delivered damageLevel delta, so a change to the shared burst loop
|
||||
cannot silently rescale one class without showing up here.
|
||||
"""
|
||||
import re, sys, collections
|
||||
|
||||
path = sys.argv[1]
|
||||
d = open(path, encoding="latin-1", errors="replace").read()
|
||||
|
||||
# [dmghit] mech=1:323 zone=4 vital=0 type=2 amt=35 burst=1 lvl 0->0.555556
|
||||
rx = re.compile(r"\[dmghit\] mech=(\S+) zone=(\d+) vital=(\d) type=(\d+) "
|
||||
r"amt=([0-9.eE+-]+) burst=([0-9.]+) lvl ([0-9.eE+-]+)->([0-9.eE+-]+)")
|
||||
rows = [m.groups() for m in rx.finditer(d)]
|
||||
print("total zone applications: %d\n" % len(rows))
|
||||
|
||||
TYPE = {0: "COLLISION(diverted)", 1: "type1", 2: "EXPLOSIVE(missile/AC)",
|
||||
3: "ENERGY(beam)", 4: "type4(short-gen)"}
|
||||
|
||||
byType = collections.defaultdict(list)
|
||||
for mech, zone, vital, ty, amt, burst, a, b in rows:
|
||||
byType[int(ty)].append((float(amt), float(burst), float(b) - float(a), int(zone)))
|
||||
|
||||
print("%-24s %6s %10s %10s %10s %8s" % ("class", "hits", "amt", "burst", "dlvl", "zones"))
|
||||
for ty in sorted(byType):
|
||||
v = byType[ty]
|
||||
amts = set(round(x[0], 3) for x in v)
|
||||
bursts = collections.Counter(int(x[1]) for x in v)
|
||||
zones = len(set(x[3] for x in v))
|
||||
print("%-24s %6d %10s %10s %10.4f %8d" % (
|
||||
TYPE.get(ty, "type%d" % ty), len(v),
|
||||
("%.3f" % list(amts)[0]) if len(amts) == 1 else "%d vals" % len(amts),
|
||||
",".join("%dx%d" % (b, c) for b, c in sorted(bursts.items())),
|
||||
sum(x[2] for x in v), zones))
|
||||
|
||||
# burst distribution for the cluster class -- should span 1..N, not be pinned
|
||||
mis = [int(x[1]) for x in byType.get(2, []) if x[1] > 1]
|
||||
if mis:
|
||||
print("\ncluster burst rolls: %s" % dict(sorted(collections.Counter(mis).items())))
|
||||
print(" -> expect a SPREAD in [n/4 .. n], not a constant")
|
||||
|
||||
# collision must never touch armour
|
||||
print("\ncollision applications that reached a zone: %d (must be 0)"
|
||||
% len(byType.get(0, [])))
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# ALL-WEAPON-CLASS damage audit (#95 burst fix).
|
||||
#
|
||||
# The cluster-burst change routes missile damage through the handler's burst loop
|
||||
# (which re-rolls the zone per burst). That loop is shared by EVERY damage
|
||||
# consumer, so this run exercises all of them together and reports the delivered
|
||||
# damage per class, so nothing is silently rescaled:
|
||||
#
|
||||
# ENERGY -- Emitter::FireWeapon -> SendDamageMessage (burst 1)
|
||||
# BALLISTIC -- ProjectileWeapon rounds (burst 1)
|
||||
# MISSILE -- cluster round (burst = rolled hit count, 1..N)
|
||||
# SPLASH -- BTApplySplashDamage (burst = distance falloff)
|
||||
# COLLISION -- type 0, diverted to internal rattle, must NEVER reach armour
|
||||
#
|
||||
# Verdict comes from [dmghit]: amt / burst / the damageLevel delta per class.
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > ALLW.EGG
|
||||
|
||||
LOG=allweap_${1:-post}.log
|
||||
rm -f "$LOG"
|
||||
|
||||
# All weapon groups auto-fire (Trigger + missile group); walk into the target so
|
||||
# ballistic/energy/missile all connect, and splash lands on the victim's zones.
|
||||
BT_PROJ_LOG=1 BT_DMG_LOG=1 BT_FIRE_LOG=1 \
|
||||
BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=2 \
|
||||
BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=2 \
|
||||
bt_launch "$LOG" ALLW.EGG 0x03
|
||||
|
||||
sleep 110
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
echo "=== see allweap.py for the per-class breakdown ==="
|
||||
Reference in New Issue
Block a user