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 tool →
create sprite sheet parser tool →
Hero 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.