Reviewed-on: #1 Co-authored-by: Bryce <bryce@brycecovertoperations.com> Co-committed-by: Bryce <bryce@brycecovertoperations.com>
791 lines
44 KiB
Markdown
791 lines
44 KiB
Markdown
# 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:
|
|
|
|
#### 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
|
|
- `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:
|
|
|
|
#### 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_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.
|
|
|
|
#### 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
|
|
|
|
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. |