Skip to content

Item Vault v1.0

Game-Owned Gameplay Integration

Connect inventory changes to healing, equipment, quests, combat, or other game-owned rules only after the inventory change succeeds.

Status
Current
Version
v1.0
Source updated
2026-08-17
Generated on
2026-09-06

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/UI

The 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_after is the unchanged input value.
  • Effects apply only on commit — the +30 happens after execute() == true, never before.

Two identities, deliberately distinct

  • Game action identity (game_action_id above): 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:

  1. Typed results — the transaction boolean, the crafting CraftingResult with reason, the overflow ItemStack from add(stack). Never infer success from a UI label or a count you polled.
  2. 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.