ff5e527ad41248c1dd4903446e215d0865622139
ci / verify (push) Successful in 46s
Bullets rendered as the entire sprite sheet, which was the clue that named the cause: a MultiMeshInstance2D cannot show a sub-region of a texture. Its mesh UVs run 0..1 across whatever texture it is handed, and an AtlasTexture does not help -- the draw resolves to the underlying atlas and the region is discarded. Frames are now pre-sliced into 32 standalone 16x16 textures at startup, so each frame is one texture assignment per kind and no region is involved. My previous two attempts fixed the atlas contents while the renderer was structurally incapable of showing one cell of it. Black voids above walls: wall_top_mid has 12 of its 16 rows transparent. It is a cap meant to be laid over a wall, not a tile, and drawn alone it was three quarters nothing. Floor is now drawn beneath every tile so transparency shows ground rather than clear colour, the wall face is drawn from the opaque wall_mid, and the cap is overlaid only where open floor lies above. Pillars used a wall segment and read as stray blocks; they now use the column tile, which looks like something you walk around. The player hitbox appeared at the head because it was: the knight occupies rows 8..27 of a 28-row cell, so anchoring on the cell centre put the draw origin about 4px above the body's centre. Added a per-sprite anchor offset that cancels it, with a test that recomputes the offset from the art so a re-cut sheet fails rather than silently misplacing the character. Tests pin the two tileset facts that caused this: the cap is mostly transparent (so it may never be drawn alone) and the wall face is fully opaque (so it may). 166 tests. check.sh, test.sh and smoke.sh 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%