Files
ai-game-2/inventory-prd.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

44 KiB

Inventory & Backpack System — Product Requirements Document

1. Overview

This document specifies the behavior of a point-and-click adventure game inventory system, consisting of a backpack HUD element in the main game view and a full-screen inventory overlay for item combination. The system tracks which items the player currently holds, which items have been acquired at least once, and supports selecting, combining, acquiring, and removing items with animated transitions.

The specification is intentionally engine-agnostic and focuses on user-observable behavior, system architecture, and acceptance criteria suitable for implementation in any 2D game engine or framework.

2. Data Models

2.1. Player Inventory State

Field Type Description
Current inventory Ordered list Items the player is carrying. Preserves insertion order.
Obtained items Set Items the player has obtained at least once. Persists across removal — used to check "have you seen this item before".
Stripped items Ordered list Vault for items forcibly removed by game events. Items can be recovered later.

2.2. Item Definition

Each item is defined by a unique identifier and a descriptor:

Field Type Description
Name String Human-readable display name.
Identifier Unique key Canonical identifier for the item.
Cursor variant Reference Sprite or cursor variant to display when this item is selected.
Interaction handlers Map or dynamic function Defines what happens when this item is combined with another. May be a static lookup by the other item's identifier, or a dynamic function that evaluates at combination time. Empty or missing means no valid combination.
Combination category Optional tag Optional marker used by dynamic combination rules (e.g., "container" items that accept other items).

2.3. Inventory Queries

The system must support the following queries:

Query Description
Has item? Is a given item currently in the player's inventory?
Has obtained? Has the player ever obtained a given item?
Has any of? Does the player currently hold any item from a given set?
Has obtained any of? Has the player ever obtained any item from a given set?
Has obtained all of? Has the player ever obtained every item in a given set?

3. System Architecture

The inventory system comprises three components:

  • Main Scene — The default game view. The player navigates the world and interacts with entities.
  • Inventory Overlay — Full-screen overlay for viewing items and combining them. Blocks main scene input while visible.
  • HUD Backpack FSM — Manages the backpack icon state, animations, and floating item display. Sits between the main scene and the overlay.
┌─────────────────────────────────────────────┐
│                 Main Scene                   │
│  ┌──────────┐    events      ┌────────────┐ │
│  │  HUD     │◄──────────────►│  Inventory │ │
│  │  Backpack│                │  Overlay   │ │
│  │  (FSM)   │                │            │ │
│  └──────────┘                └────────────┘ │
└─────────────────────────────────────────────┘

4. HUD Backpack FSM

The backpack element lives in the top-right of the main game screen. It is always rendered but its internal state drives animations and whether the inventory overlay is shown.

4.1. States

State Meaning Visual Behavior
Idle Closed, at rest Backpack icon at rest position. Idle animation.
Open Inventory overlay is visible Backpack animates to an "open" pose via rotation and repositioning. Overlay fades in.
Selected An item is selected and displayed outside the backpack Small item sprite rendered floating adjacent to the backpack.
Acquire New item received, animating into the backpack Backpack opens, item appears briefly, then drops into the backpack, backpack closes.
Remove Item being removed from inventory Backpack opens, item appears, then slides away and fades out. Backpack closes.

4.2. Transition Architecture

Transitions are multi-step. Each step is a discrete animation or action with a start trigger and a completion predicate. Steps execute sequentially — the next step begins only after the previous step reports completion. While a transition sequence is in progress, new transition requests are queued and executed in FIFO order after the current sequence finishes.

4.3. State Transitions

Idle → Open — Open Inventory

Trigger: Player clicks the backpack icon while the scene is interactable.

Steps:

  1. Animate backpack rotation to open position (0.35s).
  2. Animate backpack position to open location (0.35s).
  3. Play backpack open sprite animation to completion.
  4. Lock backpack to final open frame.
  5. Show the inventory overlay screen.

Open → Idle — Close Inventory (no item selected)

Trigger: Inventory overlay closes via click-on-empty-space, drag-out, or cancel.

Steps:

  1. Animate backpack rotation back to rest (0.35s).
  2. Animate backpack position back to rest (0.35s).
  3. Play backpack closing sprite animation to completion.
  4. Return backpack to default sprite.
  5. Clear any selected item.

Open → Selected — Select Item

Trigger: Player confirms an item selection from the inventory overlay.

Steps:

  1. Fade in the item sprite; move it to its floating position next to the backpack (0.5s).

Selected → Idle — Deselect and Close

Trigger: Player deselects the item (e.g., right-click return, or explicit deselect).

Steps:

  1. Fade out the item sprite; move it toward the backpack (0.5s).
  2. Animate backpack rotation and position back to rest, play closing animation (0.35s).
  3. Return backpack to default sprite, clear selected item.

