Skip to content

Add the BFG - #7

Open
doug-leonhardt wants to merge 1 commit into
ATLBitLab:mainfrom
doug-leonhardt:feat/bfg
Open

Add the BFG#7
doug-leonhardt wants to merge 1 commit 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.

Comment thread src/game/game.ts
// 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.

Comment thread src/game/bfg.ts
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.

Officially a Bulk Fusion Generator. Nobody calls it that.

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.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.

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.

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
while 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 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.

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

src/game/bfg.ts is pure maths over three.js vectors apart from the two meshes it
owns, so the whole weapon runs headless. Twelve of the new assertions in
simcheck.ts cover spool, abort, ammo, falloff, self-damage, knockback, mine
chaining, AI avoidance and that charging really does silence the guns. The
balance harness gained a BFG section plus two contract checks: it cannot replace
the guns over an engagement, and it has to be able to kill the pilot who fired
it. A blast that is safe to stand in is a free button, and a free button gets
pressed on cooldown.

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.

Verified in a real browser as well as headlessly — flew a Drone with the guns
never touched and killed a Hornet with the round, and confirmed the spool holds
the guns cold. The first cut of the detonation threw 600 particles past the kill
radius for two and a half seconds; the reward for the best shot in the game was
a screen you could not see out of. It is now tight, short, and its middle
shockwave ring stops exactly on the damage boundary, because a wave drawn wider
than it kills teaches the wrong distance.

The seeded end-to-end run in simcheck now clears in 13.0s instead of 13.4s. The
weapon is not in that scenario at all: three.js draws Math.random for object
UUIDs, so allocating the round meshes shifts the seeded stream. Same assertions,
same outcome, different arithmetic.

Co-authored-by: Doug Leonhardt <doug.leonhardt@gmail.com>
Signed-off-by: Doug Leonhardt <doug.leonhardt@gmail.com>
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