Documentation pass before context compaction
ci / verify (push) Successful in 47s

The roadmap was actively misleading: an earlier stage renumbering left Stage 2's
completed work sitting under a "Stage 3 -- todo" heading, and the inventory
table orphaned with no heading at all. Since that file is the primary handoff
document, a fresh session would have started by re-implementing accounts and
characters. Rewritten.

Captured in full, from the original brief rather than from memory, the two
stages not yet built:

- Stage 3 (inventory and loot): 4 slots, potions rare from trash and guaranteed
  from bosses, world-shared loot, and the player-instanced food item -- with a
  note that two loot visibilities must exist from the start, because proving the
  instanced path works is the food item's entire purpose.
- Stage 4 (upgrades): every upgrade with its exact stated effect, plus the two
  constraints it will collide with -- sniper's 2x bullet speed against the
  tunnelling threshold, and per-player bullet travel against the interest radius
  that test_interest.gd currently derives from static content.

Ten open questions are listed as explicitly do-not-guess, seven of them blocking
Stage 4.

ARCHITECTURE.md still claimed "the arena is a rectangle" and "no tilemap
collision", both untrue since Stage 1. Rewritten around MapGrid, with what the
grid costs (axis-aligned, 32px-quantised, bullets under a tile per tick) rather
than only what it buys.

Added a "verification traps" section to WORKFLOW.md recording six mistakes made
during this work, each of which cost a round trip of reporting something fixed
that was not: verifying the artefact rather than the behaviour, dumping the
wrong channel, measuring a configuration where the bug cannot exist, a test
whose setup silently invalidated it, git checkout reverting real work alongside
a probe, and a pattern edit matching in two files. They are specific enough to
be actionable.

Also documented the diagnostics in CLAUDE.md -- they were undiscoverable -- and
recorded the current verification surface so "everything passes" has a stated
meaning.

206 tests, 15 smoke assertions, both diagnostics pass.
This commit is contained in:
2026-09-04 20:34:45 +02:00
parent d8197885ca
commit ded7bf96d5
4 changed files with 235 additions and 65 deletions
+38 -6
View File
@@ -45,11 +45,32 @@ This project deliberately does not, for four reasons:
prototyping and wrong for this shape: it would replicate per-bullet state and
saturate the link. See [NETCODE.md](NETCODE.md).
The trade is that there is no physics engine — no arbitrary collision shapes, no
tilemap collision. The arena is a rectangle and every hit is circle-vs-circle.
For a bullet hell that is not a limitation; hitboxes in the genre are circles by
convention anyway. A dungeon with real geometry would need a static collision
representation added to `SimWorld`, not a switch back to nodes.
The trade is that there is no physics engine, so collision is hand-written.
Since Stage 1 that means a tile grid ([MapGrid](../src/sim/map_grid.gd)):
circle-vs-tile for actors, a point test for bullets, and Bresenham line of sight
shared by fog and aggro. Actor-vs-actor hits stay circle-vs-circle, which is the
genre convention anyway.
That was the right call rather than a compromise: the same grid answers
collision, sight, and per-peer interest management as array lookups. On arbitrary
polygons all three become intersection tests, and the fog query in particular
stops being cheap enough to run every frame.
What it costs: geometry is axis-aligned and 32px-quantised, and bullets must
travel less than one tile per tick or the point test steps over walls (pinned by
`test_bullet_speeds_stay_below_the_tunnelling_threshold`).
## Where the layers sit now
Stage 1 and 2 added two things worth knowing before reading any file:
- **Geometry is per-world.** There is no global arena. `SimWorld.map` is a
[MapGrid](../src/sim/map_grid.gd); the hub and every dungeon have their own,
and the client holds a *partial* copy streamed to it in chunks.
- **`src/meta/` is server-only.** Accounts, characters, levels and experience
live there. The simulation reads a player's level and maximum health; it never
writes progression. One writer means a level cannot disagree with the
experience that earned it.
## Tick
@@ -100,4 +121,15 @@ after `--`:
| `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. |
| `src/instances/instance.gd` | Lobby hub and dungeon progression. |
| `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. |
| `src/view/bullet_renderer.gd` | The whole bullet field in one MultiMesh draw call. |
| `src/sim/map_grid.gd` | Tile grid: collision, line of sight, chunked streaming. |
| `src/sim/map_gen.gd` | Dungeon generation. `build()` is the only entry point both sides use. |
| `src/content/rooms.gd` | Hand-authored stamps: the hub and each boss arena, as text. |
| `src/meta/progression.gd` | XP curve and what a level is worth. Pure functions. |
| `src/meta/character.gd`, `character_store.gd` | Characters and their JSON persistence. Server-owned. |
| `src/meta/auth_provider.gd` | Identity, shaped like Steamworks so it swaps out. |
| `src/net/net_codec.gd` | Snapshot / event / input / roster / character / map-chunk codecs. |
| `src/net/server_runtime.gd` | Instances, ticking, transfers, interest, progression, map streaming. |
| `src/view/bullet_renderer.gd` | The bullet field: one MultiMesh per bullet kind. |
| `src/view/art.gd` | Every atlas rect and sound path, in one table. |
| `src/view/debug_draw.gd` | F1 overlay: what the simulation collides against. |
| `src/ui/character_select.gd` | Roster screen: pick or create. |