- ItemDefinition.gd: Resource class with id, name, combination_category - ItemDefinition.gd.uid: UID cache file - InventoryManager.gd.uid: UID cache file (script was committed, UID was not) - inventory-prd.md: Original product requirements document (791 lines) - docs/plans/2026-04-26-001-feat-inventory-backpack-system-plan.md: Implementation plan with 6 units and 5 user-directed edits
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:
- Animate backpack rotation to open position (0.35s).
- Animate backpack position to open location (0.35s).
- Play backpack open sprite animation to completion.
- Lock backpack to final open frame.
- 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:
- Animate backpack rotation back to rest (0.35s).
- Animate backpack position back to rest (0.35s).
- Play backpack closing sprite animation to completion.
- Return backpack to default sprite.
- Clear any selected item.
Open → Selected — Select Item
Trigger: Player confirms an item selection from the inventory overlay.
Steps:
- 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:
- Fade out the item sprite; move it toward the backpack (0.5s).
- Animate backpack rotation and position back to rest, play closing animation (0.35s).
- 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:
- Fade out the currently selected item in place.
- Fade in the new item in place.
- Animate the new item sliding into the backpack and fading out.
- 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:
- Forward lookup: Check whether the first item has a defined interaction with the second item.
- 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.
- 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:
- Execute the combination action (typically removes consumed items, adds a result item, plays dialogue).
- 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:
- The HUD transitions to the Selected state — the item's sprite floats outside the backpack.
- The game cursor switches to the item's dedicated cursor sprite.
- 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:
- Play a pickup sound effect.
- Append the item to the current inventory list.
- Add the item to the obtained-items set.
- 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):
- Remove the item from the current inventory list.
- The item is not removed from the obtained-items set — acquisition history persists.
- 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:
- Items removable by the event are removed from the current inventory.
- Certain protected items may be exempt from removal.
- Stripped items are moved to a vault for potential later recovery.
- 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:
- No clickable entity: Run the item's self-description dialogue.
- 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.
- 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:
Option A: Hand-Rolled FSM in GDScript (Recommended for this system)
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
Tweenobjects are fire-and-forget with afinishedsignal, 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 aTweenreferenced by Godot 4.6 docs — tweens execute sequentially by default and supporttween_property(),tween_callback(),tween_interval(),set_trans(),set_ease()— matching all the animation primitives used here- The
step_finishedsignal onTweenprovides 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_rootproperty exposes this) - Godot provides
AnimationNodeStateMachineas a first-class resource forAnimationTree - Transitions between states can use blend times
Cons:
AnimationTreeis 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
AnimationTreedocs explicitly note that when usingAnimationTree, severalAnimationPlayerproperties 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
Controlwithmodulate.a(opacity) tweened for fade-in/fade-out - The "input only when fully visible" guard (§5.4) is implemented by checking
modulate.a >= 1.0in_gui_input()before processing events mouse_filtercan be set toMOUSE_FILTER_STOPon the overlay's backgroundColorRectto block input from reaching the game world beneath — confirmed by theControldocs:accept_event()stops propagation, andmouse_filterwithMOUSE_FILTER_STOPprevents child nodes behind the control from receiving input
Item Grid Layout
Two approaches for laying out items:
-
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. -
Custom Control with manual positioning — A bare
Controlthat 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
Controlreturned 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_spotparameter ofcreate_custom_cursor()— documented as aVector2ioffset 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:
AutoLoad Singleton (Recommended)
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_STOPbackground 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 (Node2Dlayer) receives input through a separate path:_input()or_unhandled_input(). AControlnode consuming an event does NOT prevent_unhandled_input()from firing onNode2Dchildren, because_gui_input()→accept_event()→ unhandled is aControl-only pipeline Node2Dinput handlers (_input_event()onNode2Dvia 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
CanvasLayerordering
Cons:
- Same fundamental limitation as Pattern 1:
Node2Dinput is not suppressed by ControlCanvasLayerhierarchy CanvasLayerinput 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_CONFINEDandMOUSE_MODE_CAPTUREDare designed for FPS-style camera control, not for UI layer gatingMOUSE_MODE_CONFINEDkeeps 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 windowMOUSE_MODE_CAPTUREDhides 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.
Recommended Combined Approach for This System
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
- Node2D vs Control input gap:
Control.accept_event()does NOT suppressNode2D._input_event(). This is the most common pitfall. Always pair Control-level blocking with a game-world-side check. - 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).
- 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 notaccept_event(), allowing the event to fall through to the HUD/game world level. This is the only case where the overlay lets input pass.