Audited the whole set rather than appending to it, and the two files a fresh session reads first were both wrong. README claimed the emergency escape takes three seconds and is CANCELLED BY DAMAGE. It takes one, and damage explicitly does not interrupt it -- that is a settled decision with its own entry in DECISIONS.md, and the front page said the opposite. It also described one stationary boss, one hub portal, and none of characters, permadeath, levels, inventory, loot, upgrades, settings or credits. Rewritten, with the "not ready for the internet" warning made explicit. ROADMAP had no "what is next" at all: every stage reads *done*, which for a cold start is a dead end. It opens with where things stand and a table of candidates -- auth, a reason to play past level 15, an economy, hit feedback, replacing the non-redistributable packs, DTLS -- each with what blocks it, and says plainly that the user has chosen none of them. Open questions renumbered from the orphaned 8-11 they were left at, with the solved one dropped and three real ones added. ARCHITECTURE's file map listed two files twice, missed five subsystems (upgrades, stats, poison, settings, credits, the UI theme), and still said MapGen.build() is "the entry point both sides use" -- it is server-only, and the whole anti-map-hack story depends on that. Rebuilt by layer and audited against the tree: every path listed exists, and every one of the 64 source files is covered. CLAUDE.md's security paragraph said a client can send "exactly two things" plus two roster requests. There are five client -> server messages. That number is the security model, so it is now a table naming each one and what it carries. Also records that nothing automated can see the screen. Smaller: a stale 156/156 test count in ASSETS.md, and test counts refreshed to 468 where they are quoted. check.sh clean, 468 tests, SMOKE PASS, all four diagnostics green, no broken internal links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ the obvious default.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/check.sh # parse-check every script (~5s) -- run after every edit
|
tools/check.sh # parse-check every script (~5s) -- run after every edit
|
||||||
tools/test.sh # GUT suite, headless (~3s)
|
tools/test.sh # 468 GUT tests, headless (~4s)
|
||||||
tools/smoke.sh # real server + 4 bot clients over ENet (~40s)
|
tools/smoke.sh # real server + 4 bot clients over ENet (~40s)
|
||||||
tools/server.sh # dedicated server
|
tools/server.sh # dedicated server
|
||||||
tools/client.sh --listen # host and play, no menu
|
tools/client.sh --listen # host and play, no menu
|
||||||
@@ -37,6 +37,12 @@ test cannot cover any of them: bots are poor shots, so they rarely kill
|
|||||||
anything, which means they neither earn levels, produce drops, nor ever reach
|
anything, which means they neither earn levels, produce drops, nor ever reach
|
||||||
the quartermaster.
|
the quartermaster.
|
||||||
|
|
||||||
|
**Nothing automated can see the screen.** `check.sh`, the suite and `smoke.sh`
|
||||||
|
have all passed with the entire interface rendering unstyled, an invisible
|
||||||
|
slider and an invisible scrollbar. After touching anything under `src/ui/` or
|
||||||
|
`src/view/`, run `screenshot.tscn` and open the files. It needs a display, so it
|
||||||
|
is not a gate.
|
||||||
|
|
||||||
Everything after `--` goes to `GameOpts.parse()`:
|
Everything after `--` goes to `GameOpts.parse()`:
|
||||||
|
|
||||||
| Flag | Effect |
|
| Flag | Effect |
|
||||||
@@ -69,12 +75,22 @@ push (~35s, skip deliberately with `SKIP_SMOKE_HOOK=1 git push`).
|
|||||||
|
|
||||||
**The server decides everything; the client only sends intent.**
|
**The server decides everything; the client only sends intent.**
|
||||||
|
|
||||||
A client can send exactly two things: an [InputFrame] (move vector, aim angle,
|
There are exactly **five** client -> server messages, and that number is worth
|
||||||
five button bits, an inventory slot) and a handshake — plus the two low-rate
|
watching:
|
||||||
character-roster requests, which are also pure intent. There is no message for
|
|
||||||
"I moved here", "I hit that", "I took damage", "my escape finished" or "I now
|
| Message | Carries |
|
||||||
own this item". Adding one would collapse the whole security model, so don't —
|
| --- | --- |
|
||||||
validate-after-the-fact is strictly weaker than having no code path at all.
|
| `c_hello` | protocol version + an opaque auth ticket |
|
||||||
|
| `c_input` | an [InputFrame]: move vector, aim angle, five button bits, an inventory slot |
|
||||||
|
| `c_select_character` | a character id, checked against *that account's* list |
|
||||||
|
| `c_create_character` | a name, sanitised at the boundary |
|
||||||
|
| `c_choose_upgrade` | an index into the three options the SERVER put on the table |
|
||||||
|
|
||||||
|
Every one is pure intent. There is no message for "I moved here", "I hit that",
|
||||||
|
"I took damage", "my escape finished", "I now own this item", "I am in this
|
||||||
|
dungeon" or "my damage is X". Adding one would collapse the whole security
|
||||||
|
model, so don't — validate-after-the-fact is strictly weaker than having no code
|
||||||
|
path at all.
|
||||||
|
|
||||||
When a new player action needs a message, look at whether it fits in the input
|
When a new player action needs a message, look at whether it fits in the input
|
||||||
frame first. Item use and drop did, and got the redundancy, the replay guard and
|
frame first. Item use and drop did, and got the redundancy, the replay guard and
|
||||||
@@ -95,7 +111,7 @@ and `tests/integration/test_replica_parity.gd` pin this down.
|
|||||||
| `src/actors/` | Data-only `Resource` definitions: `EnemyDef`, `BossDef`, `ItemDef`, `LootDrop`, `DungeonDef`, `UpgradeDef`. Shapes, not instances. |
|
| `src/actors/` | Data-only `Resource` definitions: `EnemyDef`, `BossDef`, `ItemDef`, `LootDrop`, `DungeonDef`, `UpgradeDef`. Shapes, not instances. |
|
||||||
| `src/sim/patterns/` | Bullet emitters — the authoring surface for every enemy and boss. Emitters are stateless: they are shared resources, and two bosses of the same kind must not stomp each other. |
|
| `src/sim/patterns/` | Bullet emitters — the authoring surface for every enemy and boss. Emitters are stateless: they are shared resources, and two bosses of the same kind must not stomp each other. |
|
||||||
| `src/sim/map_grid.gd` | Tile grid: collision, line of sight, chunk streaming. |
|
| `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/sim/map_gen.gd` | Dungeon generation. **Server-side only** — handing a client the seed would be a map hack with no work required. |
|
||||||
| `src/content/rooms.gd` | Hand-authored room stamps (hub, boss arenas) as text. |
|
| `src/content/rooms.gd` | Hand-authored room stamps (hub, boss arenas) as text. |
|
||||||
| `src/meta/` | Accounts, characters, persistence, XP curve. Server-owned. |
|
| `src/meta/` | Accounts, characters, persistence, XP curve. Server-owned. |
|
||||||
| `src/content/content.gd` | All enemies and bosses, defined in code. Source of truth. |
|
| `src/content/content.gd` | All enemies and bosses, defined in code. Source of truth. |
|
||||||
|
|||||||
@@ -12,39 +12,51 @@ tools/client.sh --join --name ada # connect a client
|
|||||||
|
|
||||||
Or run one client and press **Host and play** for a listen server.
|
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
|
**Controls** — WASD move, mouse aim, LMB fire, **E** to pick up loot and to use
|
||||||
enter a dungeon, **hold F** for three seconds to escape back to the hub.
|
the hub's portals and quartermaster, **1–4** use an inventory slot, **shift+1–4**
|
||||||
|
drop one, **hold F** for one second to escape a dungeon, **Esc** menu, **F1**
|
||||||
## Develop
|
hitbox overlay. Every one of those is rebindable in Settings.
|
||||||
|
|
||||||
```bash
|
|
||||||
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](CLAUDE.md) first — it is short, and the Godot CLI gotchas in it
|
|
||||||
will otherwise cost an afternoon.
|
|
||||||
|
|
||||||
## What is here
|
## What is here
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Twin-stick bullet hell** | Server-simulated bullet field, ~350 concurrent bullets at 0.24 ms/tick. |
|
| **Twin-stick bullet hell** | Server-simulated bullet field, ~300 concurrent bullets at 0.28 ms/tick — around 60x headroom against the 60 Hz budget. |
|
||||||
| **Authoritative multiplayer** | Clients send input only. Hits, damage, death and instance transfers are server decisions. Prediction + reconciliation for the local player. |
|
| **Authoritative multiplayer** | Clients send input and nothing else. Hits, damage, death, loot, upgrades and instance transfers are all server decisions. Prediction and reconciliation for the local player. |
|
||||||
| **Enemies** | Four readable behaviours (static, drift, orbit, approach, strafe) composed with bullet emitters. |
|
| **Enemies** | Five readable movements (static, drift, orbit, approach, strafe) composed with bullet emitters. Nothing deals contact damage; every threat is a bullet you can see. |
|
||||||
| **Boss** | The Warden of the Fold: stationary, four phases, each adding one idea. Defined entirely as data. |
|
| **Two bosses** | The Warden of the Fold stands still through four phases. The Cantor of the Vault walks a circuit, chases, orbits, and marks the floor before it strikes. Both are pure data. |
|
||||||
| **Lobby hub** | Shared persistent instance with a portal into dungeon runs. |
|
| **Three dungeons** | Two real runs, one per boss, plus a Proving Grounds: the same content at a fraction of the health and ten times the drop rate, for testing by hand. |
|
||||||
| **Emergency escape** | Three-second channel back to the hub, cancelled by damage. |
|
| **Characters** | Up to five living per account, permadeath, levels 1–15, experience shared across the party undivided. |
|
||||||
|
| **Inventory and loot** | Four slots, always on screen. Loot is either world-shared or instanced per player, and the instanced kind is filtered on the wire rather than hidden in the client. |
|
||||||
|
| **Upgrades** | Seven, drawn three at a time and spent at a hub NPC. Damage, fire rate, extra projectiles, splitting, poison, and a legendary that deletes enemy bullets. |
|
||||||
|
| **Emergency escape** | A one-second channel back to the hub that damage does **not** interrupt — because if it did, quitting the process would be the better escape. Dropping your connection runs the same channel. |
|
||||||
|
| **Settings** | Rebindable controls and volumes, and an in-game credits screen. |
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tools/check.sh # parse-check every script ~5s
|
||||||
|
tools/test.sh # 468 GUT tests, headless ~4s
|
||||||
|
tools/smoke.sh # server + 4 bot clients over ENet ~40s
|
||||||
|
```
|
||||||
|
|
||||||
|
Read [CLAUDE.md](CLAUDE.md) first — it is short, and the Godot CLI gotchas in it
|
||||||
|
will otherwise cost an afternoon.
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
- [docs/ROADMAP.md](docs/ROADMAP.md) — what is built, what is next, where each feature lives
|
- [docs/ROADMAP.md](docs/ROADMAP.md) — what is built, what is next, where each feature lives
|
||||||
- [docs/DECISIONS.md](docs/DECISIONS.md) — settled design decisions and their reasoning
|
- [docs/DECISIONS.md](docs/DECISIONS.md) — settled design decisions and their reasoning
|
||||||
- [CREDITS.md](CREDITS.md) — third-party asset attribution
|
|
||||||
- [CLAUDE.md](CLAUDE.md) — commands, invariants, Godot CLI gotchas
|
|
||||||
- [docs/WORKFLOW.md](docs/WORKFLOW.md) — agent-assisted Godot development, and why the setup looks like this
|
|
||||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — code map and the node-free simulation
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — code map and the node-free simulation
|
||||||
- [docs/NETCODE.md](docs/NETCODE.md) — replication model, hit validation, known gaps
|
- [docs/NETCODE.md](docs/NETCODE.md) — replication model, hit validation, known gaps
|
||||||
- [docs/ROADMAP.md](docs/ROADMAP.md) — what is built and what is next
|
- [docs/WORKFLOW.md](docs/WORKFLOW.md) — agent-assisted Godot development, and the verification traps this project has actually hit
|
||||||
|
- [docs/ASSETS.md](docs/ASSETS.md) — asset packs and their licence findings
|
||||||
|
- [CREDITS.md](CREDITS.md) — third-party attribution
|
||||||
|
- [CLAUDE.md](CLAUDE.md) — commands, invariants, Godot CLI gotchas
|
||||||
|
|
||||||
|
## Not ready for the internet
|
||||||
|
|
||||||
|
`LocalAuthProvider` lets any client claim any account id. It exists to have the
|
||||||
|
same shape as Steamworks so swapping is one class, and it must be replaced
|
||||||
|
before this is reachable from anywhere untrusted. There is no transport
|
||||||
|
encryption either. See [docs/ROADMAP.md](docs/ROADMAP.md).
|
||||||
|
|||||||
+78
-16
@@ -62,15 +62,24 @@ travel less than one tile per tick or the point test steps over walls (pinned by
|
|||||||
|
|
||||||
## Where the layers sit now
|
## Where the layers sit now
|
||||||
|
|
||||||
Stage 1 and 2 added two things worth knowing before reading any file:
|
Four things are worth knowing before reading any file:
|
||||||
|
|
||||||
- **Geometry is per-world.** There is no global arena. `SimWorld.map` is a
|
- **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,
|
[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.
|
and the client holds a *partial* copy streamed to it in chunks.
|
||||||
- **`src/meta/` is server-only.** Accounts, characters, levels and experience
|
- **`src/meta/` is server-only.** Accounts, characters, levels, experience,
|
||||||
live there. The simulation reads a player's level and maximum health; it never
|
inventories and upgrades live there. The simulation reads a player's level and
|
||||||
writes progression. One writer means a level cannot disagree with the
|
maximum health and moves items between the ground and a bag; it never writes
|
||||||
|
progression or touches the store. It announces what happened and
|
||||||
|
`ServerRuntime` banks it. One writer means a level cannot disagree with the
|
||||||
experience that earned it.
|
experience that earned it.
|
||||||
|
- **Derived, never stored.** Level comes from lifetime experience; combat
|
||||||
|
numbers come from the upgrade list ([PlayerStats](../src/sim/player_stats.gd));
|
||||||
|
maximum health comes from the level and the upgrades. Nothing that can be
|
||||||
|
recomputed is persisted, so nothing saved can disagree with what produced it.
|
||||||
|
- **`src/core/settings.gd` is the exception to "shared".** Everything else in
|
||||||
|
`src/core/` is agreed by both sides; settings are the player's own machine
|
||||||
|
and never reach the server.
|
||||||
|
|
||||||
## Tick
|
## Tick
|
||||||
|
|
||||||
@@ -96,6 +105,13 @@ BossDef = stats + ordered phases
|
|||||||
`SimWorld._run_emitters()` is shared by enemies and bosses, so any pattern can
|
`SimWorld._run_emitters()` is shared by enemies and bosses, so any pattern can
|
||||||
be dropped on either. A boss is four phases layering one idea at a time; a new
|
be dropped on either. A boss is four phases layering one idea at a time; a new
|
||||||
boss is a new function in `src/content/content.gd` and zero simulation changes.
|
boss is a new function in `src/content/content.gd` and zero simulation changes.
|
||||||
|
`SimWorld._move_boss()` is the same idea for movement: the mode is a field on
|
||||||
|
the phase, and adding a boss that walks needs no code.
|
||||||
|
|
||||||
|
Emitters are **stateless**. They are shared resources — two bosses of the same
|
||||||
|
kind would otherwise stomp each other's timers — so anything an emitter needs to
|
||||||
|
remember between two ticks has to be derived instead. `TelegraphedStrikeEmitter`
|
||||||
|
computes its strike positions from the volley number for exactly this reason.
|
||||||
|
|
||||||
## Entry point
|
## Entry point
|
||||||
|
|
||||||
@@ -108,30 +124,76 @@ after `--`:
|
|||||||
|
|
||||||
## File map
|
## File map
|
||||||
|
|
||||||
|
Grouped by layer. The rule the whole thing hangs on: **`src/sim/` imports
|
||||||
|
nothing from `net`, `view` or `ui`, and never touches `Net`.**
|
||||||
|
|
||||||
|
### Shared constants and helpers
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `src/core/sim_config.gd` | Every constant server and client must agree on. |
|
| `src/core/sim_config.gd` | Every constant server and client must agree on. |
|
||||||
| `src/core/movement.gd` | Pure movement + overlap helpers. Shared by prediction. |
|
| `src/core/movement.gd` | Pure movement + overlap helpers. Shared by prediction. |
|
||||||
| `src/core/game_log.gd`, `game_opts.gd` | Static; usable from tools and tests. |
|
| `src/core/game_log.gd`, `game_opts.gd` | Static; usable from tools and tests. |
|
||||||
| `src/sim/sim_world.gd` | The simulation. Authority flag decides what runs. |
|
| `src/core/settings.gd` | Client-local preferences. Never reaches the server. |
|
||||||
|
| `src/core/credits.gd` | Third-party assets and their licences. Two are CC BY, so this is a legal requirement. |
|
||||||
|
|
||||||
|
### The simulation
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/sim/sim_world.gd` | The simulation. The authority flag decides what runs. |
|
||||||
| `src/sim/bullet_pool.gd` | Struct-of-arrays bullet storage and integration. |
|
| `src/sim/bullet_pool.gd` | Struct-of-arrays bullet storage and integration. |
|
||||||
| `src/sim/input_frame.gd` | The only thing a client may assert about itself. |
|
| `src/sim/input_frame.gd` | The only thing a client may assert about itself. |
|
||||||
| `src/net/net_codec.gd` | Snapshot / event / input binary codecs. |
|
| `src/sim/player_stats.gd` | Combat numbers derived from a character's upgrades. |
|
||||||
| `src/net/server_runtime.gd` | Instances, ticking, transfers, broadcast. |
|
| `src/sim/poison_track.gd` | Damage over time. O(1) per actor per tick however many doses. |
|
||||||
| `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. |
|
| `src/sim/sim_loot.gd` | An item on the ground: world-shared, or owned by one peer. |
|
||||||
| `src/instances/instance.gd` | Lobby hub and dungeon progression. |
|
| `src/sim/sim_portal.gd` | A dungeon entrance, and which dungeon it opens. |
|
||||||
| `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. |
|
|
||||||
| `src/sim/map_grid.gd` | Tile grid: collision, line of sight, chunked streaming. |
|
| `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/sim/map_gen.gd` | Dungeon generation. **Server-side only** — the client is never given the seed. |
|
||||||
| `src/content/rooms.gd` | Hand-authored stamps: the hub and each boss arena, as text. |
|
| `src/sim/patterns/` | Bullet emitters. Stateless; the authoring surface for every fight. |
|
||||||
|
|
||||||
|
### Content (data, in code)
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/content/content.gd` | Every enemy and boss. `ALL_ENEMIES` / `ALL_BOSSES` are what the tools and tests iterate. |
|
||||||
| `src/content/items.gd` | Every item. `Items.ORDER` doubles as the wire format. |
|
| `src/content/items.gd` | Every item. `Items.ORDER` doubles as the wire format. |
|
||||||
| `src/sim/sim_loot.gd` | An item on the ground. World-shared, or owned by one peer. |
|
| `src/content/upgrades.gd` | The seven upgrades and their draw weights. |
|
||||||
|
| `src/content/dungeons.gd` | The kinds of run. `ORDER` is a wire format *and* the hub's portal order. |
|
||||||
|
| `src/content/rooms.gd` | Hand-authored stamps: the hub and each boss arena, as text. |
|
||||||
|
| `src/actors/` | The `Resource` definitions those tables build: enemy, boss, item, loot, upgrade, dungeon. |
|
||||||
|
|
||||||
|
### Server-owned state
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --- | --- |
|
||||||
| `src/meta/progression.gd` | XP curve and what a level is worth. Pure functions. |
|
| `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/character.gd`, `character_store.gd` | Characters and their JSON persistence. |
|
||||||
| `src/meta/auth_provider.gd` | Identity, shaped like Steamworks so it swaps out. |
|
| `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/instances/instance.gd` | The hub and one dungeon run: a world plus a peer list. |
|
||||||
| `src/net/server_runtime.gd` | Instances, ticking, transfers, interest, progression, map streaming. |
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/net/net_codec.gd` | Every binary codec: snapshot, events, input, roster, characters, map chunks, portals, upgrade state. |
|
||||||
|
| `src/net/server_runtime.gd` | Instances, ticking, transfers, interest, progression, persistence, map streaming. |
|
||||||
|
| `src/net/client_runtime.gd` | Prediction, reconciliation, interpolation, bot input. |
|
||||||
|
| `src/net/protocol.gd` | Wire version and constants. Bump `VERSION` whenever a layout changes. |
|
||||||
|
| `src/autoload/net.gd` | ENet lifecycle, RPCs, local loopback for listen servers. The only autoload. |
|
||||||
|
|
||||||
|
### View and UI (read-only; decides nothing)
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/view/world_view.gd` | Everything that is not a bullet. Fog, actors, loot, telegraphs. |
|
||||||
| `src/view/bullet_renderer.gd` | The bullet field: one MultiMesh per bullet kind. |
|
| `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/art.gd` | Every atlas rect and sound path, in one table. |
|
||||||
|
| `src/view/ui_theme.gd` | The control theme, built in code from the UI pack. |
|
||||||
| `src/view/debug_draw.gd` | F1 overlay: what the simulation collides against. |
|
| `src/view/debug_draw.gd` | F1 overlay: what the simulation collides against. |
|
||||||
|
| `src/view/game_scene.gd`, `sfx.gd` | Wiring the view to whatever client `Net` currently has. |
|
||||||
|
| `src/ui/hud.gd` | Bars, inventory, prompts. Drawn, not built from controls. |
|
||||||
|
| `src/ui/main_menu.gd`, `game_menu.gd` | Connect screen and the in-game menu. |
|
||||||
| `src/ui/character_select.gd` | Roster screen: pick or create. |
|
| `src/ui/character_select.gd` | Roster screen: pick or create. |
|
||||||
|
| `src/ui/upgrade_screen.gd` | The quartermaster's three choices, and what you hold. |
|
||||||
|
| `src/ui/settings_screen.gd`, `credits_screen.gd` | Controls, volumes, attribution. |
|
||||||
|
|||||||
+5
-3
@@ -51,9 +51,11 @@ modification.
|
|||||||
|
|
||||||
So they live in `assets/local/`, which is gitignored. `Art.bullets_texture()`
|
So they live in `assets/local/`, which is gitignored. `Art.bullets_texture()`
|
||||||
and `Art.impact_texture()` load them at runtime if present and return null if
|
and `Art.impact_texture()` load them at runtime if present and return null if
|
||||||
not; the bullet renderer falls back to a generated dot. Verified both ways: with
|
not; the bullet renderer falls back to a generated dot. Verified both ways: with the files
|
||||||
the files present the game uses them, and with them hidden the suite still
|
present the game uses them, and with them hidden the whole suite still passes
|
||||||
passes 156/156 and the game runs.
|
and the game runs — `tests/unit/test_art.gd` has an explicit case for their
|
||||||
|
absence, because a clean clone is the configuration most contributors will
|
||||||
|
actually have.
|
||||||
|
|
||||||
This is also why nothing there may be `preload()`ed — `preload` resolves at
|
This is also why nothing there may be `preload()`ed — `preload` resolves at
|
||||||
compile time and would fail the build on every machine that lacks the files.
|
compile time and would fail the build on every machine that lacks the files.
|
||||||
|
|||||||
+55
-14
@@ -13,6 +13,41 @@ that may already have an answer.
|
|||||||
|
|
||||||
Legend: **done** · **partial** (works, with a stated gap) · **todo** (not started)
|
Legend: **done** · **partial** (works, with a stated gap) · **todo** (not started)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where things stand
|
||||||
|
|
||||||
|
**Every stage in the original brief is built.** The MVP asked for a twin-stick
|
||||||
|
bullet hell, server-side hit validation, predictable enemies, an adaptable boss,
|
||||||
|
a lobby hub with dungeon runs, and an emergency escape. All of that exists, plus
|
||||||
|
the four stages that followed: characters and permadeath, inventory and loot,
|
||||||
|
upgrades, and a second boss that moves.
|
||||||
|
|
||||||
|
So there is no obvious "next ticket". What follows is the honest list of
|
||||||
|
candidates, none of which the user has chosen — **ask before starting one.**
|
||||||
|
|
||||||
|
| Candidate | Why it might be next | What it needs first |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Replace `LocalAuthProvider`** | It is the one thing standing between this and a server anyone else can reach. Any client can claim any account id. | Nothing technical — it is one `AuthProvider` subclass. Needs a decision about Steam vs something else. |
|
||||||
|
| **A reason to keep playing past level 15** | Levels cap, `--depth` never rises in play, and a cleared dungeon gives experience and a potion. The long loop is missing. | Open question 1 below. |
|
||||||
|
| **An economy** | Loot only drops from kills; nothing sells anything. The quartermaster is the obvious shop and has no currency to take. | Open questions 2 and 3. |
|
||||||
|
| **Impact and hit feedback** | `Art.IMPACT` is loaded and validated and nothing plays it. No hit flash, no screen shake, no death animation. | Nothing. This is the cheapest visible win. |
|
||||||
|
| **Replace the two non-redistributable packs** | Bullet and FX art is local-only and non-commercial, which blocks both a public repo copy and a commercial release. | CC0 or CC BY replacements. See [ASSETS.md](ASSETS.md). |
|
||||||
|
| **Transport encryption** | `ENetMultiplayerPeer` supports DTLS. Required before a public server, not before then. | Follows the auth decision. |
|
||||||
|
|
||||||
|
Each stage below also carries its own **Known gaps** section — those are smaller
|
||||||
|
and more specific than the table above.
|
||||||
|
|
||||||
|
### Known flake
|
||||||
|
|
||||||
|
`tools/smoke.sh` failed once in a pre-push hook and then passed on four
|
||||||
|
consecutive runs with no stray processes. It was not reproduced and is not
|
||||||
|
diagnosed. The suspicion is machine load — the smoke test has fixed tick budgets
|
||||||
|
and was competing with a full suite run — but that is a guess. If it recurs,
|
||||||
|
that is the first thing to check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Verification surface
|
## Verification surface
|
||||||
|
|
||||||
What "everything passes" currently means. Numbers move; the shape does not.
|
What "everything passes" currently means. Numbers move; the shape does not.
|
||||||
@@ -441,18 +476,24 @@ Not oversights — each was considered and rejected for now, with the reasoning
|
|||||||
## Open questions for the user
|
## Open questions for the user
|
||||||
|
|
||||||
Genuinely unspecified. **Do not guess at these** — each changes the design, and
|
Genuinely unspecified. **Do not guess at these** — each changes the design, and
|
||||||
several have no obvious default.
|
several have no obvious default. The seven questions that blocked Stage 4 were
|
||||||
|
answered and are recorded in [DECISIONS.md](DECISIONS.md#upgrades); these are
|
||||||
|
what is left.
|
||||||
|
|
||||||
### Blocking nothing yet
|
1. **What advances dungeon depth?** `--depth` is a dev flag and nothing raises
|
||||||
|
it in play. Depth already drives map size and could drive difficulty and
|
||||||
8. **What advances dungeon depth?** `--depth` is a dev flag; nothing raises it
|
rewards. Without an answer there is no progression past level 15.
|
||||||
in play. Depth drives map size and could drive difficulty and rewards.
|
2. **Where do items come from outside a dungeon?** Loot only drops from kills.
|
||||||
9. **Where do items come from outside a dungeon?** Loot only drops from kills.
|
If the hub should sell potions, that is the quartermaster's second job.
|
||||||
If the hub should sell potions, that is the Stage 4 NPC's second job — and
|
3. **Is there a currency?** Nothing drops money and nothing costs anything. A
|
||||||
it needs a currency, which the game does not have.
|
shop needs one; so does any reward for a cleared run beyond experience.
|
||||||
10. **Should items stack?** Four potions currently take four slots, which makes
|
4. **Should items stack?** Four potions currently take four slots, which makes a
|
||||||
a 4-slot bag small. Stacking is a count byte per slot plus a rule for
|
four-slot bag small. Stacking is a count byte per slot plus a rule for
|
||||||
splitting one; neither is hard, but both change the UI.
|
splitting one — neither is hard, and both change the UI.
|
||||||
11. **Attribution for four asset packs.** See [ASSETS.md](ASSETS.md) — two are
|
5. **Does a dungeon ever get harder within a run?** Enemies are placed once at
|
||||||
non-redistributable and local-only, and there is no in-game credits screen
|
generation and never reinforce. That is a deliberate decision
|
||||||
yet, which CC BY 4.0 requires for the audio.
|
([DECISIONS.md](DECISIONS.md#world)), but it means a slow party is never
|
||||||
|
punished for it.
|
||||||
|
6. **What happens to a party when one member dies?** Permadeath removes them to
|
||||||
|
the roster screen while the run continues without them. Nobody has said
|
||||||
|
whether that is right.
|
||||||
|
|||||||
Reference in New Issue
Block a user