Idle → Acquire → Idle — Pick Up New Item

Trigger: Player receives an item from the game world.

Visual: Backpack opens, item fades in at rest, backpack begins closing, item slides into the backpack and fades out, backpack returns to default.

Selected → Acquire → Selected — Replace Selected Item

Trigger: Player acquires a new item while another item is already selected.

Steps:

  1. Fade out the currently selected item in place.
  2. Fade in the new item in place.
  3. Animate the new item sliding into the backpack and fading out.
  4. Fade the new item back in at the floating position.

Remove Variants

Several remove transitions exist depending on the current state:

From → To Condition Behavior
Idle → Remove → Idle No item selected Backpack opens, item appears, slides up and fades, backpack closes.
Selected → Remove → Idle (same item) Item being removed IS the selected item Selected item slides up and fades out. Clear selection.
Selected → Remove → Idle (different item) Item being removed is DIFFERENT from selected Deselect current item, show item being removed, slide it up and fade, clear selection.

4.4. Item Appearance Animation Primitive

A reusable animation primitive handles showing or hiding the floating item sprite with optional vertical movement:

Fade direction Movement direction Effect
Show (fade in) Out (away from backpack) Item appears floating next to backpack
Show (fade in) None Item appears in place, no movement
Hide (fade out) In (toward backpack) Item slides into backpack and disappears
Hide (fade out) Far-out (away and further) Item slides upward and fades — used for removal
Hide (fade out) None Item fades out in place — used for swap

All fades are 0.5s with linear easing.

5. Inventory Overlay Screen

5.1. Activation

The overlay is shown when the HUD FSM enters the Open state. The overlay receives the current list of inventory items and lays them out in a grid.

5.2. Visual Layout

  • Background: Semi-transparent dark overlay covering the full screen.
  • Inventory Panel: Centered panel with a decorative frame texture.
  • Item Grid: Items laid out left-to-right, top-to-bottom, with a fixed maximum number of items per row.
  • Hover Text Label: Text area at the bottom of the panel for displaying item names and combination hints.

5.3. Open / Close Animations

Action Animation
Open Panel fades from 0% to 100% opacity over 0.2s. Items are laid out in the grid.
Close Panel fades from 100% to 0% opacity over 0.2s. On completion, all selection and hover state is cleared.

5.4. Interaction Model

The inventory overlay only processes input when it is fully visible (opacity at 100%).

Hover

  • Moving the mouse over an item highlights it and sets it as the hover target.
  • Hover text behavior:
    • If an item is already selected and a different item is hovered: Display "Use {selected item} with {hovered item}"
    • If no item is selected and an item is hovered: Display the item's name.
    • Otherwise: Clear the label.

Press and Drag

  • Pressing on an item selects it for the action.
  • Dragging the selected item moves it to follow the cursor.
  • Drag outside panel bounds: If the dragged item is moved outside the inventory panel area, the inventory closes immediately and the selection is discarded.

Left-Click Release

Condition Result
No item was pressed Close inventory, deselect.
Pressed and released on the same item, held > 0.5s Deselect (long-press cancel). Inventory stays open.
Pressed and released on the same item, held ≤ 0.5s Confirm selection. Close inventory. Item floats outside backpack in the main scene.
Pressed one item, released on a different item Attempt to combine the two items. (See Item Combination below.)

Right-Click Release

Condition Result
An item is selected or hovered Inspect the item: play its description dialogue. Close inventory.

5.5. Item Combination Logic

When two different items are clicked together:

  1. Forward lookup: Check whether the first item has a defined interaction with the second item.
  2. Reverse lookup: If no interaction is defined, check whether the second item has a defined interaction with the first item. This ensures combinations work regardless of click order.
  3. No match: If neither direction defines an interaction, show a generic refusal message.

Dynamic Combination Rules

Some items generate their interactions dynamically at evaluation time rather than using a static lookup. Common patterns include:

  • Category-based rules: Items tagged with a combination category (e.g., "container") evaluate a rule set that checks the other item's properties.
  • Prerequisite checks: A combination may require the player to have obtained a prerequisite item before the combination is allowed. If the prerequisite is missing, a different refusal message is shown.
  • Order guards: Some item pairs define a "not ready" handler when combined out of sequence. The refusal message differs depending on whether the player possesses a reference item (e.g., a recipe or instructions).

Successful Combination

When a valid combination is found:

  1. Execute the combination action (typically removes consumed items, adds a result item, plays dialogue).
  2. Close the inventory.

Refusal Handling

When a combination is not valid, the player receives context-appropriate feedback:

  • No interaction defined: Generic "I don't know how those go together" message.
  • Missing prerequisite: Message indicating the player needs more information first.
  • Wrong order ("not ready" guard): Message varies based on whether the player has reference materials.

