Three-trigger keyboard weapon groups: 1=lasers 2=PPCs 3=missiles (task #43)

Interim pod-like grouping ahead of the authentic ConfigureMappables/
ChooseButton mapper channels (decomp-anchored in open-questions):
- Three fire channels like three pod buttons: gBTWeaponTrigger (lasers,
  key 1 or SPACE), gBTPPCTrigger (PPCs, key 2), gBTMissileTrigger
  (missiles, key 3 or CTRL). Keyboard only per user (mouse buttons
  removed).
- EmitterSimulation picks its channel by classID (PPC 3028 vs Emitter
  3016); BT_AUTOFIRE holds all three.
- The BT_BEAM_LOG sampler modulus 30 aliased to ONE weapon under the
  new synchronized 5-beam volleys (5 | 30) -> 31 (coprime).

Verified: all 5 energy weapons fire (31 samples each), aimed hits land,
kill completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 08:31:16 -05:00
co-authored by Claude Fable 5
parent ae8aca0f3f
commit 14c0dd06f5
3 changed files with 42 additions and 29 deletions
+2 -1
View File
@@ -39,7 +39,8 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
- **cwd MUST be `content\`** — the engine resolves `BTL4.RES`, `VIDEO\`, `BTDPL.INI`, eggs relative
to cwd (the `loadTables` gotcha: `L4VIDEO.cpp:849` `fopen("VIDEO\\REPLACEMATS.tbl")` is relative +
unchecked → fread on NULL if cwd is wrong). Logs to `btl4.log` in `content\` (or `BT_LOG=<file>`). [T2]
- Interactive: **WASD** drive, **Space/Ctrl** fire, **X** all-stop. Default egg = `DEV.EGG`
- Interactive: **WASD** drive; weapon groups (keyboard, task #43) **1/Space** = lasers, **2** =
PPCs, **3/Ctrl** = missiles; **X** all-stop; **V** cockpit/chase view. Default egg = `DEV.EGG`
(map=grass, time=day). Swap mech via the egg's `vehicle=` (bhk1/madcat/…). The code path is
mech-agnostic (verified — Mad Cat booted + fought with zero code changes). [T2]
+8 -3
View File
@@ -267,12 +267,17 @@ void
PoweredSubsystem::PoweredSubsystemSimulation(time_slice);
// E8 (bring-up trigger): write the weapon's fireImpulse from the player fire input
// (the controls mapper is bypassed). gBTWeaponTrigger is pulsed per-FRAME by mech4
// (the controls mapper is bypassed). The triggers are pulsed per-FRAME by mech4
// (1,0,1,0...) so CheckFireEdge sees clean rising edges for repeated auto-fire under a
// held/forced trigger. HasActiveTarget (E3) limits firing to the mech with a locked
// target (the player) -- enemy emitters read this too but never fire (no target).
extern int gBTWeaponTrigger;
fireImpulse = (Scalar)gBTWeaponTrigger;
// WEAPON GROUPS (task #43, interim keyboard split standing in for the authentic
// ConfigureMappables/ChooseButton mapper channels): lasers (0xBC8=3016) and PPCs
// (0xBD4=3028) read SEPARATE fire channels, like two pod buttons.
extern int gBTWeaponTrigger; // channel 1: lasers
extern int gBTPPCTrigger; // channel 2: PPCs
fireImpulse = (Scalar)(((int)GetClassID() == 3028) ? gBTPPCTrigger
: gBTWeaponTrigger);
Logical fireEdge = CheckFireEdge(); // @004b9608
targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c
+32 -25
View File
@@ -618,12 +618,17 @@ static int gShotCount = 0;
static Scalar gBlockCooldown = 0.0f; // seconds since the last blocking contact (counts down)
static const Scalar kBlockHysteresis = 0.4f;
// E8 weapon trigger (bring-up): pulsed per player frame; read by EmitterSimulation
// (emitter.cpp) so the real weapon's CheckFireEdge sees rising edges. Non-static
// (the emitter externs it).
int gBTWeaponTrigger = 0;
int gBTMissileTrigger = 0; // the split missile-fire channel (CTRL)
static int gBTMissileKey = 0; // raw key state (set by the keyboard poll)
// E8 weapon triggers (bring-up): pulsed per player frame; read by the weapon
// sims so CheckFireEdge sees rising edges. Non-static (the weapons extern
// them). WEAPON GROUPS (task #43): three KEYBOARD fire channels -- an interim
// pod-like split standing in for the authentic ConfigureMappables/ChooseButton
// mapper channels: 1/SPACE = lasers, 2 = PPCs, 3/CTRL = missiles.
int gBTWeaponTrigger = 0; // channel 1: lasers (key 1 / SPACE)
int gBTPPCTrigger = 0; // channel 2: PPCs (key 2)
int gBTMissileTrigger = 0; // channel 3: missiles (key 3 / CTRL)
static int gBTLaserKey = 0; // raw key states (set by the keyboard poll)
static int gBTPPCKey = 0;
static int gBTMissileKey = 0;
// Damage: each shot dispatches a REAL Entity::TakeDamageMessage to the target. Now
// that the damage zones are constructed (mech.cpp Pass-3 zone build), the engine base
@@ -1278,14 +1283,16 @@ void
gBTDrive.keyBack = focused && ((pAsync('S') | pAsync(0x28 /*VK_DOWN*/)) & dn) ? 1 : 0;
gBTDrive.keyLeft = focused && ((pAsync('A') | pAsync(0x25 /*VK_LEFT*/)) & dn) ? 1 : 0;
gBTDrive.keyRight = focused && ((pAsync('D') | pAsync(0x27 /*VK_RIGHT*/)) & dn) ? 1 : 0;
// SPLIT WEAPON CONTROLS (interim, toward the real controls-mapper
// weapon groups): SPACE / LMB = the energy weapons (lasers),
// CTRL / RMB = the missile launchers. (The mouse buttons pair
// with the mouse-slewed reticle -- task #36.)
gBTDrive.fire = focused && ((pAsync(0x20 /*VK_SPACE*/) |
pAsync(0x01 /*VK_LBUTTON*/)) & dn) ? 1 : 0;
gBTMissileKey = focused && ((pAsync(0x11 /*VK_CONTROL*/) |
pAsync(0x02 /*VK_RBUTTON*/)) & dn) ? 1 : 0;
// WEAPON GROUPS (task #43, KEYBOARD only per user): three fire
// channels like three pod buttons -- 1/SPACE = lasers, 2 = PPCs,
// 3/CTRL = missiles. (Interim; the authentic system is the
// ConfigureMappables/ChooseButton mapper channels.)
gBTLaserKey = focused && ((pAsync('1') | pAsync(0x20 /*VK_SPACE*/)) & dn) ? 1 : 0;
gBTPPCKey = focused && (pAsync('2') & dn) ? 1 : 0;
gBTMissileKey = focused && ((pAsync('3') | pAsync(0x11 /*VK_CONTROL*/)) & dn) ? 1 : 0;
// gBTDrive.fire = "any weapon trigger down" (feeds the bring-up
// damage dispatcher + the beam-visual keepalive)
gBTDrive.fire = (gBTLaserKey || gBTPPCKey || gBTMissileKey) ? 1 : 0;
static int sPrevX = 0;
const int xNow = focused && (pAsync('X') & dn) ? 1 : 0;
if (xNow && !sPrevX) gBTDrive.allStop = 1; // edge -> one all-stop
@@ -2338,7 +2345,7 @@ void
BTPushBeamKind(mz.x, mz.y, mz.z, bend.x, bend.y, bend.z,
tint, ttl1, 1.0f /* natural model width */,
isPPC ? 1 : 0 /* ppc.bgf : ermlaser.bgf */);
if (getenv("BT_BEAM_LOG") && (s_beamStateLog++ % 30) == 0)
if (getenv("BT_BEAM_LOG") && (s_beamStateLog++ % 31) == 0) // 31: coprime with the 5-beam volley (a %30 sampler aliased to one weapon)
DEBUG_STREAM << "[beam] " << (isPPC ? "PPC" : "laser") << " #" << wi
<< " mz=(" << mz.x << "," << mz.y << "," << mz.z
<< ") end=(" << bend.x << "," << bend.y << "," << bend.z
@@ -2577,17 +2584,17 @@ void
const int fireWanted = gBTDrive.fireForced || gBTDrive.fire;
// E8: pulse the weapon trigger per frame (1,0,1,0...) so the real Emitter's
// CheckFireEdge sees clean rising edges for repeated auto-fire. Read by
// EmitterSimulation. AUTHENTIC gating: the trigger passes through and the
// weapon's OWN HasActiveTarget gate decides (Emitter::FireWeapon fires only
// with a designated target -- part_013.c:7758; no aim/arc test exists).
// targetInArc is the explicit BT_FIRE_ARC presentation clamp (default true).
extern int gBTWeaponTrigger;
gBTWeaponTrigger = (fireWanted && targetInArc) ? (gBTWeaponTrigger ? 0 : 1) : 0;
// the missile channel: CTRL / RMB (or the autofire harness) -- same edge pulse
extern int gBTMissileTrigger;
// E8: pulse the three fire channels per frame (1,0,1,0...) so each
// weapon sim's CheckFireEdge sees clean rising edges. AUTHENTIC
// gating: the trigger passes through and the weapon's OWN
// HasActiveTarget gate decides (FireWeapon -- part_013.c:7758; no
// aim/arc test exists). targetInArc is the explicit BT_FIRE_ARC
// presentation clamp (default true). BT_AUTOFIRE holds all three.
const int laserWanted = gBTDrive.fireForced || gBTLaserKey;
const int ppcWanted = gBTDrive.fireForced || gBTPPCKey;
const int missileWanted = gBTDrive.fireForced || gBTMissileKey;
gBTWeaponTrigger = (laserWanted && targetInArc) ? (gBTWeaponTrigger ? 0 : 1) : 0;
gBTPPCTrigger = (ppcWanted && targetInArc) ? (gBTPPCTrigger ? 0 : 1) : 0;
gBTMissileTrigger = (missileWanted && targetInArc) ? (gBTMissileTrigger ? 0 : 1) : 0;
// Resolve the "explode" effect resource once.