Files
transcience/docs/ROADMAP.md
T
claude 7ef972e3b3
ci / verify (push) Successful in 47s
Hub character swapping, XP percentage, passive health regeneration
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.
2026-09-04 00:56:29 +02:00

11 KiB
Raw Blame History

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 — the packs themselves are not in git.

Design decisions that are already settled — and the reasoning behind them — live in 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
Dungeon generation from (seed, depth) done src/sim/map_gen.gd
Hand-authored boss arenas and hub done 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
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. 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
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
SFX pool driven by server events done 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 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.

Stage 2 — Characters, persistence, levels · done

Feature State Where
Steam-shaped identity abstraction done src/meta/auth_provider.gd, local_auth_provider.gd
Character store, JSON, survives restart done 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
Levels 115, +10 max HP each done 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.
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 115, +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 or 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.

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:

  1. What advances dungeon depth? --depth is a dev flag; nothing raises it in play.