5.6. Post-Selection Flow

When the inventory overlay closes with a confirmed item selection:

  1. The HUD transitions to the Selected state — the item's sprite floats outside the backpack.
  2. The game cursor switches to the item's dedicated cursor sprite.
  3. The player can then:
    • Left-click in the game world: Use the item on the clicked entity. If nothing is clicked, show the item's self-description.
    • Click the backpack again: Re-open the inventory overlay.
    • Right-click in the game world: Return the item to the backpack. Cursor reverts to default.

6. Item Acquisition

When the player acquires an item from the game world:

  1. Play a pickup sound effect.
  2. Append the item to the current inventory list.
  3. Add the item to the obtained-items set.
  4. Trigger the acquisition animation on the HUD (backpack opens, item appears, drops in, closes).

If an item is already selected when a new item is acquired, the selected item display is replaced with the newly acquired item.

7. Item Removal

When an item is removed from inventory (consumed in combination, dropped, or stripped by a game event):

  1. Remove the item from the current inventory list.
  2. The item is not removed from the obtained-items set — acquisition history persists.
  3. Unless the removal is quiet, trigger the removal animation on the HUD (backpack opens, item appears, slides up and fades, closes).

7.1. Bulk Item Removal

When a game event strips multiple items from the player:

  1. Items removable by the event are removed from the current inventory.
  2. Certain protected items may be exempt from removal.
  3. Stripped items are moved to a vault for potential later recovery.
  4. Obtained-items history is unaffected.

8. Cursor System

The game cursor changes based on the player's current inventory selection:

State Cursor
No item selected Default pointer
Item selected Item-specific cursor sprite
System override (e.g., loading) Hourglass or wait cursor

8.1. Cursor Behavior

  • Each item's cursor sprite has a configurable hotspot offset so the click-detection point aligns with the meaningful part of the sprite.
  • Cursor updates are throttled to avoid excessive redraws.
  • When the mouse button is pressed, the cursor sprite shifts vertically to simulate a depressed state.
  • Returning an item (right-click) reverts the cursor to default.

9. Guard Conditions

The inventory system respects the following scene-level guards:

Guard Effect
Foreground action running Inventory cannot be opened. Clicking the backpack instead attempts to skip the running action.
Screen fade in progress Input is blocked. Inventory cannot be opened.
Game paused or inactive Inventory is inaccessible.
HUD element hovered A flag is set to prevent game-world clicks while the mouse is over any HUD element.

10. Using Selected Items in the Game World

When the player left-clicks in the game world with an item selected:

  1. No clickable entity: Run the item's self-description dialogue.
  2. Clickable entity found: Pass the item's identifier to the entity's interaction handler. The entity determines whether the item is useful in this context.
  3. Entity has no interaction for that item: Show a generic "nothing to do with that" message.

11. Animation Specifications

11.1. Backpack Animations

Animation Source Behavior
Default (idle) Idle sprite or animation loop Static or looping idle state. Plays when backpack is at rest.
Open Multi-frame animation Plays frames forward from closed to open. Stops at final frame.
Opened (held) Final frame of open animation Locked frame while inventory is open.
Closing Multi-frame animation Plays frames from the open frame back toward closed. Stops at final frame.

11.2. Tween Timing Reference

Tween Easing Duration
Backpack rotation (open or close) Linear ~0.35s
Backpack position (open or close) Ease-in ~0.35s
Overlay fade-in Ease-out ~0.2s
Overlay fade-out Ease-out ~0.2s
Item appear/disappear Linear ~0.5s

Durations are reference values — implementations should target similar pacing.

12. Acceptance Criteria

AC-1: Inventory Data

  • Current inventory is an ordered collection that preserves insertion order.
  • Obtained items is a set that records every item ever acquired.
  • Removing an item from the current inventory does not remove it from obtained items.
  • Initial state: empty inventory, empty obtained set.

AC-2: HUD Backpack FSM

  • The FSM starts in the Idle state.
  • State transitions execute as multi-step sequences, not instantaneous changes.
  • Each step completes before the next step begins.
  • New transition requests are queued while the FSM is busy; no transitions are dropped.
  • Queued transitions execute in FIFO order.

AC-3: Open Inventory

  • Clicking the backpack icon when the scene is interactable opens the inventory overlay.
  • The backpack icon animates to an open pose (rotation and reposition).
  • The inventory overlay fades in over ~0.2s.
  • Items are laid out in a grid with a fixed maximum per row.

AC-4: Close Inventory

  • Clicking empty space in the overlay closes the inventory.
  • Dragging an item outside the panel bounds closes the inventory and discards the selection.
  • The overlay fades out over ~0.2s.
  • The backpack animates back to its rest pose.
  • Selection and hover state are cleared on close.

