Reviewed-on: #1 Co-authored-by: Bryce <bryce@brycecovertoperations.com> Co-committed-by: Bryce <bryce@brycecovertoperations.com>
35 KiB
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
ColorRectboxes 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 forInventoryManagerautoload.MainGame.gd— Root scene (Game.tscn) that handles cursor switching viaInput.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 withScriptGraphfor cutscenes. TheNarrateclass shows the pattern for overlay fade-in/fade-out withmodulate.atweens, and theis_script_running/is_cursor_lockedpattern inMainGame.gdis 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 howActionStateandGameScript.current_scriptare checked before processing input.Game.tscn— Root scene tree withSubViewportfor game world,CanvasLayerfor overlays (NarratorOverlay at layer 10), andAnimationPlayerfor 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
ActionStatepattern. 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 inInventoryManager.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 withInputEventMouseButtonandInputEventMouseMotionin_gui_input(). - Combined input priority defense: Overlay background
ColorRectwithMOUSE_FILTER_STOP+accept_event()blocks Control input. Game world uses_unhandled_input()which is already filtered by Controlaccept_event(). An explicitoverlay_activeguard flag provides a safety net forNode2D._input_event()bypass. (PRD §13.10) - Colored boxes for placeholders: All item sprites, backpack visuals, and cursor assets use colored
ColorRectboxes 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 toPRESERVE_WIDEanchors for free movement.
Open Questions
Resolved During Planning
- Should the inventory overlay live under MainGame or be a separate autoload? Under
MainGameas a child node inGame.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'smodulate.a(screen fade), and a pause flag onMainGame. If a guard blocks opening, clicking the backpack attempts to skip the running action. - Where does item combination logic live? In
InventoryManagerasattempt_combine(item_a, item_b). The manager looks up interactions on both items (forward and reverse), evaluates dynamic rules, and emitscombination_succeededorcombination_refusedsignals.
Deferred to Implementation
- Exact animation frame counts for backpack open/close sprite sequences — depends on final sprite assets
- Whether
ItemDefinitioninteraction handlers use aDictionaryor 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
Resourcewithclass_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
Resourcepattern for serializable data - Existing
PolygonPointsResource.gdas 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
.tresfile 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
Nodewith 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)emitscombination_attemptedsignal. 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 viaregister_item(item_def: ItemDefinition)
Patterns to follow:
ActionState.gdfor autoload singleton pattern with signalsGameScript.gdNarrate 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.godotand accessible asInventoryManagerfrom 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
Tweenoperations withtween_callback()for step completion. NewTweenper transition (Godot 4 Tweens are not reusable). kill_tween()called on any active tween before starting a new transitionis_busyflag 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_acquiredandInventoryManager.item_removedsignals - Guard condition checks before
transition_to(OPEN):GameScript.current_script(foreground action), Fade spritemodulate.a(screen fade), pause flag - If guard blocks open and foreground action is running, emit
skip_action_requestedsignal
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.tscnFade 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.atweened for 0.2s fade-in/fade-out input_activeboolean flag — onlytruewhen 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
InventorySlotis a lightweight Control with aColorRectchild (colored box placeholder),custom_minimum_sizefor cell dimensions, and hover highlight modulate_refresh_grid()called oninventory_changedsignal: clears grid, instantiatesInventorySlotfor each item inInventoryManager.inventory- Combination result handling: on
combination_succeeded, grid refreshes automatically via signal. Oncombination_refused, show message viaGameScript.narrate()or dialogue system.
Patterns to follow:
GameScript.gdNarrate class for overlay fade patternGame.tscnNarratorOverlay 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
InventoryBackpackandInventoryOverlayas children ofNode2D(MainGame), under separateCanvasLayernodes at appropriate layers. Backpack at layer 5 (HUD), overlay at layer 10 (full-screen). - ActionState.gd changes:
- Add
ITEMas 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)
- Add
- MainGame.gd changes:
_on_item_selected(item_def): Store item inInventoryManager.selected_item, setActionState.current_action = ActionState.Action.ITEM- Guard conditions:
can_open_inventory()checksGameScript.current_script(foreground action), Fademodulate.a(screen fade), and pause state - World item use: When
ActionState.current_action == Action.ITEMand 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()inScene.gdchecksInventoryOverlay.input_activebefore processing game-world clicks
- Scene.gd changes:
- Add guard in
_unhandled_input(): ifInventoryOverlayis active, return early - When
ActionState.current_action == Action.ITEM, pass selected item to clicked entity's interaction handler via signal
- Add guard in
Patterns to follow:
ActionState.gdfor cursor action enum and cycling patternMainGame.gd_on_input_action()for right-click cycling integrationScene.gd_unhandled_input()for input handlingSetPiece_.gdfor 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): CallsInventoryManager.acquire_item(item_id). The manager emitsitem_acquiredwhich triggers the backpack's Acquire animation.remove_item(item_id, quiet=false): CallsInventoryManager.remove_item(item_id, quiet). The manager emitsitem_removedwhich triggers the backpack's Remove animation (unless quiet).strip_items(event_items, exempt_items): CallsInventoryManager.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
GameScriptstep class for item acquisition:GiveItemstep that plays dialogue, acquires the item, and triggers the animation. - Example usage in placeholder template: show how a SetPiece
touchedsignal handler callsgive_item().
Patterns to follow:
GameScript.gdSayandNarrateclasses for newGiveItemstepScene.gdstart_main_scriptfor script-based item acquisitionTransitionPiece.gdfor 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()andremove_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
GiveItemGameScript step integrates with existing script builder pattern
System-Wide Impact
- Interaction graph:
InventoryManagersignals connect toInventoryBackpack(animations),InventoryOverlay(grid refresh), andMainGame(cursor management).InventoryBackpacksignals connect toInventoryOverlay(show/hide).InventoryOverlaysignals connect back toInventoryManager(combination) andInventoryBackpack(close). The signal loop is: Manager → Backpack → Overlay → Manager. - Error propagation: Combination refusals propagate as
combination_refusedsignal with message, handled by the overlay or scene's dialogue system. Invalid item IDs should be guarded withpush_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'sinput_activeflag must be set/cleared at exact fade boundaries to avoid the "input black hole" problem. - API surface parity: The existing
ActionStatecursor 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 toActionStateitself. - 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
ActionStateautoload is unchanged. TheGameScriptsystem continues to work for cutscenes — the newGiveItemstep 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 (
.tresfiles) should be placed in a dedicatedinventory/items/directory when they are created - The
InventoryManagerautoload 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()