Two quality-of-life improvements landed today: a completely reworked sprite loading pipeline that eliminates the waterfall of individual image fetches, and a tighter main menu layout that breathes a little more comfortably on smaller screens.
Sprite Bundle Loader
Previously, every game session started with 80 separate HTTP requests — one per sprite PNG. Each image was decoded through the browser's Canvas API, resized on the CPU if needed, and then copied pixel-by-pixel into the WASM sprite storage. The overhead added up: parallel fetches meant the browser was juggling dozens of connections, canvas resize operations introduced extra allocations, and the loading screen had to track each asset individually.
The new approach packs all 80 sprites into a single binary file (sprites.bin) at build time using a new scripts/pack_sprites.mjs script. The bundle uses a simple custom format:
- 4-byte magic header (
SSAT) for quick integrity validation. - Compact 16-byte index entries — sprite ID, dimensions, data offset, and byte length — one per sprite.
- Raw RGBA pixel data contiguously packed after the index, ready to copy directly into WASM memory.
At runtime, a single fetch("assets/sprites.bin") retrieves the entire 5.1 MB bundle. The loader then walks the index and copies each sprite's pixel slice straight into the WASM sprite storage with Uint8Array.set(). No Canvas, no per-image decode, no individual HTTP connections — just one request and a fast memory copy.
The build scripts now run pack_sprites.mjs automatically on every ./build.sh and ./builddist.sh invocation, so the bundle is always fresh. Only sprites.bin and the OG image are shipped to dist/ — the raw PNG source tree stays out of the production bundle entirely.
Main Menu Layout Polish
The mode selection overlay received a pass to reduce visual crowding. Padding, margins, and gaps across the mode cards have been tightened uniformly, and the heading font size has been reduced from 1.6rem to 1.1rem to better match the compact pixel-art aesthetic. The container max-width was also widened slightly from 720px to 800px to make better use of the available canvas width.
The result is a menu that fits cleanly without scrolling on compact viewports, while still feeling open and navigable at full size.
Draggable Minimap
The minimap in Skirmish and AI-vs-AI modes now supports click-and-drag to scroll the camera, not just a single click-to-jump. Previously, clicking the minimap snapped the camera to the clicked world position. Now, holding the mouse button down and dragging pans the view continuously — handy for scanning across the map without repeatedly clicking.
Under the hood, minimap.zig gained three functions: minimapBounds() extracts the hit-test rectangle, repositionCamera() handles the coordinate mapping, and the new handleMouseMove() / handleMouseUp() pair manage drag state. The JS bridge was updated to forward mousemove and mouseup events to the corresponding WASM exports.
JS Correctness Fixes
A batch of correctness issues in main.js were cleaned up:
- Stale WASM buffer references. WASM memory can grow (and when it does, the backing
ArrayBufferis replaced). Several call sites were holding on to a capturedmemory.bufferreference from init time. All of them now resolveinstance.exports.memory.bufferat the moment of use, so they always see the live buffer regardless of any growth that happened mid-session. - Overlay pause stacking. Closing any overlay (help, options, research shop, upgrades) used to unconditionally call
set_paused(false), which could resume the game while another overlay was still open. Each close handler now callsset_paused(false)only whenareOverlaysOpen()returns false. - TextDecoder reuse. The Zig-to-JS console log bridge was creating a new
TextDecoderinstance on every log call. It now uses a shared singleton, avoiding repeated object allocations during debug sessions. - Sector map font. The star chart sector node labels were inheriting whatever font happened to be set on the canvas context. Font and text alignment are now explicitly set before the node draw loop.
Skirmish AI Rebalancing
Fighter AI has been tuned to handle simultaneous base assault and miner harassment more effectively. Previously, a red-alert signal pulled every available fighter back to base defence, leaving miners completely unescorted. Now, odd-indexed fighters check whether a miner is actively in distress before joining the base rush — if so, they stay on escort duty while even-indexed fighters respond to the threat. The base still gets roughly half its fighters; miners retain meaningful coverage.
Fighters on miner escort also received a proactive intercept behaviour: if an enemy closes to within 1,200 units of the escorted miner, the fighter breaks off the follow pattern and engages immediately — before the first shot lands rather than after.
RPG Escort AI Improvements
The escort fighter logic in RPG mode received two adjustments. Miner protection used to be gated behind the player's base having no active threat, which meant miners could be picked off freely during a base assault. That gate has been removed for active attacks: if a miner has taken fire in the last 30 seconds, escorts respond regardless of base status. Proactive patrol scanning (no shots fired yet, but enemies are nearby) is still suppressed during base assaults to avoid splitting the squad.
Detection radii were also widened — the proactive enemy scan range grew from 2,000 to 3,000 units, the "area clear" threshold from 2,500 to 3,500 units, and the pursuit envelope when protecting a distressed ship from 2,500 to 4,000 units. Escorts now commit harder and chase attackers further before considering the area safe.
Home Key Base Selection Fix
Pressing the Home key in Skirmish re-centres the camera on your starting base. It now also selects the station and updates the right-hand panel selection state, so the base info panel opens immediately rather than showing nothing until the player manually clicks the station.
All of these changes are live in the current build. See you in the void, Commanders.