AC-5: Item Selection

  • Clicking an item in the inventory selects it.
  • Long-press release (>0.5s) on the same item deselects it; inventory stays open.
  • Short release (≤0.5s) on the same item confirms selection; inventory closes.
  • The selected item's sprite appears floating outside the backpack.
  • The game cursor switches to the item's cursor sprite.
  • Right-clicking an item in the inventory inspects it and closes the overlay.

AC-6: Item Combination

  • Clicking item A then item B checks A's interactions for B.
  • If A has no interaction for B, B's interactions for A are checked (order-independent).
  • If no interaction exists for the pair, a refusal message is shown.
  • Category-based dynamic combination rules evaluate at interaction time.
  • Prerequisite-checked combinations show a distinct refusal when the prerequisite is missing.
  • Order-guarded combinations show context-appropriate refusals based on available reference items.
  • Successful combinations remove consumed items and add the result item.
  • Combination-triggered removals and acquisitions fire the appropriate HUD animations.
  • Hover text displays "Use {A} with {B}" when hovering B while A is selected.

AC-7: Item Acquisition

  • When an item is acquired, the backpack opens, the item appears, drops in, and the backpack closes.
  • A pickup sound plays.
  • The item is appended to the inventory and added to the obtained set.
  • If another item is currently selected, it is replaced with the newly acquired item.

AC-8: Item Removal

  • When an item is removed, the backpack opens, the item appears, slides up and fades, and the backpack closes.
  • Quiet removals skip the HUD animation.
  • Removed items are taken from the inventory but remain in the obtained set.

AC-9: Bulk Item Removal

  • A game event can strip multiple items, with certain protected items exempt.
  • Stripped items are stored in a vault for later recovery.
  • The obtained-items set is unaffected by bulk removal.

AC-10: Game World Item Use

  • Left-clicking an entity with an item selected uses the item on that entity.
  • Left-clicking empty space with an item selected shows the item's self-description.
  • Right-clicking in the game world returns the item to the backpack.
  • The cursor reverts to default after returning an item.

AC-11: Guard Conditions

  • Inventory cannot open while a foreground action is running.
  • Inventory cannot open during a screen fade transition.
  • Inventory cannot open when the game is paused or inactive.
  • Hovering over HUD elements prevents game-world interaction.
  • Clicking the backpack while a foreground action is running attempts to skip that action.

AC-12: Overlay Interactivity

  • The overlay only accepts input when fully opaque.
  • During fade-in or fade-out, the overlay renders but does not accept input.
  • Items render at the panel's current opacity during transitions.

AC-13: Visual Feedback

  • Hover text shows the item name when hovering an item with none selected.
  • Hover text shows "Use {A} with {B}" when hovering a different item than the selected one.
  • Hover text is cleared when not hovering any item.
  • Dragged items follow the cursor in real time.
  • The hovered item under the dragged item is tracked for combination hint text.

AC-14: Cursor Behavior

  • The cursor reflects the currently selected inventory item.
  • Cursor updates are throttled to avoid excessive redraws.
  • Each cursor variant has a configurable hotspot offset for accurate click targeting.
  • The depressed (mouse-down) cursor state shows a vertically shifted sprite.

13. Godot Node Architecture — Design Brainstorm

The following section explores how this system could be structured using Godot 4's node primitives. These are not prescriptive — they represent one viable approach with trade-offs flagged where relevant.

13.1. High-Level Scene Tree

MainScene (Node2D)                      # Game world layer
├── GameWorld (Node2D)                  # Entities, backgrounds, interactables
├── SubViewportContainer                # Scene fade layer
│   └── SubViewport                     # Contains a ColorRect for screen fades
├── HUD (Control)                       # Top-level UI container
│   ├── InventoryBackpack (Control)     # HUD backpack FSM node
│   │   ├── AnimationPlayer             # Backpack open/close sprite animations
│   │   ├── Sprite2D                    # Backpack sprite
│   │   └── FloatingItem (Control)      # Selected item sprite, positioned relative to backpack
│   │       └── Sprite2D                # Dynamic item sprite
│   ├── SaveButton (Button)
│   └── CloseButton (Button)
└── InventoryOverlay (Control)          # Full-screen overlay, initially hidden
    ├── ColorRect                       # Semi-transparent dark background
    ├── InventoryPanel (Control)        # Decorative frame + item grid
    │   ├── TextureRect                 # Panel frame texture
    │   ├── ItemGrid (HBoxContainer or custom Control)
    │   │   ├── InventorySlot (Control) # One per item, dynamically instantiated
    │   │   │   └── Sprite2D            # Item icon
    │   │   └── ...
    │   └── HoverLabel (Label)          # Bottom text label

