Skip to content
Draft
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
1 change: 1 addition & 0 deletions scenes/dev/dev_archipelago.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ position = Vector2(0, 64)

[node name="TemplateElder" parent="OnTheGround/Elders" unique_id=1625796178 node_paths=PackedStringArray("abandon_point") instance=ExtResource("11_ui3i3")]
position = Vector2(481, 1107)
quest_directory = "res://scenes/dev/level_design/"
abandon_point = NodePath("SpawnPoint")

[node name="SpawnPoint" parent="OnTheGround/Elders/TemplateElder" unique_id=1656237801 instance=ExtResource("18_elr7o")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
~ start
if optional_collectible_item:
Ulysses Thriftweed: If you go East, you can get out of here.
Ulysses Thriftweed: If you go through the cave up North, there's another thread you can collect first, if you're feeling brave.
else:
Ulysses Thriftweed: I hope now you've learned to stop picking your nose.
=> END

~ wow
Ulysses Thriftweed: You run THAT fast?
=> 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://b1ywwu7aese02"
path="res://.godot/imported/optional_threads.dialogue-f583759440f80634b369fea5863492be.tres"

[deps]

source_file="res://scenes/dev/level_design/optional_threads/optional_threads.dialogue"
dest_files=["res://.godot/imported/optional_threads.dialogue-f583759440f80634b369fea5863492be.tres"]

[params]

defaults=true
32 changes: 32 additions & 0 deletions scenes/dev/level_design/optional_threads/optional_threads.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
extends Node2D

@export var dark_sky_color: Color
var _tween: Tween

@onready var cave_lighting: Area2D = %CaveLighting
@onready var canvas_modulate: CanvasModulate = %CanvasModulate
@onready var optional_collectible_item: CollectibleItem = %OptionalCollectibleItem


func _ready() -> void:
optional_collectible_item.tree_exiting.connect(func() -> void: optional_collectible_item = null)
cave_lighting.body_entered.connect(_on_cave_lighting_body_entered)
cave_lighting.body_exited.connect(_on_cave_lighting_body_exited)


func _adjust_modulation(target: Color) -> void:
if _tween:
_tween.kill()

_tween = create_tween()
_tween.tween_property(canvas_modulate, "color", target, 0.5)


func _on_cave_lighting_body_entered(_body: Node2D) -> void:
_adjust_modulation(dark_sky_color)


func _on_cave_lighting_body_exited(_body: Node2D) -> void:
_adjust_modulation(Color.WHITE)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b4muk3fixh36l
196 changes: 196 additions & 0 deletions scenes/dev/level_design/optional_threads/optional_threads.tscn

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions scenes/dev/level_design/optional_threads/quest.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="Quest" format=3 uid="uid://pdpqinul8nu5"]

[ext_resource type="Script" uid="uid://dts1hwdy3phin" path="res://scenes/menus/storybook/components/quest.gd" id="1_cjuqo"]

[resource]
script = ExtResource("1_cjuqo")
title = "Optional Threads"
description = "Shows how to add an optional thread to a scene."
first_scene = "uid://cplmo457h47p8"
threads_to_collect = 2
metadata/_custom_type_script = "uid://dts1hwdy3phin"
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ func _ready() -> void:
animated_sprite_2d.connect("frame_changed", _on_frame_changed)

if GameState.quest:
GameState.quest.inventory.item_collected.connect(_update_dialogue_title)
GameState.quest.inventory.item_consumed.connect(_update_dialogue_title)
GameState.quest.inventory.item_collected.connect(_update_dialogue_title.unbind(1))
GameState.quest.inventory.item_consumed.connect(_update_dialogue_title.unbind(1))
_update_dialogue_title()


Expand Down Expand Up @@ -105,7 +105,7 @@ func congratulate_player() -> void:
await DialogueManager.dialogue_ended


func _update_dialogue_title(_item: InventoryItem = null) -> void:
func _update_dialogue_title() -> void:
if eternal_loom and eternal_loom.is_item_offering_possible():
talk_behavior.title = "go_to_loom"
elif not _quests:
Expand Down
17 changes: 17 additions & 0 deletions scenes/game_elements/fx/dither/dither.gdshader
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2024 KingToot
// SPDX-License-Identifier: CC0-1.0
// https://godotshaders.com/shader/dithering-screen-door-transparency/
shader_type canvas_item;

uniform float intensity: hint_range(0.0, 1.0, 0.05) = 0.0;

const mat4 THRESHOLD_MATRIX = mat4(
vec4(1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0),
vec4(13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0),
vec4(4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0),
vec4(16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0));

