---
title: feat: Inventory and Backpack System
type: feat
status: active
date: 2026-04-26
origin: inventory-prd.md
---

# Inventory and Backpack System

## Overview

Add a full inventory and backpack system to the point-and-click adventure game: a global inventory manager, an animated HUD backpack icon with a finite state machine, a full-screen inventory overlay for item viewing and combination, and integration with the existing cursor and scene systems.

---

## Problem Frame

The game currently has no concept of items, inventory, or item interaction. Players cannot pick up objects, combine items, or use items on entities in the game world. This system establishes the foundational item management infrastructure needed for adventure game gameplay.

---

## Requirements Trace

- R1. Current inventory is an ordered list preserving insertion order (AC-1)
- R2. Obtained items set persists across removal (AC-1)
- R3. Stripped items vault for forced removal recovery (AC-9)
- R4. Item definitions include name, identifier, and optional combination category. Interaction handlers are signals emitted by items, not embedded data (PRD §2.2)
- R5. Inventory queries: has_item, has_obtained, has_any_of, has_obtained_any_of, has_obtained_all_of (PRD §2.3)
- R6. HUD Backpack FSM with Idle, Open, Selected, Acquire, Remove states. Multi-step transition queuing is deferred — transitions execute immediately and interrupt any in-flight animation (AC-2, PRD §4)
- R7. Inventory overlay with grid layout, hover text, drag-and-drop, item selection, and combination (AC-3 through AC-6, AC-12, AC-13, PRD §5)
- R8. Item acquisition animation and sound (AC-7, PRD §6)
- R9. Item removal animation with quiet mode and bulk removal support (AC-8, AC-9, PRD §7)
- R10. Cursor system integration with item-specific cursors (AC-10, AC-14, PRD §8)
- R11. Guard conditions: foreground action, screen fade, game pause (AC-11, PRD §9)
- R12. Game world item use: left-click entity interaction, right-click return (AC-10, PRD §10)

---

## Scope Boundaries

- Audio assets are not part of this system
- Item sprite assets are out of scope — use colored `ColorRect` boxes as placeholders for all item/backpack visuals
- Save/load persistence for inventory is deferred — the manager should be structured to support it, but serialization is not implemented
- Item tooltips beyond name and combination hints are deferred
- Multi-step transition queuing is deferred — transitions interrupt in-flight animations immediately

### Deferred to Follow-Up Work

- Inventory save/load serialization: separate PR once save system infrastructure exists
- Item sprite asset creation: replace colored boxes with actual sprites in a separate art pass
- Multi-step transition queuing: add FIFO queue for rapid transitions in a follow-up

---

## Context & Research

### Relevant Code and Patterns

- `ActionState.gd` — Existing autoload singleton tracking cursor action (WALK/LOOK/TOUCH/TALK). Pattern to follow for `InventoryManager` autoload.
- `MainGame.gd` — Root scene (`Game.tscn`) that handles cursor switching via `Input.set_custom_mouse_cursor()`, scene transitions, and script-running cursor lock. The inventory system must integrate here for cursor management and guard conditions.
- `GameScript.gd` — Script builder with `ScriptGraph` for cutscenes. The `Narrate` class shows the pattern for overlay fade-in/fade-out with `modulate.a` tweens, and the `is_script_running` / `is_cursor_locked` pattern in `MainGame.gd` is the guard condition model.
- `Scene.gd` — Base room class. `_unhandled_input()` handles walk and look actions. Inventory input must be gated so overlay blocks game-world input.
- `SetPiece_.gd` — Interactive polygon areas. The `_input()` pattern shows how `ActionState` and `GameScript.current_script` are checked before processing input.
- `Game.tscn` — Root scene tree with `SubViewport` for game world, `CanvasLayer` for overlays (NarratorOverlay at layer 10), and `AnimationPlayer` for fades.
- `label.gd` — Floating label that follows mouse position in game world coordinates.

### Institutional Learnings

- None yet — no `docs/solutions/` entries exist.

### External References