Reasoning: The game world lives on the Node2D layer, while all UI sits under Control nodes in a separate canonical layer. This mirrors Godot's recommended separation of 2D game content from UI. The backpack HUD element is a Control node anchored to the screen's top-right corner using anchors/offsets. The inventory overlay is a full-screen Control with anchor_preset set to fill the viewport.

13.2. InventoryBackpack — FSM Implementation

The backpack FSM (Section 4) has several possible implementations in Godot, each with trade-offs:

A single _ready()_process() or signal-driven FSM on the InventoryBackpack node itself.

# Pseudocode sketch
enum State { IDLE, OPEN, SELECTED, ACQUIRE, REMOVE }
var state := State.IDLE
var pending_transitions : Array[Tuple] = []
var current_transition_steps : Array[Tween] = []

Pros:

  • Full control over the multi-step, queued transition sequencing described in §4.2
  • Tween objects are fire-and-forget with a finished signal, making it straightforward to chain steps sequentially
  • No engine-version dependencies or animation graph learning curve

Cons:

  • No graphical state machine editor; transitions must be understood from code
  • More boilerplate than a declarative approach

Key Godot API fit:

  • create_tween() returns a Tween referenced by Godot 4.6 docs — tweens execute sequentially by default and support tween_property(), tween_callback(), tween_interval(), set_trans(), set_ease() — matching all the animation primitives used here
  • The step_finished signal on Tween provides per-step completion notifications, suitable for the FSM's step-by-step sequencing
  • bind_node(self) ensures tweens die when the backpack node is freed

Option B: AnimationTree with AnimationNodeStateMachine

Use an AnimationTree node with an AnimationNodeStateMachine as its tree_root, linked to the backpack's AnimationPlayer.

Pros:

  • Visual state machine editor in the Godot editor (tree_root property exposes this)
  • Godot provides AnimationNodeStateMachine as a first-class resource for AnimationTree
  • Transitions between states can use blend times

Cons:

  • AnimationTree is designed primarily for character/character-state animations, not UI state — it may be over-engineered here
  • Multi-step transitions with conditional logic (e.g., "same item vs different item" removal branches) are awkward to encode purely in the animation graph
  • The AnimationTree docs explicitly note that when using AnimationTree, several AnimationPlayer properties and methods "will not function as expected" — playback should be handled solely through the tree, which adds complexity
  • The queued transition FIFO mechanism is not native to AnimationNodeStateMachine; would need to be hand-rolled anyway

Verdict: Option A is preferred for this particular FSM because the transition logic has conditional branching and queuing that maps more naturally to code than to an animation graph.

13.3. Tween Usage — Transition Steps

Each multi-step FSM transition (§4.3) can be built as a single Tween with sequentially chained tween_property() calls and tween_callback() for state changes:

# Pseudocode: Idle → Open transition
func transition_to_open():
    var t = create_tween().bind_node(self)
    t.tween_property(backpack_sprite, "rotation", deg_to_rad(-90), 0.35)
       .set_trans(Tween.TRANS_LINEAR)
    t.tween_property(backpack_sprite, "position:y", open_y, 0.35)
       .set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
    t.tween_property(backpack_sprite, "position:x", open_x, 0.35)
       .set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
    # Play the open sprite animation, then continue...
    t.tween_callback(self._on_open_animation_done)
    # ... subsequent steps added by the callback

Caveat from docs: Tween objects "are not designed to be reused" — each transition needs a fresh create_tween() call. Also: "tweens start immediately, so only create a Tween when you want to start animating." This means the multi-step approach needs to build the tween incrementally via callbacks, rather than pre-building the entire sequence.

An alternative for fully deterministic sequences is to use AnimationPlayer with tween_callback()-free tracks and await on the animation_finished signal, but this introduces the AnimationTree compatibility issues mentioned above.

13.4. Inventory Overlay — Control Hierarchy

The inventory overlay (§5) maps cleanly to Godot's Control-based UI system:

  • The overlay is a full-screen Control with modulate.a (opacity) tweened for fade-in/fade-out
  • The "input only when fully visible" guard (§5.4) is implemented by checking modulate.a >= 1.0 in _gui_input() before processing events
  • mouse_filter can be set to MOUSE_FILTER_STOP on the overlay's background ColorRect to block input from reaching the game world beneath — confirmed by the Control docs: accept_event() stops propagation, and mouse_filter with MOUSE_FILTER_STOP prevents child nodes behind the control from receiving input

Item Grid Layout

Two approaches for laying out items:

  1. GridContainer — Godot's built-in Container with configurable columns. Pros: automatic layout, respects custom_minimum_size, works with the anchor/offset system. Cons: items have to be pre-instantiated or managed dynamically; less control over exact per-cell positioning during drag.

  2. Custom Control with manual positioning — A bare Control that computes cell positions in _process() or _notification(NOTIFICATION_DRAW). Pros: full control over drag behavior, hover hit-testing, and real-time position updates. Cons: more code, must handle layout manually.

