Technical Guide · v1.9.1

JAR BOYS

Architecture, seams, tuning, and the build roadmap. Built on the Black Hole Arena shell — the whole framework layer (menus, lobby, PeerJS netcode, audio, mobile controls) is inherited and untouched.

0 · READ THIS FIRST — current state & handoff notes

Why this section exists. Jar Boys has been built through an extended back-and-forth chat with Josh, turn by turn. This section is written for whoever (or whatever — a fresh Cowork session, a new Claude Code session, another developer) picks this project up without that chat history. It captures the current build state, what's actively in progress, and the hard-won lessons that got silently reintroduced more than once before they were written down here. Read this whole section before making any change.

Where things stand — v1.9.1

Jar Boys is a live, playable, feature-complete top-down peer-to-peer shooter with six game modes, three maps, bots, and full online lobby/matchmaking. It's built on the Black Hole Arena shell (see §2) — that framework layer is stable and shouldn't need touching. All active work at this point is Jar Boys-specific gameplay, netcode robustness, and polish.

🟡 SERVER + CLIENT FIX IMPLEMENTED (awaiting deploy + device verify): public lobby capacity bug

Josh's family repeatedly could not get more than 4 players into public games larger than 4 (e.g. 4v4 / 8 players). The browse list always showed the lobby as "4/4 Full" even when the host clearly had more open slots.

Root cause (confirmed, not guessed): the public lobby storage endpoint is shared with Black Hole Arena, a fixed 4-player game. Read-backs from GET /api/lobbies show Jar Boys' published tf (team format, e.g. "2v2v2v2") field stripped entirely, and players/max both clamped to 4 regardless of what the client published. This is a server/storage-side bug, not something fixable from index.html.

What's already done (client-side, shipped in 1.7.0–1.8.4): the browse list no longer trusts the store's max/players at all. capOf(g) derives capacity only from fields that could plausibly be ours (tf, or a size larger than the legacy clamp); if capacity can't be determined trustworthily it returns null and the UI shows "N+ in lobby" with Join left enabled rather than falsely graying it out. The host is the single source of truth for whether a lobby is actually full — it already rejects a join attempt with {t:'full'} → "That game is full." So nobody is blocked from attempting to join anymore, but the browse list still can't show accurate live counts until the server side is fixed.

What still needs to happen — this is the actual fix, and it's on Allen's side (Vercel), not something to attempt in this file: the lobby storage function needs to (1) stop clamping players/max to 4, (2) persist and return the tf field, (3) persist size as sent for FFA lobbies. A full write-up with the exact request/response JSON is in NOTE_FOR_ALLEN_lobby_api.md (should be in this repo, or ask Josh for it) — hand that to Allen rather than re-deriving it. Do not attempt to "fix" this by changing more client-side heuristics — the client already does everything it reasonably can with untrustworthy data; the real fix is server-side.

UPDATE — v1.8.5 (Cowork switchover, 2026-07-11): fixed server + client, pending deploy + device verify. Jar Boys now lives in the same repo as its Vercel functions (sulzie-bros), so the fix was made directly instead of handed to Allen as a note. Server (app/api/lobby/route.js): the POST handler no longer clamps size/players/max to 4 (bounded 2–16) and persists tf/gm/mp/game, so our records survive the shared store intact. Client (capOf()): now that the store returns honest capacity it trusts it again (team cap from tf, FFA cap from size/max) — so the browse list shows real counts and correctly shows Full at true capacity, including FFA, which the 1.7–1.8.4 safety workaround could never mark full. The host is still the final authority on joins. Cross-listing: BHA’s browse now filters by game tag and no longer lists Jar Boys lobbies (BHA → 2.24.2). Verified headless (server record-building + client capOf/fullOf: 4v4 → 8-cap, Join-enabled at 1/8 and Full at 8/8; FFA 8 → Full at 8/8; BHA 4/4 still Full; browse filters isolate each game). NOT yet verified end-to-end: needs a Vercel deploy against real Upstash Redis and a real hosted 8-player game showing 1/8 — a push to main auto-deploys, so coordinate with Allen.