- PRD §13 provides detailed Godot 4 node architecture brainstorm, including FSM implementation options, drag-and-drop approach, cursor system, input priority patterns, and signal flow. The plan follows the PRD's recommended approaches throughout.

---

## Key Technical Decisions

- **Hand-rolled FSM (PRD Option A)**: The backpack FSM uses a GDScript enum + Tween-based transitions, not `AnimationTree`. Transitions execute immediately and kill any in-flight animation — no FIFO queuing.
- **AutoLoad singleton for InventoryManager**: Follows the `ActionState` pattern. Signals (`item_acquired`, `item_removed`, `inventory_changed`) decouple the manager from UI components.
- **Signal-based interaction handlers**: Items emit signals for interactions rather than embedding interaction data in dictionaries. Room scenes connect to item signals to handle entity-specific behavior.
- **Item cursor as ActionState slot 5**: Item selection sets `ActionState.current_action = Action.ITEM` (slot 5). Right-click cycles WALK→LOOK→TOUCH→TALK→ITEM→WALK. The selected item is stored in `InventoryManager.selected_item`. Scripts and interactions fire the same way as regular cursor actions — item use is handled through the existing interaction signal pattern.
- **Raw `_gui_input()` for overlay drag-and-drop**: Godot's built-in drag-and-drop is a partial fit. Real-time cursor following, drag-out-close, and hover-under-drag tracking are more naturally handled with `InputEventMouseButton` and `InputEventMouseMotion` in `_gui_input()`.
- **Combined input priority defense**: Overlay background `ColorRect` with `MOUSE_FILTER_STOP` + `accept_event()` blocks Control input. Game world uses `_unhandled_input()` which is already filtered by Control `accept_event()`. An explicit `overlay_active` guard flag provides a safety net for `Node2D._input_event()` bypass. (PRD §13.10)
- **Colored boxes for placeholders**: All item sprites, backpack visuals, and cursor assets use colored `ColorRect` boxes instead of textures. Easy to replace with actual sprites later.
- **GridContainer for item grid**: Static layout uses Godot's `GridContainer`. During drag, items are reparented or switch to `PRESERVE_WIDE` anchors for free movement.

---

## Open Questions

### Resolved During Planning

- **Should the inventory overlay live under MainGame or be a separate autoload?** Under `MainGame` as a child node in `Game.tscn`. It needs to interact with the scene's input and animation system, and keeping it in the scene tree simplifies lifecycle management.
- **How does the backpack FSM know about guard conditions?** The FSM checks `GameScript.current_script` (foreground action), the Fade sprite's `modulate.a` (screen fade), and a pause flag on `MainGame`. If a guard blocks opening, clicking the backpack attempts to skip the running action.
- **Where does item combination logic live?** In `InventoryManager` as `attempt_combine(item_a, item_b)`. The manager looks up interactions on both items (forward and reverse), evaluates dynamic rules, and emits `combination_succeeded` or `combination_refused` signals.

### Deferred to Implementation

- Exact animation frame counts for backpack open/close sprite sequences — depends on final sprite assets
- Whether `ItemDefinition` interaction handlers use a `Dictionary` or a callable-based registry — either works; the implementer should choose based on what feels cleaner with GDScript's type system

---

## Output Structure

    res://
    ├── inventory/
    │   ├── InventoryManager.gd          # AutoLoad singleton
    │   ├── InventoryManager.gd.uid
    │   ├── ItemDefinition.gd            # Custom Resource class
    │   ├── ItemDefinition.gd.uid
    │   ├── inventory_backpack/
    │   │   ├── InventoryBackpack.tscn   # HUD backpack FSM scene
    │   │   ├── InventoryBackpack.tscn.uid
    │   │   ├── InventoryBackpack.gd     # FSM script
    │   │   └── InventoryBackpack.gd.uid
    │   └── inventory_overlay/
    │       ├── InventoryOverlay.tscn    # Full-screen overlay scene
    │       ├── InventoryOverlay.tscn.uid
    │       ├── InventoryOverlay.gd      # Overlay interaction script
    │       ├── InventoryOverlay.gd.uid
    │       ├── InventorySlot.tscn       # Single item slot scene
    │       ├── InventorySlot.tscn.uid
    │       ├── InventorySlot.gd         # Slot behavior script
    │       └── InventorySlot.gd.uid

