#95: the SCOREBOARD banked one missile per salvo -- credit the DELIVERED amount

Two consumers of one impact, only one was ever verified: the victim gets
TakeDamage{amount=per-missile, burstCount=cluster roll} and applies it
burstCount times (armor was always right); the score post on the next line
sent the bare per-missile amount. An LRM10 salvo dealing 7-35 armor banked
3.5 points -- Rajel's "~3 points to score", to the digit.

Ground truth: the binary's score is the victim handler's tally (amount once
per applied burst @0x4a04da + crit bonuses) reported to the INFLICTING
player (the id-0x16 tail, deferred #45). The shooter-side stand-in now
posts amount x burstCount -- the identical figure handed to the victim, at
the identical one-post-per-TakeDamage granularity. Same pass:

* splash never credited score at all -- the binary tallies every
  TakeDamage; now posted per splash victim (amount x falloff bursts);
* the bridge credited the LOCAL player for ANY registered hit -- AI-master
  fire on the player, a dying mech's death-blast splash; now refused
  unless the shooter IS the local vehicle (MP unaffected: only local fire
  carries live damage on a node);
* direct-fire unchanged -- beams author burstCount=1 (emitter.cpp:355).

Verified per the harness doctrine: single-node field composition (madcat,
real fire, real enemy) 22/22 impact credits paired at damage x burst, zero
bare 3.33 posts; two-node replicant-victim run BOTH directions 31/31
paired across three missile authorings (3.33/2.0/5.0 per-missile) + 25-pt
ballistics, leftovers all burst-1 beam amounts. test-harness.md gains the
map=grass note (MP.EGG authors cavern/night; GOTO mechs shoot rock).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-02 14:11:28 -05:00
co-authored by Claude Fable 5
parent 08ca66e5b5
commit 152249cb76
4 changed files with 42 additions and 7 deletions
+6
View File
@@ -84,6 +84,12 @@ The vehicle sed must replace the WHOLE line (`s/^vehicle=.*/`), or you mint
`vehicle=thr1bhk1` and the spawn FATALs. Chassis codes: ava1 bhk1 lok1/2 mad1/2
own1 snd1 thr1 vul1 (own1 has NO arm zones — rack zones instead).
**Weapon/combat benches need `map=grass time=day`.** `MP.EGG` authors
`map=cavern time=night`; a bench that copies it without the map sed (e.g. via
`bt_expert_egg` alone — mp_double.sh has this gap) parks the GOTO-driven mechs
against cavern rock and they shoot terrain for the whole window. Always sed the
map/time on the COPY, single-node and two-node alike.
### Two-node bench skeleton (the MP pattern)
```bash
bt_assert_player_env
+12 -1
View File
@@ -2162,7 +2162,7 @@ Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap
// local (crediting) player is application->GetMissionPlayer(). Bridges (not inline
// in mech4.cpp) so the message construction lives in this complete-BTPlayer TU.
//
void BTPostDamageScore(Entity *victim, Scalar damage) // Step 6: per-hit SCORE
void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter) // Step 6: per-hit SCORE
{
if (application == 0 || victim == 0)
{
@@ -2173,6 +2173,17 @@ void BTPostDamageScore(Entity *victim, Scalar damage) // Step 6: per-hit SCORE
{
return;
}
// The credit belongs to the SHOOTER, not the viewer: the binary reports the
// victim handler's damage tally to the INFLICTING player (the id-0x16 report
// tail @0x4a04da, deferred #45). This stand-in can only bank score on the
// LOCAL player, so refuse any post whose shooter is not the local vehicle --
// an AI master's fire on the player (SP), or a dying mech's death-blast
// splash, must not credit the viewer. MP is unaffected: only local fire
// carries live damage on this node.
if (shooter != (Entity *)local_player->GetPlayerVehicle())
{
return;
}
// ScoreInflicted (scoreType 0): senderMechID = the mech that TOOK the damage.
// -> ScoreInflictedMessageHandler: currentScore += tonnageRatio*CalcInflictedScore.
BTPlayer::ScoreMessage message(
+19 -4
View File
@@ -706,7 +706,7 @@ static int gEnemyDestroyed = 0;
// mech locks onto it as its current target each frame (see targeting step below).
extern Entity *gEnemyMech;
// gauge scoring wave: producers (btplayer.cpp) -- feed the scoreboard from combat.
extern void BTPostDamageScore(Entity *victim, Scalar damage); // per-hit SCORE (ScoreInflicted)
extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter); // per-hit SCORE (ScoreInflicted)
extern void BTPostKillScore(Entity *victim, Scalar damage); // KILL (+ MP death)
// Mech target slots (verified vs the binary's weapon/fire path, part_013.c):
@@ -1295,6 +1295,13 @@ void
shooter->GetEntityID(), -1 /*unaimed -> cylinder resolves*/, dmg);
e->Dispatch(&td);
// SCORE (#95 follow-up): the binary's victim handler tallies EVERY
// TakeDamage -- splash included -- and reports it to the INFLICTING
// player (id-0x16, deferred #45), so a bystander blast is score too.
// The bridge itself refuses a shooter that is not the local vehicle
// (this function's death-blast caller passes the DYING mech).
BTPostDamageScore(e, dmg.damageAmount * (Scalar)bursts, shooter);
// MP MATCH FORENSICS (matchlog.hpp): shooter-side splash delivery
// to a bystander (pairs with the victim's DMG burst>1 line).
if (BTMatchLogActive())
@@ -1776,9 +1783,17 @@ static void
(p.shooter != 0) ? p.shooter->GetEntityID() : EntityID::Null,
-1 /*unaimed -> cylinder resolves*/, dmg, p.weaponSubsys);
tgt->Dispatch(&take_damage);
// gauge scoring wave (Step 6): a projectile hit credits SCORE too
// (tgt == gEnemyMech here; local player is the viewpoint shooter).
BTPostDamageScore((Entity *)tgt, p.damage);
// gauge scoring wave (Step 6): a projectile hit credits SCORE with
// the DELIVERED amount -- per-missile damage x the rolled cluster
// count, the same figure the dispatch above hands the victim's
// handler (which applies it burstCount times, tallying the amount
// per burst @0x4a04da). Posting the bare per-missile amount here
// was the "LRM10 registered ~3 points to score" defect (#95): the
// armour took the full salvo while the scoreboard banked one
// missile. (Crit bonuses in the tally remain victim-side
// knowledge -- the deferred id-0x16 report, #45.)
BTPostDamageScore((Entity *)tgt,
p.damage * (Scalar)dmg.burstCount, p.shooter);
// MP MATCH FORENSICS (matchlog.hpp): shooter-side
// projectile impact delivery (pairs with the victim's
+5 -2
View File
@@ -742,12 +742,15 @@ void
// PORT bookkeeping (score lived in the retired mech4 fire block): credit
// the delivered amount to the shooter-side score exactly as before.
extern void BTPostDamageScore(Entity *victim, Scalar damage);
extern void BTPostDamageScore(Entity *victim, Scalar damage, Entity *shooter);
extern Logical BTIsRegisteredMech(Entity *e);
if (BTIsRegisteredMech(target)
&& !((Mech *)target)->IsMechDestroyed())
{
BTPostDamageScore(target, damageData.damageAmount);
// Direct-fire damageData always authors burstCount = 1 (emitter.cpp:355),
// so the plain amount IS the delivered amount here. The shooter param
// lets the bridge refuse AI-master fire (score is the shooter's, #45).
BTPostDamageScore(target, damageData.damageAmount, (Entity *)mech);
}
// MP MATCH FORENSICS (matchlog.hpp): the shooter-side per-shot damage