Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class_name ThreadbareProjectSettings
extends Node

const DEBUG_ASPECT_RATIO = "threadbare/debugging/debug_aspect_ratio"
const SKIP_SOKOBANS = "threadbare/debugging/skip_sokobans"
const SKIP_SPLASH = "threadbare/debugging/skip_splash"

static var settings_configuration = {
Expand All @@ -15,12 +14,6 @@ static var settings_configuration = {
type = TYPE_BOOL,
hint_string = "Display a letterbox overlay in the game, to debug aspect ratio issues.",
},
SKIP_SOKOBANS:
{
value = false,
type = TYPE_BOOL,
hint_string = "Skip the sokobans from the core game loop, and complete the quest directly.",
},
SKIP_SPLASH:
{
value = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ var _random_number_generator := RandomNumberGenerator.new()

var _previous_look_at_side: Enums.LookAtSide = Enums.LookAtSide.UNSPECIFIED

## The character head node. This can be used to show the head only in the HUD.
@onready var head: AnimatedSprite2D = %AnimatedSprite2DHead


## Randomize the skin color and textures of the character.
## [br][br]
Expand Down Expand Up @@ -79,6 +82,7 @@ func randomize_character() -> void:
else:
character_seed = new_character_seed
apply_character_randomizations()
_randomize_all_sprites_progress()


func _ready() -> void:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
~ start
Do you want some help?
- Yes => help_accepted
- No => help_denied

~ help_accepted
# TODO: do something that helps the player here. Like:
# do fix_bridge()
Bye!
do GameState.global.helper.clear()
=> END

~ help_denied
OK, bye!
=> END
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[remap]

importer="dialogue_manager"
importer_version=15
type="Resource"
uid="uid://c667fv6ngpc7s"
path="res://.godot/imported/default_helper.dialogue-2d2de185b0afee791b6bd97e2398a0de.tres"

[deps]

source_file="res://scenes/game_elements/characters/npcs/components/default_helper.dialogue"
dest_files=["res://.godot/imported/default_helper.dialogue-2d2de185b0afee791b6bd97e2398a0de.tres"]

[params]

defaults=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
extends Node
## @experimental
##
## Makes a CharacterRandomizer a helper.
##
## If the global game state has help of the same type, the character becomes visible and
## interactible, using the persisted character seed and ready to help the player.
## Otherwise, it remains hidden and deactivated.
## The help itself must be implemented per scene on the instantiated character.

## The helper type to match.
@export var helper_type: InventoryItem.ItemType = InventoryItem.ItemType.MEMORY

## The CharacterRandomizer that will offer help.
@onready var character: CharacterRandomizer = get_parent()


func _ready() -> void:
GameState.global.helper.changed.connect(_on_helper_state_changed)
_on_helper_state_changed()


func _on_helper_state_changed() -> void:
var is_enabled := (
helper_type == GameState.global.helper.helper_type
and bool(GameState.global.helper.character_seed)
)
character.visible = is_enabled
character.process_mode = Node.PROCESS_MODE_INHERIT if is_enabled else Node.PROCESS_MODE_DISABLED
if is_enabled:
character.character_seed = GameState.global.helper.character_seed
character.apply_character_randomizations()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://diskln3jup064
1 change: 1 addition & 0 deletions scenes/game_elements/characters/npcs/townie.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ sprite = NodePath("..")
metadata/_custom_type_script = "uid://dy68p7gf07pi3"

[node name="AnimatedSprite2DHead" type="AnimatedSprite2D" parent="AnimatedSprite2DLegs/AnimatedSprite2DBody" unique_id=421503015]
unique_name_in_owner = true
material = ExtResource("1_nj51j")
sprite_frames = ExtResource("22_8wbfo")
animation = &"idle"
Expand Down
110 changes: 80 additions & 30 deletions scenes/game_elements/props/eternal_loom/components/eternal_loom.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@
class_name EternalLoom
extends Node2D

const ETERNAL_LOOM_INTERACTION: DialogueResource = preload("uid://yafw7bf362gh")
signal retelling_started
signal retelling_finished
signal give_retelling_upgrade(type: InventoryItem.ItemType)

## Scenes that are the first of three Sokoban puzzles. A random one will be used
## each time the player successfully interacts with the Loom.
const SOKOBANS := [
"uid://b8mywvmgsxqb",
"uid://11cdlcqge3fu",
"uid://b64uft76tbblp",
]
const ETERNAL_LOOM_INTERACTION: DialogueResource = preload("uid://yafw7bf362gh")

var elders: Array[Elder]

Expand All @@ -25,19 +21,6 @@ var _have_threads := is_item_offering_possible()
func _ready() -> void:
talk_behavior.dialogue = ETERNAL_LOOM_INTERACTION
talk_behavior.title = "have_threads" if _have_threads else "no_threads"
interact_area.interaction_ended.connect(self._on_interaction_ended)

if GameState.quest and GameState.quest.incorporating_threads:
if Transitions.is_running():
await Transitions.finished

var elder: Elder = _find_elder(GameState.quest.quest)
if elder:
await elder.congratulate_player()
else:
push_warning("Could not find elder for %s" % [GameState.quest.quest.resource_path])

GameState.mark_quest_completed()


func _find_elder(quest: Quest) -> Elder:
Expand All @@ -48,22 +31,89 @@ func _find_elder(quest: Quest) -> Elder:
return null


func _on_interaction_ended() -> void:
if _have_threads:
if not ProjectSettings.get_setting(ThreadbareProjectSettings.SKIP_SOKOBANS):
# Hide interact label during scene transition
interact_area.disabled = true
GameState.quest.incorporating_threads = true
SceneSwitcher.change_to_file_with_transition(SOKOBANS.pick_random())
else:
GameState.mark_quest_completed()
## Called from the dialogue when retelling is possible.
func start_retelling() -> void:
retelling_started.emit()


## The [member RetellingManager] calls this to display the retelling dialogue.
func show_retelling_dialogue() -> void:
DialogueManager.show_dialogue_balloon(GameState.quest.quest.retelling, "", [self])
await DialogueManager.dialogue_ended
retelling_finished.emit()


func _has_magical_thread_of_type(type: InventoryItem.ItemType) -> bool:
for item: InventoryItem in GameState.global.inventory:
if item.type == type:
return true
return false


func has_memory() -> bool:
return _has_magical_thread_of_type(InventoryItem.ItemType.MEMORY)


func has_imagination() -> bool:
return _has_magical_thread_of_type(InventoryItem.ItemType.IMAGINATION)


func has_spirit() -> bool:
return _has_magical_thread_of_type(InventoryItem.ItemType.SPIRIT)


func memory_text() -> String:
var has_it := _has_magical_thread_of_type(InventoryItem.ItemType.MEMORY)
return "(Memory available!)" if has_it else ""


func imagination_text() -> String:
var has_it := _has_magical_thread_of_type(InventoryItem.ItemType.IMAGINATION)
return "(Imagination available!)" if has_it else ""


func spirit_text() -> String:
var has_it := _has_magical_thread_of_type(InventoryItem.ItemType.SPIRIT)
return "(Spirit available!)" if has_it else ""


func _give_upgrade(type: InventoryItem.ItemType) -> void:
var has_it := _has_magical_thread_of_type(type)
if not has_it:
push_warning("Trying to give an upgrade for missing item type", type)
return
give_retelling_upgrade.emit(type)


func give_memory_upgrade() -> void:
_give_upgrade(InventoryItem.ItemType.MEMORY)


func give_imagination_upgrade() -> void:
_give_upgrade(InventoryItem.ItemType.IMAGINATION)


func give_spirit_upgrade() -> void:
_give_upgrade(InventoryItem.ItemType.SPIRIT)


func on_offering_succeeded() -> void:
loom_offering_animation_player.play(&"loom_offering")
await loom_offering_animation_player.animation_finished
GameState.global.clear_inventory()

var elder: Elder = _find_elder(GameState.quest.quest)
if elder:
await elder.congratulate_player()
else:
push_warning("Could not find elder for %s" % [GameState.quest.quest.resource_path])

GameState.mark_quest_completed()


func has_retelling() -> bool:
return GameState.quest and GameState.quest.quest and GameState.quest.quest.retelling


func is_item_offering_possible() -> bool:
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ It seems you are lacking the threads of Memory, Imagination, and Spirit... try c
~ have_threads
=>< welcome
The Memory, Imagination, and Spirit threads are ready to be incorporated into the loom!
if has_retelling():
do start_retelling()
do retelling_finished
do on_offering_succeeded()
=> END
2 changes: 2 additions & 0 deletions scenes/globals/game_state/global_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ signal completed_quests_changed
## used instead. [GameState.player] always points to the correct instance.
@export var player: PlayerState = PlayerState.new()

@export var helper: HelperCharacterState = HelperCharacterState.new()


func _validate_property(property: Dictionary) -> void:
match property.name:
Expand Down
30 changes: 30 additions & 0 deletions scenes/globals/game_state/helper_character_state.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
@tool
class_name HelperCharacterState
extends Resource

## The type of help, matching the magical threads type.
## This is left to game designers interpretation but usually:
## [br][br]
## - Memory is about expanding the lore of the game.[br]
## - Imagination is about making things appear in the level.[br]
## - Spirit is about reducing the difficulty of an action-based puzzle.[br]
@export var helper_type: InventoryItem.ItemType = InventoryItem.ItemType.NONE

## The seed to display a character with same visual features when offering help.
@export var character_seed: int = 0


## Consume the help.
func clear() -> void:
helper_type = InventoryItem.ItemType.NONE
character_seed = 0
emit_changed()


## Obtain the help.
func obtain(new_helper_type: InventoryItem.ItemType, new_character_seed: int) -> void:
helper_type = new_helper_type
character_seed = new_character_seed
emit_changed()
1 change: 1 addition & 0 deletions scenes/globals/game_state/helper_character_state.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://yu6cw51sylfu
7 changes: 7 additions & 0 deletions scenes/globals/game_state/inventory/inventory_item.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ enum ItemType {
MEMORY,
IMAGINATION,
SPIRIT,
NONE,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you put NONE at the end because you didn't want to have to update every instance in the game. OK! Perhaps we can later reorder this and update all resources.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other option is to have GameState.global.helper be null when there is no helper. Then you would not need to introduce the NONE member here, which is a potential source of bugs if someone sets NONE on a CollectibleItem.

}

const COLORS_PER_TYPE: Dictionary[ItemType, Color] = {
ItemType.MEMORY: Color(0.459, 0.867, 0.0, 1.0),
ItemType.IMAGINATION: Color(0.969, 0.792, 0.0, 1.0),
ItemType.SPIRIT: Color(0.929, 0.0, 0.0, 1.0)
}

const HUD_TEXTURES: Dictionary[ItemType, Texture2D] = {
Expand Down
4 changes: 0 additions & 4 deletions scenes/globals/game_state/quest_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ extends Resource
get = get_challenge_start_scene,
set = set_challenge_start_scene

## Set when the loom transports the player to a trio of Sokoban puzzles, so that
## when the player returns to Fray's End the loom can trigger a brief cutscene.
@export var incorporating_threads: bool

## Player state within [member quest]. If [member Quest.is_lore_quest] is
## [code]true[/code], this will be initialised as a copy of [member
## GlobalState.player], and propagated back to [GlobalState] if the quest is
Expand Down
4 changes: 4 additions & 0 deletions scenes/menus/storybook/components/quest.gd
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const FILENAME := "quest.tres"
## 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

@export_group("Animation")

## An optional sprite frame library to show in the storybook page for this quest.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ sprite = NodePath("..")
metadata/_custom_type_script = "uid://dy68p7gf07pi3"

[node name="AnimatedSprite2DHead" type="AnimatedSprite2D" parent="AnimatedSprite2DLegs/AnimatedSprite2DBody" unique_id=421503015]
unique_name_in_owner = true
material = ExtResource("2_1mlvj")
position = Vector2(-4, -16)
sprite_frames = ExtResource("15_ndexk")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
~ start
My town has been unraveled.
Do you want to hear my story?
- Yes => help_accepted
- No => help_denied

~ help_accepted
Lore lore lore.
Bye!
do GameState.global.helper.clear()
=> END

~ help_denied
OK, bye!
=> END
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[remap]

importer="dialogue_manager"
importer_version=15
type="Resource"
uid="uid://jf1tng8jxd3o"
path="res://.godot/imported/music_lore_helper.dialogue-e07faa651568cb20c63b23c6b4902663.tres"

[deps]

source_file="res://scenes/quests/lore_quests/quest_001/1_music_puzzle/components/dialogues/music_lore_helper.dialogue"
dest_files=["res://.godot/imported/music_lore_helper.dialogue-e07faa651568cb20c63b23c6b4902663.tres"]

[params]

defaults=true
Loading