From 97f141803abe4eee68b3a9eea324763238706d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 08:57:58 -0300 Subject: [PATCH 01/13] Add inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompted in Claude Code (Opus): > context: we want a simple inventory management system that allows players to store items they’ve collected in-world or via quests. > > create a new godot scene for the inventory UI, using a control node as root. It should have a "trinkets" category for now, and support > more categories in the future. Each category should scale to 10s (not 1000s) of items. This new scene should be a component that can be > included (instantiated) in other UI scenes, like the pause menu. Plan output: The project needs a simple inventory management system for items players collect in-world or via quests. The existing `InventoryItem` system (MEMORY, IMAGINATION, SPIRIT) is specifically for quest threads and gets cleared between quests. This new system is for persistent collectibles organized by category, starting with "trinkets." Create a new inventory UI component scene (Control root) following the same patterns as the Options scene — a standalone component that can be instanced into other UI scenes like the pause menu. **1. `scenes/ui_elements/inventory/components/trinket.gd`** — Trinket resource class ``` class_name Trinket extends Resource @export var id: StringName # unique identifier @export var name: String @export var description: String @export var icon: Texture2D ``` **2. `scenes/ui_elements/inventory/components/inventory.gd`** — Script for the UI scene - Extends `PanelContainer` (matches Options pattern — centered panel with theme) - Emits `signal back` (same as Options, so parent can handle navigation) - On `_ready()`, connects to `GameState.trinket_collected` / `trinket_removed` signals - Reads `GameState.trinkets` to populate the grid - Uses a `TabBar` for category switching (trinkets tab only for now; easy to add more) - Uses a `GridContainer` to display items (appropriate for 10s of items) - Each grid cell is a `TextureRect` showing the trinket icon - Back button with `FlatButton` theme variation and left arrow icon, same as Options **3. `scenes/ui_elements/inventory/inventory.tscn`** — The scene file Node tree: ``` Inventory (PanelContainer) # root, themed, script attached └─ VBoxContainer ├─ TabBar # category tabs ("Trinkets" for now) ├─ ScrollContainer # scrollable area for items │ └─ GridContainer # grid of item icons └─ BackButton (Button) # FlatButton, emits back signal ``` - Theme: `res://scenes/ui_elements/components/theme.tres` - `unique_name_in_owner = true` on BackButton for `%BackButton` access **4. `scenes/globals/game_state/game_state.gd`** — Add trinket tracking Add near existing inventory signals/properties: - `signal trinket_collected(trinket: Trinket)` - `signal trinket_removed(trinket: Trinket)` - `var trinkets: Array[Trinket] = []` - `func add_trinket(trinket: Trinket) -> void` — appends, emits signal, saves - `func remove_trinket(trinket: Trinket) -> void` — erases, emits signal, saves - `func has_trinket(id: StringName) -> bool` - Persistence: save/restore trinket IDs in the ConfigFile (new section `"trinkets"`) - Trinkets persist independently from quest state (not cleared by `start_quest` or `abandon_quest`) - Open the inventory scene directly in the Godot editor (F6) — it should display with the themed panel and tab bar - Can be instanced into pause_overlay.tscn later as a new tab, same pattern as Options Co-Authored-By: Claude Opus 4.6 --- scenes/globals/game_state/game_state.gd | 49 +++++++++++++++++ .../inventory/components/inventory.gd | 54 +++++++++++++++++++ .../inventory/components/inventory.gd.uid | 1 + .../inventory/components/trinket.gd | 9 ++++ .../inventory/components/trinket.gd.uid | 1 + scenes/ui_elements/inventory/inventory.tscn | 45 ++++++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 scenes/ui_elements/inventory/components/inventory.gd create mode 100644 scenes/ui_elements/inventory/components/inventory.gd.uid create mode 100644 scenes/ui_elements/inventory/components/trinket.gd create mode 100644 scenes/ui_elements/inventory/components/trinket.gd.uid create mode 100644 scenes/ui_elements/inventory/inventory.tscn diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index 3de919ce63..44f551edff 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -18,6 +18,12 @@ signal collected_items_changed(updated_items: Array[InventoryItem]) ## Emitted when the player's lives change. signal lives_changed(new_lives: int) +## Emitted when a [Trinket] is added to the collection. +signal trinket_collected(trinket: Trinket) + +## Emitted when a [Trinket] is removed from the collection. +signal trinket_removed(trinket: Trinket) + ## Emitted when it becomes too dark that artificial lights can turn on, or ## when darkness goes away so artificial lights should turn off. signal lights_changed(lights_on: bool, immediate: bool) @@ -37,6 +43,8 @@ const GLOBAL_SECTION := "global" const GLOBAL_INCORPORATING_THREADS_KEY := "incorporating_threads" const COMPLETED_QUESTS_KEY := "completed_quests" const LIVES_KEY := "current_lives" +const TRINKETS_SECTION := "trinkets" +const TRINKETS_IDS_KEY := "collected_ids" const MAX_LIVES := 0x7fffffffffffffff const DEBUG_LIVES := false @@ -51,6 +59,9 @@ const TRANSIENT_SCENES := [ @export var inventory: Array[InventoryItem] = [] @export var current_spawn_point: NodePath +## Trinkets the player has collected. Persisted independently from quest state. +var trinkets: Array[Trinket] = [] + ## Current number of lives the player has. var current_lives: int = MAX_LIVES @@ -277,6 +288,37 @@ func items_collected() -> Array[InventoryItem]: return inventory.duplicate() +## Add a [Trinket] to the collection if not already present. +func add_trinket(trinket: Trinket) -> void: + if has_trinket(trinket.id): + return + trinkets.append(trinket) + trinket_collected.emit(trinket) + _update_trinkets_state() + _save() + + +## Remove a [Trinket] from the collection. +func remove_trinket(trinket: Trinket) -> void: + trinkets.erase(trinket) + trinket_removed.emit(trinket) + _update_trinkets_state() + _save() + + +## Returns [code]true[/code] if a trinket with the given [param id] has been collected. +func has_trinket(id: StringName) -> bool: + return trinkets.any(func(t: Trinket) -> bool: return t.id == id) + + +func _update_trinkets_state() -> void: + _state.set_value( + TRINKETS_SECTION, + TRINKETS_IDS_KEY, + trinkets.map(func(t: Trinket) -> StringName: return t.id) + ) + + func _update_inventory_state() -> void: _state.set_value( INVENTORY_SECTION, @@ -363,6 +405,13 @@ func restore() -> Dictionary: ) completed_quests = _state.get_value(GLOBAL_SECTION, COMPLETED_QUESTS_KEY, [] as Array[String]) + # Restore trinkets from saved IDs + trinkets.clear() + for trinket_id: StringName in _state.get_value(TRINKETS_SECTION, TRINKETS_IDS_KEY, []): + var trinket := Trinket.new() + trinket.id = trinket_id + trinkets.append(trinket) + # Restore lives from saved state, default to MAX_LIVES if not found current_lives = _state.get_value(GLOBAL_SECTION, LIVES_KEY, MAX_LIVES) if DEBUG_LIVES: diff --git a/scenes/ui_elements/inventory/components/inventory.gd b/scenes/ui_elements/inventory/components/inventory.gd new file mode 100644 index 0000000000..e8230f81d6 --- /dev/null +++ b/scenes/ui_elements/inventory/components/inventory.gd @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +extends PanelContainer + +signal back + +@onready var tab_bar: TabBar = %TabBar +@onready var items_grid: GridContainer = %ItemsGrid +@onready var back_button: Button = %BackButton + + +func _ready() -> void: + _populate_trinkets() + GameState.trinket_collected.connect(_on_trinket_collected) + GameState.trinket_removed.connect(_on_trinket_removed) + _on_visibility_changed() + + +func _on_visibility_changed() -> void: + if visible and back_button: + back_button.grab_focus() + + +func _populate_trinkets() -> void: + _clear_grid() + for trinket: Trinket in GameState.trinkets: + _add_trinket_icon(trinket) + + +func _add_trinket_icon(trinket: Trinket) -> void: + var icon := TextureRect.new() + icon.texture = trinket.icon + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.custom_minimum_size = Vector2(64, 64) + icon.tooltip_text = trinket.name + items_grid.add_child(icon) + + +func _clear_grid() -> void: + for child: Node in items_grid.get_children(): + child.queue_free() + + +func _on_trinket_collected(trinket: Trinket) -> void: + _add_trinket_icon(trinket) + + +func _on_trinket_removed(_trinket: Trinket) -> void: + _populate_trinkets() + + +func _on_back_button_pressed() -> void: + back.emit() diff --git a/scenes/ui_elements/inventory/components/inventory.gd.uid b/scenes/ui_elements/inventory/components/inventory.gd.uid new file mode 100644 index 0000000000..31d3fcf724 --- /dev/null +++ b/scenes/ui_elements/inventory/components/inventory.gd.uid @@ -0,0 +1 @@ +uid://ipkyvsxlcpiv diff --git a/scenes/ui_elements/inventory/components/trinket.gd b/scenes/ui_elements/inventory/components/trinket.gd new file mode 100644 index 0000000000..c7dcb978d3 --- /dev/null +++ b/scenes/ui_elements/inventory/components/trinket.gd @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +class_name Trinket +extends Resource + +@export var id: StringName +@export var name: String +@export var description: String +@export var icon: Texture2D diff --git a/scenes/ui_elements/inventory/components/trinket.gd.uid b/scenes/ui_elements/inventory/components/trinket.gd.uid new file mode 100644 index 0000000000..7e5cb19d5d --- /dev/null +++ b/scenes/ui_elements/inventory/components/trinket.gd.uid @@ -0,0 +1 @@ +uid://pih63r764oet diff --git a/scenes/ui_elements/inventory/inventory.tscn b/scenes/ui_elements/inventory/inventory.tscn new file mode 100644 index 0000000000..c2edb9fad5 --- /dev/null +++ b/scenes/ui_elements/inventory/inventory.tscn @@ -0,0 +1,45 @@ +[gd_scene format=3] + +[ext_resource type="Theme" uid="uid://cvitou84ni7qe" path="res://scenes/ui_elements/components/theme.tres" id="1_theme"] +[ext_resource type="Script" path="res://scenes/ui_elements/inventory/components/inventory.gd" id="2_script"] +[ext_resource type="Texture2D" uid="uid://xe25gqovxxpe" path="res://assets/first_party/icons/left_arrow.png" id="3_arrow"] + +[node name="Inventory" type="PanelContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_theme") +script = ExtResource("2_script") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="TabBar" type="TabBar" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +tab_count = 1 +tab_0/title = "Trinkets" + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ItemsGrid" type="GridContainer" parent="VBoxContainer/ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +columns = 5 + +[node name="BackButton" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 4 +theme_type_variation = &"FlatButton" +text = "Back" +icon = ExtResource("3_arrow") +flat = true + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="pressed" from="VBoxContainer/BackButton" to="." method="_on_back_button_pressed"] From 2b6481a98b3842a22d7fd35dc7174c094dd6b536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 09:12:07 -0300 Subject: [PATCH 02/13] Instance inventory in pause overlay Prompt: > instance the inventory scene just created in the pause overlay, between "resume" and "abandon quest" Co-Authored-By: Claude Opus 4.6 --- scenes/globals/pause/pause_overlay.gd | 10 ++++++++++ scenes/globals/pause/pause_overlay.tscn | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/scenes/globals/pause/pause_overlay.gd b/scenes/globals/pause/pause_overlay.gd index 8f5a99e4cf..12116f28eb 100644 --- a/scenes/globals/pause/pause_overlay.gd +++ b/scenes/globals/pause/pause_overlay.gd @@ -7,6 +7,7 @@ extends CanvasLayer @onready var pause_menu: Control = %PauseMenu @onready var resume_button: Button = %ResumeButton +@onready var inventory: Control = %Inventory @onready var options: Control = %Options @onready var abandon_quest_button: Button = %AbandonQuestButton @@ -40,6 +41,15 @@ func _on_abandon_quest_pressed() -> void: ) +func _on_inventory_button_pressed() -> void: + inventory.show() + + +func _on_inventory_back() -> void: + pause_menu.show() + resume_button.grab_focus() + + func _on_options_button_pressed() -> void: options.show() diff --git a/scenes/globals/pause/pause_overlay.tscn b/scenes/globals/pause/pause_overlay.tscn index 0d14e8c211..aa78ebec98 100644 --- a/scenes/globals/pause/pause_overlay.tscn +++ b/scenes/globals/pause/pause_overlay.tscn @@ -4,6 +4,7 @@ [ext_resource type="Texture2D" uid="uid://lg5dl13njsg3" path="res://assets/first_party/tiles/Grass_And_Sand_Tiles.png" id="2_1tcw0"] [ext_resource type="Theme" uid="uid://cvitou84ni7qe" path="res://scenes/ui_elements/components/theme.tres" id="2_sd5t1"] [ext_resource type="PackedScene" uid="uid://dkeb0yjgcfi86" path="res://scenes/menus/options/options.tscn" id="3_sd5t1"] +[ext_resource type="PackedScene" path="res://scenes/ui_elements/inventory/inventory.tscn" id="4_inv"] [ext_resource type="PackedScene" uid="uid://56ja3m4283wa" path="res://scenes/menus/debug/debug_settings.tscn" id="5_ai8ue"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1tcw0"] @@ -68,6 +69,13 @@ size_flags_horizontal = 4 theme_type_variation = &"FlatButton" text = "Resume" +[node name="InventoryButton" type="Button" parent="TabContainer/PauseMenu/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 4 +theme_type_variation = &"FlatButton" +text = "Inventory" + [node name="AbandonQuestButton" type="Button" parent="TabContainer/PauseMenu/VBoxContainer" unique_id=1859424584] unique_name_in_owner = true layout_mode = 2 @@ -96,6 +104,12 @@ size_flags_horizontal = 4 theme_type_variation = &"FlatButton" text = "Exit to Title" +[node name="Inventory" parent="TabContainer" instance=ExtResource("4_inv")] +unique_name_in_owner = true +process_mode = 2 +visible = false +layout_mode = 2 + [node name="Options" parent="TabContainer" unique_id=2078715393 instance=ExtResource("3_sd5t1")] unique_name_in_owner = true process_mode = 2 @@ -108,6 +122,8 @@ layout_mode = 2 metadata/_tab_index = 2 [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/ResumeButton" to="." method="_on_resume_button_pressed"] +[connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/InventoryButton" to="." method="_on_inventory_button_pressed"] +[connection signal="back" from="TabContainer/Inventory" to="." method="_on_inventory_back"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/AbandonQuestButton" to="." method="_on_abandon_quest_pressed"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/DebugButton" to="TabContainer/DebugSettings" method="show"] From 337cdbad156c36de7253b0809db7d4acff0a2c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 09:42:22 -0300 Subject: [PATCH 03/13] grappling_hook_pins: Add test trinket --- .../2_grappling_hook/components/trinket.gd | 16 ++++++ .../components/trinket.gd.uid | 1 + .../2_grappling_hook/grappling_hook_pins.tscn | 57 +++++++++++++++---- 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd create mode 100644 scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd.uid diff --git a/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd b/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd new file mode 100644 index 0000000000..d959f861dd --- /dev/null +++ b/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +extends Node2D + +@export var trinket: Trinket + + +func _ready() -> void: + if GameState.has_trinket(trinket.id): + queue_free() + + +func _on_interact_area_interaction_started(area: InteractArea) -> void: + GameState.add_trinket(trinket) + queue_free() + area.end_interaction() diff --git a/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd.uid b/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd.uid new file mode 100644 index 0000000000..458a2dcce6 --- /dev/null +++ b/scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd.uid @@ -0,0 +1 @@ +uid://vaqmbvvfuxgp diff --git a/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn b/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn index cd55a362db..9d523d1153 100644 --- a/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn +++ b/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn @@ -17,11 +17,29 @@ [ext_resource type="SpriteFrames" uid="uid://bapks76u4hipj" path="res://scenes/game_elements/props/decoration/bush/components/bush_spriteframes_green_small.tres" id="13_udo54"] [ext_resource type="PackedScene" uid="uid://cfcgrfvtn04yp" path="res://scenes/ui_elements/hud/hud.tscn" id="14_nlj8l"] [ext_resource type="Script" uid="uid://d2m5qm4ranctd" path="res://scenes/quests/lore_quests/quest_002/2_grappling_hook/components/switch_mode_area.gd" id="15_8mj0p"] +[ext_resource type="Texture2D" uid="uid://b65dg7i548oip" path="res://scenes/game_elements/props/decoration/books/components/Books_1.png" id="16_8mj0p"] +[ext_resource type="Script" uid="uid://vaqmbvvfuxgp" path="res://scenes/quests/lore_quests/quest_002/2_grappling_hook/components/trinket.gd" id="16_tgh2l"] +[ext_resource type="PackedScene" uid="uid://dutgnbiy7xalb" path="res://scenes/game_elements/props/interact_area/interact_area.tscn" id="17_g02tq"] +[ext_resource type="Script" uid="uid://pih63r764oet" path="res://scenes/ui_elements/inventory/components/trinket.gd" id="17_mfk3e"] [ext_resource type="PackedScene" uid="uid://mruqy04d0vl8" path="res://scenes/quests/lore_quests/quest_002/2_grappling_hook/components/buttons_collector.tscn" id="18_udo54"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_8mj0p"] size = Vector2(131, 87) +[sub_resource type="Resource" id="Resource_8xcfu"] +script = ExtResource("17_mfk3e") +id = &"frith-fraywalkers-letter" +name = "Frith Fraywalker’s Letter" +description = "Found floating near the Tangled Temple, Ink Well" +icon = ExtResource("16_8mj0p") +metadata/_custom_type_script = "uid://pih63r764oet" + +[sub_resource type="CircleShape2D" id="CircleShape2D_tgh2l"] +radius = 64.0 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_mfk3e"] +size = Vector2(131, 87) + [sub_resource type="RectangleShape2D" id="RectangleShape2D_drlay"] size = Vector2(64, 128) @@ -195,18 +213,6 @@ position = Vector2(-409, 296) [node name="ButtonItem4" parent="OnTheGround/ButtonsB" unique_id=1852889363 instance=ExtResource("6_8mj0p")] position = Vector2(-450, 261) -[node name="ButtonItem5" parent="OnTheGround/ButtonsB" unique_id=761154544 instance=ExtResource("6_8mj0p")] -position = Vector2(-399, 689) - -[node name="ButtonItem6" parent="OnTheGround/ButtonsB" unique_id=544985972 instance=ExtResource("6_8mj0p")] -position = Vector2(-440, 654) - -[node name="ButtonItem7" parent="OnTheGround/ButtonsB" unique_id=1196174678 instance=ExtResource("6_8mj0p")] -position = Vector2(-354, 657) - -[node name="ButtonItem8" parent="OnTheGround/ButtonsB" unique_id=1632378500 instance=ExtResource("6_8mj0p")] -position = Vector2(-395, 622) - [node name="ButtonItem9" parent="OnTheGround/ButtonsB" unique_id=1704337129 instance=ExtResource("6_8mj0p")] position = Vector2(621, 332) @@ -336,6 +342,30 @@ script = ExtResource("15_8mj0p") position = Vector2(-1, 0) shape = SubResource("RectangleShape2D_8mj0p") +[node name="Trinket" type="Node2D" parent="OnTheGround" unique_id=1185665407] +position = Vector2(1376, 1888) +script = ExtResource("16_tgh2l") +trinket = SubResource("Resource_8xcfu") + +[node name="Books1" type="Sprite2D" parent="OnTheGround/Trinket" unique_id=629770298] +texture = ExtResource("16_8mj0p") + +[node name="InteractArea" parent="OnTheGround/Trinket" unique_id=248145530 instance=ExtResource("17_g02tq")] +interact_label_position = Vector2(0, -60) +action = "Pick" + +[node name="CollisionShape2D" type="CollisionShape2D" parent="OnTheGround/Trinket/InteractArea" unique_id=1898491232] +shape = SubResource("CircleShape2D_tgh2l") + +[node name="SwitchModeArea" type="Area2D" parent="." unique_id=321074376] +position = Vector2(1376, 1888) +collision_layer = 0 +script = ExtResource("15_8mj0p") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="SwitchModeArea" unique_id=1686379116] +position = Vector2(-1, 0) +shape = SubResource("RectangleShape2D_mfk3e") + [node name="TeleporterNext" parent="." unique_id=1560505035 instance=ExtResource("13_6ihfv")] position = Vector2(2759, 1281) next_scene = "uid://c3iv8nbog410p" @@ -358,3 +388,6 @@ metadata/_custom_type_script = "uid://0enyu5v4ra34" [connection signal="body_entered" from="OnTheGround/ButtonsCollector/SwitchModeArea" to="OnTheGround/ButtonsCollector/SwitchModeArea" method="_on_body_entered"] [connection signal="body_exited" from="OnTheGround/ButtonsCollector/SwitchModeArea" to="OnTheGround/ButtonsCollector/SwitchModeArea" method="_on_body_exited"] +[connection signal="interaction_started" from="OnTheGround/Trinket/InteractArea" to="OnTheGround/Trinket" method="_on_interact_area_interaction_started" flags=18 unbinds=2] +[connection signal="body_entered" from="SwitchModeArea" to="SwitchModeArea" method="_on_body_entered"] +[connection signal="body_exited" from="SwitchModeArea" to="SwitchModeArea" method="_on_body_exited"] From 17092cb1b0c3cd7bf913777547a99532eb18182f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 11:57:06 -0300 Subject: [PATCH 04/13] Add readable trinkets and a way to inspect them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt: > context: some trinkets should be readable. All trinkets in the inventory should be "inspected/viewed" by the player, leading either to a > zoomed-in version of the icon, or to “read” the item when they are readable. > add a new "full text" field to the trinket resource, and consider them readable when the field is not blank. Here's what changed: **`trinket.gd`** — Added `@export_multiline var full_text: String` and `is_readable() -> bool` (returns true when `full_text` is not empty). **`inventory.tscn`** — Added an `InspectPanel` (hidden by default) between the scroll area and back button, containing: - `InspectIcon` (TextureRect) — zoomed view for non-readable trinkets - `InspectText` (RichTextLabel) — shows full text for readable trinkets - `CloseButton` — returns to the grid **`inventory.gd`** — Grid items are now `TextureButton`s instead of `TextureRect`s. Pressing one hides the grid and shows the inspect panel with either the zoomed icon or the full text, depending on `trinket.is_readable()`. Co-Authored-By: Claude Opus 4.6 --- .../inventory/components/inventory.gd | 50 +++++++++++++++---- .../inventory/components/trinket.gd | 5 ++ scenes/ui_elements/inventory/inventory.tscn | 40 +++++++++++++++ 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/scenes/ui_elements/inventory/components/inventory.gd b/scenes/ui_elements/inventory/components/inventory.gd index e8230f81d6..e5782c8bc0 100644 --- a/scenes/ui_elements/inventory/components/inventory.gd +++ b/scenes/ui_elements/inventory/components/inventory.gd @@ -6,6 +6,11 @@ signal back @onready var tab_bar: TabBar = %TabBar @onready var items_grid: GridContainer = %ItemsGrid +@onready var scroll_container: ScrollContainer = %ScrollContainer +@onready var inspect_panel: PanelContainer = %InspectPanel +@onready var inspect_icon: TextureRect = %InspectIcon +@onready var inspect_text: RichTextLabel = %InspectText +@onready var close_button: Button = %CloseButton @onready var back_button: Button = %BackButton @@ -18,23 +23,44 @@ func _ready() -> void: func _on_visibility_changed() -> void: if visible and back_button: + _close_inspect() back_button.grab_focus() func _populate_trinkets() -> void: _clear_grid() for trinket: Trinket in GameState.trinkets: - _add_trinket_icon(trinket) + _add_trinket_button(trinket) -func _add_trinket_icon(trinket: Trinket) -> void: - var icon := TextureRect.new() - icon.texture = trinket.icon - icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - icon.custom_minimum_size = Vector2(64, 64) - icon.tooltip_text = trinket.name - items_grid.add_child(icon) +func _add_trinket_button(trinket: Trinket) -> void: + var button := TextureButton.new() + button.texture_normal = trinket.icon + button.ignore_texture_size = true + button.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED + button.custom_minimum_size = Vector2(64, 64) + button.tooltip_text = trinket.name + button.pressed.connect(_on_trinket_pressed.bind(trinket)) + items_grid.add_child(button) + + +func _on_trinket_pressed(trinket: Trinket) -> void: + inspect_icon.texture = trinket.icon + if trinket.is_readable(): + inspect_icon.visible = false + inspect_text.text = trinket.full_text + inspect_text.visible = true + else: + inspect_text.visible = false + inspect_icon.visible = true + scroll_container.visible = false + inspect_panel.visible = true + close_button.grab_focus() + + +func _close_inspect() -> void: + inspect_panel.visible = false + scroll_container.visible = true func _clear_grid() -> void: @@ -43,12 +69,16 @@ func _clear_grid() -> void: func _on_trinket_collected(trinket: Trinket) -> void: - _add_trinket_icon(trinket) + _add_trinket_button(trinket) func _on_trinket_removed(_trinket: Trinket) -> void: _populate_trinkets() +func _on_close_button_pressed() -> void: + _close_inspect() + + func _on_back_button_pressed() -> void: back.emit() diff --git a/scenes/ui_elements/inventory/components/trinket.gd b/scenes/ui_elements/inventory/components/trinket.gd index c7dcb978d3..44cb96e8c5 100644 --- a/scenes/ui_elements/inventory/components/trinket.gd +++ b/scenes/ui_elements/inventory/components/trinket.gd @@ -7,3 +7,8 @@ extends Resource @export var name: String @export var description: String @export var icon: Texture2D +@export_multiline var full_text: String + + +func is_readable() -> bool: + return not full_text.is_empty() diff --git a/scenes/ui_elements/inventory/inventory.tscn b/scenes/ui_elements/inventory/inventory.tscn index c2edb9fad5..d81c447f1e 100644 --- a/scenes/ui_elements/inventory/inventory.tscn +++ b/scenes/ui_elements/inventory/inventory.tscn @@ -23,6 +23,7 @@ tab_count = 1 tab_0/title = "Trinkets" [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 @@ -32,6 +33,44 @@ layout_mode = 2 size_flags_horizontal = 3 columns = 5 +[node name="InspectPanel" type="PanelContainer" parent="VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/InspectPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 16 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 16 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/InspectPanel/MarginContainer"] +layout_mode = 2 + +[node name="InspectIcon" type="TextureRect" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +expand_mode = 3 +stretch_mode = 5 + +[node name="InspectText" type="RichTextLabel" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +size_flags_vertical = 3 +fit_content = true +scroll_active = false + +[node name="CloseButton" type="Button" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 4 +theme_type_variation = &"FlatButton" +text = "Close" + [node name="BackButton" type="Button" parent="VBoxContainer"] unique_name_in_owner = true layout_mode = 2 @@ -42,4 +81,5 @@ icon = ExtResource("3_arrow") flat = true [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="pressed" from="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer/CloseButton" to="." method="_on_close_button_pressed"] [connection signal="pressed" from="VBoxContainer/BackButton" to="." method="_on_back_button_pressed"] From d00f7d59fff55aa81f7f869bacf729c1f67ae2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 12:00:56 -0300 Subject: [PATCH 05/13] grappling_hook_pins: Add full text for test trinket --- .../2_grappling_hook/grappling_hook_pins.tscn | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn b/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn index 9d523d1153..7b3c193d69 100644 --- a/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn +++ b/scenes/quests/lore_quests/quest_002/2_grappling_hook/grappling_hook_pins.tscn @@ -32,6 +32,35 @@ id = &"frith-fraywalkers-letter" name = "Frith Fraywalker’s Letter" description = "Found floating near the Tangled Temple, Ink Well" icon = ExtResource("16_8mj0p") +full_text = "- Type: Personal +- Date/Location: Found floating near the Tangled Temple, Ink Well +- Source: Frith Fraywalker’s final letter to the Storyweaver + +StoryWeaver, + +As I slowly succumb to this impenetrable darkness, sustained only by the fading glow of my ailing Wisp, I write to tell you of my time in The Void and all that I have learned. That you might find a way to defeat this scourge that stalks our lands is my last and only hope. + +Forgive my scrawl, for I write with the hand I never had to use, the only hand I have left. The other lost to the nothingness that consumes this place…and me. + +The overwhelming threat creeping through Threadbare is not a monster with teeth and claws, nor a thief with poison and a plan. It is not a storm of thunder and lightning, nor a tyrant with a crown and sword. It does not roar, scheme, strike, or march with an army at its back. + +No, The Void is not a beast, for beasts can be frightened, captured, or slain. It is not a thief, for thieves can be thwarted with puzzles and traps, or bargained with. It is not a storm, for storms pass, replaced by the sun. It is not a tyrant, for tyrants can be toppled, their armies in retreat and their thrones ablaze. + +It is neither awake nor asleep. Not alive nor dead. It does not think, care, wonder, or worry. It does not lie in wait. It is not watching you. It is not angry, or sad, or enjoying its rage across the fabric of this world. It is not desperate, for that implies desire. It is not patient, for that implies intent. + +The Void is not the beast at the gate, but the gate left unlocked, the watch unkept, the beacon unlit, the alarm unsung. + +The Void is unthinking and unfeeling. The very absence of culture, community, and care. The loss of memory. A lack of imagination. Broken spirit. Forgotten language. A scattered, fragmented humanity. + +The Void is not an other...The Void is us! + +The very act of writing these words is one of defiance, Storyweaver. Do you understand? To care, to connect, to create - these are the tenets of rebellion. Reading, writing, art, and song - the instruments of insurrection. Memory, Imagination and Spirit - the threads of resistance. + +And so, with these words, with this story, I resist. + +Until my dying light is dimmed, + +-- Frith Fraywalker" metadata/_custom_type_script = "uid://pih63r764oet" [sub_resource type="CircleShape2D" id="CircleShape2D_tgh2l"] From db6c6293404aa5b6225b97c9a7c8b0e6c6a0eaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 14:54:51 -0300 Subject: [PATCH 06/13] The inventory UI is too narrow. Make it wider. - Added `custom_minimum_size = Vector2(800, 600)` to the Inventory root `PanelContainer`, matching the debug settings UI dimensions. Co-Authored-By: Claude Opus 4.6 --- scenes/ui_elements/inventory/inventory.tscn | 1 + 1 file changed, 1 insertion(+) diff --git a/scenes/ui_elements/inventory/inventory.tscn b/scenes/ui_elements/inventory/inventory.tscn index d81c447f1e..f917c7e4d2 100644 --- a/scenes/ui_elements/inventory/inventory.tscn +++ b/scenes/ui_elements/inventory/inventory.tscn @@ -5,6 +5,7 @@ [ext_resource type="Texture2D" uid="uid://xe25gqovxxpe" path="res://assets/first_party/icons/left_arrow.png" id="3_arrow"] [node name="Inventory" type="PanelContainer"] +custom_minimum_size = Vector2(800, 600) anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 From eca8a37abab48690757fc86598dd2017af73c693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 15:03:09 -0300 Subject: [PATCH 07/13] Update pause_overlay and inventory scenes Claude Code created them. Opening these scenes in Godot and then saving them add unique IDs to the file. --- scenes/globals/pause/pause_overlay.tscn | 12 +++++---- scenes/ui_elements/inventory/inventory.tscn | 29 +++++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/scenes/globals/pause/pause_overlay.tscn b/scenes/globals/pause/pause_overlay.tscn index aa78ebec98..caf711a0d6 100644 --- a/scenes/globals/pause/pause_overlay.tscn +++ b/scenes/globals/pause/pause_overlay.tscn @@ -4,7 +4,7 @@ [ext_resource type="Texture2D" uid="uid://lg5dl13njsg3" path="res://assets/first_party/tiles/Grass_And_Sand_Tiles.png" id="2_1tcw0"] [ext_resource type="Theme" uid="uid://cvitou84ni7qe" path="res://scenes/ui_elements/components/theme.tres" id="2_sd5t1"] [ext_resource type="PackedScene" uid="uid://dkeb0yjgcfi86" path="res://scenes/menus/options/options.tscn" id="3_sd5t1"] -[ext_resource type="PackedScene" path="res://scenes/ui_elements/inventory/inventory.tscn" id="4_inv"] +[ext_resource type="PackedScene" uid="uid://fdedj5gjhhh3" path="res://scenes/ui_elements/inventory/inventory.tscn" id="4_inv"] [ext_resource type="PackedScene" uid="uid://56ja3m4283wa" path="res://scenes/menus/debug/debug_settings.tscn" id="5_ai8ue"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1tcw0"] @@ -69,7 +69,7 @@ size_flags_horizontal = 4 theme_type_variation = &"FlatButton" text = "Resume" -[node name="InventoryButton" type="Button" parent="TabContainer/PauseMenu/VBoxContainer"] +[node name="InventoryButton" type="Button" parent="TabContainer/PauseMenu/VBoxContainer" unique_id=551488208] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 4 @@ -104,29 +104,31 @@ size_flags_horizontal = 4 theme_type_variation = &"FlatButton" text = "Exit to Title" -[node name="Inventory" parent="TabContainer" instance=ExtResource("4_inv")] +[node name="Inventory" parent="TabContainer" unique_id=1305017818 instance=ExtResource("4_inv")] unique_name_in_owner = true process_mode = 2 visible = false layout_mode = 2 +metadata/_tab_index = 1 [node name="Options" parent="TabContainer" unique_id=2078715393 instance=ExtResource("3_sd5t1")] unique_name_in_owner = true process_mode = 2 visible = false layout_mode = 2 +metadata/_tab_index = 2 [node name="DebugSettings" parent="TabContainer" unique_id=1197092088 instance=ExtResource("5_ai8ue")] visible = false layout_mode = 2 -metadata/_tab_index = 2 +metadata/_tab_index = 3 [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/ResumeButton" to="." method="_on_resume_button_pressed"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/InventoryButton" to="." method="_on_inventory_button_pressed"] -[connection signal="back" from="TabContainer/Inventory" to="." method="_on_inventory_back"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/AbandonQuestButton" to="." method="_on_abandon_quest_pressed"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/DebugButton" to="TabContainer/DebugSettings" method="show"] [connection signal="pressed" from="TabContainer/PauseMenu/VBoxContainer/TitleScreenButton" to="." method="_on_title_screen_button_pressed"] +[connection signal="back" from="TabContainer/Inventory" to="." method="_on_inventory_back"] [connection signal="back" from="TabContainer/Options" to="." method="_on_options_back"] [connection signal="back" from="TabContainer/DebugSettings" to="." method="_on_options_back"] diff --git a/scenes/ui_elements/inventory/inventory.tscn b/scenes/ui_elements/inventory/inventory.tscn index f917c7e4d2..7a3af1c739 100644 --- a/scenes/ui_elements/inventory/inventory.tscn +++ b/scenes/ui_elements/inventory/inventory.tscn @@ -1,10 +1,10 @@ -[gd_scene format=3] +[gd_scene format=3 uid="uid://fdedj5gjhhh3"] [ext_resource type="Theme" uid="uid://cvitou84ni7qe" path="res://scenes/ui_elements/components/theme.tres" id="1_theme"] -[ext_resource type="Script" path="res://scenes/ui_elements/inventory/components/inventory.gd" id="2_script"] +[ext_resource type="Script" uid="uid://ipkyvsxlcpiv" path="res://scenes/ui_elements/inventory/components/inventory.gd" id="2_script"] [ext_resource type="Texture2D" uid="uid://xe25gqovxxpe" path="res://assets/first_party/icons/left_arrow.png" id="3_arrow"] -[node name="Inventory" type="PanelContainer"] +[node name="Inventory" type="PanelContainer" unique_id=1518531873] custom_minimum_size = Vector2(800, 600) anchors_preset = 15 anchor_right = 1.0 @@ -14,50 +14,51 @@ grow_vertical = 2 theme = ExtResource("1_theme") script = ExtResource("2_script") -[node name="VBoxContainer" type="VBoxContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=716636447] layout_mode = 2 -[node name="TabBar" type="TabBar" parent="VBoxContainer"] +[node name="TabBar" type="TabBar" parent="VBoxContainer" unique_id=935986579] unique_name_in_owner = true layout_mode = 2 +current_tab = 0 tab_count = 1 tab_0/title = "Trinkets" -[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer" unique_id=766950734] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 -[node name="ItemsGrid" type="GridContainer" parent="VBoxContainer/ScrollContainer"] +[node name="ItemsGrid" type="GridContainer" parent="VBoxContainer/ScrollContainer" unique_id=824202967] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 columns = 5 -[node name="InspectPanel" type="PanelContainer" parent="VBoxContainer"] +[node name="InspectPanel" type="PanelContainer" parent="VBoxContainer" unique_id=1837968640] unique_name_in_owner = true visible = false layout_mode = 2 size_flags_vertical = 3 -[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/InspectPanel"] +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/InspectPanel" unique_id=1290645998] layout_mode = 2 theme_override_constants/margin_left = 16 theme_override_constants/margin_top = 16 theme_override_constants/margin_right = 16 theme_override_constants/margin_bottom = 16 -[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/InspectPanel/MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/InspectPanel/MarginContainer" unique_id=1945517949] layout_mode = 2 -[node name="InspectIcon" type="TextureRect" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +[node name="InspectIcon" type="TextureRect" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer" unique_id=439455063] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 3 expand_mode = 3 stretch_mode = 5 -[node name="InspectText" type="RichTextLabel" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +[node name="InspectText" type="RichTextLabel" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer" unique_id=1340829475] unique_name_in_owner = true visible = false layout_mode = 2 @@ -65,14 +66,14 @@ size_flags_vertical = 3 fit_content = true scroll_active = false -[node name="CloseButton" type="Button" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer"] +[node name="CloseButton" type="Button" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer" unique_id=800569538] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 4 theme_type_variation = &"FlatButton" text = "Close" -[node name="BackButton" type="Button" parent="VBoxContainer"] +[node name="BackButton" type="Button" parent="VBoxContainer" unique_id=1566964954] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 4 From ae26b5c8c41d5b5b84fcf3ddb5808b58f720174c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 15:09:21 -0300 Subject: [PATCH 08/13] In the inventory, when inspecting a trinket with full text, the view doesn't scroll, so the text overflows. Make that text scrollable. - Removed `fit_content = true` and `scroll_active = false` from the `InspectText` RichTextLabel so it stays within its allocated space and uses its built-in scrollbar when content overflows. Co-Authored-By: Claude Opus 4.6 --- scenes/ui_elements/inventory/inventory.tscn | 2 -- 1 file changed, 2 deletions(-) diff --git a/scenes/ui_elements/inventory/inventory.tscn b/scenes/ui_elements/inventory/inventory.tscn index 7a3af1c739..1bad45c321 100644 --- a/scenes/ui_elements/inventory/inventory.tscn +++ b/scenes/ui_elements/inventory/inventory.tscn @@ -63,8 +63,6 @@ unique_name_in_owner = true visible = false layout_mode = 2 size_flags_vertical = 3 -fit_content = true -scroll_active = false [node name="CloseButton" type="Button" parent="VBoxContainer/InspectPanel/MarginContainer/VBoxContainer" unique_id=800569538] unique_name_in_owner = true From 0919773b3828894106dfbf99effe6056c1d64287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 15:34:44 -0300 Subject: [PATCH 09/13] The inventory should clear only when GameState.clear() is called. Commit 635e10b7 made the inventory clear in the wrong moments: when starting a quest and when abandoning a quest. - Removed `_do_clear_inventory()` and `_update_inventory_state()` from `start_quest()`. - Removed `clear_inventory()` from `abandon_quest()`. - Added `_do_clear_inventory()` and `trinkets.clear()` to `clear()`. Co-Authored-By: Claude Opus 4.6 --- scenes/globals/game_state/game_state.gd | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index 44f551edff..ecf9152103 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -120,8 +120,6 @@ func set_incorporating_threads(new_incorporating_threads: bool) -> void: ## Set [member current_quest] and clear the [member inventory]. ## Also resets lives to maximum when starting a quest. func start_quest(quest: Quest) -> void: - _do_clear_inventory() - _update_inventory_state() current_quest = quest _state.set_value(QUEST_SECTION, QUEST_PATH_KEY, quest.resource_path) _do_set_scene(quest.first_scene, ^"") @@ -245,7 +243,6 @@ func abandon_quest() -> void: set_incorporating_threads(false) _clear_quest_state() current_quest = null - clear_inventory() ## Updates [member completed_quests] to include [param quest] if [param @@ -370,6 +367,8 @@ func change_lights(new_lights_on: bool, immediate: bool = false) -> void: func clear() -> void: _state.clear() completed_quests = [] + _do_clear_inventory() + trinkets.clear() current_lives = MAX_LIVES if DEBUG_LIVES: prints("[LIVES DEBUG] State cleared. Lives reset to:", current_lives) From 3e8f71c736d04a18e019ffd146482fac3c7d1e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 15:40:28 -0300 Subject: [PATCH 10/13] When resuming a game, the inventory persisted is not loaded. Fix it. - Updated `_update_trinkets_state()` to save all trinket fields (`id`, `name`, `description`, `icon` resource path, `full_text`) as dictionaries instead of just IDs. - Updated `restore()` to reconstruct full `Trinket` objects from saved data, including loading the icon texture from its resource path. Co-Authored-By: Claude Opus 4.6 --- scenes/globals/game_state/game_state.gd | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index ecf9152103..749444572e 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -312,7 +312,16 @@ func _update_trinkets_state() -> void: _state.set_value( TRINKETS_SECTION, TRINKETS_IDS_KEY, - trinkets.map(func(t: Trinket) -> StringName: return t.id) + trinkets.map( + func(t: Trinket) -> Dictionary: + return { + "id": t.id, + "name": t.name, + "description": t.description, + "icon": t.icon.resource_path if t.icon else "", + "full_text": t.full_text, + } + ) ) @@ -404,11 +413,17 @@ func restore() -> Dictionary: ) completed_quests = _state.get_value(GLOBAL_SECTION, COMPLETED_QUESTS_KEY, [] as Array[String]) - # Restore trinkets from saved IDs + # Restore trinkets from saved data trinkets.clear() - for trinket_id: StringName in _state.get_value(TRINKETS_SECTION, TRINKETS_IDS_KEY, []): + for trinket_data: Dictionary in _state.get_value(TRINKETS_SECTION, TRINKETS_IDS_KEY, []): var trinket := Trinket.new() - trinket.id = trinket_id + trinket.id = trinket_data.get("id", &"") + trinket.name = trinket_data.get("name", "") + trinket.description = trinket_data.get("description", "") + var icon_path: String = trinket_data.get("icon", "") + if not icon_path.is_empty(): + trinket.icon = load(icon_path) + trinket.full_text = trinket_data.get("full_text", "") trinkets.append(trinket) # Restore lives from saved state, default to MAX_LIVES if not found From ecd5b777f15c746c0b0684916405ee68a88765d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 15:53:07 -0300 Subject: [PATCH 11/13] Restarting the game from the title screen main menu should clear the persisted inventory. Currently the GameState._do_clear_inventory() is being called instead, which only updates the running game without saving. - Replaced `_do_clear_inventory()` with `clear_inventory()` in `clear()`, so the inventory state is updated and saved, not just cleared in memory. Co-Authored-By: Claude Opus 4.6 --- scenes/globals/game_state/game_state.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index 749444572e..0dfdde152d 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -376,7 +376,7 @@ func change_lights(new_lights_on: bool, immediate: bool = false) -> void: func clear() -> void: _state.clear() completed_quests = [] - _do_clear_inventory() + clear_inventory() trinkets.clear() current_lives = MAX_LIVES if DEBUG_LIVES: From 4c6038eac6383fa2e48fc807b0d9b88336c33293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Fri, 27 Feb 2026 16:08:42 -0300 Subject: [PATCH 12/13] GameState: Fix parse error gdformat is adding a parse error here! The formatter is not being disabled by the commented lines introduced, so this is a commit bypassing the formatter with --no-verify --- scenes/globals/game_state/game_state.gd | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index 0dfdde152d..c07991a9aa 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -308,21 +308,22 @@ func has_trinket(id: StringName) -> bool: return trinkets.any(func(t: Trinket) -> bool: return t.id == id) +# gdformat:disable func _update_trinkets_state() -> void: _state.set_value( TRINKETS_SECTION, TRINKETS_IDS_KEY, trinkets.map( - func(t: Trinket) -> Dictionary: - return { - "id": t.id, - "name": t.name, - "description": t.description, - "icon": t.icon.resource_path if t.icon else "", - "full_text": t.full_text, - } + func(t: Trinket) -> Dictionary: return { + "id": t.id, + "name": t.name, + "description": t.description, + "icon": t.icon.resource_path if t.icon else "", + "full_text": t.full_text, + } ) ) +# gdformat:enable func _update_inventory_state() -> void: From f8e4ab55c4e2966ecc23f1589f3505d851a4bed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Qui=C3=B1ones?= Date: Mon, 2 Mar 2026 12:19:49 -0300 Subject: [PATCH 13/13] GameState: The lambda function inside trinkets.map(), make it a named function. Extract the lambda into a named _trinket_to_dict() function and simplify the _update_trinkets_state() call. Remove the gdformat:disable/enable comments that are no longer needed. Co-Authored-By: Claude Opus 4.6 --- scenes/globals/game_state/game_state.gd | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index c07991a9aa..62dc121e16 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -308,22 +308,18 @@ func has_trinket(id: StringName) -> bool: return trinkets.any(func(t: Trinket) -> bool: return t.id == id) -# gdformat:disable +func _trinket_to_dict(t: Trinket) -> Dictionary: + return { + "id": t.id, + "name": t.name, + "description": t.description, + "icon": t.icon.resource_path if t.icon else "", + "full_text": t.full_text, + } + + func _update_trinkets_state() -> void: - _state.set_value( - TRINKETS_SECTION, - TRINKETS_IDS_KEY, - trinkets.map( - func(t: Trinket) -> Dictionary: return { - "id": t.id, - "name": t.name, - "description": t.description, - "icon": t.icon.resource_path if t.icon else "", - "full_text": t.full_text, - } - ) - ) -# gdformat:enable + _state.set_value(TRINKETS_SECTION, TRINKETS_IDS_KEY, trinkets.map(_trinket_to_dict)) func _update_inventory_state() -> void: