Devlog · Swarm Architect

Thirteen days, then the cliff

566 commits between 19 June and 12 July 2026 — and 511 of them, ninety percent, landed in the first thirteen days at between nineteen and seventy-eight a day. Then it stopped, hard, and everything after is infrastructure. Here is what got built in that fortnight, in order.

19 Jun 2026566 commits · peak 78/day12 Jul

About the project


19 – 20 June 2026 · days 1–2

Two inits and a webview

The repository opens with init, then init again, then add webview, then Steam support — and then one commit that is really a week of work: itch.io support, an in-game console on the tilde key, a lobby, the documentation index, an icon and a splash screen.

Day two also removes Lit and adds hot reload, and ports UDP netcode — ENet transport, prediction, delta snapshots — from a predecessor project. Networking exists on day two because it did not have to be written on day two.

Lit comes back in the second-to-last commit of the repository, three weeks later, as the only runtime dependency in the package file. A full round trip.

21 June 2026 · day 3, 61 commits

The commit the whole codebase is shaped by

After a morning of Discord presence, localisation, accessibility and keybinds, the largest structural change in the project's history landed in one commit:

refactor: collapse netcode into server/client pillars over pluggable transports; reorganize whole game by role (server/client/shared/services) — single-player + multiplayer now run one authoritative-sim + view pipeline (loopback for SP, ENet for MP), with headless/dedicated-server support and a CI sync harness

Everything since obeys it. There are two pillars and nothing orchestrating them; they talk only through a transport, and that is the whole contract. Single-player is not a special case — it is a loopback with zero sockets running the same authoritative sim the dedicated server runs.

Immediately before it, a bug that explains the motive: make EnemySpawner server-only so clients can’t simulate ghost monsters the host never has. Immediately after it, tests, a launcher menu, and a stress harness — the refactor was landed with the means to prove it.

22 June 2026 · day 4

Squeezing the wire

Four optimisation commits in a row, each one a different axis. Data-driven authoritative enemies, dropping about three hidden nodes per creature. Per-client snapshot culling through a spatial grid instead of scanning every enemy. Then the one that defines the protocol:

netcode: shrink combat snapshots — viewport-relative 0.5px monster positions, drop velocity (interpolate)

A monster's position is not a world coordinate. It is a 12-bit signed half-pixel offset from the receiving player's own hero, with the origin sent once per snapshot. Three bytes, ±1024 pixels, and bounded so tightly that it physically cannot drift far enough to become a ghost. Velocity is never transmitted at all — the client derives it. Total cost of a moving monster on the wire: six bytes.

Same day: a lag-simulating transport so the netcode can be tested under conditions, and a hundred hand-authored monsters across ten tiers.

23 – 25 June 2026 · days 5–7

The Dungeon Master appears — and the game starts playing itself

Added Dungeon Master role to multiplayer: a free pan/zoom camera, a horde-summoning menu, and a live player minimap, selectable in the lobby

The asymmetry arrives on day five. Two days later, so does the thing that makes balancing it possible: a headless balance simulator where an AI hero plays the real game, auto-drafting level-ups, time compressed. It drives the actual server sim — the same spawner, boids, contact damage and gem merging — so what it measures is what the game does.

Crucially it is seeded. Identical inputs reproduce exactly, so two builds can be A/B'd on the same run. It has a bot-versus-bot mode where it plays the Dungeon Master too. The balance commits that came straight out of it are specific in a way that guesswork never is: cap player snowball + ramp late-game threat so a typical run ends ~7–11 min.

Two developers were working in parallel through all of this, and the log shows the handoffs verbatim: “Good night the stick is passed to you.”

26 June 2026 · day 8, 78 commits — the peak

Fifty weapons, and a day lost to DPI

Expand weapon roster to 50 across 14 fire-mode families + 9 elements (add Water/Wind/Earth, Nova/Spiral/Bounce/Homing/ GroundZone/Smite modes, and burn/slow/pull status effects)

Then the busiest day in the repository turns into a fight with Windows display scaling, in three acts: viewport units break at 150% zoom, then the debug panel jams partway across the screen on high-DPI, then the actual fix — scale the entire web interface to a constant fraction of the screen.

The underlying trap is genuinely nasty. The UI scales with CSS zoom, and Chromium does not apply zoom to viewport units — so a raw 100vh resolves against the unzoomed viewport and breaks on every screen that is not 1080p at 100%. Which is to say: invisible on the development machine, broken everywhere else.

The response was a zero-dependency build guard that forbids viewport units across the whole UI, wired into dev, build and lint, with exactly one allowlisted file — described in the docs as “the ONE authorized place.”

27 – 28 June 2026 · days 9–10, 64 commits

A second game mode in a single day

Maps and game modes became data, then Survival World was built almost entirely on 28 June — roughly twenty-five commits in a row all prefixed World:. Damageable cores, siege beams, core line-of-sight, wall avoidance, monsters leashing to cores, heal zones, in-world health and shield and XP bars, core destruction effects.

The same day the Dungeon Master got its two best toys: a gift-chest system where reward chests hide arming traps that survivors spring by walking near them, and a wave-timeline editor wired through one client-to-server path.

29 June – 1 July 2026 · days 11–13

The pipeline, then the effects binge

Day eleven is a small masterclass in sequencing: create plan for sprite sheet parser toolcreate sprite sheet parser toolHero Creator + animated directional heroes. Plan, tool, content, in that order, in one day. The result is a browser application that eats heavy multi-animation sheets and emits a manifest, a packed sheet, a portrait and a roster entry, with the wire protocol following automatically.

Then 1 July, which is ten post-processing effects in a single day — bloom, colour grading, dynamic muzzle-flash lights, contact shadows, vignette, CRT scanlines, trauma-based screen shake, fog and falling ash, projectile trails, sun rays — each with its own console variable, and a new Effects tab created to hold them all.

2 – 7 July 2026

The cliff

Four commits in six days after thirteen days of fifty. All three substantial ones are infrastructure, and they read like someone cleaning up after a party they enjoyed.

Every setting in the game collapsed into one prefixed manifest, where the key is the wire name and its prefix is its category — save defaults, console variables, UI defaults and the options menu rows all generate from it. A review pass fixed real trust bugs, including hosts now clamping guest stat loadouts and ignoring guest god-mode. And then this:

Shortened every comment across the whole repo (C#, TS, CSS, tools, tests — 224 files, ~−3,800 lines) to one-line essentials while keeping load-bearing invariants, with zero code changes — build, typecheck, and tests verified clean.

That pass is why the codebase today contains exactly one TODO marker, and it is a section header.

8 July 2026

Souls

The Dungeon Master's resource had already been renamed three times — Threat, then Essence, then Rage. The fourth was not a rename. Your form burns souls to stay manifested; every slain monster feeds you its soul; at zero you fade away. The economy and the fiction became the same object.

And one clause in the patch note is worth the whole feature:

Soul wisps: dying monsters release glowing souls that fly across the battlefield into your meter, landing with a coin-like chime — the bar grows when the soul arrives, not before.

0.9.13 — Soul Harvest

The same day brought twenty-five amplifiers, a weighted level-up draft with rarity, and multiplayer rematch — when a co-op match ends, the party returns to the lobby together, still connected, without re-entering the room code.

11 – 12 July 2026

Weapons that manifest

The last feature in the log is a particle system built on a two-word thesis: sub-emitters and curl noise. Not a trail renderer — a ribbon draws one continuous strip behind the object, which is not what makes a projectile feel alive. Instead the projectile emits thousands of independent particles that then live their own lives.

Two details carry the whole effect. Particles spawn per pixel travelled, not per frame, so a fast bolt leaves a continuous line instead of dashes — and the straight-versus-curved look is free, because it is just the projectile's own ballistics imprinted on the spawn positions. Then, while a particle is still fast, its own velocity dominates and the line stays straight; as drag kills that velocity, the noise field takes over and the trail visibly melts into drifting wisps.

The implementation is a struct-of-arrays CPU pool — sixteen parallel arrays, sixteen thousand particle capacity — drawn as exactly two instanced meshes, one additive for glow and one alpha-blended for smoke. Thousands of living particles, two draw calls. It drops excess spawns rather than queueing them, so a burst storm degrades instead of stuttering.


Three war stories worth keeping

The FPS killer was not the swarm

With two thousand monsters on screen, turning the swarm on cost about one frame per second. The cliff was the weapons: every bullet was its own node doing a per-frame vector draw. Baking each weapon's procedural visual once into a shared atlas and making every bullet a sprite over an atlas region collapsed the whole swarm into roughly one draw call. Solo went from 5 to 40 FPS.

Render caps were tried first and reverted — capping projectile counts worked numerically but hid bullets, which is the wrong answer in a swarm game. The documentation records the rejection so nobody reintroduces it.

The black frames

On a busy host, the first render of an offscreen viewport can come back as an opaque black buffer — the transparent clear has not applied yet. So a bake captured early was entirely black on the host, while a pure client baking a moment later was fine. The fix is to retry across frames and only commit a frame that validates: transparent in the corner, opaque in a drawn cell.

The lesson that got written down next to it is the better artifact:

“FPS looks fine” does NOT prove correctness — black squares are just as cheap to draw as correct ones (that is how the blackout bug hid). Always eyeball, or assert, the actual pixels.

docs/netcode.md §5

The key-up that was stolen

The webview sits on top of the game window, and only one of them can own the operating system's keyboard focus. The game owns it by default. So when you held a movement key and clicked a level-up card with the mouse, the webview transiently stole focus, received the key-up the game was waiting for, and your hero walked off by itself.

The obvious fix — release all four movement keys when focus returns — is documented as wrong, with the reason: a key you are still physically holding only re-asserts for one key, because Windows key repeat only repeats the last one. That fix silently breaks diagonal movement. The real fix forwards the stolen key-up.


The thing that makes the log readable

Every version in this project has its own file, the version number is derived from the highest one rather than hand-edited, and there is a rule that a new day gets a new entry rather than piling onto yesterday's. Twenty-seven of them exist, each with a real title — the documentation explicitly forbids leaving one as “TBD.” There is even a rule, born of two people working in parallel, to check the last few commits first so a session does not claim what another already shipped.

The result is that the version table reads as a clean narrative — not because it was reconstructed afterwards, but because it was maintained as a hard rule while the sprint was happening.