Hard-won lessons (repeatedly reintroduced — don't redo these mistakes)

Verification approach (adopt this — it's how every bug above got actually confirmed, not guessed)

This project has a headless Node smoke-test harness (extract the <script> block, stub the DOM/canvas, run targeted drivers). It's what caught the vehicle-color-duplication bug, the shuffle-slot-range bug, the territory/hardzone sum-mismatch bugs, and the map-drift bug — in each case by writing a small script that runs many simulated matches/rebuilds and asserts an invariant (all colors reachable, PTS sums equal team score, struct positions stable across rebuilds), rather than trusting a single playtest or code read-through. Known blind spot: the canvas is stubbed as a Proxy in this harness, so real pixel/canvas-sizing bugs (like the resize issues above) genuinely cannot be verified numerically headless — those need an actual browser/device test. Be honest about this distinction rather than claiming headless verification proves something it can't.

Working agreement with Josh

1 · Files & deploy

index.html                    the game (rename stays index.html when you ship)
peerjs.min.js                 the PeerJS library — local copy (already referenced)
music/                        optional audio (see §8)
JarBoys_UsersGuide.html       player-facing guide
JarBoys_TechnicalGuide.html   this file

Must be served over http(s), not file:// — WebRTC (online play) and the WebAudio unlock both need a real origin. Local file open = online + sound silently disabled; single-player still runs.

Project constants live at the top of the <script>: API_ORIGIN / LOBBY_API / TURN_API (point at your backend; code-only online still works via public STUN/TURN without them) and APP_VERSION.

2 · Framework vs. game

The split from the shell holds. Framework (leave alone): screen flow, the PeerJS star topology (host authoritative, guests send input), lobby/browse/join, the audio engine, the 3-2-1 countdown, HUD plumbing, the mobile control containers, settings/theme. Game (all Jar Boys logic) lives in a handful of functions the framework calls by name.

FunctionRole
CONFIG / TUNEAll gameplay numbers (top of script).
stepShip(p,e,dt)Soldier movement. Direct twin-stick walking; aim is handled separately.
simulate(dt)Authoritative tick: bots → move → collide → bullets → respawns. Host/practice only.
firePulse(i)Fire a round from soldier i (repurposed PRIMARY action). Cooldown-gated.
dropBomb(i)Plant a claymore at soldier i's feet (one per life). Guests request it via the bf input edge.
botThink(dt)Enemy AI: writes me._in (move) + me._aim (facing), calls firePulse.
draw() / drawShip(p)Scrolling map + the bent-arm soldier art + floating name tags.
hostBroadcast / applyStateThe snapshot: players, aim, bullets, kills/deaths, scores.

3 · World & camera

The shell drew a fixed 1000×640 arena scaled to fit. Jar Boys keeps a fixed zoom and scrolls a camera over a big world.

4 · Movement & aim (the shooter rework)

Black Hole Arena tied facing to thrust direction. Jar Boys fully decouples them:

5 · Bullets, kills, respawns

stepBullets(dt) (authoritative) advances each round in a dead-straight line — velocity is fixed at spawn and never re-steered (no homing, by design). It substeps the ray so a fast round can't tunnel through a soldier, tests against players (respecting spawn-invulnerability and the friendly-fire rule), and expires at world edges or BULLET_RANGE.

killPlayer(victim, killerIdx) flags the victim dead, starts their RESPAWN_DELAY timer, credits the killer (kills + team score; a team-kill costs a point instead), and calls checkWin(). respawnPlayer drops them back in their team's spawn zone with brief invulnerability. checkWin ends the match at KILL_TARGET.

Continuous match, no rounds. The shell's round/best-of-3 system is replaced: tickClock only runs the opening countdown, checkRoundEnd/endRound are no-op stubs, and the win comes from kill totals.

6 · Netcode & the snapshot

Star topology is unchanged. What changed is the packet contents.

Guest → host input {t:'i'}

{ u,d,l,r,   // movement (analog on mobile)
  am,        // aim angle (radians)
  f,         // trigger held (0/1)
  bf }       // claymore edge (v0.2)

Host → guest snapshot {t:'s'}

pl: [ [x, y, alive, aim, invulnFlag], ... ]   // one row per soldier
bu: [ [x, y, vx, vy], ... ]                    // live rounds (guests extrapolate between snapshots)
sc, kl, de, wn, ph, tm, e, fl, sh, ev  // scores, kills, deaths, winner, phase, timer, clock, flash, shake, sound-events

Adding a synced entity (claymores, tags…) follows the same two-line pattern: serialize compactly in hostBroadcast, rebuild in applyState. Keep payloads small (round coordinates, drop anything guests can recompute).

7 · Config reference verified against v1.9.1

The table below reflects the numbers actually live in CONFIG/mode-target constants at the top of the script as of v1.9.1 — the weapon model changed from an auto-fire cooldown to a magazine+reload since this table was first written, so check here rather than assume.

KeyMeaningCurrent
MOVE_SPEEDTop walking speed (world u/s)340
MOVE_ACCELHow snappily you reach top speed14
BULLET_SPEEDRound velocity (fast — crosses a screen in a blink)2600
BULLET_RANGEDistance a round travels before fizzling2200
MAG_SIZERounds before you must reload1 (bolt-action feel)
RELOAD_SECONDSAuto-reload time after firing3s
RESPAWN_DELAYSeconds down before redeploying2.5
SPAWN_PROTECTPost-respawn invulnerability1.2
COLLISION_STRENGTHSoldier-vs-soldier bump1.4

Per-mode win targets

ModeTargetNotes
Deathmatch (KILL_TARGET)10 killsFFA or teams
Recon (RECON_TARGET)20 pointsKills+tags combined; tags expire after RECON_EXPIRE=10s; teams only
Hardzone (HARDZONE_TARGET)25 pointsPure zone-share only (kills NOT folded in — see §0 lessons); HZ_RADIUS=230, HZ_TICK=1.0s, relocates every HZ_MOVE=30s to a fully random valid spot
Capture / Bacon (FLAG_TARGET)10 capturesKills+captures combined in PTS; FFA (Capture/Bacon) or teams (Capture)
Territory (TERR_TARGET)75 pointsPure zone-share only (same reasoning as Hardzone); TERR_RADIUS=170, TERR_CAP=8s to flip uncontested, TERR_TICK=1.5s; teams only

Teams use TEAM_COLORS (4, dark/light each); free-for-all uses FFA_COLORS (8 distinct hues). Both are always accessed through teamColor(t), which is format-aware — never index either palette array directly (see the §0 lessons-learned entry on this). At draw time, shadeFor(p) renders you in your dark shade with a white ring, teammates in the light shade, and enemies in their own dark shade — a per-viewer decision.

8 · Audio — event map all sounds TBD

The engine calls sounds by name; the actual SFX are placeholders to be replaced with your own. Every hook is already wired — here's the master list of what each event is and when it fires. World events go through snd(...) (play locally on the authoritative side + ride the snapshot's ev queue to guests via playEvent); UI sounds call sfx(...) locally.

EventFires whenScope
shotA round is firedWorld (+ instant local on the shooter)
elimA soldier is killedWorld
beatEach 3-2-1 countdown tickLocal
goMatch starts ("Fight!")Local + starts gameplay music
overMatch ends — win vs. loss stingLocal (per-player win/loss)
click / wooshButton press / screen changeUI
keystrokeTyping name / codeUI
connect / lobbyLeaveA player joins / leavesUI
errorFailed join / full lobbyUI

Music tracks to provide

Sounds to add in later phases

To drive these from your own audio files instead of the synth placeholders, we swap the SFX_GEN generators for a small Audio-element player keyed by event name — a quick v0.2 wiring task. Hand off the files and it's a drop-in.

9 · Build roadmap

PhaseScope
1.9.1REMATCH DROPPED PLAYERS FIXED. On starting the next match, some players were left behind (stuck on results or in the lobby). Two causes: (1) no keepalive on the peer data channel — while players lingered on the results screen the idle WebRTC channel got dropped by NAT, so they never received tolobby/start. The host now pings all guests every 2.5s whenever online but not running (during a match the snapshot stream keeps it warm); guests echo it. (2) hostReturnToLobby and shuffleRoster filtered on c.open, which flickers false for live guests and dropped them from the rebuild — replaced with "include every connection still in conns" (hostDropConn already removes genuinely-closed ones; the v1.8.9 .left handling covers any unclean straggler). Verified headless (include-set + keepalive gating); needs real multi-device rematch testing.this build
1.9.0CLIENT-SIDE PREDICTION + INPUT DECOUPLING. (1) Input decoupled from myIdx: keyboard (setLocalMove) and mobile joystick (setMoveVec/clearMove) now write a standalone localMove object, and guestSend transmits it directly instead of reading game.players[myIdx]._in. A slot/binding race can no longer source your input from the wrong or missing player and send zeros — the "couldn't move or shoot the whole match" bug (intermittent public-join). Each frame the loop copies localMove onto the local soldier's _in for the host sim / prediction / mobile aim-from-walk. (2) Prediction: guests now advance their OWN soldier locally each frame (predictLocal = the exact stepShip movement model + resolveWalls/clampToBounds), then softly reconcile toward the host's authoritative position (snap on >180u = respawn/teleport). Removes the run-stop-run-stop jolt from waiting a round-trip for your own movement. Movement-only; firing/kills/scoring stay host-authoritative. Verified headless (movement + reconcile math); real multi-device testing still needed for feel and to confirm the binding fix.shipped
1.8.9NETCODE ROBUSTNESS + MOBILE LAG. (1) Ghost fix: a booted or departed player no longer lingers as a frozen soldier that still respawns — hostDropConn now marks the slot .left (respawn loop and draw loop both skip it; guests hide it on the left message), so they vanish from play instantly. (2) Rate-gate: hostBroadcast now sends at ~25Hz (BCAST_MS) instead of every frame (~60Hz). Per-frame broadcast flooded guests — badly on mobile in 5–6 player matches (delayed kills, own-player jolting, remote disappear/reappear). Guests interpolate between snapshots so 25Hz stays smooth; the claymore-edge apply still runs every sim frame, and match-over forces an immediate send. (3) Flicker fix: applyState drops stale/out-of-order play snapshots (guarded on elapsed e), so a just-killed player can’t be resurrected for a frame by a reordered packet on the unreliable channel. Host-leaves-ends-for-all already worked (hostleft). Verified headless (logic + syntax); true smoothness/feel still needs real multi-device testing. Note: the guest’s own soldier still has no client-side prediction — the rate-gate should cut the congestion-driven jolt a lot, but full local smoothness is a later prediction/reconciliation task.shipped
1.8.8LOBBY-LIST vs LOBBY MISMATCH FIXED (FFA). An FFA lobby still published a leftover team-format tf (e.g. "2v2"), and capOf() trusted tf for any game, so a 2-player FFA listed as "4p / 1/4". Now the client only sends tf for team games, and capOf() derives capacity from tf only when fmt==='team' (FFA uses the real size/max) — a 2-player FFA now lists as "FFA · 2p · 1/2" and shows Full at 2. A guard also makes stale FFA records that still carry tf read correctly during the deploy transition. Separately, a server HOTFIX restored the /api/lobby POST handler, which had been truncated by a tooling glitch and silently broke ALL public lobby publishing. Verified headless end-to-end (publish → store → browse).shipped
1.8.7BRANDING: the sulziebros.com app-card icon and the in-game browser favicon were switched from the old gun-sight optic to the game's own top-down soldier — green camo helmet with bent arms and rifle, aimed toward the corner, on a Dustbowl dirt background (rendered from the same sprite recipe drawShip() uses). Cosmetic only; no gameplay or netcode change.shipped
1.8.6GUEST SMOOTHNESS + EFFICIENCY TRIM. Guests now render remote soldiers with true time-based interpolation: each snapshot is buffered with an arrival timestamp and remote entities are drawn ~100ms (INTERP_DELAY) in the past, lerped between the two bracketing snapshots (teleport/respawn jumps >200u snap; buffer-starve holds the newest). The LOCAL soldier is exempt — it keeps the responsive glide-to-latest so input never lags. Replaces the old chase-the-latest exponential glide that stuttered under irregular snapshot arrival. Trim (dead code, verified no readers): removed the defunct game.msg/mg pipeline (snapshot field, applyState, render path — #centerMsg kept, still used for countdown + disconnect); deleted slotColor() (was byte-equivalent to teamColor(), callers repointed); deleted the COLORS Proxy shim (slotGlow() repointed to teamColor().light); removed legacy BHA game.pulses/game.meteors arrays. Silent audio scaffolding intentionally kept. Headless-verified (interpolation math + node --check); guest smoothness itself needs real two-device testing.shipped
1.8.5LOBBY CAPACITY FIX (server + client). Server (app/api/lobby/route.js) no longer clamps size/players/max to a 4-player shape and now persists tf/gm/mp/game, so Jar Boys lobbies bigger than 4 (e.g. 4v4 / 8) survive the shared BHA store intact. Client capOf() trusts the now-honest capacity again (team cap from tf, FFA cap from size/max), so the public games list shows real counts AND correctly shows "Full" / greys out Join when a lobby is truly at cap — including FFA, which the 1.7–1.8.4 workaround could never mark full. Host stays the final authority on joins. Cross-listing fixed: Black Hole Arena’s browse now filters by game tag and no longer lists Jar Boys lobbies (BHA bumped to 2.24.2). Verified headless; NOT yet device-verified — needs a deploy + a real hosted 8-player game showing 1/8.shipped
1.8.4MAP DRIFT FIXED: reflect2/4 push the ORIGINAL struct object and vehicle stamping mutated cx/cy/w/h on it, so every buildMap (each round/mode change/lobby return) shifted wrecks another ±nudge and WALLS drifted with them — clients that had rebuilt different numbers of times had visibly different maps AND collision (the desktop-vs-mobile misalignment). buildMap now clones every struct; verified byte-identical positions across 6 rebuilds on all 3 maps. Resize made SELF-HEALING: verifyViewport() polls the #stage rect ~10×/sec in the game loop and re-syncs if drifted (rejecting <80px), plus visibilitychange/pageshow re-measure. Mobile "TAP ANYWHERE TO SHOOT" moved below the countdown (was hidden behind the mode banner at 15–16%). Public list: store data can no longer mark a game Full (server clamps records — see §0); unknown capacity shows "N+ in lobby" with Join enabled. Guests keep a 66ms input pulse while their tab is hidden (browsers freeze rAF in background tabs).shipped
1.8.3White flash + sudden zoom-out fixed (1.8.1 regression): ResizeObserver fires while the container is mid-animation/hidden, when getBoundingClientRect() is near-zero; the 1px clamp collapsed the canvas (flash) and drove scale to its 0.55 floor (zoom-out). resize() now rejects measurements <80px and coalesces callbacks to one/frame. Sports-car rear wing was a rect hanging 54px off the flank (placed at −0.43W with height 1.72W); rebuilt centred (0.90W blade on stanchions). Truck tailgate had the identical defect; centred at 0.74W.shipped
1.8.2Vehicles REDRAWN with curved bezier hulls (tapered nose, cabin shoulder, narrowed tail, tyres under the fenders) — the rounded-rect look read as "cybertrucks". Per-type detail: sedan (greenhouse, raked glass, door seams), sports (nose taper, haunches, canopy, stripe, wing), truck (ribbed bed, proud cab, grille), jeep (fender flares, open tub, roll bar, tail spare). Every wreck got a PERMANENT random heading (0/90/180/270); the collision AABB swaps with the turn; two Dustbowl jeeps nudged ±40y clear of the central building. Headings across all 10: S,W,E,N,N,E,S,S,W,N.shipped
1.8.1Stale pixels bleeding past the map boundary fixed: draw() now resets the transform and clears + void-fills the ENTIRE backing store in device px (clearRect(0,0,vw,vh) missed the edge strip when the canvas outgrew stale vw/vh); resize() measures the #stage container (fixed; inset:0) with a ResizeObserver + orientationchange re-measure, since embedded hosts resize the container without firing window resize.shipped
1.8.0VEHICLE SYSTEM: four top-down templates (sedan/truck/sports/jeep) replace the single grey blob. Shape and colour are separate: a 10-entry palette gives EVERY wreck on EVERY map its own permanent colour (no repeats anywhere, even same-template); mirrored copies stamped individually. Distribution: Dustbowl 3 jeeps + 1 truck, Overgrowth 2 sedans + 2 sports, Sandstorm 2 trucks. Cosmetic only — collision unchanged.shipped
1.7.2DUPLICATE ID fixed: the pre-match banner was given id="modeTitle", already used by the setup screen's h2 — the banner CSS splashed "Choose a mode" across the Teams button and getElementById grabbed the wrong node. Renamed to #gmBanner; full audit — 0 duplicate IDs. Territory target 40 → 75 (constant, setup button, guides). Corrected two self-contradicting/stale Users-Guide rows (Territory said 40 and 25; Hardzone described removed fairness-weighting).shipped
1.7.1Feed flag glyphs: "[Name in their colour] took/dropped/scored [flag tinted to the flag-owner]" — Bacon's flag neutral white; Territory stays text-only. Pre-match MODE TITLE (DEATHMATCH/RECON/…) in large letters above the countdown, fading in sync with FIGHT!, shown to host and guests.shipped
1.7.0UNIFIED MESSAGE SYSTEM: one colour-coded event feed (top-right, fades ~3.5s) for kills/flags/bacon/territory events, synced to guests; persistent Territory A/B/C strip (top-left, letters tinted by owner, pulsing while contested); persistent Hardzone status chip (top-right, always visible, with relocation countdown) replacing canvas text you could only read next to the zone. Public "4/4 Full" bug: browse capacity derived from published size/team-format via capOf(g) instead of the store's stale legacy max. Lobby DOM had a stray </div> closing .lobbySettings early — the mobile-landscape squish; fixed + roster scrolls. Pre-match countdown 5s and guests now SEE their shuffled colour + teammates during it. Default bot level 1.shipped
1.6.3The REAL FFA colour-duplication fix: teamColor() wrapped through the 4-entry TEAM_COLORS palette (%4), so 8-player FFA painted teams 4–7 the same as 0–3 (duplicate spawn circles even after 1.6.2). teamColor() is now format-aware (FFA → 8-colour palette, team → 4-colour) and every draw site funnels through it.shipped
1.6.2FFA colour/spawn desync ("hybrid green-red spawn"): FFA colours were keyed to array POSITION while team/spawn/flag keyed to team NUMBER — after the shuffle they disagreed. slotColor now keys FFA colour off the team number like everything else.shipped
1.6.1Settings button icon swapped from a gear to the three-slider mixer matching Black Hole Arena's.shipped
1.6.0Removed 1v1 team format (was a loophole giving 2-player games access to team-only modes). Disconnect messaging fixed: host now sees "You left the session", connected players see "The host ended the session"; a leaving guest sees "You left the session" on their own screen (was a peer.destroy() side-effect showing false "X left" toasts to the host). Color shuffle bug fixed: humans could previously only land in the first N roster slots (missing colors entirely e.g. never Blue/Amber in a 4p FFA) — now scatters across the FULL slot range, plus remembers each player's last color to reduce repeats. Territory: fixed a bug where capturers were wiped one frame after a steal, causing multi-holder over-counting; rebuilt using the same fractional-share+resolve-once pattern as Hardzone — sum of players' points now always equals the team score exactly (was 37 vs 40). Hardzone: fixed a tick-loop overshoot bug (checkWin didn't stop extra ticks in the same frame) and removed kills from the displayed PTS for Hardzone/Territory specifically (kills stay in their own K column) so team PTS always sums exactly to the real score instead of ballooning past it (was 60-something in a 25-point game)shipped
1.5.9Hardzone point-split fix: multiple holders no longer each get the full team point (which double-counted). Each tick's point is now accrued as a fractional share (game.oShare), then resolved to whole numbers ONLY on the results page so each team's players sum exactly to the team's zone score, no decimals, remainder to the higher-ranked player. Synced to guests. Kills still add on top as whole points.this build
1.5.8Territory attribution corrected: presence matters only AT CAPTURE TIME — capturers keep earning after they leave or die, and only lose it when another team takes the zone (reverting an over-strict 1.5.7 change). Remainder point now rotates among co-capturers so splits are even over time, decimal-free, with no points lost vs the team scorethis build
1.5.7INTEGRITY AUDIT: fixed territory attribution to match spec — points now only go to capturers STILL inside the zone that tick (leaving or dying forfeits your share, as designed); verified all targets/mechanics/descriptions against every request in the build historythis build
1.5.6PERF PASS: cached scoreboard .k elements (no getElementById/querySelector per player per frame); cached reloadHint + skip redundant innerHTML writes; snapshot only sends scores/kills/deaths/points when they change (~117 bytes/frame/guest saved when static, forced resend every 2s for late joiners); mode-gated objective arrays (tags/flags/zones/hz only sent in their mode); removed dead code (randomZonePoint, drawDeployRingLabel)this build
1.5.5NETCODE FIXES: guest lobby showed only 4 slots in 4v4 (hardcoded cap -> capacity()); snapshot fl key collision (flash vs flags) corrupting online flag/capture sync -> renamed flags to fg; 3s start desync that booted guests (start now sent at end of countdown, not beginning); clean lobby rebuild on return/rematch (fixes false "full" + stale slots); shuffleRoster skips dead conns; removed flag marker from Hardzone centerthis build
1.5.4Dustbowl top awning tb; Bacon ring capped to map inset (no out-of-bounds spawns); Bacon desc "first to 10"; practice full shuffle (all slots); lobby countdown dot colors fixed; Recon/Hardzone/Territory kills add to PTS; Territory capturer attribution (split points among who was present at cap, clear on steal); territory zone-empty always resets cap progressthis build
1.4.1Kill scoring gated to DM+Recon only; Capture→10 caps; Recon→30 tags; Territory→40 pts, no-score-during-cap, multi-teammate cap speed, multi-zone bonus; Bacon equidistant ring spawns + center wall removed for flag access; flag drawn above helmet; winner text colored; Hardzone starts at center+spawn-distance fairness heuristic; reload 3s; bot accuracy recalibrated; lobby 3-2-1 with team colors; territory zone spread algorithm (covers full map area)this build
v0.2Randomized team/color assignment, bot claymore use, more maps + polishshipped (superseded by 1.x line)
v0.1Fixed dirt map with buildings/cover + wall collision (players & bullets), twin-stick move + independent aim, one-shot-one-kill with auto 7s reload, claymores (one per life, single wall-aware trigger/blast radius, device visible to all but ring only to your team), respawns; three themed maps with distinct SHAPES — rectangle (Dustbowl), circle (Overgrowth), triangle (Sandstorm) — each its own world size with void past the boundary; per-map spawns; concealing foliage; roofed "go-under" buildings (one-way concealment from enemies, bot saw-you-enter awareness, hidden claymores); host map picker; FFA (2–8) + six team formats to 15 kills, host friendly-fire toggle (COD-style: no enemy score, honest KDR, 3-strike boot; claymore blasts exempt), per-level bot accuracy, camo-helmet soldiers, K/D/KDR resultsshipped
v0.9All music + procedural SFX stripped (sfx/snd/playMusic/fanfare/drone now no-ops, mp3 sources removed) with the settings mute/volume UI kept as infrastructure; Recon target 15 + 10s dog-tag expiry (remaining life synced for guest fade); results "session record — won/lost" relabelthis build
v0.8Three new modes: Capture (CTF, flags at spawns), Bacon (single center flag), Territory (3-zone Domination with capture bars); shared flag-carry system (drop-on-death, home-return, own-spawn scoring); per-mode bot objective AI; public search now publishes + shows game type AND map; 6-mode lobby/setup pickers with FFA gatingthis build
v0.7Capture renamed to Hardzone (frees "Capture" for CTF); target 25; zone now spawns at a random valid spot (full circle in-bounds, center not wall-buried) and relocates every 30s; per-level bot objective motivation (obj weight) so difficulty + game type both shape how hard bots chase the objectivethis build
v0.6Game type moved into the lobby (host-editable, guests read-only); on Start: random slot shuffle -> coherent team assignment + 3-2-1; lobby team dots/labels/reorder removed; claymore chain-detonation; deploy ring drawn over roofs; Vercel/API_ORIGIN + jarboys lobby tag confirmedthis build
v0.5Deathmatch / Recon (Kill-Confirmed dog tags) / Capture (central hardpoint) — team-only for the objective modes; per-player points + points-aware leaderboard; game type on the setup page, map moved to the lobbythis build
v0.4Bot map-navigation/pathfinding, sound design pass, mobile + balance tuning, map art polishplanned

10 · Dev notes