Add a second dungeon: the Proving Grounds, a test harness you walk into
ci / verify (push) Successful in 47s

Two labelled portals now stand side by side in the hub. The Proving Grounds
runs the same generator, the same rooms, the same enemies and the same
four-phase Warden -- enemies at a fifth health, the boss at 288 instead of
3600, and trash dropping potions 80% of the time instead of 8%. A manual pass
over loot, the inventory, dropping and every boss phase takes a couple of
minutes rather than a quarter of an hour.

It is multipliers over the shared content rather than a parallel copy: a
duplicated Content would drift the first time anything was tuned, and
"identical but easier" would quietly stop being true. And it is a portal
rather than a launch flag, so the two can be compared back to back without
restarting the server -- which is most of the point.

Which dungeon you enter is resolved from the player's server-side position,
and PORTAL_USED carries the answer. There is deliberately no client message
that names a dungeon: one would let any client ask for the generous loot table
and bring the results back to the hub. Instance matching compares dungeon ids
too, so walking into one entrance can never drop you into the other's run on
timing alone.

SimWorld.portals replaces portal_pos/portal_enabled, enter_instance carries
the portal list and the dungeon id (the client needs the latter to scale the
boss bar's ceiling the way the server scaled the boss), and Protocol.VERSION
goes to 7.

Also pins what happens when two players reach for one item on the same tick:
exactly one gets it -- the loop is sequential and the pickup erases the entity
before the next player looks. The tie-break is join order rather than distance,
which is arbitrary rather than designed, so it is recorded as such.

Stale doc fixed while here: MapGen.build() still claimed the client rebuilds
the map from the seed, which has not been true since map streaming landed and
is the opposite of the rule.

check.sh clean, 288 tests, SMOKE PASS (18 assertions, both dungeon kinds
opened over a real socket), all three diagnostics green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 21:34:41 +02:00
parent 050b8251a7
commit a943aa19f6
28 changed files with 733 additions and 74 deletions
+10 -2
View File
@@ -89,7 +89,7 @@ and `tests/integration/test_replica_parity.gd` pin this down.
| Path | What lives there |
| --- | --- |
| `src/sim/` | The whole game as plain RefCounted objects. No nodes, no physics server, no rendering. |
| `src/actors/` | Data-only `Resource` definitions: `EnemyDef`, `BossDef`, `ItemDef`, `LootDrop`. Shapes, not instances. |
| `src/actors/` | Data-only `Resource` definitions: `EnemyDef`, `BossDef`, `ItemDef`, `LootDrop`, `DungeonDef`. Shapes, not instances. |
| `src/sim/patterns/` | Bullet emitters — the authoring surface for every enemy and boss. |
| `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. |
@@ -97,6 +97,7 @@ and `tests/integration/test_replica_parity.gd` pin this down.
| `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/items.gd` | All items, same idea. `Items.ORDER` is the wire format — append only. |
| `src/content/dungeons.gd` | The kinds of run. `Dungeons.ORDER` is both a wire format and the hub's portal order. |
| `src/net/` | Codec, `ServerRuntime`, `ClientRuntime`. |
| `src/instances/` | Lobby hub and dungeon runs. |
| `src/view/`, `src/ui/` | Read-only rendering. Never decides anything. |
@@ -168,7 +169,14 @@ ticks in milliseconds with no SceneTree.
- **`Items.ORDER` is a wire format.** An item's index in it is the byte that
rides the snapshot and every item event. Append, never reorder — reordering
makes every existing client decode a potion as a ration, so it needs a
`Protocol.VERSION` bump.
`Protocol.VERSION` bump. `Dungeons.ORDER` is the same, and additionally
decides which hub portal opens which dungeon (Nth `P` marker in the lobby
stamp → Nth entry).
- **Which dungeon you enter comes from where you are standing, never from the
client.** `SimWorld.portal_at()` resolves the player's server-side position
to a portal, and the `PORTAL_USED` event carries the answer. There is no
message that names a dungeon, and adding one would let any client pick the
easy variant's loot rate.
- **Loot has two visibilities, and the instanced one is enforced in the codec.**
`NetCodec.encode_snapshot` filters items owned by another peer, exactly like
the actor interest radius. Never move that check into the client: hiding an