Skip to content

Add the BFG - #7

Merged
sbddesign merged 2 commits into
ATLBitLab:mainfrom
doug-leonhardt:feat/bfg
Sep 6, 2026
Merged

sbddesign merged 2 commits into
ATLBitLab:mainfrom
doug-leonhardt:feat/bfg

Conversation

@doug-leonhardt

Copy link
Copy Markdown
Contributor

Requested in the #gamedev channel as "BFG sized really big guns". The damage was the easy part. The interesting part is what it costs to use.

Hold F (or right mouse) for 1.3 seconds and a slow green round leaves the nose. Anything within 340 units of where it stops takes up to 260 damage on a steep falloff — lethal to any airframe at the centre, a hard shove and a scare at the rim.

distance from centre damage vs Wasp vs Hornet vs Drone
centre 260 KILL KILL KILL
quarter radius 164 KILL KILL 82%
half radius 86 KILL 71% 43%
three quarters 28 40% 24% 14%

Three rules do the design work

Two rounds a run, no refills. Anything renewable turns into a rotation you press on cooldown. Two means every launch is a judgement about whether this is the moment, and firing the second one is a small tragedy.

Charging costs you everything else. Guns cold, dash locked, throttle capped at 55% for the whole spool. You are committing to a heading for a second and a half in an arena where everyone else is still manoeuvring. Releasing early aborts and keeps the round, so a mispress costs a moment rather than a third of your firepower.

The blast does not care who fired it. The pilot takes 60% — 156 damage at point-blank, which kills a Wasp outright and takes a bite out of a Drone. The distance you keep is the price of the damage you get, in the same currency the enemy pays.

Hostiles also run from a live round: each one registers as an AI steering hazard the size of its own blast, so the squadron scatters as it crosses the arena. A round that hits nothing still breaks a formation off your tail — it is a zoning tool as much as a killing one. It chain-detonates any mines it goes off near, which the minefield makes considerably more interesting.

Player-only. An AI holding one of these would either never use it or nuke its own wing, and neither is a fight anyone wants.

Shape of the change

src/game/bfg.ts is pure maths over three.js vectors apart from the two meshes it owns, so the whole weapon runs headless. The game loop translates its events into sound, light and score; the weapon itself knows nothing about either.

  • input.tssecondary on the input state (F / right mouse, context menu suppressed over the canvas)
  • game.ts — the interlock, event handling, AI hazard concat, score and accuracy
  • hud.ts / style.css — spool gauge and two ammo pips
  • audio.ts — a spool ratchet, launch and detonation. Deliberately a ratchet rather than a held whine: every other voice in the mix is a transient with its stop already scheduled, and a sustained tone would be the only thing needing to be turned off by hand — the exact bug that got the engine note deleted in Remove the engine sound #4. It also carries information a drone does not, since the interval tightens as the charge fills.
  • fx.ts — the detonation

Accuracy stays honest. A launch counts as one shot, and a blast that catches anything counts as exactly one hit rather than one per hull — otherwise a single trigger pull could report three hits and put accuracy over 100%.

Verification

