7ef972e3b3
ci / verify (push) Successful in 47s
Character swapping is hub-only, and refused by the SERVER rather than merely greyed out in the menu. Allowing it inside a dungeon would be an instant, uninterruptible exit from danger -- strictly better than the one-second escape channel, which would make that channel pointless. Creating a character in a dungeon is refused for the same reason. Both are covered by diag_progression. Health regenerates at 0.5% of MAXIMUM per second. A percentage rather than a flat rate so it does not become irrelevant at level 15: a capped character regains 1.2 hp/s against a level 1's 0.5, and both take about 200 seconds to heal from nothing. No out-of-combat gate -- at this rate it cannot out-heal anything actually shooting at you, and a trickle that never stops is easier to reason about than a timer players have to learn. A fractional carry is needed because a tick heals well under one hit point, so truncating each tick would heal exactly nothing; there is a test for that specifically. The XP bar now states the percentage and the level it leads to, since a bar answers "how far" vaguely and a number answers it exactly. Recorded the two Stage 3 answers: 4 inventory slots, and the boss's food item is player-instanced now so the mechanism gets exercised rather than deferred. Writing the swap guard's test caught my own mistake: the first version created its spare character through the store, which has no opinion about where you are, so it bypassed the guard it was meant to prove and left a stray character behind that broke a later assertion. 200 tests. check.sh, test.sh, smoke.sh and both diagnostics pass.
204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# Status and roadmap
|
||
|
||
**Read this first when picking up work.** It maps every feature in the design
|
||
brief to its current state and the files that implement it, so you can find the
|
||
relevant code without re-reading the whole project.
|
||
|
||
Art and audio source packs, and their licence status, are recorded in
|
||
[ASSETS.md](ASSETS.md) — the packs themselves are not in git.
|
||
|
||
Design decisions that are already settled — and the reasoning behind them — live
|
||
in [DECISIONS.md](DECISIONS.md). Check there before asking the user something
|
||
that may already have an answer.
|
||
|
||
Legend: **done** · **partial** (works, with a stated gap) · **todo** (not started)
|
||
|
||
---
|
||
|
||
## Stage 1 — World and exploration · *done*
|
||
|
||
| Feature | State | Where |
|
||
| --- | --- | --- |
|
||
| Tile grid: movement / bullet / sight blocking | done | [src/sim/map_grid.gd](../src/sim/map_grid.gd) |
|
||
| Dungeon generation from (seed, depth) | done | [src/sim/map_gen.gd](../src/sim/map_gen.gd) |
|
||
| Hand-authored boss arenas and hub | done | [src/content/rooms.gd](../src/content/rooms.gd) |
|
||
| Walls, pillars, pits, barricades | done | `MapGrid.Kind` + the three `BLOCKS_*` tables |
|
||
| Enemies placed per room at creation | done | `Instance._populate()` |
|
||
| Scrolling camera | done | [src/view/game_scene.gd](../src/view/game_scene.gd) |
|
||
| Hard fog of war | done | `WorldView._draw_terrain` / `_visible` |
|
||
| Per-peer map streaming (anti map-hack) | done | `ServerRuntime._stream_map` |
|
||
| Aggro: range **and** line of sight | done | `SimWorld._aggro_target` |
|
||
| Cursor-to-world aiming under a scrolling camera | done | `ClientRuntime.screen_to_world` |
|
||
| Boss confined to its room | done | `SimWorld._step_boss` clamps to `SimBoss.room`. A no-op while bosses are stationary — established now so Stage 5's movement cannot quietly break it. |
|
||
| Actor interest management | done | `NetCodec.encode_snapshot(world, countdown, for_peer)`; per-peer encode in `ServerRuntime`. Bullet spawns filtered separately, see below. |
|
||
|
||
### The invariants that matter most here
|
||
|
||
**Maps are streamed per peer, and the generation seed is never sent.** See
|
||
[NETCODE.md](NETCODE.md#maps-are-streamed-never-sent). `MAP_STREAM_RADIUS` must
|
||
stay wider than `FOG_VIEW_RADIUS`, or the client predicts movement against
|
||
terrain it does not have.
|
||
|
||
**Three radii, deliberately different, and each has a floor it must respect:**
|
||
|
||
| Radius | Value | Must exceed | Why |
|
||
| --- | --- | --- | --- |
|
||
| `FOG_VIEW_RADIUS` | 460 | — | What the player can see. |
|
||
| `ACTOR_INTEREST_RADIUS` | 800 | fog radius | Enemies beyond it are never sent. Above the fog radius so nothing pops in at the edge of sight. |
|
||
| `MAP_STREAM_RADIUS` | 900 | fog radius | Client predicts movement and simulates bullets against terrain it cannot see. |
|
||
| `BULLET_INTEREST_RADIUS` | 2200 | longest bullet travel + fog radius | A bullet is announced once at spawn. Withhold one that later flies into view and it becomes invisible damage. `test_interest.gd` computes the floor from real content, so a faster bullet fails a test instead. |
|
||
|
||
Fog is a *rendering* rule and defends nothing on its own — a modified client
|
||
draws whatever it holds. The defence is what the server declines to send.
|
||
|
||
---
|
||
|
||
## Art and audio · *first pass done*
|
||
|
||
Placeholders are gone: terrain, actors and bullets are sprites, and four sounds
|
||
play off server events. Enough to prove the pipeline, not a finished look.
|
||
|
||
| Feature | State | Where |
|
||
| --- | --- | --- |
|
||
| Atlas/sound table in one place | done | [src/view/art.gd](../src/view/art.gd) |
|
||
| Terrain, actors, boss from the 0x72 atlas | done | `WorldView._draw_tile` / `_draw_sprite` |
|
||
| Animated bullet sprites, one MultiMesh per kind | done | [src/view/bullet_renderer.gd](../src/view/bullet_renderer.gd) |
|
||
| SFX pool driven by server events | done | [src/view/sfx.gd](../src/view/sfx.gd) |
|
||
| Rects validated without a display | done | `tests/unit/test_art.gd` |
|
||
| Impact/death VFX animation | todo | `Art.IMPACT` is loaded and validated but nothing plays it yet |
|
||
| Directional sprites, hit flashes, screen shake | todo | |
|
||
| Audio buses and a volume setting | todo | Everything plays on Master at hardcoded dB |
|
||
| **In-game credits screen** | **todo** | Not cosmetic: the SFX are CC BY 4.0 and attribution is a licence *requirement*. [CREDITS.md](../CREDITS.md) is not reachable by a player. |
|
||
| Replace the two non-redistributable packs | todo | Bullet and FX art is local-only and non-commercial. CC0 replacements would let them into the repo and unblock a commercial release. See [ASSETS.md](ASSETS.md). |
|
||
|
||
## Stage 2 — Characters, persistence, levels · *done*
|
||
|
||
| Feature | State | Where |
|
||
| --- | --- | --- |
|
||
| Steam-shaped identity abstraction | done | [src/meta/auth_provider.gd](../src/meta/auth_provider.gd), [local_auth_provider.gd](../src/meta/local_auth_provider.gd) |
|
||
| Character store, JSON, survives restart | done | [src/meta/character_store.gd](../src/meta/character_store.gd) |
|
||
| Up to 5 living characters, random colour | done | `CharacterStore.MAX_ACTIVE`, `Character.create` |
|
||
| Last-played auto-selected on login | done | `CharacterStore.last_played` |
|
||
| Permadeath → retired, never deleted | done | `ServerRuntime._on_player_died` |
|
||
| Roster screen: pick or create | done | [src/ui/character_select.gd](../src/ui/character_select.gd) |
|
||
| Levels 1–15, +10 max HP each | done | [src/meta/progression.gd](../src/meta/progression.gd) |
|
||
| XP from kills, bosses worth far more | done | `ServerRuntime._award_kill` |
|
||
| Colour visible in world and on the HUD | done | snapshot carries it; `WorldView._draw_ship` tints |
|
||
| Swap character from the hub | done | Esc menu → Change character; refused server-side in a dungeon |
|
||
| XP percentage to next level | done | `HUD._draw_xp_bar` |
|
||
| Passive health regeneration | done | `SimPlayer.regenerate`, 0.5%/s of maximum |
|
||
|
||
Verified end to end by `tools/diag_progression.tscn`, which drives the real
|
||
server through kill → xp → level → health and death → retire → roster. That
|
||
path cannot be covered by the bot smoke test, because bots are poor shots.
|
||
|
||
### Still open in this area
|
||
|
||
- **The local identity provider is insecure by design.** Any client can claim
|
||
any account id. Fine for a LAN; must be replaced before the game is reachable
|
||
from the internet. Swapping in Steam is one `AuthProvider` subclass and no
|
||
schema change.
|
||
- `--account` and `--store` exist so several clients and test runs can coexist
|
||
on one machine. A real provider makes `--account` unnecessary.
|
||
|
||
## Stage 3 — Inventory and loot · *todo, next*
|
||
|
||
Depends on nothing in Stage 1 except a place to stand. Blocked only on the
|
||
identity layer, which is decided but unbuilt.
|
||
|
||
| Feature | Notes |
|
||
| --- | --- |
|
||
| Identity abstraction | Shaped like Steamworks so it swaps out cleanly: opaque ticket from client → server validates → stable 64-bit account id (a SteamID64 stand-in). Local dev provider persists a generated id in `user://`. See [DECISIONS.md](DECISIONS.md#identity). |
|
||
| Character store | Server-side, keyed by account id. Needs a schema and a file format; nothing persistent exists in the project today. |
|
||
| Up to 5 characters, random colour each | Colour is a placeholder for a later cosmetic system. |
|
||
| Last-played character auto-selected on join | |
|
||
| Permadeath → mark inactive, never delete | Archival/troubleshooting. Confirm whether the 5-character cap counts only *active* characters. |
|
||
| Death flow: pick another character or create one | Replaces today's "return to hub" button, which currently just revives you. |
|
||
| Levels 1–15, +10 max HP per level | Confirm whether level 1 is 100 HP (→ 240 at cap) or 110. |
|
||
| XP from kills, bosses worth much more | First full dungeon should give slightly more than one level. |
|
||
|
||
Current behaviour to replace: `SimPlayer` has no identity beyond a peer id;
|
||
`ServerRuntime.peer_names` is client-supplied and trusted for display only.
|
||
|
||
---
|
||
|
||
## Stage 4 — Upgrades · *todo*
|
||
|
||
| Feature | Notes |
|
||
| --- | --- |
|
||
| Lobby NPC, 3 random choices per level gained | Confirm whether unclaimed level-ups queue. |
|
||
| Every upgrade also grants +5% damage, additive | Displayed on the choice alongside its specific effects. |
|
||
| Upgrade list | split shot, glass cannon, spread, sniper, doubleshot (rarer), poison (rare), eraser (legendary) |
|
||
| Rarity weights | **Unspecified.** Needs numbers. |
|
||
| Damage stacking formula | **Unspecified.** Sniper is explicitly multiplicative; the rest read as additive. Order needs pinning down before any of it is built. |
|
||
| "Chosen upgrades" screen | |
|
||
|
||
Damage is currently the constant `SimConfig.PLAYER_BULLET_DAMAGE`; upgrades turn
|
||
it into a per-player computed stat, so `SimWorld._fire_player_shot` grows a
|
||
stats block. Several upgrades (split, spread, doubleshot) change how many
|
||
bullets spawn per shot, so they belong in the same place.
|
||
|
||
---
|
||
|
||
| Feature | Notes |
|
||
| --- | --- |
|
||
| Small always-on-screen inventory | **4 slots** for now, may grow. |
|
||
| Health potions: rare from trash, guaranteed from bosses | |
|
||
| World-shared loot | Player-instanced loot planned later. |
|
||
| A unique, useless food item from bosses | **Player-instanced now**, so the mechanism is exercised rather than deferred. |
|
||
| Dropping items so others can pick them up | |
|
||
|
||
---
|
||
|
||
## Stage 5 — Boss features and new bosses · *todo*
|
||
|
||
| Feature | State |
|
||
| --- | --- |
|
||
| Stationary phases | done — every current phase |
|
||
| Roaming / chasing within the boss room | todo; needs `SimBoss.room` clamping (see Stage 1 partial) |
|
||
| Phases that move to preset locations | todo |
|
||
| Attacks spawned at a distance with a telegraph indicator | todo — new event type plus a renderer, and it must survive fog |
|
||
| More bosses | partial — `Rooms.choir_vault()` is authored but has no `BossDef` yet |
|
||
|
||
The boss format itself is proven: `tests/unit/test_boss.gd` builds a boss from
|
||
scratch and asserts the simulation needs no changes to run it. Movement will be
|
||
the first thing that format has not covered, so expect `BossPhase` to gain a
|
||
movement field rather than `SimWorld` gaining a branch.
|
||
|
||
---
|
||
|
||
## Deliberate omissions
|
||
|
||
Not oversights — each was considered and rejected for now, with the reasoning in
|
||
[NETCODE.md](NETCODE.md) or [DECISIONS.md](DECISIONS.md):
|
||
|
||
- **Lag compensation.** Rewinding to a shooter's view means a player who dodged
|
||
still gets hit; wrong trade for this genre.
|
||
- **Snapshot delta compression.** Fine at current actor counts.
|
||
- **DTLS / encryption.** `ENetMultiplayerPeer` supports it. Required before any
|
||
public server, not before then.
|
||
- **Pattern-level bullet replication.** A real bandwidth win that couples the
|
||
client to emitter behaviour.
|
||
- **`MultiplayerSynchronizer` / `MultiplayerSpawner`.** Right tools, wrong shape
|
||
for a bullet hell — see [ARCHITECTURE.md](ARCHITECTURE.md).
|
||
|
||
---
|
||
|
||
## Open questions for the user
|
||
|
||
Genuinely unspecified; do not guess at these, they change the design:
|
||
|
||
Deferred to the Stage 4 discussion (upgrades), but they block that stage:
|
||
|
||
1. Damage stacking order — additive pool then multiplicative, or something else?
|
||
2. Rarity weights for the four upgrade tiers.
|
||
3. Split shot geometry: ±22.5° from the original heading, or 45° to each side?
|
||
4. Poison: do applications stack, or refresh a single DoT?
|
||
5. Eraser: does it delete *enemy bullets* it passes through?
|
||
6. Do unclaimed level-ups queue at the NPC?
|
||
7. Glass cannon's −50% health: of base HP, or of levelled max HP?
|
||
|
||
Still open outside Stage 4:
|
||
|
||
8. What advances dungeon depth? `--depth` is a dev flag; nothing raises it in play.
|