Recommendation: GridContainer for static layout, with items switched to set_anchors_preset(PRESET_WIDE) or reparented during drag to allow free movement. Alternatively, a Control subclass that computes grid positions in _get_minimum_size() and _layout_children() (Godot 4.3+ pattern for Container-like behavior without the full Container overhead).

Drag and Drop

Godot has a built-in drag-and-drop system via _get_drag_data(), _can_drop_data(), and _drop_data() on Control nodes. However, the inventory's drag behavior (§5.4) has custom requirements:

  • Items must follow the mouse cursor in real time (Godot's drag preview is a separate Control returned by _get_drag_data(), which could work, but the PRD requires the actual item sprite to move)
  • Drag outside bounds must close the inventory immediately
  • Hover state must be tracked under the dragged item for combination hints

Verdict: Godot's built-in drag-and-drop is a partial fit, but the real-time follow-cursor and drag-out-close behaviors are more naturally handled through raw _gui_input() with InputEventMouseButton and InputEventMouseMotion tracking. The drag preview mechanism would require a custom preview Control that follows global mouse coordinates, and the hover-under-drag item tracking would need manual bounding-box checks.

13.5. Cursor System — Custom Cursor

Godot supports custom cursors through Input.set_custom_mouse_cursor(texture) and per-control cursors via Control.mouse_default_cursor_shape. For item-specific cursors:

  • Godot's Input.create_custom_cursor(texture, shape, hot_spot) creates a named cursor variant. Each item would register its cursor with a unique name.
  • Switching cursors: Input.set_custom_mouse_cursor(cursor_name)
  • Hotspot offsets map to the hot_spot parameter of create_custom_cursor() — documented as a Vector2i offset from the top-left of the texture
  • The depressed (mouse-down) cursor state would need two cursor textures per item (normal and pressed), or the sprite could be shifted by temporarily adding a vertical offset to the custom cursor's shape

Throttling (§8.1): Godot doesn't have a built-in cursor-update throttle. This would be implemented as a simple timestamp check in _process() comparing against the last cursor switch time.

Alternative: For games with many unique cursor items, some Godot projects skip the OS-level cursor entirely, hide it with Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN), and draw a custom cursor as a Sprite2D in the game world that follows the mouse position. This gives full control over appearance, hotspot, and depressed-state offset without the overhead of creating many custom cursor resources.

13.6. Global State — Inventory Manager

The player inventory state (§2) needs a globally accessible, persistent data store. Two Godot patterns:

A Node exported as an AutoLoad singleton (InventoryManager) holds the inventory list, obtained-items set, and stripped-items vault. Signals are emitted for state changes:

# Pseudocode
extends Node
signal item_acquired(item_id)
signal item_removed(item_id)
signal inventory_changed

var inventory : Array = []
var obtained_items : Array = []      # Used as a set
var stripped_items : Array = []

func has_item(item_id) -> bool: ...
func acquire_item(item_id): ...
func remove_item(item_id): ...

The HUD backpack, inventory overlay, and main scene all connect to this singleton's signals. This avoids tight coupling between the UI layers and the game world.

Autoload + Resource

For save-game persistence, the inventory data could be wrapped in a Resource (custom InventoryResource extending Resource) that the singleton references and saves/loads. This is a common Godot pattern for serializable game state.

13.7. Overlay Fade Layer

The full-screen fade overlay (§5.3) and guard condition fade layer (§9) can share a single CanvasLayer with a ColorRect:

FadeOverlay (CanvasLayer, layer=10)
└── ColorRect                       # color = (0,0,0,0) by default

Tweening ColorRect.modulate.a handles fade-in/out. The CanvasLayer ensures it renders above both the game world and HUD regardless of z-index. Guard conditions check ColorRect.modulate.a > 0.0 to block input.

13.8. Signal Flow Summary

The communication between components would rely on Godot's signal system:

InventoryManager (singleton)
  ├─ signal item_acquired          → InventoryBackpack._on_give_item()
  ├─ signal item_removed           → InventoryBackpack._on_remove_item()
  └─ signal inventory_changed      → InventoryOverlay._refresh_grid()

InventoryBackpack (FSM)
  ├─ signal transition_to_open     → InventoryOverlay._show()
  ├─ signal item_selected(item)    → MainScene._on_item_chose(item)
  └─ signal returning_to_idle      → InventoryOverlay._hide()

InventoryOverlay
  ├─ signal close_requested        → InventoryBackpack.transition_to(State.IDLE)
  ├─ signal combine(items)         → InventoryManager.attempt_combine(items)
  └─ signal inspect(item)          → MainScene.play_ego_script(item)

MainScene
  ├─ signal backpack_clicked       → InventoryBackpack.transition_to(State.OPEN) (guarded)
  ├─ signal world_entity_clicked   → InventoryManager.use_item_on_entity(item, entity)
  └─ script_finished              → Game resumes normal input

13.9. Godot-Specific Considerations and Risks

Concern Detail
Tween ownership Godot 4 Tween objects are not reusable. Each FSM transition must create a new Tween. If a transition is interrupted, the old tween should be kill()-ed before starting a new one.
Control vs Node2D mixing The game world (Node2D) and UI (Control) live in different rendering spaces. Converting between them (e.g., for screen-position calculations) requires get_global_transform_with_canvas() or get_viewport().get_mouse_position().
Input event consumption The overlay's background ColorRect must use accept_event() in _gui_input() to prevent clicks from reaching the game world behind it. Godot propagates unhandled events to _unhandled_input() on all nodes, so careful ordering is needed.
Anchor-based UI positioning The backpack HUD icon should use Control anchors for screen-relative positioning (top-right anchor preset), so it stays correctly positioned across screen resolutions.
AnimationPlayer queue Godot 4's AnimationPlayer.queue() allows queuing animations, but the queue clears on play(). This differs from the PRD's FIFO transition queue, which is why the hand-rolled FSM (Option A) is preferred.
Sprite flipping The Sprite2D.flip_h property handles horizontal flipping (needed for backpack animations per §11.1).
Custom minimum size Item cell sizing uses Control.custom_minimum_size to enforce grid cell dimensions, regardless of texture size.

13.10. Input Priority — Layer Suppression Patterns

When the inventory overlay is open, it must take exclusive input priority. The game world, HUD elements (other than the overlay itself), and any background interaction must be suppressed. This section brainstorms common patterns for managing input priority across layered 2D game UI, and how they map to this system.

Pattern 1: Control Hierarchy Input Consumption

How it works: The overlay sits at the top of a Control tree. Its background ColorRect has mouse_filter = MOUSE_FILTER_STOP. Events are consumed in _gui_input() via accept_event(), so they never propagate downward.

Godot mechanics: Per the Control docs, _gui_input() "filters out unrelated input events, such as by checking z-order, mouse_filter, focus, or if the event was inside of the control's bounding box." Calling accept_event() marks the event as handled. Unhandled events reach _unhandled_input() on all nodes.

Pros:

  • Simple — a full-screen MOUSE_FILTER_STOP background automatically blocks all mouse input from reaching controls beneath
  • No extra logic needed; the engine's event propagation handles it

Cons:

  • Only blocks Control-tree input. The game world (Node2D layer) receives input through a separate path: _input() or _unhandled_input(). A Control node consuming an event does NOT prevent _unhandled_input() from firing on Node2D children, because _gui_input()accept_event() → unhandled is a Control-only pipeline
  • Node2D input handlers (_input_event() on Node2D via viewport forwarding) operate independently

Verdict: Necessary but insufficient on its own. This pattern handles Control-vs-Control blocking (overlay blocks HUD), but does NOT handle Control-vs-Node2D blocking (overlay blocks game world).

Pattern 2: Guard Flag (Input Owner)

How it works: A boolean flag (e.g., input_layer_active or overlay_grabs_focus) is set when the overlay opens. Both the overlay and the game world check this flag before processing input.

# Pseudocode
# In InventoryOverlay
func _ready():
    GameInput.input_owner = Self   # Claim input

func _on_close():
    GameInput.input_owner = null   # Release input

# In MainScene (game world)
func _input(event):
    if GameInput.input_owner != null:
        return   # Another layer owns input
    # ... process clicks on entities

Pros:

  • Explicit, easy to reason about
  • Works across any node types (Node2D, Control, mixed)
  • Single source of truth for "who gets input right now"

Cons:

  • Every input handler must check the flag — easy to forget, leading to input leaking through
  • No engine-level enforcement — it's a convention, not a guarantee
  • If multiple layers use the flag, race conditions can occur during transitions (e.g., overlay closing and game world reclaiming input on the same frame)

Variant — Central input router: Instead of a boolean flag, route all input through a single _unhandled_input() handler at the root that dispatches based on a priority stack. This centralizes the guard logic and eliminates the "every layer must check" problem.

Verdict: The guard flag approach is the simplest and most common pattern in small-to-medium Godot projects. Paired with Pattern 1, it covers both Control and Node2D layers.

Pattern 3: CanvasLayer + Viewport Input Forwarding

How it works: Use a CanvasLayer set to a high layer value for the overlay, and accept_event() on its controls. The CanvasLayer renders above the game world. Controls in higher CanvasLayer values receive _gui_input() before lower layers.

