Compare commits
1 Commits
sugary-pan
...
better-mcp
| Author | SHA1 | Date | |
|---|---|---|---|
| a0afc9bd1f |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "kq4-sierra-decompile"]
|
||||
path = kq4-sierra-decompile
|
||||
url = ssh://raspberrypi/~/git/sierra-decompile
|
||||
[submodule "godot-mcp"]
|
||||
path = godot-mcp
|
||||
url = https://github.com/tugcantopaloglu/godot-mcp
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
## Goal
|
||||
- Implement inventory and backpack system in Godot 4.6 point-and-click adventure game per plan at `docs/plans/2026-04-26-001-feat-inventory-backpack-system-plan.md`
|
||||
|
||||
## Constraints & Preferences
|
||||
- Interaction handlers are signals emitted by items, not embedded dictionary data
|
||||
- No multi-step transition queues — FSM transitions interrupt in-flight animations immediately
|
||||
- No sound required
|
||||
- Item cursor as 5th `ActionState` slot (`ITEM`, value 4); right-click cycles WALK→LOOK→TOUCH→TALK→ITEM→WALK
|
||||
- Use colored `ColorRect` boxes as placeholders for all item/backpack visuals
|
||||
- Follow PRD §13 recommended Godot architecture approaches
|
||||
- PRD is engine-agnostic but implementation targets Godot 4.6
|
||||
|
||||
## Progress
|
||||
### Done
|
||||
- Read and analyzed `inventory-prd.md` (791 lines)
|
||||
- Researched existing codebase patterns
|
||||
- Wrote complete plan to `docs/plans/2026-04-26-001-feat-inventory-backpack-system-plan.md` with 6 implementation units
|
||||
- Applied 5 user edits to plan: signal-based interactions, no transition queues, no sound, ActionState ITEM slot, colored boxes
|
||||
- Created directory structure: `inventory/inventory_backpack/`, `inventory/inventory_overlay/`
|
||||
- Generated 12 UIDs for new files
|
||||
- U1 complete: `inventory/ItemDefinition.gd` written (Resource with id, name, combination_category)
|
||||
- U2 complete: `inventory/InventoryManager.gd` written (AutoLoad singleton with signals, queries, mutations, combination emission, selection)
|
||||
- U3 complete: `inventory/inventory_backpack/InventoryBackpack.gd` + `.tscn` (FSM with IDLE/OPEN/SELECTED/ACQUIRE/REMOVE, Tween animations, guard checks)
|
||||
- U4 complete: `inventory/inventory_overlay/InventoryOverlay.gd` + `.tscn` + `InventorySlot.gd` + `.tscn` (full-screen overlay, grid, drag-and-drop, hover, combination)
|
||||
- U5 complete: Modified `ActionState.gd` (ITEM=4), `MainGame.gd` (5-action cycling), `Scene.gd` (overlay guard, ITEM input), `GameScript.gd` (GiveItem step)
|
||||
- U6 complete: Modified `Scene.gd` (give_item/remove_item/strip_items helpers), `SetPiece_.gd` (ITEM action handling)
|
||||
- InventoryManager registered as AutoLoad in `project.godot`
|
||||
- All commits pushed to remote
|
||||
|
||||
### In Progress
|
||||
- (none)
|
||||
|
||||
### Blocked
|
||||
- (none)
|
||||
|
||||
## Key Decisions
|
||||
- **Signal-based interactions**: Items emit signals; `InventoryManager.attempt_combine` emits `combination_attempted`; scene scripts handle results
|
||||
- **No transition queues**: FSM transitions kill in-flight tweens and start fresh; `is_busy` flag prevents concurrency but does not queue
|
||||
- **ActionState ITEM slot**: `ITEM = 4` added to enum; right-click cycles all 5 states; selected item stored in `InventoryManager.selected_item`
|
||||
- **Colored boxes**: `ColorRect` placeholders replace all `Sprite2D` references
|
||||
- **Hand-rolled FSM (PRD Option A)** over AnimationTree: conditional branching maps better to code
|
||||
- **AutoLoad singleton** for InventoryManager following ActionState pattern
|
||||
- **Raw `_gui_input()`** for overlay drag-and-drop instead of Godot's built-in system
|
||||
- **Combined input priority defense**: `MOUSE_FILTER_STOP` background + `_unhandled_input()` gate + explicit `overlay_active` guard flag
|
||||
- **ItemDefinition as Resource** for serializable `.tres` item data
|
||||
- **GridContainer** for static item grid layout
|
||||
|
||||
## Next Steps
|
||||
- Test all units in Godot editor
|
||||
- Register sample item definitions (`.tres` files)
|
||||
- Wire up signal connections in `Game.tscn` (backpack ↔ overlay ↔ manager)
|
||||
- Create example room script demonstrating `give_item()` usage
|
||||
- Replace colored box placeholders with actual item sprites
|
||||
|
||||
## Critical Context
|
||||
- Godot 4.6 project, 1920x1080 viewport
|
||||
- Branch: `sugary-panda` (worktree), default branch: `master`
|
||||
- Existing autoloads: `ActionState`, `CameraTransition`, `InventoryManager` (new)
|
||||
- Root scene `Game.tscn` uses `SubViewport` for game world, `CanvasLayer` for overlays
|
||||
- `ActionState.gd` enum: `WALK=0, LOOK=1, TOUCH=2, TALK=3, ITEM=4`
|
||||
- `MainGame.gd` has `cursors` array with 4 cursor textures — 5th slot needed for ITEM (currently skips if no item selected)
|
||||
- All code committed and pushed to `origin/sugary-panda`
|
||||
|
||||
## Relevant Files
|
||||
- `inventory-prd.md`: Origin requirements document (791 lines, 14 acceptance criteria)
|
||||
- `docs/plans/2026-04-26-001-feat-inventory-backpack-system-plan.md`: Implementation plan (6 units, edited with 5 user changes)
|
||||
- `inventory/ItemDefinition.gd`: Custom Resource class (DONE)
|
||||
- `inventory/ItemDefinition.gd.uid`: UID file for ItemDefinition (DONE)
|
||||
- `inventory/InventoryManager.gd`: AutoLoad singleton with signals, queries, mutations (DONE)
|
||||
- `inventory/InventoryManager.gd.uid`: UID file for InventoryManager (DONE)
|
||||
- `inventory/inventory_backpack/InventoryBackpack.gd`: FSM script (DONE)
|
||||
- `inventory/inventory_backpack/InventoryBackpack.gd.uid`: UID file (DONE)
|
||||
- `inventory/inventory_backpack/InventoryBackpack.tscn`: HUD backpack scene (DONE)
|
||||
- `inventory/inventory_backpack/InventoryBackpack.tscn.uid`: UID file (DONE)
|
||||
- `inventory/inventory_overlay/InventoryOverlay.gd`: Overlay interaction script (DONE)
|
||||
- `inventory/inventory_overlay/InventoryOverlay.gd.uid`: UID file (DONE)
|
||||
- `inventory/inventory_overlay/InventoryOverlay.tscn`: Full-screen overlay scene (DONE)
|
||||
- `inventory/inventory_overlay/InventoryOverlay.tscn.uid`: UID file (DONE)
|
||||
- `inventory/inventory_overlay/InventorySlot.gd`: Slot behavior script (DONE)
|
||||
- `inventory/inventory_overlay/InventorySlot.gd.uid`: UID file (DONE)
|
||||
- `inventory/inventory_overlay/InventorySlot.tscn`: Single item slot scene (DONE)
|
||||
- `inventory/inventory_overlay/InventorySlot.tscn.uid`: UID file (DONE)
|
||||
- `MainGame.gd`: Modified for 5-action cursor cycling with ITEM
|
||||
- `Scene.gd`: Modified for overlay guard, ITEM action, helper methods
|
||||
- `ActionState.gd`: Modified to add ITEM action
|
||||
- `GameScript.gd`: Modified to add GiveItem/GiveItemDeferred steps
|
||||
- `SetPiece_.gd`: Modified to handle ITEM action
|
||||
- `project.godot`: InventoryManager AutoLoad registered
|
||||
@@ -1,6 +1,6 @@
|
||||
extends Node
|
||||
|
||||
enum Action { WALK = 0, LOOK = 1, TOUCH = 2, TALK = 3, ITEM = 4 }
|
||||
enum Action { WALK = 0, LOOK = 1, TOUCH = 2, TALK = 3 }
|
||||
|
||||
var current_action: int = Action.WALK:
|
||||
set(value):
|
||||
@@ -22,7 +22,6 @@ func get_action_name() -> String:
|
||||
Action.LOOK: return "look"
|
||||
Action.TOUCH: return "touch"
|
||||
Action.TALK: return "talk"
|
||||
Action.ITEM: return "item"
|
||||
return "walk"
|
||||
|
||||
func get_action_enum() -> int:
|
||||
|
||||
22
Game.tscn
22
Game.tscn
@@ -10,8 +10,6 @@
|
||||
[ext_resource type="PackedScene" uid="uid://c0mp4a2u3jkd" path="res://portrait.tscn" id="7_fj12q"]
|
||||
[ext_resource type="Script" uid="uid://bsvab128vy1ip" path="res://OffsetCameraBasedOnMovement.gd" id="8_cvftx"]
|
||||
[ext_resource type="PackedScene" uid="uid://dyk4rcqsk3aed" path="res://scenes/kq4_003_fountain_pool/kq4_003_fountain_pool.tscn" id="8_yx171"]
|
||||
[ext_resource type="PackedScene" uid="uid://dxkyfas46q7ef" path="res://inventory/inventory_backpack/InventoryBackpack.tscn" id="9_backpack"]
|
||||
[ext_resource type="PackedScene" uid="uid://djoycn4xfa8p3" path="res://inventory/inventory_overlay/InventoryOverlay.tscn" id="a_overlay"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_44mjr"]
|
||||
shader = ExtResource("2_jr51a")
|
||||
@@ -185,23 +183,3 @@ label_settings = SubResource("LabelSettings_narrator")
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="HUD" type="CanvasLayer" parent="." unique_id=-294967295]
|
||||
layer = 5
|
||||
|
||||
[node name="InventoryBackpack" parent="HUD" unique_id=-294967294 instance=ExtResource("9_backpack")]
|
||||
anchors_preset = 0
|
||||
anchor_top = 0.0
|
||||
anchor_bottom = 0.0
|
||||
offset_top = 0.0
|
||||
offset_bottom = 70.0
|
||||
grow_vertical = 1
|
||||
|
||||
[node name="InventoryOverlayLayer" type="CanvasLayer" parent="." unique_id=-294967293]
|
||||
layer = 10
|
||||
|
||||
[node name="InventoryOverlay" parent="InventoryOverlayLayer" unique_id=-294967292 instance=ExtResource("a_overlay")]
|
||||
|
||||
[connection signal="overlay_hide_requested" from="HUD/InventoryBackpack" to="." method="_on_backpack_hide_overlay"]
|
||||
[connection signal="overlay_show_requested" from="HUD/InventoryBackpack" to="." method="_on_backpack_show_overlay"]
|
||||
[connection signal="item_confirmed" from="InventoryOverlayLayer/InventoryOverlay" to="." method="_on_overlay_item_confirmed"]
|
||||
|
||||
@@ -476,54 +476,5 @@ func switch_camera(path):
|
||||
func reset_camera():
|
||||
var thing = ResetCamera.new()
|
||||
return thing
|
||||
|
||||
class GiveItem:
|
||||
extends ScriptNode
|
||||
var item_id: String = ""
|
||||
var text: String = ""
|
||||
var subject: Node2D = null
|
||||
var done = false
|
||||
var started = false
|
||||
|
||||
func step_type():
|
||||
return "GiveItem " + item_id
|
||||
|
||||
func init(scene):
|
||||
super(scene)
|
||||
|
||||
func do(delta):
|
||||
if not started:
|
||||
started = true
|
||||
if text:
|
||||
await scene.find_child("dialogue").say(text)
|
||||
InventoryManager.acquire_item(item_id)
|
||||
done = true
|
||||
|
||||
func is_done():
|
||||
return done
|
||||
|
||||
func interrupt():
|
||||
done = true
|
||||
|
||||
class GiveItemDeferred:
|
||||
extends GiveItem
|
||||
var subject_name: String = ""
|
||||
|
||||
func init(scene):
|
||||
super(scene)
|
||||
subject = scene.get_node(subject_name)
|
||||
|
||||
func give_item(item_id: String, text: String = ""):
|
||||
var step = GiveItem.new()
|
||||
step.item_id = item_id
|
||||
step.text = text
|
||||
return step
|
||||
|
||||
func give_item_deferred(item_id: String, subject_name: String, text: String = ""):
|
||||
var step = GiveItemDeferred.new()
|
||||
step.item_id = item_id
|
||||
step.subject_name = subject_name
|
||||
step.text = text
|
||||
return step
|
||||
|
||||
var current_script : ScriptGraph = null
|
||||
|
||||
41
MainGame.gd
41
MainGame.gd
@@ -3,7 +3,6 @@ extends Node2D
|
||||
|
||||
var cursors = [load("res://boot_icon.png"), load("res://eye_icon.png"), load("res://hand_icon.png"), load("res://speech_icon.png")]
|
||||
var hourglass_cursor = load("res://hourglass_icon.png")
|
||||
var item_cursor: Texture2D = null
|
||||
var previous_cursor_index: int = 0
|
||||
var is_script_running: bool = false
|
||||
var is_cursor_locked: bool = false # When true, hourglass is shown and cursor can't be changed
|
||||
@@ -70,46 +69,14 @@ func set_script_cursor() -> void:
|
||||
func restore_cursor() -> void:
|
||||
is_script_running = false
|
||||
is_cursor_locked = false # Unlock cursor
|
||||
if previous_cursor_index == ActionState.Action.ITEM:
|
||||
if item_cursor:
|
||||
Input.set_custom_mouse_cursor(item_cursor)
|
||||
else:
|
||||
Input.set_custom_mouse_cursor(cursors[ActionState.Action.WALK])
|
||||
else:
|
||||
Input.set_custom_mouse_cursor(cursors[previous_cursor_index])
|
||||
|
||||
Input.set_custom_mouse_cursor(cursors[previous_cursor_index])
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
$SceneViewport.push_input(event)
|
||||
|
||||
func _on_backpack_show_overlay() -> void:
|
||||
$InventoryOverlayLayer/InventoryOverlay.show_overlay()
|
||||
|
||||
func _on_backpack_hide_overlay() -> void:
|
||||
$InventoryOverlayLayer/InventoryOverlay.hide_overlay()
|
||||
|
||||
func _on_overlay_item_confirmed(item_id: String) -> void:
|
||||
InventoryManager.select_item(item_id)
|
||||
var def = InventoryManager.get_item_definition(item_id)
|
||||
if def and def.icon:
|
||||
item_cursor = def.icon
|
||||
Input.set_custom_mouse_cursor(item_cursor)
|
||||
ActionState.current_action = ActionState.Action.ITEM
|
||||
else:
|
||||
Input.set_custom_mouse_cursor(cursors[ActionState.Action.WALK])
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_released("quit"):
|
||||
get_tree().quit()
|
||||
if event.is_action_released("right_click") and not is_cursor_locked:
|
||||
var prev_action = ActionState.current_action
|
||||
ActionState.current_action = (ActionState.current_action + 1) % 5
|
||||
if ActionState.current_action == ActionState.Action.ITEM:
|
||||
if InventoryManager.selected_item and item_cursor:
|
||||
Input.set_custom_mouse_cursor(item_cursor, Input.CursorShape.CURSOR_ARROW, Vector2(0,0))
|
||||
else:
|
||||
ActionState.current_action = (ActionState.current_action + 1) % 5
|
||||
Input.set_custom_mouse_cursor(cursors[ActionState.current_action], Input.CursorShape.CURSOR_ARROW, Vector2(0,0))
|
||||
else:
|
||||
if prev_action == ActionState.Action.ITEM:
|
||||
InventoryManager.clear_selection()
|
||||
Input.set_custom_mouse_cursor(cursors[ActionState.current_action], Input.CursorShape.CURSOR_ARROW, Vector2(0,0))
|
||||
ActionState.current_action = (ActionState.current_action + 1) % 4
|
||||
Input.set_custom_mouse_cursor(cursors[ActionState.current_action], Input.CursorShape.CURSOR_ARROW, Vector2(0,0))
|
||||
|
||||
46
Scene.gd
46
Scene.gd
@@ -128,12 +128,7 @@ func _unhandled_input(event):
|
||||
if ScriptBuilder.current_script and not ScriptBuilder.current_script.can_interrupt:
|
||||
if ScriptBuilder.current_script.handle_input(event):
|
||||
return
|
||||
|
||||
# Block input when inventory overlay is active
|
||||
var overlay = get_node_or_null("/root/Node2D/InventoryOverlay")
|
||||
if overlay and overlay is InventoryOverlay and overlay.is_active():
|
||||
return
|
||||
|
||||
|
||||
var root = get_node("/root/Node2D")
|
||||
# If look cursor is active and we got here, no SetPiece handled the input
|
||||
# so this is a room-wide look
|
||||
@@ -143,46 +138,7 @@ func _unhandled_input(event):
|
||||
if ActionState.current_action == ActionState.Action.WALK:
|
||||
var path = NavigationServer2D.map_get_path(map, ego.position, pathfind.to_local(get_global_mouse_position()), true)
|
||||
start_main_script(ScriptBuilder.init(ScriptBuilder.walk_path(ego, path)).can_interrupt().build(self, "_on_script_complete"))
|
||||
if ActionState.current_action == ActionState.Action.ITEM:
|
||||
_on_item_use_in_world()
|
||||
|
||||
func _on_room_looked() -> void:
|
||||
# Default room look description - override in room scripts
|
||||
pass
|
||||
|
||||
func _on_item_use_in_world() -> void:
|
||||
if not InventoryManager.selected_item:
|
||||
return
|
||||
var item_id = InventoryManager.selected_item
|
||||
var top_piece = ActionState.get_top_hovered_setpiece()
|
||||
if top_piece:
|
||||
_use_item_on_setpiece(item_id, top_piece)
|
||||
else:
|
||||
_on_item_used_empty_space(item_id)
|
||||
|
||||
func _use_item_on_setpiece(item_id: String, piece: SetPiece) -> void:
|
||||
if piece.has_method("_on_item_used"):
|
||||
piece._on_item_used(item_id)
|
||||
else:
|
||||
var def = InventoryManager.get_item_definition(item_id)
|
||||
var item_name = item_id
|
||||
if def:
|
||||
item_name = def.name
|
||||
var piece_name = piece.label if piece.label else piece.name
|
||||
start_main_script(ScriptBuilder.init(ScriptBuilder.say(ego, "I can't use the %s on the %s." % [item_name, piece_name])).build(self, "_on_script_complete"))
|
||||
|
||||
func _on_item_used_empty_space(item_id: String) -> void:
|
||||
var def = InventoryManager.get_item_definition(item_id)
|
||||
var item_name = item_id
|
||||
if def:
|
||||
item_name = def.name
|
||||
start_main_script(ScriptBuilder.init(ScriptBuilder.say(ego, "There's nothing to use the %s on here." % [item_name])).build(self, "_on_script_complete"))
|
||||
|
||||
func give_item(item_id: String) -> void:
|
||||
InventoryManager.acquire_item(item_id)
|
||||
|
||||
func remove_item(item_id: String, quiet: bool = false) -> void:
|
||||
InventoryManager.remove_item(item_id, quiet)
|
||||
|
||||
func strip_items(event_items: Array[String], exempt_items: Array[String] = []) -> void:
|
||||
InventoryManager.bulk_strip_items(event_items, exempt_items)
|
||||
|
||||
@@ -75,9 +75,3 @@ func _input(event):
|
||||
ActionState.Action.TALK:
|
||||
if talked.get_connections().size() > 0:
|
||||
emit_signal("talked")
|
||||
ActionState.Action.ITEM:
|
||||
if InventoryManager.selected_item:
|
||||
var item_id = InventoryManager.selected_item
|
||||
var scene = get_node_or_null("/root/Node2D/SceneViewport/background")
|
||||
if scene and scene is Scene:
|
||||
scene._use_item_on_setpiece(item_id, self)
|
||||
|
||||
@@ -1,546 +0,0 @@
|
||||
---
|
||||
title: feat: Inventory and Backpack System
|
||||
type: feat
|
||||
status: active
|
||||
date: 2026-04-26
|
||||
origin: inventory-prd.md
|
||||
---
|
||||
|
||||
# Inventory and Backpack System
|
||||
|
||||
## Overview
|
||||
|
||||
Add a full inventory and backpack system to the point-and-click adventure game: a global inventory manager, an animated HUD backpack icon with a finite state machine, a full-screen inventory overlay for item viewing and combination, and integration with the existing cursor and scene systems.
|
||||
|
||||
---
|
||||
|
||||
## Problem Frame
|
||||
|
||||
The game currently has no concept of items, inventory, or item interaction. Players cannot pick up objects, combine items, or use items on entities in the game world. This system establishes the foundational item management infrastructure needed for adventure game gameplay.
|
||||
|
||||
---
|
||||
|
||||
## Requirements Trace
|
||||
|
||||
- R1. Current inventory is an ordered list preserving insertion order (AC-1)
|
||||
- R2. Obtained items set persists across removal (AC-1)
|
||||
- R3. Stripped items vault for forced removal recovery (AC-9)
|
||||
- R4. Item definitions include name, identifier, and optional combination category. Interaction handlers are signals emitted by items, not embedded data (PRD §2.2)
|
||||
- R5. Inventory queries: has_item, has_obtained, has_any_of, has_obtained_any_of, has_obtained_all_of (PRD §2.3)
|
||||
- R6. HUD Backpack FSM with Idle, Open, Selected, Acquire, Remove states. Multi-step transition queuing is deferred — transitions execute immediately and interrupt any in-flight animation (AC-2, PRD §4)
|
||||
- R7. Inventory overlay with grid layout, hover text, drag-and-drop, item selection, and combination (AC-3 through AC-6, AC-12, AC-13, PRD §5)
|
||||
- R8. Item acquisition animation and sound (AC-7, PRD §6)
|
||||
- R9. Item removal animation with quiet mode and bulk removal support (AC-8, AC-9, PRD §7)
|
||||
- R10. Cursor system integration with item-specific cursors (AC-10, AC-14, PRD §8)
|
||||
- R11. Guard conditions: foreground action, screen fade, game pause (AC-11, PRD §9)
|
||||
- R12. Game world item use: left-click entity interaction, right-click return (AC-10, PRD §10)
|
||||
|
||||
---
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
- Audio assets are not part of this system
|
||||
- Item sprite assets are out of scope — use colored `ColorRect` boxes as placeholders for all item/backpack visuals
|
||||
- Save/load persistence for inventory is deferred — the manager should be structured to support it, but serialization is not implemented
|
||||
- Item tooltips beyond name and combination hints are deferred
|
||||
- Multi-step transition queuing is deferred — transitions interrupt in-flight animations immediately
|
||||
|
||||
### Deferred to Follow-Up Work
|
||||
|
||||
- Inventory save/load serialization: separate PR once save system infrastructure exists
|
||||
- Item sprite asset creation: replace colored boxes with actual sprites in a separate art pass
|
||||
- Multi-step transition queuing: add FIFO queue for rapid transitions in a follow-up
|
||||
|
||||
---
|
||||
|
||||
## Context & Research
|
||||
|
||||
### Relevant Code and Patterns
|
||||
|
||||
- `ActionState.gd` — Existing autoload singleton tracking cursor action (WALK/LOOK/TOUCH/TALK). Pattern to follow for `InventoryManager` autoload.
|
||||
- `MainGame.gd` — Root scene (`Game.tscn`) that handles cursor switching via `Input.set_custom_mouse_cursor()`, scene transitions, and script-running cursor lock. The inventory system must integrate here for cursor management and guard conditions.
|
||||
- `GameScript.gd` — Script builder with `ScriptGraph` for cutscenes. The `Narrate` class shows the pattern for overlay fade-in/fade-out with `modulate.a` tweens, and the `is_script_running` / `is_cursor_locked` pattern in `MainGame.gd` is the guard condition model.
|
||||
- `Scene.gd` — Base room class. `_unhandled_input()` handles walk and look actions. Inventory input must be gated so overlay blocks game-world input.
|
||||
- `SetPiece_.gd` — Interactive polygon areas. The `_input()` pattern shows how `ActionState` and `GameScript.current_script` are checked before processing input.
|
||||
- `Game.tscn` — Root scene tree with `SubViewport` for game world, `CanvasLayer` for overlays (NarratorOverlay at layer 10), and `AnimationPlayer` for fades.
|
||||
- `label.gd` — Floating label that follows mouse position in game world coordinates.
|
||||
|
||||
### Institutional Learnings
|
||||
|
||||
- None yet — no `docs/solutions/` entries exist.
|
||||
|
||||
### External References
|
||||
|
||||
- PRD §13 provides detailed Godot 4 node architecture brainstorm, including FSM implementation options, drag-and-drop approach, cursor system, input priority patterns, and signal flow. The plan follows the PRD's recommended approaches throughout.
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
- **Hand-rolled FSM (PRD Option A)**: The backpack FSM uses a GDScript enum + Tween-based transitions, not `AnimationTree`. Transitions execute immediately and kill any in-flight animation — no FIFO queuing.
|
||||
- **AutoLoad singleton for InventoryManager**: Follows the `ActionState` pattern. Signals (`item_acquired`, `item_removed`, `inventory_changed`) decouple the manager from UI components.
|
||||
- **Signal-based interaction handlers**: Items emit signals for interactions rather than embedding interaction data in dictionaries. Room scenes connect to item signals to handle entity-specific behavior.
|
||||
- **Item cursor as ActionState slot 5**: Item selection sets `ActionState.current_action = Action.ITEM` (slot 5). Right-click cycles WALK→LOOK→TOUCH→TALK→ITEM→WALK. The selected item is stored in `InventoryManager.selected_item`. Scripts and interactions fire the same way as regular cursor actions — item use is handled through the existing interaction signal pattern.
|
||||
- **Raw `_gui_input()` for overlay drag-and-drop**: Godot's built-in drag-and-drop is a partial fit. Real-time cursor following, drag-out-close, and hover-under-drag tracking are more naturally handled with `InputEventMouseButton` and `InputEventMouseMotion` in `_gui_input()`.
|
||||
- **Combined input priority defense**: Overlay background `ColorRect` with `MOUSE_FILTER_STOP` + `accept_event()` blocks Control input. Game world uses `_unhandled_input()` which is already filtered by Control `accept_event()`. An explicit `overlay_active` guard flag provides a safety net for `Node2D._input_event()` bypass. (PRD §13.10)
|
||||
- **Colored boxes for placeholders**: All item sprites, backpack visuals, and cursor assets use colored `ColorRect` boxes instead of textures. Easy to replace with actual sprites later.
|
||||
- **GridContainer for item grid**: Static layout uses Godot's `GridContainer`. During drag, items are reparented or switch to `PRESERVE_WIDE` anchors for free movement.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Resolved During Planning
|
||||
|
||||
- **Should the inventory overlay live under MainGame or be a separate autoload?** Under `MainGame` as a child node in `Game.tscn`. It needs to interact with the scene's input and animation system, and keeping it in the scene tree simplifies lifecycle management.
|
||||
- **How does the backpack FSM know about guard conditions?** The FSM checks `GameScript.current_script` (foreground action), the Fade sprite's `modulate.a` (screen fade), and a pause flag on `MainGame`. If a guard blocks opening, clicking the backpack attempts to skip the running action.
|
||||
- **Where does item combination logic live?** In `InventoryManager` as `attempt_combine(item_a, item_b)`. The manager looks up interactions on both items (forward and reverse), evaluates dynamic rules, and emits `combination_succeeded` or `combination_refused` signals.
|
||||
|
||||
### Deferred to Implementation
|
||||
|
||||
- Exact animation frame counts for backpack open/close sprite sequences — depends on final sprite assets
|
||||
- Whether `ItemDefinition` interaction handlers use a `Dictionary` or a callable-based registry — either works; the implementer should choose based on what feels cleaner with GDScript's type system
|
||||
|
||||
---
|
||||
|
||||
## Output Structure
|
||||
|
||||
res://
|
||||
├── inventory/
|
||||
│ ├── InventoryManager.gd # AutoLoad singleton
|
||||
│ ├── InventoryManager.gd.uid
|
||||
│ ├── ItemDefinition.gd # Custom Resource class
|
||||
│ ├── ItemDefinition.gd.uid
|
||||
│ ├── inventory_backpack/
|
||||
│ │ ├── InventoryBackpack.tscn # HUD backpack FSM scene
|
||||
│ │ ├── InventoryBackpack.tscn.uid
|
||||
│ │ ├── InventoryBackpack.gd # FSM script
|
||||
│ │ └── InventoryBackpack.gd.uid
|
||||
│ └── inventory_overlay/
|
||||
│ ├── InventoryOverlay.tscn # Full-screen overlay scene
|
||||
│ ├── InventoryOverlay.tscn.uid
|
||||
│ ├── InventoryOverlay.gd # Overlay interaction script
|
||||
│ ├── InventoryOverlay.gd.uid
|
||||
│ ├── InventorySlot.tscn # Single item slot scene
|
||||
│ ├── InventorySlot.tscn.uid
|
||||
│ ├── InventorySlot.gd # Slot behavior script
|
||||
│ └── InventorySlot.gd.uid
|
||||
|
||||
---
|
||||
|
||||
## High-Level Technical Design
|
||||
|
||||
> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Game.tscn │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ SceneViewport (SubViewport) ││
|
||||
│ │ └── background (Scene) ││
|
||||
│ │ └── game world entities ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ HUD (CanvasLayer, layer=5) ││
|
||||
│ │ └── InventoryBackpack (FSM) ││
|
||||
│ │ ├── AnimationPlayer (backpack sprites) ││
|
||||
│ │ ├── ColorRect (backpack icon placeholder) ││
|
||||
│ │ └── FloatingItem (selected item placeholder) ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ InventoryOverlay (CanvasLayer, layer=10) ││
|
||||
│ │ ├── ColorRect (MOUSE_FILTER_STOP background) ││
|
||||
│ │ └── InventoryPanel ││
|
||||
│ │ ├── TextureRect (decorative frame) ││
|
||||
│ │ ├── GridContainer (item grid) ││
|
||||
│ │ │ └── InventorySlot × N (dynamic) ││
|
||||
│ │ └── HoverLabel ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ InventoryManager (AutoLoad singleton) │
|
||||
│ ├── inventory: Array[String] (ordered) │
|
||||
│ ├── obtained_items: Dictionary (set) │
|
||||
│ ├── stripped_items: Array[String] (ordered) │
|
||||
│ ├── selected_item: String (currently held item) │
|
||||
│ └── signals: item_acquired, item_removed, │
|
||||
│ inventory_changed, combination_attempted │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Signal Flow
|
||||
|
||||
```
|
||||
InventoryManager
|
||||
├─ item_acquired(item_id) → InventoryBackpack._on_item_acquired()
|
||||
├─ item_removed(item_id) → InventoryBackpack._on_item_removed()
|
||||
└─ inventory_changed → InventoryOverlay._refresh_grid()
|
||||
|
||||
InventoryBackpack (FSM)
|
||||
├─ overlay_show_requested → InventoryOverlay.show()
|
||||
├─ item_selected(item_def) → MainGame._on_item_selected()
|
||||
│ (set ActionState.ITEM, store selected item)
|
||||
└─ returning_to_idle → InventoryOverlay.hide()
|
||||
|
||||
InventoryOverlay
|
||||
├─ close_requested → InventoryBackpack.transition_to(IDLE)
|
||||
├─ combine_requested(a, b) → InventoryManager.attempt_combine(a, b)
|
||||
└─ inspect_requested(item) → Scene.play_ego_script(item)
|
||||
|
||||
ActionState
|
||||
├─ right-click cycles: WALK→LOOK→TOUCH→TALK→ITEM→WALK
|
||||
└─ ITEM action triggers same interaction flow as other cursors
|
||||
|
||||
MainGame / Scene
|
||||
├─ backpack_clicked → InventoryBackpack.transition_to(OPEN) (guarded)
|
||||
├─ world_entity_clicked (ITEM) → entity receives item interaction signal
|
||||
└─ right-click in world → cycles cursor (ITEM→WALK, returns item)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Units
|
||||
|
||||
- U1. **ItemDefinition Resource**
|
||||
|
||||
**Goal:** Define the data structure for inventory items as a Godot `Resource`.
|
||||
|
||||
**Requirements:** R4
|
||||
|
||||
**Dependencies:** None
|
||||
|
||||
**Files:**
|
||||
- Create: `inventory/ItemDefinition.gd`
|
||||
- Create: `inventory/ItemDefinition.gd.uid`
|
||||
|
||||
**Approach:**
|
||||
- Extend `Resource` with `class_name ItemDefinition`
|
||||
- Fields: `id` (String, unique key), `name` (String), `combination_category` (String, optional)
|
||||
- Signals emitted by items when interacted with: `item_used_on_entity(entity)`, `item_combined_with(other_item)`, `item_inspected()`. Room scenes connect to these signals to define entity-specific behavior.
|
||||
- No embedded interaction data — all interaction logic lives in the scene scripts that receive the signals.
|
||||
|
||||
**Patterns to follow:**
|
||||
- Godot `Resource` pattern for serializable data
|
||||
- Existing `PolygonPointsResource.gd` as a simple example of a custom resource
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: Create ItemDefinition with id, name, and combination_category; verify fields are set correctly
|
||||
- Edge case: Create ItemDefinition with no combination_category; verify it works as standalone item
|
||||
- Edge case: Create ItemDefinition with empty name; verify id is still valid
|
||||
|
||||
**Verification:**
|
||||
- Resource can be instantiated in code with all fields populated
|
||||
- Resource can be saved as a `.tres` file and reloaded
|
||||
|
||||
---
|
||||
|
||||
- U2. **InventoryManager AutoLoad Singleton**
|
||||
|
||||
**Goal:** Global inventory state management with signals, queries, and combination logic.
|
||||
|
||||
**Requirements:** R1, R2, R3, R5, R8
|
||||
|
||||
**Dependencies:** U1 (ItemDefinition)
|
||||
|
||||
**Files:**
|
||||
- Create: `inventory/InventoryManager.gd`
|
||||
- Create: `inventory/InventoryManager.gd.uid`
|
||||
- Modify: `project.godot` (add autoload)
|
||||
|
||||
**Approach:**
|
||||
- Extend `Node` with signals: `item_acquired(item_id: String)`, `item_removed(item_id: String)`, `inventory_changed`, `combination_attempted(item_a_id: String, item_b_id: String)`
|
||||
- State: `inventory` (Array[String], ordered), `obtained_items` (Dictionary used as set), `stripped_items` (Array[String])
|
||||
- Query methods: `has_item(item_id)`, `has_obtained(item_id)`, `has_any_of(item_ids)`, `has_obtained_any_of(item_ids)`, `has_obtained_all_of(item_ids)`
|
||||
- Mutation methods: `acquire_item(item_id)`, `remove_item(item_id, quiet=false)`, `bulk_strip_items(event_items, exempt_items)` — moves stripped items to vault
|
||||
- Combination logic: `attempt_combine(item_a_id, item_b_id)` emits `combination_attempted` signal. Room scripts or scene handlers connect to this signal to define what happens when two items are combined. No embedded combination data in the manager.
|
||||
- Item definitions registry: `get_item_definition(item_id)` loads from a registry. Items are registered via `register_item(item_def: ItemDefinition)`
|
||||
|
||||
**Patterns to follow:**
|
||||
- `ActionState.gd` for autoload singleton pattern with signals
|
||||
- `GameScript.gd` Narrate class for overlay fade pattern (reference for how overlays interact with game state)
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: Acquire item → verify it's in inventory and obtained_items
|
||||
- Happy path: Remove item → verify it's removed from inventory but stays in obtained_items
|
||||
- Happy path: attempt_combine emits combination_attempted signal → connected handler receives both item IDs
|
||||
- Edge case: Acquire already-owned item → item added to inventory again (duplicates allowed in current inventory)
|
||||
- Edge case: Remove item not in inventory → no crash, no change
|
||||
- Edge case: Query has_any_of with empty set → returns false
|
||||
- Edge case: Query has_obtained_all_of with empty set → returns true
|
||||
- Integration: acquire_item emits item_acquired signal → connected handler receives correct item_id
|
||||
- Integration: remove_item emits item_removed signal → connected handler receives correct item_id
|
||||
- Integration: bulk_strip_items moves items to vault, exempts protected items, leaves obtained_items untouched
|
||||
|
||||
**Verification:**
|
||||
- AutoLoad registered in `project.godot` and accessible as `InventoryManager` from any scene
|
||||
- All query methods return correct boolean values for various inventory states
|
||||
- combination_attempted signal emits correct item pair to connected handlers
|
||||
- Signal emissions are correct for acquire, remove, and combination events
|
||||
|
||||
---
|
||||
|
||||
- U3. **HUD Backpack FSM**
|
||||
|
||||
**Goal:** Animated backpack icon with state machine for Idle, Open, Selected, Acquire, Remove states and multi-step queued transitions.
|
||||
|
||||
**Requirements:** R6, R7, R8, R9
|
||||
|
||||
**Dependencies:** U2 (InventoryManager)
|
||||
|
||||
**Files:**
|
||||
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn`
|
||||
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn.uid`
|
||||
- Create: `inventory/inventory_backpack/InventoryBackpack.gd`
|
||||
- Create: `inventory/inventory_backpack/InventoryBackpack.gd.uid`
|
||||
- Create: `inventory/inventory_backpack/InventoryBackpack.tscn.uid`
|
||||
|
||||
**Approach:**
|
||||
- Scene tree: `InventoryBackpack` (Control) → `AnimationPlayer`, `ColorRect` (backpack icon placeholder), `FloatingItem` (Control) → `ColorRect` (dynamic item placeholder)
|
||||
- Anchored to top-right using Control anchors/offsets
|
||||
- FSM enum: `IDLE`, `OPEN`, `SELECTED`, `ACQUIRE`, `REMOVE`
|
||||
- Transitions execute immediately. No FIFO queuing — a new transition kills any in-flight tween and starts fresh.
|
||||
- Each transition is a chain of `Tween` operations with `tween_callback()` for step completion. New `Tween` per transition (Godot 4 Tweens are not reusable).
|
||||
- `kill_tween()` called on any active tween before starting a new transition
|
||||
- `is_busy` flag prevents concurrent transitions but does NOT queue — new transitions during busy state are dropped or interrupt immediately.
|
||||
- Item appearance animation primitive: `_animate_item(fade_dir, move_dir)` with parameters for show/hide and movement direction (out/none/in/far-out)
|
||||
- Connect to `InventoryManager.item_acquired` and `InventoryManager.item_removed` signals
|
||||
- Guard condition checks before `transition_to(OPEN)`: `GameScript.current_script` (foreground action), Fade sprite `modulate.a` (screen fade), pause flag
|
||||
- If guard blocks open and foreground action is running, emit `skip_action_requested` signal
|
||||
|
||||
**Tween timing (from PRD §11.2):**
|
||||
- Backpack rotation: linear, 0.35s
|
||||
- Backpack position: ease-in, 0.35s
|
||||
- Item appear/disappear: linear, 0.5s
|
||||
|
||||
**Patterns to follow:**
|
||||
- PRD §13.2 (Option A: Hand-Rolled FSM) for FSM structure
|
||||
- PRD §13.3 for Tween chaining with callbacks
|
||||
- `Game.tscn` Fade animation pattern for reference on AnimationPlayer usage
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: Click backpack in Idle → transitions to Open, overlay shown
|
||||
- Happy path: Acquire item in Idle → backpack opens, item appears and drops in, closes
|
||||
- Happy path: Select item from overlay → transitions to Selected, floating item appears
|
||||
- Happy path: Remove item in Idle → backpack opens, item appears and slides up, closes
|
||||
- Edge case: Rapid transition requests → new transition interrupts in-flight animation immediately
|
||||
- Edge case: Acquire item while Selected → replaces selected item with acquire animation
|
||||
- Edge case: Remove selected item (same item) → item slides up and fades, clears selection
|
||||
- Edge case: Remove different item while Selected → deselects current, shows removed item, slides up
|
||||
- Edge case: Transition interrupted by new transition → old tween killed, new transition starts
|
||||
- Integration: item_acquired signal from InventoryManager triggers Acquire animation
|
||||
- Integration: item_removed signal from InventoryManager triggers Remove animation
|
||||
- Integration: Guard condition blocks open when script running → skip_action_requested emitted
|
||||
|
||||
**Verification:**
|
||||
- FSM starts in IDLE state
|
||||
- All state transitions execute their multi-step animation sequences
|
||||
- New transitions interrupt in-flight animations (no queuing)
|
||||
- Tween objects are properly killed and recreated per transition
|
||||
- Guard conditions prevent opening during foreground action, screen fade, or pause
|
||||
|
||||
---
|
||||
|
||||
- U4. **Inventory Overlay Screen**
|
||||
|
||||
**Goal:** Full-screen overlay with item grid, hover text, drag-and-drop selection, and item combination.
|
||||
|
||||
**Requirements:** R7, R12
|
||||
|
||||
**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack)
|
||||
|
||||
**Files:**
|
||||
- Create: `inventory/inventory_overlay/InventoryOverlay.tscn`
|
||||
- Create: `inventory/inventory_overlay/InventoryOverlay.tscn.uid`
|
||||
- Create: `inventory/inventory_overlay/InventoryOverlay.gd`
|
||||
- Create: `inventory/inventory_overlay/InventoryOverlay.gd.uid`
|
||||
- Create: `inventory/inventory_overlay/InventorySlot.tscn`
|
||||
- Create: `inventory/inventory_overlay/InventorySlot.tscn.uid`
|
||||
- Create: `inventory/inventory_overlay/InventorySlot.gd`
|
||||
- Create: `inventory/inventory_overlay/InventorySlot.gd.uid`
|
||||
|
||||
**Approach:**
|
||||
- Scene tree: `InventoryOverlay` (Control, full-screen) → `ColorRect` (background, `MOUSE_FILTER_STOP`), `InventoryPanel` (Control, centered) → `TextureRect` (frame), `GridContainer` (item grid), `HoverLabel` (Label)
|
||||
- Overlay opacity controlled via `modulate.a` tweened for 0.2s fade-in/fade-out
|
||||
- `input_active` boolean flag — only `true` when fully opaque (AC-12)
|
||||
- `_gui_input()` handles all interaction:
|
||||
- Hover: track mouse-over item, update hover label text
|
||||
- Press: select item for drag
|
||||
- Motion during press: move item sprite to follow cursor, track hover under dragged item
|
||||
- Release on same item ≤0.5s: confirm selection, emit `item_confirmed`, close
|
||||
- Release on same item >0.5s: deselect (long-press cancel), stay open
|
||||
- Release on different item: emit `combine_requested(a, b)` to InventoryManager
|
||||
- Right-click on item: emit `inspect_requested(item)`, close
|
||||
- Release on empty space: close
|
||||
- Drag outside panel bounds: close immediately, discard selection
|
||||
- `InventorySlot` is a lightweight Control with a `ColorRect` child (colored box placeholder), `custom_minimum_size` for cell dimensions, and hover highlight modulate
|
||||
- `_refresh_grid()` called on `inventory_changed` signal: clears grid, instantiates `InventorySlot` for each item in `InventoryManager.inventory`
|
||||
- Combination result handling: on `combination_succeeded`, grid refreshes automatically via signal. On `combination_refused`, show message via `GameScript.narrate()` or dialogue system.
|
||||
|
||||
**Patterns to follow:**
|
||||
- `GameScript.gd` Narrate class for overlay fade pattern
|
||||
- `Game.tscn` NarratorOverlay for CanvasLayer overlay pattern
|
||||
- PRD §13.4 for Control hierarchy and drag-and-drop approach
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: Open overlay → items displayed in grid, left-to-right, top-to-bottom
|
||||
- Happy path: Hover item with no selection → hover label shows item name
|
||||
- Happy path: Select item A, hover item B → hover label shows "Use A with B"
|
||||
- Happy path: Short-click item → item confirmed, overlay closes
|
||||
- Happy path: Drag item A onto item B → combination attempted
|
||||
- Happy path: Right-click item → inspect dialogue played, overlay closes
|
||||
- Happy path: Click empty space → overlay closes, selection cleared
|
||||
- Edge case: Long-press release (>0.5s) on item → deselected, overlay stays open
|
||||
- Edge case: Drag item outside panel bounds → overlay closes, selection discarded
|
||||
- Edge case: Overlay during fade-in → renders but doesn't accept input
|
||||
- Edge case: Overlay during fade-out → renders but doesn't accept input
|
||||
- Edge case: Empty inventory → grid shows no items
|
||||
- Error path: Combine items with no interaction → refusal message shown
|
||||
- Integration: inventory_changed signal refreshes grid with new items
|
||||
- Integration: combine_requested signal sent to InventoryManager with correct item pair
|
||||
- Integration: Hover text tracks item under dragged item during drag
|
||||
|
||||
**Verification:**
|
||||
- Overlay fades in/out with correct timing
|
||||
- Grid layout respects fixed maximum items per row
|
||||
- All interaction outcomes match PRD §5.4 table
|
||||
- Input is blocked during fade transitions
|
||||
- Drag-out escape closes overlay and discards selection
|
||||
|
||||
---
|
||||
|
||||
- U5. **MainGame Integration — Cursor, Guards, World Item Use**
|
||||
|
||||
**Goal:** Integrate inventory system with existing cursor management, guard conditions, and game world item interaction.
|
||||
|
||||
**Requirements:** R10, R11, R12
|
||||
|
||||
**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack), U4 (InventoryOverlay)
|
||||
|
||||
**Files:**
|
||||
- Modify: `MainGame.gd`
|
||||
- Modify: `Game.tscn`
|
||||
- Modify: `Scene.gd`
|
||||
|
||||
**Approach:**
|
||||
- **Game.tscn changes:** Add `InventoryBackpack` and `InventoryOverlay` as children of `Node2D` (MainGame), under separate `CanvasLayer` nodes at appropriate layers. Backpack at layer 5 (HUD), overlay at layer 10 (full-screen).
|
||||
- **ActionState.gd changes:**
|
||||
- Add `ITEM` as the 5th action (value 4): `WALK=0`, `LOOK=1`, `TOUCH=2`, `TALK=3`, `ITEM=4`
|
||||
- Right-click cycling logic updated: cycles through 0→1→2→3→4→0
|
||||
- When cycling to ITEM, the cursor uses the selected item's colored box as cursor (placeholder)
|
||||
- **MainGame.gd changes:**
|
||||
- `_on_item_selected(item_def)`: Store item in `InventoryManager.selected_item`, set `ActionState.current_action = ActionState.Action.ITEM`
|
||||
- Guard conditions: `can_open_inventory()` checks `GameScript.current_script` (foreground action), Fade `modulate.a` (screen fade), and pause state
|
||||
- World item use: When `ActionState.current_action == Action.ITEM` and player left-clicks in game world, emit item's interaction signal for the clicked entity. If no entity clicked, show item's self-description.
|
||||
- Right-click in game world with item selected → cycles cursor to next action (ITEM→WALK), effectively returning item to backpack
|
||||
- Input routing: Ensure `_unhandled_input()` in `Scene.gd` checks `InventoryOverlay.input_active` before processing game-world clicks
|
||||
- **Scene.gd changes:**
|
||||
- Add guard in `_unhandled_input()`: if `InventoryOverlay` is active, return early
|
||||
- When `ActionState.current_action == Action.ITEM`, pass selected item to clicked entity's interaction handler via signal
|
||||
|
||||
**Patterns to follow:**
|
||||
- `ActionState.gd` for cursor action enum and cycling pattern
|
||||
- `MainGame.gd` `_on_input_action()` for right-click cycling integration
|
||||
- `Scene.gd` `_unhandled_input()` for input handling
|
||||
- `SetPiece_.gd` for entity interaction pattern
|
||||
- PRD §13.10 recommended combined input priority approach
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: Item selected from overlay → ActionState set to ITEM, item stored in InventoryManager
|
||||
- Happy path: Right-click in world with ITEM active → cursor cycles to WALK, item returned
|
||||
- Happy path: Left-click entity with ITEM active → entity receives item interaction signal
|
||||
- Happy path: Left-click empty space with ITEM active → item self-description shown
|
||||
- Edge case: Click backpack while script running → skip action attempted, inventory doesn't open
|
||||
- Edge case: Click backpack during screen fade → inventory doesn't open
|
||||
- Edge case: Inventory overlay active → game world input is suppressed
|
||||
- Edge case: Entity has no handler for selected item → generic "nothing to do" message
|
||||
- Integration: InventoryBackpack guard check returns false when foreground action running
|
||||
- Integration: Scene._unhandled_input returns early when overlay is active
|
||||
- Integration: Right-click cycling includes ITEM state (WALK→LOOK→TOUCH→TALK→ITEM→WALK)
|
||||
|
||||
**Verification:**
|
||||
- ActionState includes ITEM as value 4, right-click cycles through all 5 states
|
||||
- Guard conditions prevent inventory from opening during foreground action, screen fade, or pause
|
||||
- Game world input is properly suppressed when overlay is active
|
||||
- Item use triggers entity interaction signals when ActionState is ITEM
|
||||
|
||||
---
|
||||
|
||||
- U6. **Scene Integration Hooks — Acquisition and Removal**
|
||||
|
||||
**Goal:** Provide hooks for room scenes to acquire and remove items, with proper signal propagation and animation triggering.
|
||||
|
||||
**Requirements:** R6, R7, R8, R9
|
||||
|
||||
**Dependencies:** U2 (InventoryManager), U3 (InventoryBackpack)
|
||||
|
||||
**Files:**
|
||||
- Modify: `Scene.gd` (add helper methods)
|
||||
- Modify: `scenes/kq4_placeholder_template/kq4_placeholder_template.gd` (example usage)
|
||||
|
||||
**Approach:**
|
||||
- Add helper methods to `Scene.gd` (or as static utilities accessible from any scene):
|
||||
- `give_item(item_id)`: Calls `InventoryManager.acquire_item(item_id)`. The manager emits `item_acquired` which triggers the backpack's Acquire animation.
|
||||
- `remove_item(item_id, quiet=false)`: Calls `InventoryManager.remove_item(item_id, quiet)`. The manager emits `item_removed` which triggers the backpack's Remove animation (unless quiet).
|
||||
- `strip_items(event_items, exempt_items)`: Calls `InventoryManager.bulk_strip_items()`. Stripped items go to vault.
|
||||
- `use_item_on_entity(item_id, entity)`: Checks if entity has an interaction handler for the item. If yes, executes it. If no, shows generic refusal.
|
||||
- Room scripts can call these helpers from SetPiece signal handlers or GameScript steps.
|
||||
- Add a `GameScript` step class for item acquisition: `GiveItem` step that plays dialogue, acquires the item, and triggers the animation.
|
||||
- Example usage in placeholder template: show how a SetPiece `touched` signal handler calls `give_item()`.
|
||||
|
||||
**Patterns to follow:**
|
||||
- `GameScript.gd` `Say` and `Narrate` classes for new `GiveItem` step
|
||||
- `Scene.gd` `start_main_script` for script-based item acquisition
|
||||
- `TransitionPiece.gd` for how room scripts interact with global systems
|
||||
|
||||
**Test scenarios:**
|
||||
- Happy path: SetPiece touched → give_item called → item in inventory, backpack Acquire animation plays
|
||||
- Happy path: Combination consumes item → remove_item called → item removed from inventory, backpack Remove animation plays
|
||||
- Edge case: Quiet removal → item removed from inventory, no backpack animation
|
||||
- Edge case: Give item while another item selected → selected item display replaced with new item
|
||||
- Integration: give_item triggers InventoryManager.acquire_item → signal propagates to InventoryBackpack → Acquire animation starts
|
||||
- Integration: bulk_strip_items moves items to vault, exempt items stay in inventory
|
||||
|
||||
**Verification:**
|
||||
- Room scripts can call `give_item()` and `remove_item()` from signal handlers
|
||||
- Item acquisition triggers proper backpack animation via signal chain
|
||||
- Item removal respects quiet flag
|
||||
- Bulk strip moves items to vault and exempts protected items
|
||||
- `GiveItem` GameScript step integrates with existing script builder pattern
|
||||
|
||||
---
|
||||
|
||||
## System-Wide Impact
|
||||
|
||||
- **Interaction graph:** `InventoryManager` signals connect to `InventoryBackpack` (animations), `InventoryOverlay` (grid refresh), and `MainGame` (cursor management). `InventoryBackpack` signals connect to `InventoryOverlay` (show/hide). `InventoryOverlay` signals connect back to `InventoryManager` (combination) and `InventoryBackpack` (close). The signal loop is: Manager → Backpack → Overlay → Manager.
|
||||
- **Error propagation:** Combination refusals propagate as `combination_refused` signal with message, handled by the overlay or scene's dialogue system. Invalid item IDs should be guarded with `push_warning()`.
|
||||
- **State lifecycle risks:** The FSM's transition queue must handle rapid state changes correctly. If the backpack node is freed during a transition, `bind_node(self)` on Tweens ensures they die cleanly. The overlay's `input_active` flag must be set/cleared at exact fade boundaries to avoid the "input black hole" problem.
|
||||
- **API surface parity:** The existing `ActionState` cursor actions (WALK/LOOK/TOUCH/TALK) continue to work normally when no item is selected. When an item is selected, the walk cursor effectively becomes "use item" — this is a behavioral override, not a change to `ActionState` itself.
|
||||
- **Integration coverage:** The full signal chain from `InventoryManager.acquire_item()` → backpack Acquire animation → FSM returning to Idle must work as an integration scenario that mocks alone won't prove. Similarly, the overlay combination flow: click A → click B → `attempt_combine()` → `combination_succeeded` → items removed/added → grid refreshes → overlay closes.
|
||||
- **Unchanged invariants:** The existing cursor cycling (right-click cycles WALK/LOOK/TOUCH/TALK) is unchanged. The `ActionState` autoload is unchanged. The `GameScript` system continues to work for cutscenes — the new `GiveItem` step extends it without modifying existing steps.
|
||||
|
||||
---
|
||||
|
||||
## Risks & Dependencies
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Tween lifecycle bugs: FSM transitions interrupted mid-animation leave visual state inconsistent | Always `kill()` active tween before starting new transition. Use `bind_node(self)` to auto-clean on node free. |
|
||||
| Input black hole during overlay fade transitions | `input_active` flag toggled at exact fade boundaries. Overlay blocks input from fade-start through fade-end. Game world regains input only after fade-out completes. |
|
||||
| Node2D vs Control input gap: overlay `accept_event()` doesn't suppress `Node2D._input_event()` | Combined defense: `MOUSE_FILTER_STOP` background + `_unhandled_input()` gate in Scene + explicit `overlay_active` guard flag checked by game world. |
|
||||
| GridContainer limitations during drag: item reparenting causes layout flicker | Test drag behavior early. If GridContainer causes issues, switch to manual grid positioning in a custom Control. |
|
||||
| Combination logic complexity: dynamic rules, prerequisites, order guards | Start with static dictionary lookups (forward + reverse). Add dynamic category rules and guards as separate methods that can be extended per item. |
|
||||
|
||||
---
|
||||
|
||||
## Documentation / Operational Notes
|
||||
|
||||
- Item definitions (`.tres` files) should be placed in a dedicated `inventory/items/` directory when they are created
|
||||
- The `InventoryManager` autoload should be documented in a brief comment at the top of its file, listing all signals and public methods
|
||||
- No CLAUDE.md or AGENTS.md updates needed — the inventory system follows existing patterns (autoload singletons, scene hierarchy, signal-based communication)
|
||||
|
||||
---
|
||||
|
||||
## Sources & References
|
||||
|
||||
- **Origin document:** [inventory-prd.md](inventory-prd.md)
|
||||
- Existing code: `ActionState.gd`, `MainGame.gd`, `GameScript.gd`, `Scene.gd`, `SetPiece_.gd`, `Game.tscn`
|
||||
- Godot 4.6 docs: `Tween`, `Control`, `Resource`, `CanvasLayer`, `Input.create_custom_cursor()`
|
||||
1
godot-mcp
Submodule
1
godot-mcp
Submodule
Submodule godot-mcp added at b77dfc7c62
791
inventory-prd.md
791
inventory-prd.md
@@ -1,791 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,101 +0,0 @@
|
||||
extends Node
|
||||
|
||||
# Signals
|
||||
signal item_acquired(item_id: String)
|
||||
signal item_removed(item_id: String)
|
||||
signal inventory_changed
|
||||
signal combination_attempted(item_a_id: String, item_b_id: String)
|
||||
|
||||
# State
|
||||
var inventory: Array[String] = []
|
||||
var obtained_items: Dictionary = {}
|
||||
var stripped_items: Array[String] = []
|
||||
var selected_item: String = ""
|
||||
|
||||
# Item definitions registry
|
||||
var _item_definitions: Dictionary = {}
|
||||
|
||||
|
||||
# -- Queries --
|
||||
|
||||
func has_item(item_id: String) -> bool:
|
||||
return inventory.has(item_id)
|
||||
|
||||
func has_obtained(item_id: String) -> bool:
|
||||
return obtained_items.has(item_id)
|
||||
|
||||
func has_any_of(item_ids: Array[String]) -> bool:
|
||||
if item_ids.is_empty():
|
||||
return false
|
||||
for item_id in item_ids:
|
||||
if has_item(item_id):
|
||||
return true
|
||||
return false
|
||||
|
||||
func has_obtained_any_of(item_ids: Array[String]) -> bool:
|
||||
if item_ids.is_empty():
|
||||
return false
|
||||
for item_id in item_ids:
|
||||
if has_obtained(item_id):
|
||||
return true
|
||||
return false
|
||||
|
||||
func has_obtained_all_of(item_ids: Array[String]) -> bool:
|
||||
if item_ids.is_empty():
|
||||
return true
|
||||
for item_id in item_ids:
|
||||
if not has_obtained(item_id):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# -- Mutations --
|
||||
|
||||
func acquire_item(item_id: String) -> void:
|
||||
inventory.append(item_id)
|
||||
obtained_items[item_id] = true
|
||||
item_acquired.emit(item_id)
|
||||
inventory_changed.emit()
|
||||
|
||||
func remove_item(item_id: String, quiet: bool = false) -> void:
|
||||
var idx = inventory.find(item_id)
|
||||
if idx == -1:
|
||||
return
|
||||
inventory.remove_at(idx)
|
||||
if not quiet:
|
||||
item_removed.emit(item_id)
|
||||
inventory_changed.emit()
|
||||
|
||||
func bulk_strip_items(event_items: Array[String], exempt_items: Array[String] = []) -> void:
|
||||
var remaining: Array[String] = []
|
||||
for item_id in inventory:
|
||||
if event_items.has(item_id) and not exempt_items.has(item_id):
|
||||
stripped_items.append(item_id)
|
||||
else:
|
||||
remaining.append(item_id)
|
||||
inventory = remaining
|
||||
inventory_changed.emit()
|
||||
|
||||
|
||||
# -- Items --
|
||||
|
||||
func register_item(item_def: ItemDefinition) -> void:
|
||||
_item_definitions[item_def.id] = item_def
|
||||
|
||||
func get_item_definition(item_id: String) -> ItemDefinition:
|
||||
return _item_definitions.get(item_id, null)
|
||||
|
||||
|
||||
# -- Combinations --
|
||||
|
||||
func attempt_combine(item_a_id: String, item_b_id: String) -> void:
|
||||
combination_attempted.emit(item_a_id, item_b_id)
|
||||
|
||||
|
||||
# -- Selection --
|
||||
|
||||
func select_item(item_id: String) -> void:
|
||||
selected_item = item_id
|
||||
|
||||
func clear_selection() -> void:
|
||||
selected_item = ""
|
||||
@@ -1 +0,0 @@
|
||||
uid://2am6u2ux8s96o
|
||||
@@ -1,7 +0,0 @@
|
||||
extends Resource
|
||||
class_name ItemDefinition
|
||||
|
||||
@export var id: String = ""
|
||||
@export var name: String = ""
|
||||
@export var icon: Texture2D
|
||||
@export var combination_category: String = ""
|
||||
@@ -1 +0,0 @@
|
||||
uid://34ph99jcuowua
|
||||
@@ -1,238 +0,0 @@
|
||||
extends Control
|
||||
class_name InventoryBackpack
|
||||
|
||||
signal overlay_show_requested
|
||||
signal overlay_hide_requested
|
||||
signal item_selected(item_id: String)
|
||||
signal returning_to_idle
|
||||
signal skip_action_requested
|
||||
|
||||
enum State { IDLE, OPEN, SELECTED, ACQUIRE, REMOVE }
|
||||
|
||||
var _state: State = State.IDLE
|
||||
var _animating: bool = false
|
||||
var _active_tween: Tween = null
|
||||
var _floating_item_color: Color = Color(1, 1, 1, 0)
|
||||
var _home_position: Vector2 = Vector2(0, 0)
|
||||
|
||||
@onready var backpack_icon: ColorRect = $BackpackIcon
|
||||
@onready var floating_item: ColorRect = $FloatingItem
|
||||
@onready var animation_player: AnimationPlayer = $AnimationPlayer
|
||||
|
||||
func _ready() -> void:
|
||||
_home_position = backpack_icon.position
|
||||
floating_item.modulate = Color(1, 1, 1, 0)
|
||||
floating_item.visible = false
|
||||
InventoryManager.item_acquired.connect(_on_item_acquired)
|
||||
InventoryManager.item_removed.connect(_on_item_removed)
|
||||
InventoryManager.inventory_changed.connect(_on_inventory_changed)
|
||||
_update_floating_item()
|
||||
|
||||
func get_state() -> State:
|
||||
return _state
|
||||
|
||||
func is_busy() -> bool:
|
||||
return _animating
|
||||
|
||||
func _update_floating_item() -> void:
|
||||
if InventoryManager.selected_item:
|
||||
var def = InventoryManager.get_item_definition(InventoryManager.selected_item)
|
||||
if def:
|
||||
floating_item.visible = true
|
||||
floating_item.modulate = Color(1, 0.6, 0.2, 1)
|
||||
else:
|
||||
floating_item.visible = true
|
||||
floating_item.modulate = Color(0.8, 0.8, 0.8, 1)
|
||||
else:
|
||||
floating_item.visible = false
|
||||
|
||||
func transition_to(new_state: State) -> void:
|
||||
if _animating and new_state != _state:
|
||||
_kill_tween()
|
||||
match new_state:
|
||||
State.IDLE:
|
||||
_transition_to_idle()
|
||||
State.OPEN:
|
||||
_transition_to_open()
|
||||
State.SELECTED:
|
||||
_transition_to_selected()
|
||||
State.ACQUIRE:
|
||||
_transition_to_acquire()
|
||||
State.REMOVE:
|
||||
_transition_to_remove()
|
||||
|
||||
func _transition_to_idle() -> void:
|
||||
if _state == State.IDLE:
|
||||
return
|
||||
_set_state(State.IDLE)
|
||||
overlay_hide_requested.emit()
|
||||
backpack_icon.modulate = Color(1, 1, 1, 1)
|
||||
floating_item.visible = false
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
_animating = true
|
||||
tween.tween_property(backpack_icon, "rotation", 0.0, 0.35).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_property(backpack_icon, "position", _home_position, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(_on_transition_complete.bind(State.IDLE))
|
||||
|
||||
func _transition_to_open() -> void:
|
||||
var can_open = _check_guards()
|
||||
if not can_open:
|
||||
if _state == State.IDLE:
|
||||
skip_action_requested.emit()
|
||||
return
|
||||
|
||||
_set_state(State.OPEN)
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
_animating = true
|
||||
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_property(backpack_icon, "position", _home_position + Vector2(20, 20), 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
tween.tween_callback(_on_transition_complete.bind(State.OPEN))
|
||||
tween.tween_callback(overlay_show_requested.emit)
|
||||
|
||||
func _transition_to_selected() -> void:
|
||||
if _state == State.SELECTED:
|
||||
return
|
||||
|
||||
_set_state(State.SELECTED)
|
||||
_update_floating_item()
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
_animating = true
|
||||
|
||||
floating_item.visible = true
|
||||
floating_item.modulate = Color(1, 1, 1, 0)
|
||||
tween.tween_property(floating_item, "modulate", Color(1, 1, 1, 1), 0.5).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_callback(_on_transition_complete.bind(State.SELECTED))
|
||||
|
||||
func _transition_to_acquire() -> void:
|
||||
if _state == State.ACQUIRE or _state == State.REMOVE:
|
||||
_kill_tween()
|
||||
|
||||
_set_state(State.ACQUIRE)
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
_animating = true
|
||||
|
||||
backpack_icon.modulate = Color(1, 1, 1, 1)
|
||||
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_callback(_start_acquire_item_anim)
|
||||
|
||||
func _start_acquire_item_anim() -> void:
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
|
||||
floating_item.visible = true
|
||||
floating_item.modulate = Color(1, 1, 1, 0)
|
||||
floating_item.position = Vector2(backpack_icon.position.x - 20, backpack_icon.position.y - 40)
|
||||
|
||||
tween.tween_property(floating_item, "modulate", Color(1, 1, 1, 1), 0.5).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_property(floating_item, "position", backpack_icon.position, 0.5).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_callback(_on_acquire_complete)
|
||||
|
||||
func _on_acquire_complete() -> void:
|
||||
floating_item.visible = false
|
||||
transition_to(State.IDLE)
|
||||
|
||||
func _transition_to_remove() -> void:
|
||||
if _state == State.ACQUIRE or _state == State.REMOVE:
|
||||
_kill_tween()
|
||||
|
||||
_set_state(State.REMOVE)
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
_animating = true
|
||||
|
||||
backpack_icon.modulate = Color(1, 1, 1, 1)
|
||||
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_callback(_start_remove_item_anim)
|
||||
|
||||
func _start_remove_item_anim() -> void:
|
||||
var tween = create_tween().bind_node(self)
|
||||
_active_tween = tween
|
||||
|
||||
floating_item.visible = true
|
||||
floating_item.modulate = Color(1, 1, 1, 1)
|
||||
floating_item.position = backpack_icon.position
|
||||
|
||||
tween.tween_property(floating_item, "position", Vector2(backpack_icon.position.x - 20, backpack_icon.position.y - 80), 0.5).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.parallel().tween_property(floating_item, "modulate", Color(1, 1, 1, 0), 0.5).set_trans(Tween.TRANS_LINEAR)
|
||||
tween.tween_callback(_on_remove_complete)
|
||||
|
||||
func _on_remove_complete() -> void:
|
||||
floating_item.visible = false
|
||||
var was_selected = InventoryManager.selected_item != ""
|
||||
transition_to(State.IDLE)
|
||||
if was_selected:
|
||||
InventoryManager.clear_selection()
|
||||
|
||||
func _set_state(new_state: State) -> void:
|
||||
_state = new_state
|
||||
|
||||
func _check_guards() -> bool:
|
||||
var main_game = get_node_or_null("/root/Node2D")
|
||||
if not main_game:
|
||||
return true
|
||||
|
||||
if main_game.is_script_running:
|
||||
return false
|
||||
|
||||
var fade = get_node_or_null("/root/Node2D/SceneDisplay/Fade")
|
||||
if fade and fade.modulate.a > 0.5:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func _kill_tween() -> void:
|
||||
if _active_tween:
|
||||
_active_tween.kill()
|
||||
_active_tween = null
|
||||
_animating = false
|
||||
|
||||
func _on_transition_complete(completed_state: State) -> void:
|
||||
_animating = false
|
||||
_active_tween = null
|
||||
if completed_state == State.OPEN:
|
||||
pass
|
||||
elif completed_state == State.IDLE:
|
||||
returning_to_idle.emit()
|
||||
|
||||
func _on_item_acquired(item_id: String) -> void:
|
||||
if _state == State.IDLE:
|
||||
transition_to(State.ACQUIRE)
|
||||
|
||||
func _on_item_removed(item_id: String) -> void:
|
||||
if _state == State.IDLE:
|
||||
if InventoryManager.selected_item == item_id:
|
||||
InventoryManager.clear_selection()
|
||||
_update_floating_item()
|
||||
transition_to(State.REMOVE)
|
||||
else:
|
||||
transition_to(State.REMOVE)
|
||||
elif _state == State.SELECTED and InventoryManager.selected_item == item_id:
|
||||
InventoryManager.clear_selection()
|
||||
_update_floating_item()
|
||||
transition_to(State.REMOVE)
|
||||
|
||||
func _on_inventory_changed() -> void:
|
||||
_update_floating_item()
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
print("[BACKPACK] _gui_input: %s, state=%s" % [event, State.keys()[_state]])
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
|
||||
if _state == State.IDLE:
|
||||
print("[BACKPACK] transitioning to OPEN")
|
||||
transition_to(State.OPEN)
|
||||
elif _state == State.OPEN:
|
||||
print("[BACKPACK] transitioning to IDLE")
|
||||
transition_to(State.IDLE)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_kill_tween()
|
||||
@@ -1 +0,0 @@
|
||||
uid://2x3g0ethsdcgo
|
||||
@@ -1,30 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://dxkyfas46q7ef"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://v8du0eptw65c" path="res://inventory/inventory_backpack/InventoryBackpack.gd" id="1"]
|
||||
|
||||
[node name="InventoryBackpack" type="Control" unique_id=1000000001]
|
||||
layout_mode = 3
|
||||
anchors_preset = 2
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -70.0
|
||||
offset_right = 70.0
|
||||
grow_vertical = 0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="BackpackIcon" type="ColorRect" parent="." unique_id=1000000002]
|
||||
layout_mode = 0
|
||||
offset_right = 60.0
|
||||
offset_bottom = 60.0
|
||||
mouse_filter = 1
|
||||
color = Color(0.4, 0.6, 0.9, 1)
|
||||
|
||||
[node name="FloatingItem" type="ColorRect" parent="." unique_id=1000000003]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_left = 20.0
|
||||
offset_top = -30.0
|
||||
offset_right = 50.0
|
||||
color = Color(1, 0.6, 0.2, 1)
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1000000004]
|
||||
@@ -1 +0,0 @@
|
||||
uid://1406xmcnkygw0
|
||||
@@ -1,204 +0,0 @@
|
||||
extends Control
|
||||
class_name InventoryOverlay
|
||||
|
||||
signal close_requested
|
||||
signal item_confirmed(item_id: String)
|
||||
signal combine_requested(item_a_id: String, item_b_id: String)
|
||||
signal inspect_requested(item_id: String)
|
||||
|
||||
var input_active: bool = false
|
||||
var _is_visible: bool = false
|
||||
var _selected_slot: InventorySlot = null
|
||||
var _drag_start_time: float = 0.0
|
||||
var _is_dragging: bool = false
|
||||
var _dragged_item: Control = null
|
||||
var _hovered_slot: InventorySlot = null
|
||||
var _fade_tween: Tween = null
|
||||
|
||||
const FADE_DURATION: float = 0.2
|
||||
const LONG_PRESS_THRESHOLD: float = 0.5
|
||||
const SLOTS_PER_ROW: int = 8
|
||||
const SLOT_SIZE: Vector2i = Vector2i(64, 64)
|
||||
|
||||
@onready var background: ColorRect = $Background
|
||||
@onready var panel: Control = $InventoryPanel
|
||||
@onready var frame: ColorRect = $InventoryPanel/Frame
|
||||
@onready var grid: GridContainer = $InventoryPanel/ItemGrid
|
||||
@onready var hover_label: Label = $InventoryPanel/HoverLabel
|
||||
|
||||
func _ready() -> void:
|
||||
hide()
|
||||
background.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
InventoryManager.inventory_changed.connect(_refresh_grid)
|
||||
InventoryManager.combination_attempted.connect(_on_combination_attempted)
|
||||
|
||||
func show_overlay() -> void:
|
||||
print("[OVERLAY] show_overlay called, inventory has %d items" % InventoryManager.inventory.size())
|
||||
_refresh_grid()
|
||||
if _fade_tween:
|
||||
_fade_tween.kill()
|
||||
_fade_tween = null
|
||||
|
||||
_is_visible = true
|
||||
show()
|
||||
modulate = Color(1, 1, 1, 0)
|
||||
input_active = false
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_fade_tween = tween
|
||||
tween.tween_property(self, "modulate", Color(1, 1, 1, 1), FADE_DURATION)
|
||||
tween.tween_callback(_on_fade_in_complete)
|
||||
|
||||
func hide_overlay() -> void:
|
||||
print("[OVERLAY] hide_overlay called")
|
||||
if _fade_tween:
|
||||
_fade_tween.kill()
|
||||
_fade_tween = null
|
||||
|
||||
input_active = false
|
||||
_clear_selection()
|
||||
|
||||
var tween = create_tween().bind_node(self)
|
||||
_fade_tween = tween
|
||||
tween.tween_property(self, "modulate", Color(1, 1, 1, 0), FADE_DURATION)
|
||||
tween.tween_callback(_on_fade_out_complete)
|
||||
|
||||
func _on_fade_in_complete() -> void:
|
||||
input_active = true
|
||||
|
||||
func _on_fade_out_complete() -> void:
|
||||
_is_visible = false
|
||||
hide()
|
||||
input_active = false
|
||||
|
||||
func is_active() -> bool:
|
||||
return _is_visible and input_active
|
||||
|
||||
func _refresh_grid() -> void:
|
||||
for child in grid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
for item_id in InventoryManager.inventory:
|
||||
var def = InventoryManager.get_item_definition(item_id)
|
||||
if not def:
|
||||
def = ItemDefinition.new()
|
||||
def.id = item_id
|
||||
def.name = item_id
|
||||
|
||||
var slot_scene = load("res://inventory/inventory_overlay/InventorySlot.tscn")
|
||||
var slot: InventorySlot = slot_scene.instantiate()
|
||||
grid.add_child(slot)
|
||||
slot.set_item(def)
|
||||
slot.clicked.connect(_on_slot_clicked)
|
||||
slot.right_clicked.connect(_on_slot_right_clicked)
|
||||
slot.hovered.connect(_on_slot_hovered)
|
||||
slot.unhovered.connect(_on_slot_unhovered)
|
||||
|
||||
grid.columns = SLOTS_PER_ROW
|
||||
|
||||
func _on_slot_clicked(item_id: String) -> void:
|
||||
print("[OVERLAY] _on_slot_clicked: '%s', input_active=%s" % [item_id, input_active])
|
||||
if not input_active:
|
||||
return
|
||||
|
||||
for child in grid.get_children():
|
||||
if child is InventorySlot and child.item_id == item_id:
|
||||
if _selected_slot == null:
|
||||
InventoryManager.select_item(item_id)
|
||||
item_confirmed.emit(item_id)
|
||||
hide_overlay()
|
||||
elif _selected_slot.item_id == item_id:
|
||||
InventoryManager.select_item(item_id)
|
||||
item_confirmed.emit(item_id)
|
||||
hide_overlay()
|
||||
else:
|
||||
combine_requested.emit(_selected_slot.item_id, item_id)
|
||||
_clear_selection()
|
||||
break
|
||||
|
||||
func _on_slot_right_clicked(item_id: String) -> void:
|
||||
if not input_active:
|
||||
return
|
||||
inspect_requested.emit(item_id)
|
||||
hide_overlay()
|
||||
|
||||
func _on_slot_hovered(item_id: String) -> void:
|
||||
if not input_active:
|
||||
return
|
||||
_hovered_slot = null
|
||||
for child in grid.get_children():
|
||||
if child is InventorySlot and child.item_id == item_id:
|
||||
_hovered_slot = child
|
||||
break
|
||||
_update_hover_label()
|
||||
|
||||
func _on_slot_unhovered(item_id: String) -> void:
|
||||
if _hovered_slot and _hovered_slot.item_id == item_id:
|
||||
_hovered_slot = null
|
||||
_update_hover_label()
|
||||
|
||||
func _create_drag_preview(slot: InventorySlot) -> void:
|
||||
var preview = ColorRect.new()
|
||||
preview.color = Color(1, 0.6, 0.2, 0.7)
|
||||
preview.size = Vector2(48, 48)
|
||||
preview.position = slot.global_position - global_position
|
||||
panel.add_child(preview)
|
||||
_dragged_item = preview
|
||||
|
||||
func _handle_release_same_item() -> void:
|
||||
var elapsed = Time.get_ticks_msec() / 1000.0 - _drag_start_time
|
||||
if elapsed <= LONG_PRESS_THRESHOLD:
|
||||
item_confirmed.emit(_selected_slot.item_id)
|
||||
hide_overlay()
|
||||
else:
|
||||
_clear_selection()
|
||||
|
||||
func _clear_selection() -> void:
|
||||
_selected_slot = null
|
||||
_is_dragging = false
|
||||
if _dragged_item:
|
||||
_dragged_item.queue_free()
|
||||
_dragged_item = null
|
||||
_update_hover_label()
|
||||
|
||||
func _update_hover_label() -> void:
|
||||
if _selected_slot and _hovered_slot and _selected_slot.item_id != _hovered_slot.item_id:
|
||||
var def_a = InventoryManager.get_item_definition(_selected_slot.item_id)
|
||||
var def_b = InventoryManager.get_item_definition(_hovered_slot.item_id)
|
||||
var name_a = _selected_slot.item_id
|
||||
var name_b = _hovered_slot.item_id
|
||||
if def_a:
|
||||
name_a = def_a.name
|
||||
if def_b:
|
||||
name_b = def_b.name
|
||||
hover_label.text = "Use %s with %s" % [name_a, name_b]
|
||||
elif _hovered_slot:
|
||||
var def = InventoryManager.get_item_definition(_hovered_slot.item_id)
|
||||
if def:
|
||||
hover_label.text = def.name
|
||||
else:
|
||||
hover_label.text = _hovered_slot.item_id
|
||||
else:
|
||||
hover_label.text = ""
|
||||
|
||||
func _on_background_gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.button_index == 1 and event.pressed:
|
||||
hide_overlay()
|
||||
close_requested.emit()
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.button_index == 1 and event.pressed:
|
||||
var panel_rect = panel.get_global_rect()
|
||||
if not panel_rect.has_point(get_global_mouse_position()):
|
||||
hide_overlay()
|
||||
close_requested.emit()
|
||||
|
||||
|
||||
func _on_combination_attempted(item_a_id: String, item_b_id: String) -> void:
|
||||
pass
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
if _fade_tween:
|
||||
_fade_tween.kill()
|
||||
_fade_tween = null
|
||||
@@ -1 +0,0 @@
|
||||
uid://3mkdj9s1oe1jz
|
||||
@@ -1,77 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://djoycn4xfa8p3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bkpafveapyv8n" path="res://inventory/inventory_overlay/InventoryOverlay.gd" id="1"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_inv"]
|
||||
font_size = 20
|
||||
outline_size = 3
|
||||
outline_color = Color(0, 0, 0, 1)
|
||||
|
||||
[node name="InventoryOverlay" type="Control" unique_id=-1294967295]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="." unique_id=-1294967294]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
color = Color(0, 0, 0, 0.7)
|
||||
|
||||
[node name="InventoryPanel" type="Control" parent="." unique_id=-1294967293]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -350.0
|
||||
offset_top = -150.0
|
||||
offset_right = 350.0
|
||||
offset_bottom = 150.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Frame" type="ColorRect" parent="InventoryPanel" unique_id=-1294967292]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_filter = 1
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.15, 0.15, 0.2, 1)
|
||||
|
||||
[node name="ItemGrid" type="GridContainer" parent="InventoryPanel" unique_id=-1294967291]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 10.0
|
||||
offset_top = 10.0
|
||||
offset_right = -10.0
|
||||
offset_bottom = -50.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HoverLabel" type="Label" parent="InventoryPanel" unique_id=-1294967290]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_bottom = -10.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
label_settings = SubResource("LabelSettings_inv")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[connection signal="gui_input" from="Background" to="." method="_on_background_gui_input"]
|
||||
@@ -1 +0,0 @@
|
||||
uid://1p46uzngsih9o
|
||||
@@ -1,57 +0,0 @@
|
||||
extends Control
|
||||
class_name InventorySlot
|
||||
|
||||
signal clicked(item_id: String)
|
||||
signal right_clicked(item_id: String)
|
||||
signal hovered(item_id: String)
|
||||
signal unhovered(item_id: String)
|
||||
|
||||
var item_id: String = ""
|
||||
var is_hovered: bool = false
|
||||
|
||||
@onready var item_box: ColorRect = $ItemBox
|
||||
@onready var hover_highlight: ColorRect = $HoverHighlight
|
||||
|
||||
func _ready() -> void:
|
||||
hover_highlight.visible = false
|
||||
|
||||
func set_item(item_def: ItemDefinition) -> void:
|
||||
item_id = item_def.id
|
||||
item_box.color = Color(1, 0.6, 0.2, 1)
|
||||
|
||||
func set_item_color(color: Color) -> void:
|
||||
item_box.color = color
|
||||
|
||||
func set_hover(hovered: bool) -> void:
|
||||
is_hovered = hovered
|
||||
hover_highlight.visible = hovered
|
||||
if hovered:
|
||||
item_box.color = item_box.color.lightened(0.2)
|
||||
else:
|
||||
var def = InventoryManager.get_item_definition(item_id)
|
||||
if def:
|
||||
item_box.color = Color(1, 0.6, 0.2, 1)
|
||||
else:
|
||||
item_box.color = Color(0.8, 0.8, 0.8, 1)
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
print("[SLOT:%s] _gui_input: %s" % [item_id, event])
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == 1 and event.pressed:
|
||||
print("[SLOT:%s] emitting clicked" % item_id)
|
||||
clicked.emit(item_id)
|
||||
elif event.button_index == 2:
|
||||
right_clicked.emit(item_id)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
var rect = get_global_rect()
|
||||
var mouse_pos = get_global_mouse_position()
|
||||
var was_hovered = is_hovered
|
||||
is_hovered = rect.has_point(mouse_pos)
|
||||
if is_hovered != was_hovered:
|
||||
set_hover(is_hovered)
|
||||
if is_hovered:
|
||||
hovered.emit(item_id)
|
||||
else:
|
||||
unhovered.emit(item_id)
|
||||
@@ -1 +0,0 @@
|
||||
uid://oegm753jbl9m
|
||||
@@ -1,32 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://c7depvvxf5s6l"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://oegm753jbmam" path="res://inventory/inventory_overlay/InventorySlot.gd" id="1"]
|
||||
|
||||
[node name="InventorySlot" type="Control" unique_id=2000000001]
|
||||
custom_minimum_size = Vector2(64, 64)
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="ItemBox" type="ColorRect" parent="." unique_id=2000000002]
|
||||
layout_mode = 1
|
||||
anchors_preset = -1
|
||||
anchor_left = 0.062
|
||||
anchor_top = 0.062
|
||||
anchor_right = 0.938
|
||||
anchor_bottom = 0.938
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
color = Color(1, 0.6, 0.2, 1)
|
||||
|
||||
[node name="HoverHighlight" type="ColorRect" parent="." unique_id=2000000003]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 1
|
||||
color = Color(1, 1, 1, 0.3)
|
||||
@@ -1 +0,0 @@
|
||||
uid://1esl88fgtd2p6
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"item_id": "splash",
|
||||
"name": "Splash",
|
||||
"icon": "res://splash.png",
|
||||
"combination_category": "potion"
|
||||
}
|
||||
@@ -23,7 +23,7 @@ config/icon="res://icon.png"
|
||||
|
||||
ActionState="*res://ActionState.gd"
|
||||
CameraTransition="*res://camera_transition.tscn"
|
||||
InventoryManager="*res://inventory/InventoryManager.gd"
|
||||
McpInteractionServer="*uid://dovjioj1jyqpp"
|
||||
|
||||
[display]
|
||||
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
extends Scene
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
var def = ItemDefinition.new()
|
||||
def.id = "splash"
|
||||
def.name = "Splash"
|
||||
def.icon = load("res://splash.png")
|
||||
def.combination_category = "potion"
|
||||
InventoryManager.register_item(def)
|
||||
give_item("splash")
|
||||
|
||||
|
||||
func _on_meadow_interacted() -> void:
|
||||
$kq4_002_meadow.default_script(self)
|
||||
|
||||
|
||||
4421
scripts/mcp_interaction_server.gd
Normal file
4421
scripts/mcp_interaction_server.gd
Normal file
File diff suppressed because it is too large
Load Diff
1
scripts/mcp_interaction_server.gd.uid
Normal file
1
scripts/mcp_interaction_server.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dovjioj1jyqpp
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c1f66a4950146f2127b1e1c306a7034a155f64cbdc77bddc1810928e4d9de164
|
||||
size 208
|
||||
@@ -1,40 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cxu3klicoldpv"
|
||||
path="res://.godot/imported/splash.png-929ed8a00b89ba36c51789452f874c77.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://splash.png"
|
||||
dest_files=["res://.godot/imported/splash.png-929ed8a00b89ba36c51789452f874c77.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Reference in New Issue
Block a user