void fragment() {
vec2 uv = UV / TEXTURE_PIXEL_SIZE;
COLOR.a *= step(0.0, THRESHOLD_MATRIX[int(uv.x) % 4][int(uv.y) % 4] - intensity);
}
1 change: 1 addition & 0 deletions scenes/game_elements/fx/dither/dither.gdshader.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cj250ayvmpwf3
8 changes: 8 additions & 0 deletions scenes/game_elements/fx/dither/dither.tres
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[gd_resource type="ShaderMaterial" format=3 uid="uid://dlnyw2c84jxlj"]

[ext_resource type="Shader" uid="uid://cj250ayvmpwf3" path="res://scenes/game_elements/fx/dither/dither.gdshader" id="1_xv4rv"]

[resource]
resource_local_to_scene = true
shader = ExtResource("1_xv4rv")
shader_parameter/intensity = 0.0
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[ext_resource type="Script" uid="uid://be7lr8ty24cix" path="res://scenes/game_elements/props/collectible_item/components/collectible_item.gd" id="1_7ff3m"]
[ext_resource type="Script" uid="uid://du8wfijr35r35" path="res://scenes/game_elements/props/interact_area/interact_area.gd" id="2_1kegn"]
[ext_resource type="Material" uid="uid://dlnyw2c84jxlj" path="res://scenes/game_elements/fx/dither/dither.tres" id="4_hhgm5"]
[ext_resource type="AudioStream" uid="uid://bhgpds38q0j4r" path="res://assets/third_party/sounds/collectibles/MemoryCollectable.ogg" id="5_hhgm5"]
[ext_resource type="AudioStream" uid="uid://38xwdjm46478" path="res://assets/third_party/sounds/collectibles/CollectibleAppear.ogg" id="5_oh62b"]

Expand Down Expand Up @@ -311,7 +312,7 @@ metadata/_custom_type_script = "uid://du8wfijr35r35"

[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractArea" unique_id=1121983883]
shape = SubResource("CircleShape2D_ghwae")
debug_color = Color(0.600391, 0.54335, 0, 0.42)
debug_color = Color(0.6, 0.545, 0, 0.42)

[node name="Marker" type="Marker2D" parent="InteractArea" unique_id=1961850851]
position = Vector2(0, -48)
Expand All @@ -322,6 +323,7 @@ libraries/ = SubResource("AnimationLibrary_8dm1m")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1620327010]
visible = false
modulate = Color(1, 1, 1, 0)
material = ExtResource("4_hhgm5")

[node name="CollectedSound" type="AudioStreamPlayer" parent="." unique_id=420528907]
unique_name_in_owner = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ extends SceneLink
## The dialogue title from where [member collected_dialogue] will start.
@export var dialogue_title: StringName = ""

var _ghost := false:
set(new_value):
_ghost = new_value
# TODO: effect is ugly
var sm := sprite_2d.material as ShaderMaterial
sm.set_shader_parameter("intensity", 0.25 if _ghost else 0.0)
# Key in GameState.quest.facts which, if present, indicates that this item was
# previously collected on the current run.
var _fact_key: String

@onready var interact_area: InteractArea = $InteractArea
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var sprite_2d: Sprite2D = $Sprite2D
Expand Down Expand Up @@ -82,6 +92,21 @@ func _ready() -> void:
if Engine.is_editor_hint():
return

# Collected in current run through this quest?
# TODO: if TaggedItem was constructed here we could put a workaround for
# use_unique_path not working there.
_fact_key = "%s:%s:collected" % [owner.scene_file_path, owner.get_path_to(self, true)]
if GameState.quest.facts.get(_fact_key, false):
# We assume any side-effects from collecting it have also been saved.
queue_free()
return

# Collected in previous run? TODO: supposedly quest.inventory is populated
# from global.inventory when starting quest so only the latter check should
# be needed
if GameState.global.inventory.has(item, self) or GameState.quest.inventory.has(item, self):
_ghost = true

interact_area.interaction_started.connect(self._on_interacted)


Expand Down Expand Up @@ -111,13 +136,15 @@ func _on_interacted(player: Player, _from_right: bool) -> void:
animation_player.play("collected")
await animation_player.animation_finished

GameState.quest.inventory.add_collected_item(item)
if not _ghost:
GameState.quest.inventory.add_collected_item(item, self)

if collected_dialogue:
DialogueManager.show_dialogue_balloon(collected_dialogue, dialogue_title, [self, player])
await DialogueManager.dialogue_ended

interact_area.end_interaction()
GameState.quest.facts[_fact_key] = true
queue_free()

if next_scene:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func show_retelling_dialogue() -> void:


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

Expand Down Expand Up @@ -92,7 +92,7 @@ func on_offering_succeeded() -> void:

loom_offering_animation_player.play(&"loom_offering")
await loom_offering_animation_player.animation_finished
GameState.quest.inventory.clear_inventory()
GameState.global.inventory.merge(GameState.quest.inventory)

if load_error != OK:
on_rewoven_finished()
Expand Down Expand Up @@ -121,6 +121,8 @@ func is_item_offering_possible() -> bool:
if not GameState.quest:
return false

return true

if GameState.quest.quest.threads_to_collect <= 0:
return false

Expand Down
3 changes: 3 additions & 0 deletions scenes/globals/game_state/game_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ func set_quest(new_quest: Quest) -> void:
quest_player_state = PlayerState.new()

quest = QuestState.new(new_quest, quest_player_state)
quest.inventory.items = global.inventory.items.filter(
func(i: TaggedItem) -> bool: return i.quest_path == new_quest.resource_path
)


## Guess which quest the given scene is part of, and set [member quest]
Expand Down
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 @@ -45,6 +45,8 @@ signal helper_changed

@export var helper: HelperCharacterState

@export var inventory: InventoryState = InventoryState.new()

## Quests that were started & abandoned without being completed, keyed by
## [member Resource.resource_path] of each [Quest].
## Captures the [QuestState] and [PerSceneState] at point, so that the player
Expand Down
35 changes: 35 additions & 0 deletions scenes/globals/game_state/inventory/tagged_item.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# SPDX-FileCopyrightText: The Threadbare Authors
# SPDX-License-Identifier: MPL-2.0
class_name TaggedItem
extends Resource
## An [InventoryItem] that knows where it came from

@export var item: InventoryItem
@export var quest_path: String
@export var source_scene: String
## TODO: construct unique node path here...
@export var source_node: NodePath


static func make(an_item: InventoryItem, quest: Quest, a_source_node: Node = null) -> TaggedItem:
var ci := new()
ci.item = an_item
ci.quest_path = quest.resource_path if quest else ""
if a_source_node:
ci.source_scene = a_source_node.owner.scene_file_path
# TODO: use_unique_path doesn't work when called on the target's owner.
# Reported as https://github.com/godotengine/godot/issues/122411. If
# eventually fixed, remove the condition and just keep the else branch.
if a_source_node.unique_name_in_owner:
ci.source_node = "%" + a_source_node.name
else:
ci.source_node = a_source_node.owner.get_path_to(a_source_node, true)
return ci


func matches(that: TaggedItem) -> bool:
return (
self.item == that.item
and self.source_scene == that.source_scene
and self.source_node == that.source_node
)
1 change: 1 addition & 0 deletions scenes/globals/game_state/inventory/tagged_item.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cvmcbvc6skodh
50 changes: 39 additions & 11 deletions scenes/globals/game_state/inventory_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,55 @@ class_name InventoryState
extends Resource

## Emitted when a new item is collected and added to [member items].
signal item_collected(item: InventoryItem)
signal item_collected(item: TaggedItem)

## Emitted when a item is consumed, causing it to be removed from
## [member items].
signal item_consumed(item: InventoryItem)
signal item_consumed(item: TaggedItem)

## Collected threads. Modify with [member add_collected_item] and
## [member clear_inventory].
@export var items: Array[InventoryItem]
@export var items: Array[TaggedItem]


## Add the [InventoryItem] to [member items].
func add_collected_item(item: InventoryItem) -> void:
items.append(item)
item_collected.emit(item)
## Move all items from [param other] to this inventory.
func merge(other: InventoryState) -> void:
for ci: TaggedItem in other.items:
if not _has(ci):
_add(ci)
other.clear_inventory()


## Returns true if the inventory contains [param item],
## collected in the current quest (TODO), specifically from [param source_node]
## (if provided).
func has(item: InventoryItem, source_node: Node = null) -> bool:
# TODO: should the caller make the TaggedItem instead?
return _has(TaggedItem.make(item, GameState.quest.quest, source_node))


## Add [param item] to [member items], tagging it as having been collected in
## the current quest from [param source_node].
func add_collected_item(item: InventoryItem, source_node: Node = null) -> void:
_add(TaggedItem.make(item, GameState.quest.quest, source_node))


func _has(ci: TaggedItem) -> bool:
return items.find_custom(ci.matches) >= 0


func _add(ci: TaggedItem) -> void:
if _has(ci):
return

items.append(ci)
item_collected.emit(ci)
emit_changed()


## Remove all [InventoryItem]s from [member items].
## Remove all items from [member items].
func clear_inventory() -> void:
for item: InventoryItem in items.duplicate():
items.erase(item)
item_consumed.emit(item)
for ci: TaggedItem in items.duplicate():
items.erase(ci)
item_consumed.emit(ci)
emit_changed()
Loading