Stage 2: accounts, characters, permadeath, levels and experience
ci / verify (push) Successful in 47s

Identity is shaped like Steamworks so swapping to it is one subclass and no
schema change: the client presents an opaque ticket, the server validates it
into a stable 64-bit account id, and nothing downstream sees anything else.
LocalAuthProvider takes any ticket at face value -- insecure on purpose, and
labelled as such everywhere, because the point is the shape rather than the
security. Do not ship it.

Characters persist as JSON keyed by account. Account ids are written as decimal
strings because they are 64-bit and JSON numbers are doubles, which would
silently round them. A corrupt store aborts the server rather than starting
empty: starting empty looks like it worked and then saves over every character
on the first level-up.

Levels 1-15, +10 max health each, level DERIVED from lifetime experience rather
than stored beside it, so a hand-edited save cannot produce a level 12 character
with a level 3's experience. Experience is shared undivided across everyone
alive in the instance -- splitting it would make bringing a friend cost you
progress. A level-up heals by what it added, so gaining one mid-fight is relief
rather than a bar that moved further from full.

Death is permanent and unbinds the character entirely: no "return to the hub as
the character who just died", because the run is over. The record is retired,
never deleted. The five-character cap counts LIVING characters only -- counting
the dead would lock a player out of their own account after five deaths.

Verified by tools/diag_progression.tscn, which drives the real server through
kill -> xp -> level -> health and death -> retire -> roster. The bot smoke test
cannot cover that: bots are poor shots and rarely kill anything. Writing it
caught two real ordering bugs -- the death event was dispatched before the
payload that tells the player they died, and the dead character stayed bound to
the peer.

Also added --account and --store so several clients and test runs can coexist
on one machine. The smoke test now uses a scratch store; without it a rerun
resumed the previous run's characters and "a character was created" quietly
stopped being true.

193 tests. check.sh, test.sh, smoke.sh, diag_progression and diag_prediction
all pass.
This commit is contained in:
2026-09-04 00:44:34 +02:00
parent ff5e527ad4
commit 4765bbce28
36 changed files with 1613 additions and 34 deletions
+9
View File
@@ -32,6 +32,8 @@ Everything after `--` goes to `GameOpts.parse()`:
| `--autoquit N` | Quit after N physics ticks. |
| `--boss-rush` | Server-side: dungeons spawn the boss and no trash. |
| `--depth N` | Server-side: depth of new dungeons, which drives map size. |
| `--account N` | Client-side: override the local account, so several clients can coexist on one machine. |
| `--store PATH` | Server-side: character store location. Use a scratch path in tests. |
| `--verbose` / `--quiet` | Log level. |
A change is done when `check.sh`, `test.sh` and — if it touched networking,
@@ -70,6 +72,7 @@ and `tests/integration/test_replica_parity.gd` pin this down.
| `src/sim/map_grid.gd` | Tile grid: collision, line of sight, chunk streaming. |
| `src/sim/map_gen.gd` | Dungeon generation; `build()` is the only entry point. |
| `src/content/rooms.gd` | Hand-authored room stamps (hub, boss arenas) as text. |
| `src/meta/` | Accounts, characters, persistence, XP curve. Server-owned. |
| `src/content/content.gd` | All enemies and bosses, defined in code. Source of truth. |
| `src/net/` | Codec, `ServerRuntime`, `ClientRuntime`. |
| `src/instances/` | Lobby hub and dungeon runs. |
@@ -133,6 +136,12 @@ ticks in milliseconds with no SceneTree.
- **Bullet speed must stay under one tile per tick.** Wall collision samples
position once per tick, so anything faster tunnels. Pinned by
`test_bullet_speeds_stay_below_the_tunnelling_threshold`.
- **Only `ServerRuntime` writes progression.** The simulation reads a player's
level and max health; it never grants experience or retires a character. One
writer means a level can never disagree with the experience that earned it.
- **`LocalAuthProvider` is insecure on purpose.** Any client can claim any
account. It exists to have the same shape as Steamworks (opaque ticket in,
64-bit account id out) so swapping is one class. Do not ship it.
- **No contact damage.** Every enemy threatens through bullets only; touching
one is harmless. `tests/unit/test_content.gd` enforces that every hostile has
an emitter.