---

## High-Level Technical Design

> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*

```
┌─────────────────────────────────────────────────────────────┐
│                        Game.tscn                             │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ SceneViewport (SubViewport)                             ││
│  │  └── background (Scene)                                  ││
│  │      └── game world entities                            ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────┐│
│  │ HUD (CanvasLayer, layer=5)                              ││
│  │  └── InventoryBackpack (FSM)                            ││
│  │      ├── AnimationPlayer (backpack sprites)             ││
│  │      ├── ColorRect (backpack icon placeholder)          ││
│  │      └── FloatingItem (selected item placeholder)       ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────┐│
│  │ InventoryOverlay (CanvasLayer, layer=10)                ││
│  │  ├── ColorRect (MOUSE_FILTER_STOP background)           ││
│  │  └── InventoryPanel                                     ││
│  │      ├── TextureRect (decorative frame)                 ││
│  │      ├── GridContainer (item grid)                      ││
│  │      │   └── InventorySlot × N (dynamic)                ││
│  │      └── HoverLabel                                     ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  InventoryManager (AutoLoad singleton)                      │
│    ├── inventory: Array[String] (ordered)                   │
│    ├── obtained_items: Dictionary (set)                     │
│    ├── stripped_items: Array[String] (ordered)             │
│    ├── selected_item: String (currently held item)         │
│    └── signals: item_acquired, item_removed,               │
│         inventory_changed, combination_attempted            │
└─────────────────────────────────────────────────────────────┘
```

### Signal Flow

```
InventoryManager
  ├─ item_acquired(item_id)        → InventoryBackpack._on_item_acquired()
  ├─ item_removed(item_id)         → InventoryBackpack._on_item_removed()
  └─ inventory_changed             → InventoryOverlay._refresh_grid()

InventoryBackpack (FSM)
  ├─ overlay_show_requested        → InventoryOverlay.show()
  ├─ item_selected(item_def)       → MainGame._on_item_selected()
  │                                    (set ActionState.ITEM, store selected item)
  └─ returning_to_idle             → InventoryOverlay.hide()

InventoryOverlay
  ├─ close_requested               → InventoryBackpack.transition_to(IDLE)
  ├─ combine_requested(a, b)       → InventoryManager.attempt_combine(a, b)
  └─ inspect_requested(item)       → Scene.play_ego_script(item)

ActionState
  ├─ right-click cycles: WALK→LOOK→TOUCH→TALK→ITEM→WALK
  └─ ITEM action triggers same interaction flow as other cursors

MainGame / Scene
  ├─ backpack_clicked              → InventoryBackpack.transition_to(OPEN) (guarded)
  ├─ world_entity_clicked (ITEM)   → entity receives item interaction signal
  └─ right-click in world          → cycles cursor (ITEM→WALK, returns item)
```

---

## Implementation Units

- U1. **ItemDefinition Resource**

**Goal:** Define the data structure for inventory items as a Godot `Resource`.

**Requirements:** R4

**Dependencies:** None

**Files:**
- Create: `inventory/ItemDefinition.gd`
- Create: `inventory/ItemDefinition.gd.uid`

**Approach:**
- Extend `Resource` with `class_name ItemDefinition`
- Fields: `id` (String, unique key), `name` (String), `combination_category` (String, optional)
- Signals emitted by items when interacted with: `item_used_on_entity(entity)`, `item_combined_with(other_item)`, `item_inspected()`. Room scenes connect to these signals to define entity-specific behavior.
- No embedded interaction data — all interaction logic lives in the scene scripts that receive the signals.

**Patterns to follow:**
- Godot `Resource` pattern for serializable data
- Existing `PolygonPointsResource.gd` as a simple example of a custom resource

