Four always-on-screen slots, items as data, and loot tables on enemies and bosses. Health potions drop rarely from trash and always from the Warden; the Warden also drops a Warden's Ration, one per living player, which does nothing at all. The ration is not filler. Player-instanced loot is a separate code path from shared loot -- a distinct entity per owner, filtered per peer in the snapshot encoder -- and the cheapest way to keep that path honest is to have something in the game that exercises it on every boss kill. Item actions ride the input frame rather than becoming new client messages. InputFrame gained BTN_USE, BTN_DROP and a slot byte, which buys the packet-loss redundancy, the replay guard on last_input_tick, ordering against movement on the same tick, and a rate limit of one action per tick -- all of which a separate RPC would have needed bolted back on. The cost is that anything in the frame which must not repeat has to be edge-triggered, since frames are resent and a starved server coasts on the last one it holds. Instanced loot is enforced in NetCodec.encode_snapshot, beside the actor interest radius: a peer is never told another player's copy exists. Hiding it client-side would have been the same mistake as relying on fog to hide enemies. Inventories live on the character and are written to the store on every transaction, so a crash between "picked it up" and "wrote it down" cannot lose or duplicate an item. Anything dropped becomes world-shared whatever it was before, and a potion used at full health is refused rather than spent. tools/diag_loot.tscn covers drop -> snapshot -> pick up -> persist -> use -> drop plus both visibilities on the wire, for the same reason diag_progression exists: bots are poor shots and almost never produce a drop. It asserts each input frame was actually consumed, after an early version silently dropped its first press and every later check passed for the wrong reason. check.sh clean, 266 tests, SMOKE PASS, all three diagnostics green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+66
-34
@@ -20,14 +20,15 @@ What "everything passes" currently means. Numbers move; the shape does not.
|
||||
| Gate | Covers | Runtime |
|
||||
| --- | --- | --- |
|
||||
| `tools/check.sh` | every script parses and type-checks | ~5s |
|
||||
| `tools/test.sh` | 206 GUT tests, no SceneTree | ~3s |
|
||||
| `tools/smoke.sh` | 15 assertions over a real ENet socket: handshake, auth, character creation and persistence, portal, escape, hard kill, polite disconnect | ~40s |
|
||||
| `tools/test.sh` | 266 GUT tests, no SceneTree | ~3s |
|
||||
| `tools/smoke.sh` | 16 assertions over a real ENet socket: handshake, auth, character creation and persistence, portal, escape, hard kill, polite disconnect | ~40s |
|
||||
| `diag_prediction.tscn` | client-prediction gap, with injected clock drift | ~10s |
|
||||
| `diag_progression.tscn` | kill → xp → level → health, death → retire → roster, swap guards | ~10s |
|
||||
| `diag_loot.tscn` | drop → snapshot → pick up → persist → use → drop, and both loot visibilities on the wire | ~10s |
|
||||
|
||||
The two diagnostics exist because the smoke test structurally cannot reach what
|
||||
they cover: bots are poor shots, and a listen server cannot drift its own clock
|
||||
against itself.
|
||||
The three diagnostics exist because the smoke test structurally cannot reach
|
||||
what they cover: bots are poor shots (so they neither level up nor produce
|
||||
drops), and a listen server cannot drift its own clock against itself.
|
||||
|
||||
---
|
||||
|
||||
@@ -119,39 +120,66 @@ path cannot be covered by the bot smoke test, because bots are poor shots.
|
||||
- `--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*
|
||||
## Stage 3 — Inventory and loot · *done*
|
||||
|
||||
Nothing blocks this. The pieces it needs — accounts, characters, a server that
|
||||
owns per-player state — all exist.
|
||||
|
||||
### Requirements
|
||||
|
||||
| Feature | Decided | Notes |
|
||||
| Feature | State | Where |
|
||||
| --- | --- | --- |
|
||||
| Inventory, small enough to sit on screen permanently | **4 slots** | May grow later; do not build a paged or scrolling UI for it. |
|
||||
| Health potions | rare from normal enemies, **guaranteed from bosses** | The only item with an effect for now. |
|
||||
| World-shared loot | shared between all players in the instance | First to reach it takes it. |
|
||||
| A unique, useless food item from bosses | **player-instanced now** | Its whole purpose is to exercise the instanced-loot path rather than defer it. Always dropped by bosses, does nothing when used. |
|
||||
| Dropping items | any item, back into the world, pickable by others | The path that makes shared loot meaningful. |
|
||||
| 4 slots, permanently on screen | done | `SimConfig.INVENTORY_SLOTS`, `HUD._draw_inventory` |
|
||||
| Items defined as data, not code | done | [src/content/items.gd](../src/content/items.gd), [src/actors/items/item_def.gd](../src/actors/items/item_def.gd) |
|
||||
| Loot tables on enemies and bosses | done | `EnemyDef.loot` / `BossDef.loot`, rolled in `SimWorld._drop_loot` |
|
||||
| Health potion, rare from trash | done | `Content.TRASH_POTION_CHANCE` = 0.08 |
|
||||
| …guaranteed from the boss | done | `Content.warden()` loot table, chance 1.0 |
|
||||
| World-shared loot | done | `SimLoot.owner_peer == 0` |
|
||||
| Player-instanced loot | done | one `SimLoot` per living player, filtered per peer in `NetCodec.encode_snapshot` |
|
||||
| Warden's Ration — useless, instanced | done | `Items.wardens_ration()` |
|
||||
| Pick up, use, drop | done | `SimWorld._try_pickup` / `_use_slot` / `_drop_slot` |
|
||||
| Inventories persist | done | stored on `Character`, written by `ServerRuntime._persist_inventory` |
|
||||
| Ground loot drawn with a pickup prompt | done | `WorldView._draw_loot`, `HUD._draw_pickup_prompt` |
|
||||
|
||||
### What this implies
|
||||
Controls: **E** picks up, **1–4** use a slot, **shift+1–4** drop one.
|
||||
|
||||
Two loot *visibilities* have to exist from the start, because the food item is
|
||||
specifically there to prove the second one works:
|
||||
### The decisions worth knowing before touching this
|
||||
|
||||
- **World-shared:** one entity in the instance, visible to everyone, gone when
|
||||
anyone picks it up.
|
||||
- **Player-instanced:** one entity per eligible player, each seeing and taking
|
||||
only their own. Others must not see it, which makes it an interest-management
|
||||
question as much as a loot one — see `ACTOR_INTEREST_RADIUS` and how the
|
||||
snapshot is already encoded per peer.
|
||||
**Item actions ride the input frame; they are not new messages.** `InputFrame`
|
||||
gained `BTN_USE`, `BTN_DROP` and a slot byte. That buys the redundancy that
|
||||
covers a dropped packet, the replay guard on `last_input_tick`, ordering against
|
||||
movement on the same tick, and a natural rate limit of one action per tick — all
|
||||
of which a separate reliable RPC would have needed bolted back on.
|
||||
|
||||
Server owns all of it: the client sends "I want to pick that up" as intent and
|
||||
learns the outcome. There is no message that grants an item.
|
||||
**Item actions are edge-triggered; movement and fire are not.** The client
|
||||
repeats its last few frames every tick and a starved server coasts on the last
|
||||
one it was given, so a level-triggered read empties the whole inventory in four
|
||||
ticks. `SimPlayer.prev_buttons` and `prev_slot` hold the edge, and the *slot* is
|
||||
part of it — tapping 2 while 1 is held is a second, distinct action.
|
||||
|
||||
Persistence is an open question — inventories are not in `CharacterStore` yet,
|
||||
and a potion that vanishes on server restart may or may not matter at this
|
||||
stage.
|
||||
**Instanced loot is enforced on the wire, not in the client.** A peer is never
|
||||
told another player's copy exists. That makes it an interest-management rule of
|
||||
the same kind as `ACTOR_INTEREST_RADIUS`, and it is why the ration is worth
|
||||
having: every boss kill exercises the path.
|
||||
|
||||
**Anything dropped becomes world-shared, even if it arrived instanced.** That is
|
||||
what makes dropping worth having — a trophy you do not want should be able to
|
||||
reach someone who does.
|
||||
|
||||
**A potion at full health is refused rather than spent.** Nobody drinks one on
|
||||
purpose at full health, so a mistimed keypress must not do it for them.
|
||||
|
||||
**A full bag leaves the item on the floor** and does not block the portal, which
|
||||
shares the interact key.
|
||||
|
||||
### Known gaps
|
||||
|
||||
- **Nothing sells items.** Loot only comes from kills; the hub has no source.
|
||||
The Stage 4 upgrade NPC is the natural place, and is the reason this is a gap
|
||||
rather than a decision.
|
||||
- **No stacking.** Four potions take four slots. A count byte per slot is cheap
|
||||
to add; nothing needed it yet, so the wire, the save record and the HUD all
|
||||
stayed simpler for not having one.
|
||||
- **Ground loot never expires**, it is only capped at
|
||||
`SimConfig.MAX_LOOT_PER_INSTANCE` per world, oldest evicted. Dungeons close
|
||||
and take their litter with them; only the hub can realistically reach the cap.
|
||||
- **The ration draws as a gold flask.** The tileset has no food sprite. See
|
||||
[ASSETS.md](ASSETS.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -264,8 +292,12 @@ several have no obvious default.
|
||||
|
||||
8. **What advances dungeon depth?** `--depth` is a dev flag; nothing raises it
|
||||
in play. Depth drives map size and could drive difficulty and rewards.
|
||||
9. **Do inventories persist?** Characters do. A potion surviving a server
|
||||
restart may or may not matter at this stage.
|
||||
10. **Attribution for four asset packs.** See [ASSETS.md](ASSETS.md) — two are
|
||||
9. **Where do items come from outside a dungeon?** Loot only drops from kills.
|
||||
If the hub should sell potions, that is the Stage 4 NPC's second job — and
|
||||
it needs a currency, which the game does not have.
|
||||
10. **Should items stack?** Four potions currently take four slots, which makes
|
||||
a 4-slot bag small. Stacking is a count byte per slot plus a rule for
|
||||
splitting one; neither is hard, but both change the UI.
|
||||
11. **Attribution for four asset packs.** See [ASSETS.md](ASSETS.md) — two are
|
||||
non-redistributable and local-only, and there is no in-game credits screen
|
||||
yet, which CC BY 4.0 requires for the audio.
|
||||
|
||||
Reference in New Issue
Block a user