From 7612bb12ef11486cdfadabcf53335ed5c7821b4c Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Wed, 3 Jun 2026 15:21:10 +0100 Subject: [PATCH] Add a LoreQuest subclass of Quest There are some properties that only make sense on LoreQuests, and we treat them differently in the game state. Rather than using `_validate_property` magic to hide those properties on quests that don't have the is_lore_quest property set, make a subclass, update all quest resources for lorequests to be LoreQuest instances instead, and adjust code that uses these LoreQuest-specific properties. Arguably the authors/affiliation properties of Quest only make sense for StoryQuests, so perhaps those should have a subclass, but that's one for another day. --- scenes/globals/game_state/game_state.gd | 4 ++-- scenes/globals/game_state/quest_state.gd | 4 ++-- scenes/globals/pause/pause_overlay.gd | 7 +++--- .../menus/storybook/components/lore_quest.gd | 24 +++++++++++++++++++ .../storybook/components/lore_quest.gd.uid | 1 + scenes/menus/storybook/components/quest.gd | 22 +---------------- .../quests/lore_quests/quest_000/quest.tres | 9 ++++--- .../quests/lore_quests/quest_001/quest.tres | 5 ++-- .../quests/lore_quests/quest_002/quest.tres | 5 ++-- .../quests/lore_quests/quest_003/quest.tres | 5 ++-- .../quests/lore_quests/quest_004/quest.tres | 5 ++-- 11 files changed, 45 insertions(+), 46 deletions(-) create mode 100644 scenes/menus/storybook/components/lore_quest.gd create mode 100644 scenes/menus/storybook/components/lore_quest.gd.uid diff --git a/scenes/globals/game_state/game_state.gd b/scenes/globals/game_state/game_state.gd index a38f6f5ae..fb1bf5c36 100644 --- a/scenes/globals/game_state/game_state.gd +++ b/scenes/globals/game_state/game_state.gd @@ -96,7 +96,7 @@ func start_quest(new_quest: Quest) -> void: global.clear_inventory() var quest_player_state: PlayerState - if new_quest.is_lore_quest: + if new_quest is LoreQuest: # Duplicate the current global player state. If the quest is completed, # it will be copied back; if abandoned, it will be discarded. quest_player_state = global.player.duplicate() @@ -146,7 +146,7 @@ func set_scene(scene_path: String, spawn_point: NodePath = ^"") -> void: func mark_quest_completed() -> void: assert(quest) - if quest.quest.is_lore_quest: + if quest.quest is LoreQuest: # Copy quest abilities to game abilities. global.player = quest.player diff --git a/scenes/globals/game_state/quest_state.gd b/scenes/globals/game_state/quest_state.gd index 5af0c7611..0ab463ba6 100644 --- a/scenes/globals/game_state/quest_state.gd +++ b/scenes/globals/game_state/quest_state.gd @@ -25,8 +25,8 @@ extends Resource get = get_challenge_start_scene, set = set_challenge_start_scene -## Player state within [member quest]. If [member Quest.is_lore_quest] is -## [code]true[/code], this will be initialised as a copy of [member +## Player state within [member quest]. If the quest is a [LoreQuest], +## this will be initialised as a copy of [member ## GlobalState.player], and propagated back to [GlobalState] if the quest is ## completed. For StoryQuests, this is a fresh state at the start of the quest ## and is discarded at the end. diff --git a/scenes/globals/pause/pause_overlay.gd b/scenes/globals/pause/pause_overlay.gd index 60bbda7a8..89c8d3e0e 100644 --- a/scenes/globals/pause/pause_overlay.gd +++ b/scenes/globals/pause/pause_overlay.gd @@ -33,7 +33,7 @@ func toggle_pause() -> void: if not GameState.quest: skip_tutorial_button.hide() abandon_quest_button.hide() - elif GameState.quest.quest.skippable: + elif GameState.quest.quest is LoreQuest and GameState.quest.quest.skippable: skip_tutorial_button.show() abandon_quest_button.hide() else: @@ -53,9 +53,8 @@ func _on_abandon_quest_pressed() -> void: func _on_skip_tutorial_pressed() -> void: toggle_pause() - assert(GameState.quest) - assert(GameState.quest.quest) - for ability: Enums.PlayerAbilities in GameState.quest.quest.skip_abilities: + var lq := GameState.quest.quest as LoreQuest + for ability: Enums.PlayerAbilities in lq.skip_abilities: GameState.player.set_ability(ability, true) GameState.mark_quest_completed() SceneSwitcher.change_to_file_with_transition( diff --git a/scenes/menus/storybook/components/lore_quest.gd b/scenes/menus/storybook/components/lore_quest.gd new file mode 100644 index 000000000..2393862d5 --- /dev/null +++ b/scenes/menus/storybook/components/lore_quest.gd @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +@tool +class_name LoreQuest +extends Quest +## A quest that is part of the main game lore. + +## Whether this quest is skippable (i.e. is the tutorial) +@export var skippable: bool = false: + set(new_value): + skippable = new_value + notify_property_list_changed() + +## Which abilities to award if the quest is skipped +@export var skip_abilities: Array[Enums.PlayerAbilities] = [] + + +func _validate_property(property: Dictionary) -> void: + super._validate_property(property) + + match property["name"]: + "skip_abilities": + if not skippable: + property.usage = PROPERTY_USAGE_NONE diff --git a/scenes/menus/storybook/components/lore_quest.gd.uid b/scenes/menus/storybook/components/lore_quest.gd.uid new file mode 100644 index 000000000..9240585d3 --- /dev/null +++ b/scenes/menus/storybook/components/lore_quest.gd.uid @@ -0,0 +1 @@ +uid://b23ralr2qbarg diff --git a/scenes/menus/storybook/components/quest.gd b/scenes/menus/storybook/components/quest.gd index bdcc31fb9..80c222c47 100644 --- a/scenes/menus/storybook/components/quest.gd +++ b/scenes/menus/storybook/components/quest.gd @@ -42,21 +42,6 @@ const FILENAME := "quest.tres" ## [CollectibleItem]s in the quest. @export_range(0, 6, 1, "suffix:threads") var threads_to_collect: int = 3 -## Whether this is a lore quest (part of the main storyline). -@export var is_lore_quest: bool = false: - set(new_value): - is_lore_quest = new_value - notify_property_list_changed() - -## Whether this quest is skippable (i.e. is the tutorial). Only supported for lore quests. -@export var skippable: bool = false: - set(new_value): - skippable = new_value - notify_property_list_changed() - -## Which abilities to award if the quest is skipped -@export var skip_abilities: Array[Enums.PlayerAbilities] = [] - ## Optional dialogue to retell the adventures that occurred in the quest, ## when returning the magical threads to the loom. @export var retelling: DialogueResource @@ -99,15 +84,10 @@ func _validate_property(property: Dictionary) -> void: property.hint_string = ",".join(sprite_frames.get_animation_names()) else: property.usage |= PROPERTY_USAGE_READ_ONLY - "skippable": - if not is_lore_quest: - property.usage = PROPERTY_USAGE_NONE - "skip_abilities": - if not (is_lore_quest and skippable): - property.usage = PROPERTY_USAGE_NONE func _to_string() -> String: + # TODO: subclass name return '' % [resource_path, title] diff --git a/scenes/quests/lore_quests/quest_000/quest.tres b/scenes/quests/lore_quests/quest_000/quest.tres index fc8f4a07e..9a4e4986f 100644 --- a/scenes/quests/lore_quests/quest_000/quest.tres +++ b/scenes/quests/lore_quests/quest_000/quest.tres @@ -1,12 +1,11 @@ -[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://0dcffjdxn6g2"] +[gd_resource type="Resource" script_class="LoreQuest" format=3 uid="uid://0dcffjdxn6g2"] -[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_036vf"] +[ext_resource type="Script" uid="uid://b23ralr2qbarg" path="res://scenes/menus/storybook/components/lore_quest.gd" id="1_036vf"] [resource] script = ExtResource("1_036vf") -title = "Tutorial" -first_scene = "uid://ck22vke6i1jyq" -is_lore_quest = true skippable = true skip_abilities = Array[int]([1, 16]) +title = "Tutorial" +first_scene = "uid://ck22vke6i1jyq" metadata/_custom_type_script = "uid://dts1hwdy3phin" diff --git a/scenes/quests/lore_quests/quest_001/quest.tres b/scenes/quests/lore_quests/quest_001/quest.tres index 534631efe..0c0b809c3 100644 --- a/scenes/quests/lore_quests/quest_001/quest.tres +++ b/scenes/quests/lore_quests/quest_001/quest.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://doovydomib7rj"] +[gd_resource type="Resource" script_class="LoreQuest" format=3 uid="uid://doovydomib7rj"] -[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_3ntet"] +[ext_resource type="Script" uid="uid://b23ralr2qbarg" path="res://scenes/menus/storybook/components/lore_quest.gd" id="1_3ntet"] [ext_resource type="Resource" uid="uid://cs8ay5uof8uap" path="res://scenes/quests/lore_quests/quest_001/quest_001_retelling.dialogue" id="1_8ruhb"] [ext_resource type="Texture2D" uid="uid://gcollkx3tkmm" path="res://scenes/quests/lore_quests/quest_001/Musician.png" id="2_xjg3h"] @@ -21,7 +21,6 @@ status = 1 title = "The Musician's Quest" description = "StoryWeaver meets a musician and a series of terrifying creatures." first_scene = "uid://7hoy2p14t6kc" -is_lore_quest = true retelling = ExtResource("1_8ruhb") sprite_frames = SubResource("SpriteFrames_8ruhb") animation_name = &"default" diff --git a/scenes/quests/lore_quests/quest_002/quest.tres b/scenes/quests/lore_quests/quest_002/quest.tres index 37764ab62..6805ac3ea 100644 --- a/scenes/quests/lore_quests/quest_002/quest.tres +++ b/scenes/quests/lore_quests/quest_002/quest.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://t50glay2iqhg"] +[gd_resource type="Resource" script_class="LoreQuest" format=3 uid="uid://t50glay2iqhg"] -[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_dlhxi"] +[ext_resource type="Script" uid="uid://b23ralr2qbarg" path="res://scenes/menus/storybook/components/lore_quest.gd" id="1_dlhxi"] [ext_resource type="Resource" uid="uid://uxwsefmegw3o" path="res://scenes/quests/lore_quests/quest_002/quest_002_retelling.dialogue" id="1_l1xe8"] [ext_resource type="Texture2D" uid="uid://csbhg24hsxecd" path="res://scenes/quests/lore_quests/quest_002/Void.png" id="2_a6kb4"] @@ -20,7 +20,6 @@ script = ExtResource("1_dlhxi") title = "The Void" description = "StoryWeaver runs away from a growing emptiness that spreads across the land, smothering and swallowing everything it covers." first_scene = "uid://bm4ewr8p48x0i" -is_lore_quest = true retelling = ExtResource("1_l1xe8") sprite_frames = SubResource("SpriteFrames_l1xe8") animation_name = &"default" diff --git a/scenes/quests/lore_quests/quest_003/quest.tres b/scenes/quests/lore_quests/quest_003/quest.tres index 9ff235018..3cddfed2c 100644 --- a/scenes/quests/lore_quests/quest_003/quest.tres +++ b/scenes/quests/lore_quests/quest_003/quest.tres @@ -1,6 +1,6 @@ -[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://dx8ew8remlcps"] +[gd_resource type="Resource" script_class="LoreQuest" format=3 uid="uid://dx8ew8remlcps"] -[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_onpv7"] +[ext_resource type="Script" uid="uid://b23ralr2qbarg" path="res://scenes/menus/storybook/components/lore_quest.gd" id="1_onpv7"] [resource] script = ExtResource("1_onpv7") @@ -8,5 +8,4 @@ title = "Placeholder" description = "A quest to hold scenes that don't yet have a permanent home." first_scene = "uid://d2ejk3qrh0fo3" threads_to_collect = 2 -is_lore_quest = true metadata/_custom_type_script = "uid://dts1hwdy3phin" diff --git a/scenes/quests/lore_quests/quest_004/quest.tres b/scenes/quests/lore_quests/quest_004/quest.tres index 462a59e11..2ce82779b 100644 --- a/scenes/quests/lore_quests/quest_004/quest.tres +++ b/scenes/quests/lore_quests/quest_004/quest.tres @@ -1,11 +1,10 @@ -[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://cgr54yigkbxp3"] +[gd_resource type="Resource" script_class="LoreQuest" format=3 uid="uid://cgr54yigkbxp3"] -[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_rp1q0"] +[ext_resource type="Script" uid="uid://b23ralr2qbarg" path="res://scenes/menus/storybook/components/lore_quest.gd" id="1_rp1q0"] [resource] script = ExtResource("1_rp1q0") title = "Mythical Meadows" first_scene = "uid://bsptn2yxyv22" threads_to_collect = 1 -is_lore_quest = true metadata/_custom_type_script = "uid://dts1hwdy3phin"