**Test scenarios:**
- Happy path: Create ItemDefinition with id, name, and combination_category; verify fields are set correctly
- Edge case: Create ItemDefinition with no combination_category; verify it works as standalone item
- Edge case: Create ItemDefinition with empty name; verify id is still valid

**Verification:**
- Resource can be instantiated in code with all fields populated
- Resource can be saved as a `.tres` file and reloaded

---

- U2. **InventoryManager AutoLoad Singleton**

**Goal:** Global inventory state management with signals, queries, and combination logic.

**Requirements:** R1, R2, R3, R5, R8

**Dependencies:** U1 (ItemDefinition)

**Files:**
- Create: `inventory/InventoryManager.gd`
- Create: `inventory/InventoryManager.gd.uid`
- Modify: `project.godot` (add autoload)

**Approach:**
- Extend `Node` with signals: `item_acquired(item_id: String)`, `item_removed(item_id: String)`, `inventory_changed`, `combination_attempted(item_a_id: String, item_b_id: String)`
- State: `inventory` (Array[String], ordered), `obtained_items` (Dictionary used as set), `stripped_items` (Array[String])
- Query methods: `has_item(item_id)`, `has_obtained(item_id)`, `has_any_of(item_ids)`, `has_obtained_any_of(item_ids)`, `has_obtained_all_of(item_ids)`
- Mutation methods: `acquire_item(item_id)`, `remove_item(item_id, quiet=false)`, `bulk_strip_items(event_items, exempt_items)` — moves stripped items to vault
- Combination logic: `attempt_combine(item_a_id, item_b_id)` emits `combination_attempted` signal. Room scripts or scene handlers connect to this signal to define what happens when two items are combined. No embedded combination data in the manager.
- Item definitions registry: `get_item_definition(item_id)` loads from a registry. Items are registered via `register_item(item_def: ItemDefinition)`

**Patterns to follow:**
- `ActionState.gd` for autoload singleton pattern with signals
- `GameScript.gd` Narrate class for overlay fade pattern (reference for how overlays interact with game state)

**Test scenarios:**
- Happy path: Acquire item → verify it's in inventory and obtained_items
- Happy path: Remove item → verify it's removed from inventory but stays in obtained_items
- Happy path: attempt_combine emits combination_attempted signal → connected handler receives both item IDs
- Edge case: Acquire already-owned item → item added to inventory again (duplicates allowed in current inventory)
- Edge case: Remove item not in inventory → no crash, no change
- Edge case: Query has_any_of with empty set → returns false
- Edge case: Query has_obtained_all_of with empty set → returns true
- Integration: acquire_item emits item_acquired signal → connected handler receives correct item_id
- Integration: remove_item emits item_removed signal → connected handler receives correct item_id
- Integration: bulk_strip_items moves items to vault, exempts protected items, leaves obtained_items untouched

**Verification:**
- AutoLoad registered in `project.godot` and accessible as `InventoryManager` from any scene
- All query methods return correct boolean values for various inventory states
- combination_attempted signal emits correct item pair to connected handlers
- Signal emissions are correct for acquire, remove, and combination events

---

- U3. **HUD Backpack FSM**

**Goal:** Animated backpack icon with state machine for Idle, Open, Selected, Acquire, Remove states and multi-step queued transitions.

**Requirements:** R6, R7, R8, R9

**Dependencies:** U2 (InventoryManager)

