This guide is for integrations that go beyond pickups and crafting: applying gameplay consequences (health, buffs, quest progress, AI intent) from inventory actions while ItemVault stays the sole custody authority.
Advanced. If you only need pickups → inventory, read Getting Started and Runtime and Pickup Bridge first. This pattern adds value once your game has its own rules layer.
The boundary in one diagram
Player / AI action
↓
Game-owned ItemUseAuthority (YOU write this class)
- can this actor use this item? (game rules)
- target/range/game checks
- game action identity
↓
ItemVault public transaction API
- inventory/item identity
- atomic mutation
- typed result
↓
Game-owned consequence (only after a committed result)
- apply healing/effect
- complete task/action
- animation/audio/UIThe full contract this boundary implements is the consumer contract: ItemVault owns custody, identity, atomic mutation, and committed events; the game owns meaning.
The rule: consequence follows commit
A gameplay effect may apply only after ItemVault commits the inventory mutation. A rejected mutation (rule gate, missing item, no capacity) must never apply the effect.
The pattern: ItemUseAuthority
The runnable example lives at
test/item_vault/tests/inventory/game_owned_gameplay_example_test.gd
(runs with the repo's test suite). The core class:
class ItemUseAuthority:
var _inventory:Inventory # ItemVault handles given at bootstrap
var _database:ItemDatabase
var _next_game_action_id:int = 1 # GAME identity, not ItemVault's
func use_consumable(actor_hp:int, actor_max_hp:int, item_id:StringName)-> Dictionary:
var game_action_id:= _next_game_action_id
_next_game_action_id+= 1
# 1) Game rules FIRST — a rule rejection never touches inventory.
var gate:= can_use(actor_hp, actor_max_hp, item_id)
if not gate["allowed"]:
return {"game_action_id": game_action_id,"committed":false,
"reason": gate["reason"],"hp_after": actor_hp}
# 2) Atomic exact-quantity removal through the PUBLIC transaction API.
# Failure = zero mutation (consumer contract surface 3).
var tx:= _inventory.create_transaction()
if not tx.remove_item(item_id,1).execute():
return {"game_action_id": game_action_id,"committed":false,
"reason":"item_not_in_inventory","hp_after": actor_hp}
# 3) Committed → the game NOW applies its own consequence.
return {"game_action_id": game_action_id,"committed":true,
"hp_after":mini(actor_hp+ 30, actor_max_hp)}What makes this correct:
- Rule rejections never reach ItemVault — "no healing at full health" is checked before any transaction exists.
- Vault rejections apply nothing — if the transaction fails,
hp_afteris the unchanged input value. - Effects apply only on commit — the
+30happens afterexecute() == true, never before.
Two identities, deliberately distinct
- Game action identity (
game_action_idabove): minted by your game, sequences your rules and consequences. - ItemVault operation identity: inventory-domain operation records and committed events carry ItemVault's own ids.
Never mix them. When you correlate (telemetry, replays), store both and join in your game layer. ItemVault's ids tell you what custody changed; your ids tell you why the actor did it.
Consume typed results and events — not UI state
Two supported evidence sources:
- Typed results — the transaction boolean, the crafting
CraftingResultwithreason, the overflowItemStackfromadd(stack). Never infer success from a UI label or a count you polled. - Committed events — subscribe to the inventory's scoped
ItemVaultBus:
inventory.get_bus().item_removed.connect(
func(item_id, quantity, container_id):
refresh_ui_for(item_id)# event-driven refresh, no polling
)Committed events fire exactly once per committed mutation — never on rolled-back or rejected operations — so a UI refresh driven from the bus can never show a phantom change.
Save/load: two documents, one boundary
ItemVault persists inventory facts: containers, stacks, instance ids, wallet. It never persists gameplay meaning. Your game persists its own domain state — hp, buffs, quest flags, action history — in its own save file.
var inventory_save:Dictionary = inventory.to_dict()# ItemVault facts
var game_save:Dictionary = {"hp": actor_hp,"history": ...}# your domain
# Restore each from its own document; the two never merge.What NOT to put in ItemVault
Health, hunger, stamina, quests, factions, pawns, AI tasks, combat targeting, cross-plugin sequencing, presentation policy. ItemVault is architecture-neutral: if a concept only exists in your game, it belongs in your game.
Related
- Consumer contract — the eight stable surfaces this pattern consumes.
- Crafting integration — the crafting-shaped slice of the same boundary.
- Retained Item Resources — mutating typed charges on an instance without removing it (also game-owned policy on top of a public surface).