Files
ai-game-2/docs/plans/2026-04-26-001-feat-inventory-backpack-system-plan.md
Bryce 639060fa7f sugary-panda (#1)
Reviewed-on: #1
Co-authored-by: Bryce <bryce@brycecovertoperations.com>
Co-committed-by: Bryce <bryce@brycecovertoperations.com>
2026-04-28 22:05:11 -07:00

35 KiB
Raw Permalink Blame History

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
  • 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()