**Files:**
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn`
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn.uid`
- Create: `inventory/inventory_backpack/InventoryBackpack.gd`
- Create: `inventory/inventory_backpack/InventoryBackpack.gd.uid`
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn.uid`

**Approach:**
- Scene tree: `InventoryBackpack` (Control) → `AnimationPlayer`, `ColorRect` (backpack icon placeholder), `FloatingItem` (Control) → `ColorRect` (dynamic item placeholder)
- Anchored to top-right using Control anchors/offsets
- FSM enum: `IDLE`, `OPEN`, `SELECTED`, `ACQUIRE`, `REMOVE`
- Transitions execute immediately. No FIFO queuing — a new transition kills any in-flight tween and starts fresh.
- Each transition is a chain of `Tween` operations with `tween_callback()` for step completion. New `Tween` per transition (Godot 4 Tweens are not reusable).
- `kill_tween()` called on any active tween before starting a new transition
- `is_busy` flag prevents concurrent transitions but does NOT queue — new transitions during busy state are dropped or interrupt immediately.
- Item appearance animation primitive: `_animate_item(fade_dir, move_dir)` with parameters for show/hide and movement direction (out/none/in/far-out)
- Connect to `InventoryManager.item_acquired` and `InventoryManager.item_removed` signals
- Guard condition checks before `transition_to(OPEN)`: `GameScript.current_script` (foreground action), Fade sprite `modulate.a` (screen fade), pause flag
- If guard blocks open and foreground action is running, emit `skip_action_requested` signal

**Tween timing (from PRD §11.2):**
- Backpack rotation: linear, 0.35s
- Backpack position: ease-in, 0.35s
- Item appear/disappear: linear, 0.5s

**Patterns to follow:**
- PRD §13.2 (Option A: Hand-Rolled FSM) for FSM structure
- PRD §13.3 for Tween chaining with callbacks
- `Game.tscn` Fade animation pattern for reference on AnimationPlayer usage

**Test scenarios:**
- Happy path: Click backpack in Idle → transitions to Open, overlay shown
- Happy path: Acquire item in Idle → backpack opens, item appears and drops in, closes
- Happy path: Select item from overlay → transitions to Selected, floating item appears
- Happy path: Remove item in Idle → backpack opens, item appears and slides up, closes
- Edge case: Rapid transition requests → new transition interrupts in-flight animation immediately
- Edge case: Acquire item while Selected → replaces selected item with acquire animation
- Edge case: Remove selected item (same item) → item slides up and fades, clears selection
- Edge case: Remove different item while Selected → deselects current, shows removed item, slides up
- Edge case: Transition interrupted by new transition → old tween killed, new transition starts
- Integration: item_acquired signal from InventoryManager triggers Acquire animation
- Integration: item_removed signal from InventoryManager triggers Remove animation
- Integration: Guard condition blocks open when script running → skip_action_requested emitted

**Verification:**
- FSM starts in IDLE state
- All state transitions execute their multi-step animation sequences
- New transitions interrupt in-flight animations (no queuing)
- Tween objects are properly killed and recreated per transition
- Guard conditions prevent opening during foreground action, screen fade, or pause

---

- U4. **Inventory Overlay Screen**

**Goal:** Full-screen overlay with item grid, hover text, drag-and-drop selection, and item combination.

**Requirements:** R7, R12

**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack)

**Files:**
- Create: `inventory/inventory_overlay/InventoryOverlay.tscn`
- Create: `inventory/inventory_overlay/InventoryOverlay.tscn.uid`
- Create: `inventory/inventory_overlay/InventoryOverlay.gd`
- Create: `inventory/inventory_overlay/InventoryOverlay.gd.uid`
- Create: `inventory/inventory_overlay/InventorySlot.tscn`
- Create: `inventory/inventory_overlay/InventorySlot.tscn.uid`
- Create: `inventory/inventory_overlay/InventorySlot.gd`
- Create: `inventory/inventory_overlay/InventorySlot.gd.uid`

**Approach:**
- Scene tree: `InventoryOverlay` (Control, full-screen) → `ColorRect` (background, `MOUSE_FILTER_STOP`), `InventoryPanel` (Control, centered) → `TextureRect` (frame), `GridContainer` (item grid), `HoverLabel` (Label)
- Overlay opacity controlled via `modulate.a` tweened for 0.2s fade-in/fade-out
- `input_active` boolean flag — only `true` when fully opaque (AC-12)
- `_gui_input()` handles all interaction:
  - Hover: track mouse-over item, update hover label text
  - Press: select item for drag
  - Motion during press: move item sprite to follow cursor, track hover under dragged item
  - Release on same item ≤0.5s: confirm selection, emit `item_confirmed`, close
  - Release on same item >0.5s: deselect (long-press cancel), stay open
  - Release on different item: emit `combine_requested(a, b)` to InventoryManager
  - Right-click on item: emit `inspect_requested(item)`, close
  - Release on empty space: close
  - Drag outside panel bounds: close immediately, discard selection
- `InventorySlot` is a lightweight Control with a `ColorRect` child (colored box placeholder), `custom_minimum_size` for cell dimensions, and hover highlight modulate
- `_refresh_grid()` called on `inventory_changed` signal: clears grid, instantiates `InventorySlot` for each item in `InventoryManager.inventory`
- Combination result handling: on `combination_succeeded`, grid refreshes automatically via signal. On `combination_refused`, show message via `GameScript.narrate()` or dialogue system.

**Patterns to follow:**
- `GameScript.gd` Narrate class for overlay fade pattern
- `Game.tscn` NarratorOverlay for CanvasLayer overlay pattern
- PRD §13.4 for Control hierarchy and drag-and-drop approach

**Test scenarios:**
- Happy path: Open overlay → items displayed in grid, left-to-right, top-to-bottom
- Happy path: Hover item with no selection → hover label shows item name
- Happy path: Select item A, hover item B → hover label shows "Use A with B"
- Happy path: Short-click item → item confirmed, overlay closes
- Happy path: Drag item A onto item B → combination attempted
- Happy path: Right-click item → inspect dialogue played, overlay closes
- Happy path: Click empty space → overlay closes, selection cleared
- Edge case: Long-press release (>0.5s) on item → deselected, overlay stays open
- Edge case: Drag item outside panel bounds → overlay closes, selection discarded
- Edge case: Overlay during fade-in → renders but doesn't accept input
- Edge case: Overlay during fade-out → renders but doesn't accept input
- Edge case: Empty inventory → grid shows no items
- Error path: Combine items with no interaction → refusal message shown
- Integration: inventory_changed signal refreshes grid with new items
- Integration: combine_requested signal sent to InventoryManager with correct item pair
- Integration: Hover text tracks item under dragged item during drag

**Verification:**
- Overlay fades in/out with correct timing
- Grid layout respects fixed maximum items per row
- All interaction outcomes match PRD §5.4 table
- Input is blocked during fade transitions
- Drag-out escape closes overlay and discards selection

---

- U5. **MainGame Integration — Cursor, Guards, World Item Use**

**Goal:** Integrate inventory system with existing cursor management, guard conditions, and game world item interaction.

**Requirements:** R10, R11, R12

**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack), U4 (InventoryOverlay)

**Files:**
- Modify: `MainGame.gd`
- Modify: `Game.tscn`
- Modify: `Scene.gd`

**Approach:**
- **Game.tscn changes:** Add `InventoryBackpack` and `InventoryOverlay` as children of `Node2D` (MainGame), under separate `CanvasLayer` nodes at appropriate layers. Backpack at layer 5 (HUD), overlay at layer 10 (full-screen).
- **ActionState.gd changes:**
  - Add `ITEM` as the 5th action (value 4): `WALK=0`, `LOOK=1`, `TOUCH=2`, `TALK=3`, `ITEM=4`
  - Right-click cycling logic updated: cycles through 0→1→2→3→4→0
  - When cycling to ITEM, the cursor uses the selected item's colored box as cursor (placeholder)
- **MainGame.gd changes:**
  - `_on_item_selected(item_def)`: Store item in `InventoryManager.selected_item`, set `ActionState.current_action = ActionState.Action.ITEM`
  - Guard conditions: `can_open_inventory()` checks `GameScript.current_script` (foreground action), Fade `modulate.a` (screen fade), and pause state
  - World item use: When `ActionState.current_action == Action.ITEM` and player left-clicks in game world, emit item's interaction signal for the clicked entity. If no entity clicked, show item's self-description.
  - Right-click in game world with item selected → cycles cursor to next action (ITEM→WALK), effectively returning item to backpack
  - Input routing: Ensure `_unhandled_input()` in `Scene.gd` checks `InventoryOverlay.input_active` before processing game-world clicks
- **Scene.gd changes:**
  - Add guard in `_unhandled_input()`: if `InventoryOverlay` is active, return early
  - When `ActionState.current_action == Action.ITEM`, pass selected item to clicked entity's interaction handler via signal

**Patterns to follow:**
- `ActionState.gd` for cursor action enum and cycling pattern
- `MainGame.gd` `_on_input_action()` for right-click cycling integration
- `Scene.gd` `_unhandled_input()` for input handling
- `SetPiece_.gd` for entity interaction pattern
- PRD §13.10 recommended combined input priority approach

**Test scenarios:**
- Happy path: Item selected from overlay → ActionState set to ITEM, item stored in InventoryManager
- Happy path: Right-click in world with ITEM active → cursor cycles to WALK, item returned
- Happy path: Left-click entity with ITEM active → entity receives item interaction signal
- Happy path: Left-click empty space with ITEM active → item self-description shown
- Edge case: Click backpack while script running → skip action attempted, inventory doesn't open
- Edge case: Click backpack during screen fade → inventory doesn't open
- Edge case: Inventory overlay active → game world input is suppressed
- Edge case: Entity has no handler for selected item → generic "nothing to do" message
- Integration: InventoryBackpack guard check returns false when foreground action running
- Integration: Scene._unhandled_input returns early when overlay is active
- Integration: Right-click cycling includes ITEM state (WALK→LOOK→TOUCH→TALK→ITEM→WALK)

**Verification:**
- ActionState includes ITEM as value 4, right-click cycles through all 5 states
- Guard conditions prevent inventory from opening during foreground action, screen fade, or pause
- Game world input is properly suppressed when overlay is active
- Item use triggers entity interaction signals when ActionState is ITEM

---

- U6. **Scene Integration Hooks — Acquisition and Removal**

**Goal:** Provide hooks for room scenes to acquire and remove items, with proper signal propagation and animation triggering.

**Requirements:** R6, R7, R8, R9

**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack)

**Files:**
- Modify: `Scene.gd` (add helper methods)
- Modify: `scenes/kq4_placeholder_template/kq4_placeholder_template.gd` (example usage)

**Approach:**
- Add helper methods to `Scene.gd` (or as static utilities accessible from any scene):
  - `give_item(item_id)`: Calls `InventoryManager.acquire_item(item_id)`. The manager emits `item_acquired` which triggers the backpack's Acquire animation.
  - `remove_item(item_id, quiet=false)`: Calls `InventoryManager.remove_item(item_id, quiet)`. The manager emits `item_removed` which triggers the backpack's Remove animation (unless quiet).
  - `strip_items(event_items, exempt_items)`: Calls `InventoryManager.bulk_strip_items()`. Stripped items go to vault.
  - `use_item_on_entity(item_id, entity)`: Checks if entity has an interaction handler for the item. If yes, executes it. If no, shows generic refusal.
- Room scripts can call these helpers from SetPiece signal handlers or GameScript steps.
- Add a `GameScript` step class for item acquisition: `GiveItem` step that plays dialogue, acquires the item, and triggers the animation.
- Example usage in placeholder template: show how a SetPiece `touched` signal handler calls `give_item()`.

**Patterns to follow:**
- `GameScript.gd` `Say` and `Narrate` classes for new `GiveItem` step
- `Scene.gd` `start_main_script` for script-based item acquisition
- `TransitionPiece.gd` for how room scripts interact with global systems

**Test scenarios:**
- Happy path: SetPiece touched → give_item called → item in inventory, backpack Acquire animation plays
- Happy path: Combination consumes item → remove_item called → item removed from inventory, backpack Remove animation plays
- Edge case: Quiet removal → item removed from inventory, no backpack animation
- Edge case: Give item while another item selected → selected item display replaced with new item
- Integration: give_item triggers InventoryManager.acquire_item → signal propagates to InventoryBackpack → Acquire animation starts
- Integration: bulk_strip_items moves items to vault, exempt items stay in inventory

**Verification:**
- Room scripts can call `give_item()` and `remove_item()` from signal handlers
- Item acquisition triggers proper backpack animation via signal chain
- Item removal respects quiet flag
- Bulk strip moves items to vault and exempts protected items
- `GiveItem` GameScript step integrates with existing script builder pattern

---

## System-Wide Impact

- **Interaction graph:** `InventoryManager` signals connect to `InventoryBackpack` (animations), `InventoryOverlay` (grid refresh), and `MainGame` (cursor management). `InventoryBackpack` signals connect to `InventoryOverlay` (show/hide). `InventoryOverlay` signals connect back to `InventoryManager` (combination) and `InventoryBackpack` (close). The signal loop is: Manager → Backpack → Overlay → Manager.
- **Error propagation:** Combination refusals propagate as `combination_refused` signal with message, handled by the overlay or scene's dialogue system. Invalid item IDs should be guarded with `push_warning()`.
- **State lifecycle risks:** The FSM's transition queue must handle rapid state changes correctly. If the backpack node is freed during a transition, `bind_node(self)` on Tweens ensures they die cleanly. The overlay's `input_active` flag must be set/cleared at exact fade boundaries to avoid the "input black hole" problem.
- **API surface parity:** The existing `ActionState` cursor actions (WALK/LOOK/TOUCH/TALK) continue to work normally when no item is selected. When an item is selected, the walk cursor effectively becomes "use item" — this is a behavioral override, not a change to `ActionState` itself.
- **Integration coverage:** The full signal chain from `InventoryManager.acquire_item()` → backpack Acquire animation → FSM returning to Idle must work as an integration scenario that mocks alone won't prove. Similarly, the overlay combination flow: click A → click B → `attempt_combine()` → `combination_succeeded` → items removed/added → grid refreshes → overlay closes.
- **Unchanged invariants:** The existing cursor cycling (right-click cycles WALK/LOOK/TOUCH/TALK) is unchanged. The `ActionState` autoload is unchanged. The `GameScript` system continues to work for cutscenes — the new `GiveItem` step extends it without modifying existing steps.

---

## Risks & Dependencies

| Risk | Mitigation |
|------|-----------|
| Tween lifecycle bugs: FSM transitions interrupted mid-animation leave visual state inconsistent | Always `kill()` active tween before starting new transition. Use `bind_node(self)` to auto-clean on node free. |
| Input black hole during overlay fade transitions | `input_active` flag toggled at exact fade boundaries. Overlay blocks input from fade-start through fade-end. Game world regains input only after fade-out completes. |
| Node2D vs Control input gap: overlay `accept_event()` doesn't suppress `Node2D._input_event()` | Combined defense: `MOUSE_FILTER_STOP` background + `_unhandled_input()` gate in Scene + explicit `overlay_active` guard flag checked by game world. |
| GridContainer limitations during drag: item reparenting causes layout flicker | Test drag behavior early. If GridContainer causes issues, switch to manual grid positioning in a custom Control. |
| Combination logic complexity: dynamic rules, prerequisites, order guards | Start with static dictionary lookups (forward + reverse). Add dynamic category rules and guards as separate methods that can be extended per item. |

---

## Documentation / Operational Notes

- Item definitions (`.tres` files) should be placed in a dedicated `inventory/items/` directory when they are created
- The `InventoryManager` autoload should be documented in a brief comment at the top of its file, listing all signals and public methods
- No CLAUDE.md or AGENTS.md updates needed — the inventory system follows existing patterns (autoload singletons, scene hierarchy, signal-based communication)

---

## Sources & References

- **Origin document:** [inventory-prd.md](inventory-prd.md)
- Existing code: `ActionState.gd`, `MainGame.gd`, `GameScript.gd`, `Scene.gd`, `SetPiece_.gd`, `Game.tscn`
- Godot 4.6 docs: `Tween`, `Control`, `Resource`, `CanvasLayer`, `Input.create_custom_cursor()`