Headless — 30 new assertions in simcheck.ts across four scenarios: spool and abort, ammo limits, falloff at the centre / edge / outside, self-damage ratio, knockback, mine chaining, AI avoidance appearing and clearing, and that charging really does hold the guns cold and hand them back on release. The balance harness gained a BFG damage table plus two contract checks: it cannot replace the guns over an engagement (17.3 DPS across 30s against the weakest gun's 46.7), and it has to be able to kill the pilot who fired it. A blast that is safe to stand inside is a free button, and a free button gets pressed on cooldown.

In a real browser (Playwright, Chromium) — flew a Drone with the guns never touched and killed a Hornet with the round alone, confirmed the spool holds shotsFired at zero and hands the guns straight back, and confirmed the HUD pips and gauge track ammo and charge.

That browser pass earned its keep: the first cut of the detonation threw 600 particles past the kill radius for two and a half seconds, so the reward for landing the best shot in the game was a screen you could not see out of. It is now tight and short, and its middle shockwave ring stops exactly on the damage boundary — a wave drawn wider than it kills teaches the wrong distance, and this is a weapon where distance is the entire decision.

One thing to know before merging

The seeded end-to-end run now clears in 13.0s instead of 13.4s. The BFG is not in that scenario at all — the scripted pilot never touches the trigger. three.js draws Math.random for object UUIDs, so allocating the round meshes at game construction shifts the seeded stream. Same assertions, same outcome, different arithmetic. Worth knowing because it will happen again to anyone who adds an object to createGame.

npm run check is green: typecheck, simulation and the balance contract.

Not done

Nobody has played this against their instincts. Every number is at the top of src/game/bfg.tsSPOOL_TIME, BLAST_RADIUS, BLAST_DAMAGE, SELF_DAMAGE, BFG_CHARGES. My suspicion is that two charges is right and 1.3 seconds is the dial most likely to be wrong.

Opened from a fork: no write access on this repo.

Discussed in #gamedev on Buzz.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

@doug-leonhardt is attempting to deploy a commit to the Delineator Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neon-orbit Ready Ready Preview Aug 5, 2026 8:11pm

Request Review

@sbddesign sbddesign left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The weapon is thoughtfully separated and the exact PR head passes npm run check plus npm run build, but I found two control-contract bugs that should be fixed before merge, and current main adds a Shield interaction that must be made explicit during the rebase.

The PR is also currently non-mergeable after #6: Git reports conflicts in README.md, both harnesses, fx.ts, game.ts, hud.ts, and style.css. Please rebase/merge current main, preserve the power-up behavior while resolving those files, add the release-edge and post-warp first-frame regression cases below, define and test Shield versus BFG self-damage, then rerun the full package checks and production build on the new head.

Comment thread src/game/bfg.ts
if (event) events.push(event)
// A launch consumes the trigger. Holding the button down does not
// immediately start winding the next one — let go and mean it.
recovery = ABORT_RECOVERY

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Require a release edge before the spool can re-arm. recovery only delays the next canSpool check; once the 0.6s expires, frame.hold is still true and the second round automatically spools and launches. That contradicts the comment immediately above and can consume both scarce rounds from one long hold. The ammo test currently masks this by holding continuously and expecting two launches—please latch needsRelease (or model trigger edges) and assert that holding after the first launch leaves one charge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in dd2f8d5: the trigger now latches needsRelease on launch. Holding through the shot leaves the second charge aboard; you have to let go and mean it. testBfgAmmoAndChaining asserts the long hold yields one round, and a release then a second hold yields the second.

Comment thread src/game/game.ts Outdated
// Spooling the BFG costs you everything else. Cold guns, no dash and a
// throttle ceiling is what turns "press the big button" into a decision
// about where you are willing to be for the next second and a half.
if (bfg.spooling) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Gate on the current secondary-trigger intent, not only the previous frame's bfg.spooling state. player.step(readPlayerControls(...)) runs before resolveBfg, so on the first charge frame this condition is false and a ready primary gun fires; the Hornet can initiate dash on that same frame too. I reproduced the gun leak after waiting for warp-in to clear: the regression probe failed with shots=1. The committed test starts immediately after game.start, so the ship's normal 0.85s warp fire lock hides the bug.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in dd2f8d5: the interlock now gates on this tick's secondary intent via wouldCharge, not last tick's spooling flag. testSpoolingSilencesTheGuns waits out warp-in, then charges with fire held, and asserts shotsFired === 0 on that first frame.

Comment thread src/game/bfg.ts Outdated
target.velocity.addScaledVector(_push.normalize(), BLAST_KNOCKBACK * fraction)

const before = target.alive
target.takeDamage(damage, own ? 'enemy' : round.team)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reconcile this with the Shield now on main before resolving the conflicts. Current Ship.takeDamage returns without applying damage whenever shieldTimer > 0, so after the rebase a shielded pilot can stand in their own blast for zero damage. That bypasses this PR's stated principal cost and its “point-blank BFG kills its pilot” balance contract. Either make Shield an intentional exception and update the contract/docs, or make BFG self-damage bypass Shield; add a merged-state regression test for the chosen rule.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in dd2f8d5: BFG blast damage pierces Shield — the Shield is for the gunfight, not for standing in a fusion blast. Ship.takeDamage takes an explicit pierceShield flag, and testBfgPiercesShield asserts a shielded pilot still takes their own blast while the Shield stays up.

Officially a Bulk Fusion Generator. Nobody calls it that.

Hold F (or right mouse) for 1.3s and a slow green round leaves the nose.
Anything within 340 units of where it stops takes up to 260 damage on a
steep falloff — lethal to any airframe at the centre, a hard shove and a
scare at the rim.

Rebased onto current main: the weapon is handed intent like everything
else, one copy per seat, and the blast speaks factions rather than the
old two-sided team split. Charging still costs the guns, the dash and
half the throttle — gated on this tick's secondary trigger, so a ready
gun cannot fire on the first charge frame after warp-in. A launch
consumes the trigger; holding through the shot does not wind the second
round.

The blast still does not care who fired it, and a held Shield does not
change that. The Shield is for the gunfight. A fusion sphere a shielded
pilot can stand in for free is a button you press on cooldown.

Hostiles run from a live round. It chain-detonates mines. Two rounds a
run, no refills.

Review on the original fork PR asked for the release-edge latch, the
first-frame gun silence, and an explicit Shield rule. All three have
headless checks. npm run check and the production build are green.

Co-authored-by: Doug Leonhardt <doug.leonhardt@gmail.com>
Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
@sbddesign

Copy link
Copy Markdown
Contributor

Rebased onto current main and pushed to this branch (dd2f8d5).

The BFG now speaks intent and roster — secondary is a real control, one weapon per seat, factions instead of the old two-sided team split. The three review items are in that same commit:

  • Release edge. A launch latches needsRelease. Holding through the shot leaves the second charge aboard.
  • First-frame guns. The interlock gates on this tick's trigger via wouldCharge, after warp-in. A ready gun cannot fire on the first charge frame.
  • Shield. BFG blast damage pierces Shield. The Shield is for the gunfight, not for standing in a fusion blast.

npm run check (typecheck + sim + balance) and npm run build are green on this head.

One thing still true from the original: live rounds are not on the snapshot yet, so a joined client will not draw the ball in flight. Solo is the shipped game and is unaffected. Happy to follow that if we want the round visible over the wire before merge.

Ready for a re-review.

The suite grew by forty-six assertions. EXPECTED_ASSERTIONS is 675 so a
green-but-short run cannot masquerade as complete. The three intent-routing
mutants now include the BFG interlock they sit next to, and five new mutants
ask whether a long hold spends the second round, whether the guns leak on
the first charge frame, whether a Shield eats the blast, whether the pilot
walks away, and whether charging still silences the guns.

Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
@sbddesign

Copy link
Copy Markdown
Contributor

Follow-up e6dd43a: mutation gate updated. EXPECTED_ASSERTIONS is 675, the intent-routing mutants sit next to the BFG interlock, and five new mutants cover the review items. Locally: 125/125 caught, 0 survived, 0 not testable.

check is still green. The remaining red Vercel status is the fork deploy authorization, same as before this rebase.

@sbddesign
sbddesign merged commit b7367b5 into ATLBitLab:main Sep 6, 2026
4 checks passed

This branch was successfully deployed

1 active deployment
Preview e6dd43a9 Deployed Sep 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants