From 0e70c30a377beb6a4f3aa2fdc67cfe13c9d0eac1 Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Tue, 16 Jun 2026 09:41:03 +0100 Subject: [PATCH] Move inventory to QuestState Previously the inventory was stored in GlobalState, but was cleared when starting or abandoning a quest, because the only collectible items are the threads of Memory, Imagination, and Spirit that are found during quests. Move the inventory to the QuestState. Represent it as a separate resource. I anticipate that we may want a global inventory as well in future, with items moving from the QuestState to the GlobalState. But I also think it makes the code easier to read in both places. --- .../characters/npcs/elder/components/elder.gd | 5 +- .../components/collectible_item.gd | 4 +- .../eternal_loom/components/eternal_loom.gd | 16 +++--- scenes/globals/game_state/game_state.gd | 8 +-- scenes/globals/game_state/global_state.gd | 46 +--------------- scenes/globals/game_state/inventory_state.gd | 54 +++++++++++++++++++ .../globals/game_state/inventory_state.gd.uid | 1 + scenes/globals/game_state/quest_state.gd | 3 ++ .../components/closing_cinematic.dialogue | 2 +- .../el_ojo_revelador_outro.dialogue | 2 +- .../components/story_quest_progress.gd | 6 +-- .../world_map/components/frays_end.dialogue | 2 +- scenes/world_map/components/frays_end.gd | 5 +- 13 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 scenes/globals/game_state/inventory_state.gd create mode 100644 scenes/globals/game_state/inventory_state.gd.uid diff --git a/scenes/game_elements/characters/npcs/elder/components/elder.gd b/scenes/game_elements/characters/npcs/elder/components/elder.gd index 4f821ebcc8..2fe1491dce 100644 --- a/scenes/game_elements/characters/npcs/elder/components/elder.gd +++ b/scenes/game_elements/characters/npcs/elder/components/elder.gd @@ -41,8 +41,9 @@ func _ready() -> void: interact_area.interaction_ended.connect(_on_interaction_ended) animated_sprite_2d.connect("frame_changed", _on_frame_changed) - GameState.global.item_collected.connect(_update_dialogue_title) - GameState.global.item_consumed.connect(_update_dialogue_title) + if GameState.quest: + GameState.quest.inventory.item_collected.connect(_update_dialogue_title) + GameState.quest.inventory.item_consumed.connect(_update_dialogue_title) _update_dialogue_title() diff --git a/scenes/game_elements/props/collectible_item/components/collectible_item.gd b/scenes/game_elements/props/collectible_item/components/collectible_item.gd index 0503303dcf..1da2367426 100644 --- a/scenes/game_elements/props/collectible_item/components/collectible_item.gd +++ b/scenes/game_elements/props/collectible_item/components/collectible_item.gd @@ -5,7 +5,7 @@ class_name CollectibleItem extends SceneLink ## Overworld collectible that can be interacted with. When a player interacts -## with it, an [InventoryItem] is added to the [Inventory] +## with it, an [InventoryItem] is added to the [InventoryState]. ## Wether the collectible can be seen or collected. This allows the collectible ## to be placed in the scene even when some condition has to be met for it to @@ -93,7 +93,7 @@ func _on_interacted(player: Player, _from_right: bool) -> void: animation_player.play("collected") await animation_player.animation_finished - GameState.global.add_collected_item(item) + GameState.quest.inventory.add_collected_item(item) if collected_dialogue: DialogueManager.show_dialogue_balloon(collected_dialogue, dialogue_title, [self, player]) diff --git a/scenes/game_elements/props/eternal_loom/components/eternal_loom.gd b/scenes/game_elements/props/eternal_loom/components/eternal_loom.gd index 3032074e8d..e50b444d0b 100644 --- a/scenes/game_elements/props/eternal_loom/components/eternal_loom.gd +++ b/scenes/game_elements/props/eternal_loom/components/eternal_loom.gd @@ -35,7 +35,7 @@ func show_retelling_dialogue() -> void: func _has_magical_thread_of_type(type: InventoryItem.ItemType) -> bool: - for item: InventoryItem in GameState.global.inventory: + for item: InventoryItem in GameState.quest.inventory.items: if item.type == type: return true return false @@ -91,7 +91,7 @@ func give_spirit_upgrade() -> void: func on_offering_succeeded() -> void: loom_offering_animation_player.play(&"loom_offering") await loom_offering_animation_player.animation_finished - GameState.global.clear_inventory() + GameState.quest.inventory.clear_inventory() var elder: Elder = _find_elder(GameState.quest.quest) if elder: @@ -108,8 +108,10 @@ func has_retelling() -> bool: func is_item_offering_possible() -> bool: - return ( - GameState.quest - and GameState.quest.quest.threads_to_collect > 0 - and GameState.global.inventory.size() >= GameState.quest.quest.threads_to_collect - ) + if not GameState.quest: + return false + + if GameState.quest.quest.threads_to_collect <= 0: + return false + + return GameState.quest.inventory.items.size() >= GameState.quest.quest.threads_to_collect diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index fb1bf5c364..540a9eb2c2 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -88,13 +88,10 @@ func _ready() -> void: return -## Sets [member quest], sets up a new [PlayerState] if necessary, and clears the -## inventory. Note that this does not actually switch to the first scene of [param +## Sets [member quest], setting up a new [PlayerState] if necessary. +## Note that this does not actually switch to the first scene of [param ## new_quest]. func start_quest(new_quest: Quest) -> void: - # TODO: this suggests that the inventory should be part of QuestState - global.clear_inventory() - var quest_player_state: PlayerState if new_quest is LoreQuest: # Duplicate the current global player state. If the quest is completed, @@ -157,7 +154,6 @@ func mark_quest_completed() -> void: ## Abandon the current [member quest] without marking it as completed. func abandon_quest() -> void: quest = null - global.clear_inventory() ## Clear the persisted state. diff --git a/scenes/globals/game_state/global_state.gd b/scenes/globals/game_state/global_state.gd index 79d63e6d2e..2d1b04c015 100644 --- a/scenes/globals/game_state/global_state.gd +++ b/scenes/globals/game_state/global_state.gd @@ -4,41 +4,12 @@ class_name GlobalState extends Resource -## Emitted when a new item is collected. -signal item_collected(item: InventoryItem) - -## Emitted when a item is consumed, causing it to be removed from the -## [member inventory]. -signal item_consumed(item: InventoryItem) - ## Emitted when a quest is added or removed from [member completed_quests]. signal completed_quests_changed ## Emitted when the [member helper] changes. signal helper_changed -## Inventory of collected threads. Modify with [member add_collected_item] and -## [member clear_inventory]. -@export var inventory: Array[InventoryItem] - -## Types of items in [member inventory], for storage. Use [member inventory] -## in game code. -@export_storage var inventory_item_types: Array[String]: - get(): - var types: Array[String] - for item: InventoryItem in inventory: - types.append(InventoryItem.ItemType.keys()[item.type]) - return types - set(new_value): - var items: Array[InventoryItem] - for type_name: String in new_value: - if InventoryItem.ItemType.has(type_name): - items.append(InventoryItem.with_type(InventoryItem.ItemType[type_name])) - else: - push_warning("Ignoring unknown inventory item type: %s" % type_name) - inventory = items - emit_changed() - ## [Quest]s which the player has previously completed. Modify this with ## [method set_quest_completed_state]. @export var completed_quests: Array[Quest] @@ -74,25 +45,10 @@ signal helper_changed func _validate_property(property: Dictionary) -> void: match property.name: - "completed_quests", "inventory": + "completed_quests": property.usage &= ~PROPERTY_USAGE_STORAGE -## Add the [InventoryItem] to the [member inventory]. -func add_collected_item(item: InventoryItem) -> void: - inventory.append(item) - item_collected.emit(item) - emit_changed() - - -## Remove all [InventoryItem]s from the [member inventory]. -func clear_inventory() -> void: - for item: InventoryItem in inventory.duplicate(): - inventory.erase(item) - item_consumed.emit(item) - emit_changed() - - ## Updates [member completed_quests] to include [param quest] if [param ## is_completed] is true, or remove [param quest] if [param is_completed] is ## false. diff --git a/scenes/globals/game_state/inventory_state.gd b/scenes/globals/game_state/inventory_state.gd new file mode 100644 index 0000000000..d7f1b98928 --- /dev/null +++ b/scenes/globals/game_state/inventory_state.gd @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +@tool +class_name InventoryState +extends Resource + +## Emitted when a new item is collected and added to [member items]. +signal item_collected(item: InventoryItem) + +## Emitted when a item is consumed, causing it to be removed from +## [member items]. +signal item_consumed(item: InventoryItem) + +## Collected threads. Modify with [member add_collected_item] and +## [member clear_inventory]. +@export var items: Array[InventoryItem] + +## Types of items in [member inventory], for storage. Use [member inventory] +## in game code. +@export_storage var item_types: Array[String]: + get(): + var types: Array[String] + for item: InventoryItem in items: + types.append(InventoryItem.ItemType.keys()[item.type]) + return types + set(new_value): + items = [] + for type_name: String in new_value: + if InventoryItem.ItemType.has(type_name): + items.append(InventoryItem.with_type(InventoryItem.ItemType[type_name])) + else: + push_warning("Ignoring unknown inventory item type: %s" % type_name) + emit_changed() + + +## Add the [InventoryItem] to [member items]. +func add_collected_item(item: InventoryItem) -> void: + items.append(item) + item_collected.emit(item) + emit_changed() + + +## Remove all [InventoryItem]s from [member items]. +func clear_inventory() -> void: + for item: InventoryItem in items.duplicate(): + items.erase(item) + item_consumed.emit(item) + emit_changed() + + +func _validate_property(property: Dictionary) -> void: + match property.name: + "inventory": + property.usage &= ~PROPERTY_USAGE_STORAGE diff --git a/scenes/globals/game_state/inventory_state.gd.uid b/scenes/globals/game_state/inventory_state.gd.uid new file mode 100644 index 0000000000..07014a3680 --- /dev/null +++ b/scenes/globals/game_state/inventory_state.gd.uid @@ -0,0 +1 @@ +uid://bv4qgda3w0f0t diff --git a/scenes/globals/game_state/quest_state.gd b/scenes/globals/game_state/quest_state.gd index 4122e64987..55461406be 100644 --- a/scenes/globals/game_state/quest_state.gd +++ b/scenes/globals/game_state/quest_state.gd @@ -39,6 +39,9 @@ extends Resource ## quest is abandoned. @export var abandon_spawn_point: NodePath +## Inventory of collected threads. +@export var inventory := InventoryState.new() + func _init(q: Quest = null, p: PlayerState = null) -> void: quest = q diff --git a/scenes/quests/lore_quests/quest_002/4_closing_transition/components/closing_cinematic.dialogue b/scenes/quests/lore_quests/quest_002/4_closing_transition/components/closing_cinematic.dialogue index 63a4412210..f7940841c2 100644 --- a/scenes/quests/lore_quests/quest_002/4_closing_transition/components/closing_cinematic.dialogue +++ b/scenes/quests/lore_quests/quest_002/4_closing_transition/components/closing_cinematic.dialogue @@ -11,7 +11,7 @@ do animation_player.play("retreat") do animation_player.animation_finished Flaxfred: Stranger, you have helped us more than you know! Flaxfred: It feels like LinenVille’s spirit has come back to us. Here: take this thread as a parting gift. -do GameState.global.add_collected_item(InventoryItem.with_type(InventoryItem.ItemType.SPIRIT)) +do GameState.quest.inventory.add_collected_item(InventoryItem.with_type(InventoryItem.ItemType.SPIRIT)) do animation_player.play("leave") do wait(1.0) => END diff --git a/scenes/quests/story_quests/el_ojo_revelador/4_outro/outro_components/el_ojo_revelador_outro.dialogue b/scenes/quests/story_quests/el_ojo_revelador/4_outro/outro_components/el_ojo_revelador_outro.dialogue index 4543fc7882..c7f99db250 100644 --- a/scenes/quests/story_quests/el_ojo_revelador/4_outro/outro_components/el_ojo_revelador_outro.dialogue +++ b/scenes/quests/story_quests/el_ojo_revelador/4_outro/outro_components/el_ojo_revelador_outro.dialogue @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: The Threadbare Authors # SPDX-License-Identifier: MPL-2.0 ~ start -do GameState.global.add_collected_item(InventoryItem.with_type(InventoryItem.ItemType.SPIRIT)) +do GameState.quest.inventory.add_collected_item(InventoryItem.with_type(InventoryItem.ItemType.SPIRIT)) Jeremy, envuelto en polvo y con la ropa rota, mira salir el deslumbrante sol. Por primera vez en su vida , no se siente apurado ni ansioso por lo que venga después. Está disfrutando del silencio, el aire y la luz. El Ojo Revelador mostró su alma. le permitió diff --git a/scenes/ui_elements/story_quest_progress/components/story_quest_progress.gd b/scenes/ui_elements/story_quest_progress/components/story_quest_progress.gd index 6b1c828a1b..d7d65c01d3 100644 --- a/scenes/ui_elements/story_quest_progress/components/story_quest_progress.gd +++ b/scenes/ui_elements/story_quest_progress/components/story_quest_progress.gd @@ -29,13 +29,13 @@ func _ready() -> void: # On ready, the HUD is populated with the items that were collected so # far in the quest. - var items_collected := GameState.global.inventory + var items_collected := GameState.quest.inventory.items for i: int in min(items_collected.size(), n): items_container.get_child(i).start_as_filled(items_collected[i]) # Then, when each new item is collected, it is added to the progress UI - GameState.global.item_collected.connect(self._on_item_collected) - GameState.global.item_consumed.connect(self._on_item_consumed) + GameState.quest.inventory.item_collected.connect(self._on_item_collected) + GameState.quest.inventory.item_consumed.connect(self._on_item_consumed) func _on_helper_state_changed() -> void: diff --git a/scenes/world_map/components/frays_end.dialogue b/scenes/world_map/components/frays_end.dialogue index fcf5267cca..1d49cf05bb 100644 --- a/scenes/world_map/components/frays_end.dialogue +++ b/scenes/world_map/components/frays_end.dialogue @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MPL-2.0 ~ hotel_california # TODO: Uh oh, no ngettext() support in Dialogue Manager... -match GameState.global.inventory.size(): +match GameState.quest.inventory.items.size(): when 1: {{player_name}}: I’d better take this thread to the Eternal Loom first. else: diff --git a/scenes/world_map/components/frays_end.gd b/scenes/world_map/components/frays_end.gd index 7fe2ea658b..feb173ca95 100644 --- a/scenes/world_map/components/frays_end.gd +++ b/scenes/world_map/components/frays_end.gd @@ -11,8 +11,9 @@ extends Node2D func _ready() -> void: _update_story_quest_progress_visibility() - GameState.global.item_collected.connect(_update_story_quest_progress_visibility) - GameState.global.item_consumed.connect(_update_story_quest_progress_visibility) + if GameState.quest: + GameState.quest.inventory.item_collected.connect(_update_story_quest_progress_visibility) + GameState.quest.inventory.item_consumed.connect(_update_story_quest_progress_visibility) func _update_story_quest_progress_visibility(_item: InventoryItem = null) -> void: