d8197885ca5d03b1753d055b6a779a32f45a79b0
ci / verify (push) Successful in 46s
The roster sent to clients now contains living characters only. Retirement stays server-side bookkeeping for archival; from the player's side a dead character is simply gone, and listing it offers a choice that cannot be taken. The XP bar only moved on a level-up or a character swap because it read the character roster, which is re-sent only when the SET of characters changes. Experience now rides the snapshot -- four bytes on a message already going out at 20Hz -- and the server mirrors each grant into the world immediately rather than only when a level is crossed. The create field starts with a suggested name instead of blank, and offers another after each creation. Two bugs found while testing, both mine: The first was a bad patch of my own: a change meant for the snapshot decoder also matched inside decode_characters, which then read a four-byte field its encoder never wrote and ran off the end of every packet. This is precisely the "encodes but decodes wrong" failure the codec tests exist to catch, and it was caught within a minute of the test being written. Chasing that exposed a real robustness gap: StreamPeerBuffer.get_utf8_string() pushes an engine error and returns garbage when the buffer is short, so a truncated or hostile character/roster packet produced error spam instead of degrading. Both decoders now bounds-check every field, with tests that slice each packet at many lengths and assert it degrades rather than inventing entries -- the same guarantee the input decoder already had. 206 tests. check.sh, test.sh, smoke.sh and both diagnostics pass.
Transcience
Top-down twin-stick bullet-hell with a dedicated, server-authoritative backend. Godot 4.7, GDScript, no external runtime dependencies.
Play
tools/server.sh # dedicated server on :27015
tools/client.sh --join --name ada # connect a client
Or run one client and press Host and play for a listen server.
Controls — WASD move, mouse aim, LMB fire, E on the ring in the hub to enter a dungeon, hold F for three seconds to escape back to the hub.
Develop
tools/check.sh # parse-check every script ~5s
tools/test.sh # GUT suite, headless ~2s
tools/smoke.sh # server + 2 bot clients, ENet ~35s
godot --headless --path . --script tools/bench.gd
Read CLAUDE.md first — it is short, and the Godot CLI gotchas in it will otherwise cost an afternoon.
What is here
| Twin-stick bullet hell | Server-simulated bullet field, ~350 concurrent bullets at 0.24 ms/tick. |
| Authoritative multiplayer | Clients send input only. Hits, damage, death and instance transfers are server decisions. Prediction + reconciliation for the local player. |
| Enemies | Four readable behaviours (static, drift, orbit, approach, strafe) composed with bullet emitters. |
| Boss | The Warden of the Fold: stationary, four phases, each adding one idea. Defined entirely as data. |
| Lobby hub | Shared persistent instance with a portal into dungeon runs. |
| Emergency escape | Three-second channel back to the hub, cancelled by damage. |
Docs
- docs/ROADMAP.md — what is built, what is next, where each feature lives
- docs/DECISIONS.md — settled design decisions and their reasoning
- CREDITS.md — third-party asset attribution
- CLAUDE.md — commands, invariants, Godot CLI gotchas
- docs/WORKFLOW.md — agent-assisted Godot development, and why the setup looks like this
- docs/ARCHITECTURE.md — code map and the node-free simulation
- docs/NETCODE.md — replication model, hit validation, known gaps
- docs/ROADMAP.md — what is built and what is next
Description
Languages
GDScript
98.6%
Shell
1.1%
Python
0.3%