Godot mechanics: CanvasLayer controls are composited on top and receive GUI input in layer order. However, Node2D input events still flow through _input_event() independently.

Pros:

  • Layer ordering is explicit and editor-visible
  • Godot's GUI input pipeline already respects CanvasLayer ordering

Cons:

  • Same fundamental limitation as Pattern 1: Node2D input is not suppressed by Control CanvasLayer hierarchy
  • CanvasLayer input forwarding is per-layer, not a global gate

Verdict: Useful for organizing Control-layer input priority, but doesn't solve the Node2D crossover problem. Works alongside Pattern 2.

Pattern 4: Engine-Level Input Mode Switch

How it works: When the overlay opens, call Input.set_mouse_mode(Input.MOUSE_MODE_CONFINED) or Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) to change how the engine handles mouse input.

Pros:

  • Engine-enforced — no per-layer checks needed

Cons:

  • MOUSE_MODE_CONFINED and MOUSE_MODE_CAPTURED are designed for FPS-style camera control, not for UI layer gating
  • MOUSE_MODE_CONFINED keeps the cursor within the viewport bounds, which would interfere with the "drag item outside panel bounds to close" behavior (AC-4) — the cursor can't leave the window
  • MOUSE_MODE_CAPTURED hides the cursor entirely
  • Neither mode provides layer-selective suppression; it's all-or-nothing

Verdict: Not suitable for this use case. The "drag outside panel bounds" interaction explicitly requires the cursor to be able to leave the inventory area, which these modes prevent.

Pattern 5: _unhandled_input() Gate at Scene Root

How it works: The main scene has a top-level _unhandled_input() handler that checks the overlay's visibility state. If the overlay is active, the handler returns early before any game-world _input_event() handlers are reached.

# Pseudocode in MainScene
func _unhandled_input(event):
    if inventory_overlay.is_visible():
        return   # Overlay owns input; game world does nothing
    if is_instance_valid(clicked_entity):
        clicked_entity.interact(event)

Pros:

  • Single gate point — only one place to maintain
  • _unhandled_input() fires for events that were NOT consumed by _gui_input(), so it naturally catches the "Control consumed this but Node2D shouldn't" case
  • Combines well with Pattern 1: overlay's accept_event() prevents the event from reaching _unhandled_input(), and the gate provides a safety net for edge cases

Cons:

  • Requires the game world to use _unhandled_input() instead of _input() — this is actually the recommended Godot pattern for gameplay input anyway
  • Some events may reach Node2D children via _input_event() (the viewport's direct forwarding to foreground controls), bypassing _unhandled_input()

Verdict: Strong approach for Godot. The game world should ideally use _unhandled_input() for all gameplay input (Godot's docs recommend this pattern), which naturally gives Control UI priority. Combined with the overlay's background MOUSE_FILTER_STOP, input from both sources is suppressed.

No single pattern covers all cases. The robust solution combines multiple layers of defense:

Layer Mechanism What it blocks
Overlay background ColorRect mouse_filter = MOUSE_FILTER_STOP + accept_event() in _gui_input() Other Control input (HUD buttons, backpack icon)
Game world input Uses _unhandled_input() instead of _input() Already filtered by the overlay's accept_event() — unhandled events only reach game world if no Control consumed them
Explicit guard flag inventory_active boolean on the overlay, checked by game world Safety net: game world double-checks that overlay is closed before processing any input, including _input_event() from viewport forwarding
CanvasLayer ordering Overlay CanvasLayer at layer 10, HUD at layer 5, game world at layer 0 Ensures visual and input priority ordering

Transition edge case: During the overlay fade-in/fade-out (§AC-12), the overlay is rendering but shouldn't accept input. The guard flag handles this:

func _on_fade_in_complete():
    overlay.input_active = true    # Only after fade finishes

func _on_fade_out_start():
    overlay.input_active = false   # Immediately when fade begins

This prevents the common bug where the overlay is half-visible, the user clicks, and input goes to neither the overlay nor the game world (the "input black hole" problem).

Summary: What to Watch Out For

  1. Node2D vs Control input gap: Control.accept_event() does NOT suppress Node2D._input_event(). This is the most common pitfall. Always pair Control-level blocking with a game-world-side check.
  2. Fade transition gap: Input priority must switch exactly at the opacity threshold, not at visibility. The overlay should block input the moment fading begins and release it the moment fading completes (or vice versa, depending on the desired user experience).
  3. Drag-out escape: The "drag item outside panel bounds" interaction intentionally sends input past the overlay. This is a controlled exception to the blocking rule — the overlay's _gui_input() must detect the drag-out condition and choose to not accept_event(), allowing the event to fall through to the HUD/game world level. This is the only case where the overlay lets input pass.