Docs Design

Contents

Using up a carried item

Status: built 2026-10-02, the sixth gap the adventure template found (M10b, docs/design/m4-templates.md). Read with ADR-0002 (games are data), ADR-0004 (replayable simulation) and m1-player.md §7 (views never write the simulation).

A game could not use up a carried item later. Thornwick's berries healed the moment they were picked, because nothing else could be said: the core refuses a Recipe that makes nothing (recipe.empty), and holding an item sends no event. Every adventure, survival or RPG game meets this: bread eaten when hungry, a potion drunk in a fight, a waterskin of three sips, a torch that lights five times, a horn blown again and again.

What a game must be able to say: using this carried item does X, and it is gone (or loses a charge). And what a person must see: that they can use it, how, and that it worked.

1. What already worked

  • A key on a door. The door is a usable (kit.harvest); a rule on harvest.used where item_count(event.entity, 'item.key') > 0 takes the key and opens it. The thing used is in front of you, so the use key is the right key. Nothing to add.
  • A weapon in hand. Melee damage is an Expr over held(self).
  • An effect. add_stat, set_var, show_message, play_cue, give, take, spawn: a RuleSet already says what anything does, once something says it happened.

So the gap is narrow: nothing says "the person used this carried thing", and nothing takes it away (or a charge of it) when they do.

2. Options

A. Let a Recipe make nothing (drop recipe.empty; a rule on crafting.made heals). Rejected. Making is not using. It needs kit.crafting and a crafter on every body that eats, and the eating shows in the make app ("Berries: ready"), away from the pack the berries are in. A recipe passes world minutes and checks stations and tools, none of which a snack wants. And recipe.empty guards a real mistake, a recipe whose outputs were forgotten: it stays, and its hint now points here.

B. An event when an item is put in hand (inventory.held), answered by a rule that eats what is held. Rejected. Holding is not using: taking the berries out would eat them, and eating would mean putting the weapon away.

C. The use key works the held item when nothing is in front (Minecraft's right click; the hint would say "E eat berries"). Considered, not built. The item has to be in hand, so eating in a fight means swapping the hatchet out and back; and a press that just misses a bush eats a berry instead of picking one. The use key keeps one job: the thing in front of you.

D. The Item says what using it costs; a rule says what it does. Chosen. It is the usable's shape again, so a Director who has written one has written the other:

a thing in front (M10b) a thing carried (this)
declared on the Archetype: usable {verb, dialogue} the Item: use {verb, charges}
how the person does it the use key, facing it the carry app's confirm on its row
the event harvest.used, latch used:<arch> inventory.used, latch used:<item>
what it does a rule (or its Dialogue) a rule

Within D, two smaller choices:

  • The effect in the Item (use.do: [actions]). Rejected: a second home for actions, with its own rules for self and conditions. A rule already has where (only when hurt: two rules, two messages), and an event is how every kit hands a moment to the game.
  • A condition on the use (use.when: refuse when not hurt). Deferred. A refusal needs words of its own and a dim row in the pack, and a rule can already tell the person what happened ("you weren't hurt, but they were good"). Add it the day a game must refuse a use.

3. The design

The Item gets one optional block (schemas/v1/item.schema.json):

"use": { "verb": "eat", "charges": 1 }
  • verb (a slug): the word the pack shows on its key strip and the event carries: eat, drink, read, light, blow.
  • charges (0 to 999, default 1): how many uses one of it gives before it is gone. 1: gone at once (berries, a potion). 3: a waterskin, three sips each. 0: never used up (a horn, a map).

kit.inventory reads a press use:<item id> for each body its input drives, as kit.crafting reads make:<recipe id>, so a use replays from the frames like any key (ADR-0004). It uses one:

  1. Nothing happens unless the body carries one and the Item has a use.
  2. The use spends a charge. When it spends the last (always, with charges 1), one is taken away through GwCarry.take, which sends inventory.changed (and empties the hand if it was the last one held). Charges spent on the one in use live on the carrier (comp.inventory.spent[item]), written only for charges above 1 and dropped when that one is gone, so a game with no such item keeps its state hash.
  3. inventory.used {entity, item, verb, gone, left, latch: used:<item>} goes out. gone is true when one was used up; left is how many are carried now. RuleSets answer it in the same tick (rules step last).

It also reads hold:<item id>: put a carried item in hand (the hold_item action, from input).

kit.ui's carry app gets a default act for its list, as the make app has one: confirm on a row presses use:<id> when the Item has a use, else hold:<id>. Each row carries its verb (the use's verb, else hold, and none for what is in hand already, whose detail says "in hand"), and the slate's key strip shows the chosen row's verb, so the pack's confirm key says "eat" on the berries, "hold" on the hatchet, and is left off the strip on the staff in hand (confirm there says "in hand already"). While an app is open the slate veils the HUD, so a message a rule shows then is written on the slate's own line. Before this, the template's carry screens said confirm holds the chosen thing and nothing answered: the slate refused with "nothing answers".

The bots. kit.inventory's use_item ITEM tour verb presses use:<item> and fails naming why when the player carries none or the Item has no use. kit.core's seek takes use:<item id>: come by one first if none is carried (as have:), then use it; met where it stands in the list, so it may come twice. The monkey presses use:<item> for each Item with a use, so the fuzz reaches it; a game with no such Item gets no new presses, and its runs are what they were.

Keyboard and touch. The carry app opens on its key (Tab or I in the adventure template; the Carry button along the top of a phone), the rows move on up and down (W and S; the d-pad's arms), and confirm uses (E or Enter; OK on a phone). No new action, no new button.

What the person sees (Thornwick):

  • Wren says to keep berries for when you're hurt. The first pick says they went into the pack and to eat them when hurt.
  • While the hero is hurt and carries berries, the HUD shows a hint under the quest line: the carry key's cap and "eat berries".
  • In the pack: "Berries ×3", the note "Eat them when you're hurt: they heal", and the key strip "Enter eat" (OK on a phone); on the staff in hand, "in hand" and no confirm key at all. Confirm: the slate says "Sweet and sharp. You feel better.", the count drops, the hero's eat cue (two bites and a rising note) plays, and the health cells are fuller when the pack closes. Eaten at full health, the berries are gone and the line says so.

4. What L0 catches

A Director's likely mistakes, each with a message that says what to do:

Code When Severity
item.use_unanswered an Item has a use and no rule (or state machine) answers inventory.used for it: using it would only use it up error
rule.use_never a rule on inventory.used names an item (event.item == 'item.x') that has no use: nothing ever uses it, so the rule never runs error
rule.use_unknown_item that item is no Item at all (a typo) error
rule.use_takes_twice the rule also takes the same item: the use already took one (the old eat-on-pick rule, kept) warning
playtest.goal_use a seek goal use:<id> names no Item, or one with no use error
recipe.empty unchanged, with a hint: to use an item up, give the Item a use error

A rule "names" an item when its where compares event.item with a string; a rule whose where doesn't (or reads event.item some other way) answers every item.

5. The count

No new kit, no new kind. One optional Item field (use), one event (inventory.used), two presses (use:, hold:), a tour verb (use_item), a seek goal (use:), and the carry app's default act.

6. As built (2026-10-02)

  • Tests first, red then green. core/test/item-use.test.ts (11 cases: the schema, Thornwick and the template clean, each L0 code with its message and hint, the recipe hint) failed 11 of 11 before the checks existed. engine/tests/kits/inventory/gw_item_use_test.gd (9 cases), engine/tests/kits/ui/gw_ui_carry_test.gd (3) and the new case in gw_seek_test.gd failed before the engine code (the two "nothing happens" cases passed, as they should with nothing built); the seek CLI case went red too, since kit.json declared use_item before any system registered it. All green after, and the whole engine suite (751 cases) and core suite with them.
  • The carry app's confirm never answered. Its screen said "confirm holds the chosen thing"; confirm returned "!nothing answers" with the refuse cue in every game with a carry app. Fixed by the default act; the T5 fixture's stand-in act had always done what the screen promised.
  • A person's path, driven for real. The player, booted on a copy of Thornwick whose hero starts hurt with three handfuls, driven by key events (Enter, Tab, E, S, Tab) and again by the InputEventActions a touch control sends: the pack opens on Berries ×3 with "Enter eat" on the strip, E takes health 3 to 5 and the berries to two, the slate reads "Sweet and sharp. You feel better.", the staff's row shows "in hand" and no confirm key, and the HUD after shows five cells and the line. On a phone (tools/web.sh --touch on the same copy) the pack offers the arms and OK, and every text is at least 11 CSS px. That visit caught the first place of the eat hint, over what is in hand: on an upright phone it ran into the message line. It sits under the quest line now.
  • Thornwick. L0–L3 green: the quest on 4 seeds in 3,432 to 3,752 ticks, never downed, two of four bitten at the shrine and so healed by the berries they eat after it (the other two eat them whole and are told so); the proof tour bitten once at the clearing and healed (var.berries_healed == 1), ending at tick 4,482; 16 L3 frames, two of them new (the berries with the hint, and the line once eaten).
  • Everything else unchanged. Tideline's, Nightcoast's and Fog Signal's proof tours end on the state hashes they had before, and every one of their L1 and L2 runs (14, 58 and 13) is identical: a game with no Item that has a use gets no new press and no new state.
  • Gates. tools/check-all.sh green on 2c5e40c: lint, hidden inputs, the boundary, presentation reads, GDScript format, 877 core tests (one skipped on purpose), 663 engine scripts loaded, 751 engine cases. The web rung, browser against desktop: Thornwick's proof at tick 4,482 on sha256:c488f661… with the phone visit green (32 checks); Tideline's walk, Nightcoast's proof and Fog Signal's proof on their old hashes.

From docs/design/item-use.md in the Gamewright repository.