Repository: endlessm/threadbare
Pull request #399: Indicate checkpoints visually
Author: login: wjt
Created: 2025-04-30T15:22:33Z | Merged: 2025-04-30T19:18:15Z

Previously, there was no visible indication that there are checkpoints in the stealth level, or that you have reached one. In playtesting, some players suggested that we should add checkpoints to the stealth level, not realising that they were already present.

Add a sprite to the checkpoint scene. Hide it until the player enters the checkpoint area.

Add a small collision shape to it, so that the player can't walk through it. Deactivate that collision shape when the sprite is invisible.

Enable y-sorting on the parent node of all the checkpoints in the two stealth levels currently in the source tree.

For now, a scarecrow is used to indicate the checkpoint, as a placeholder. I have not added any kind of animation or sound effect when the checkpoint is reached.

You can interact with the scarecrow to trigger a brief bit of dialogue that explains, in-game, what it does. The (first line of) dialogue is taken from #240.

Adjust a few checkpoint areas and positions, as explained in each commit message.

Fixes #240


Linked issue #240: Indicate checkpoints visually
@PlayMatters passed on feedback from young people who playtested the game: they said they wanted checkpoints in the stealth puzzle. When shown that there are already checkpoints, they remarked that they wanted them to be visible.
Linked issue comments: user: login: PlayMatters
created_at: 2025-04-25T14:43:43Z
body: These chackpoints will be KnitWitches - An animated caracter who appears when a checkpoint has been reached. The Witch can be engageed with only to say something like "I have knitted your progress so far, Storyweaver". 

In the meantime, let's use the Tiny Swords Scarecrow for these checkpoints. 
author_association: CONTRIBUTOR
user: login: manuq
created_at: 2025-04-30T13:41:16Z
body: Adding this to the board as I think it's important and we are forgetting about it.
author_association: COLLABORATOR
user: login: wjt
created_at: 2025-04-30T13:47:16Z
body: Well remembered. I'll take this.
author_association: MEMBER

Diff:
diff --git a/scenes/game_elements/props/checkpoint/checkpoint.tscn b/scenes/game_elements/props/checkpoint/checkpoint.tscn
index 6053c56e08..8165f7e1e9 100644
--- a/scenes/game_elements/props/checkpoint/checkpoint.tscn
+++ b/scenes/game_elements/props/checkpoint/checkpoint.tscn
@@ -1,7 +1,15 @@
-[gd_scene load_steps=3 format=3 uid="uid://dua6mynlw2ptw"]
+[gd_scene load_steps=7 format=3 uid="uid://dua6mynlw2ptw"]
 
 [ext_resource type="Script" uid="uid://bk5ri1yoakaqp" path="res://scenes/game_elements/props/checkpoint/components/checkpoint.gd" id="1_kkoqv"]
 [ext_resource type="Script" uid="uid://0enyu5v4ra34" path="res://scenes/game_elements/props/spawn_point/components/spawn_point.gd" id="2_s5d1s"]
+[ext_resource type="Texture2D" uid="uid://7afi18x3x1dm" path="res://assets/third_party/tiny-swords/Deco/18.png" id="3_wd3ro"]
+[ext_resource type="PackedScene" uid="uid://dutgnbiy7xalb" path="res://scenes/game_elements/props/interact_area/interact_area.tscn" id="4_cukws"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_wd3ro"]
+radius = 11.4018
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_3xcwf"]
+radius = 48.0
 
 [node name="Checkpoint" type="Area2D"]
 collision_layer = 0
@@ -10,3 +18,31 @@ script = ExtResource("1_kkoqv")
 [node name="SpawnPoint" type="Marker2D" parent="." groups=["spawn_point"]]
 unique_name_in_owner = true
 script = ExtResource("2_s5d1s")
+
+[node name="Sprite" type="Sprite2D" parent="."]
+unique_name_in_owner = true
+position = Vector2(0, -64)
+texture = ExtResource("3_wd3ro")
+
+[node name="StaticBody2D" type="StaticBody2D" parent="."]
+collision_layer = 16
+collision_mask = 0
+
+[node name="CollisionShape" type="CollisionShape2D" parent="StaticBody2D"]
+unique_name_in_owner = true
+position = Vector2(1, -4)
+shape = SubResource("CircleShape2D_wd3ro")
+disabled = true
+debug_color = Color(0.744429, 0.463578, 0, 0.42)
+
+[node name="InteractArea" parent="." instance=ExtResource("4_cukws")]
+unique_name_in_owner = true
+collision_layer = 0
+interact_label_position = Vector2(0, -128)
+disabled = true
+action = "Admire"
+
+[node name="CollisionShape" type="CollisionShape2D" parent="InteractArea"]
+position = Vector2(1, -4)
+shape = SubResource("CircleShape2D_3xcwf")
+debug_color = Color(0, 0.606449, 0.661205, 0.42)
diff --git a/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue b/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue
new file mode 100644
index 0000000000..cc76278260
--- /dev/null
+++ b/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: The Threadbare Authors
+# SPDX-License-Identifier: MPL-2.0
+~ start
+KnitWitch: I have knitted your progress so far, StoryWeaver.
+%9
+%
+	StoryWeaver: Wow, you can talk!
+	KnitWitch: Wow, so can you. What a coincidence.
+	StoryWeaver: I didn't think scarecrows could talk.
+	KnitWitch: Maybe they just never had anything to say to you. Ever think of that?
+=> END
diff --git a/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue.import b/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue.import
new file mode 100644
index 0000000000..19a8bceed2
--- /dev/null
+++ b/scenes/game_elements/props/checkpoint/components/checkpoint.dialogue.import
@@ -0,0 +1,16 @@
+[remap]
+
+importer="dialogue_manager"
+importer_version=15
+type="Resource"
+uid="uid://bug2aqd47jgyu"
+path="res://.godot/imported/checkpoint.dialogue-6cb1fcad38bf52f5e1b089b4445c4a1d.tres"
+
+[deps]
+
+source_file="res://scenes/game_elements/props/checkpoint/components/checkpoint.dialogue"
+dest_files=["res://.godot/imported/checkpoint.dialogue-6cb1fcad38bf52f5e1b089b4445c4a1d.tres"]
+
+[params]
+
+defaults=true
diff --git a/scenes/game_elements/props/checkpoint/components/checkpoint.gd b/scenes/game_elements/props/checkpoint/components/checkpoint.gd
index fc19ffd524..2817114409 100644
--- a/scenes/game_elements/props/checkpoint/components/checkpoint.gd
+++ b/scenes/game_elements/props/checkpoint/components/checkpoint.gd
@@ -1,17 +1,40 @@
 # SPDX-FileCopyrightText: The Threadbare Authors
 # SPDX-License-Identifier: MPL-2.0
-class_name Checkpoint extends Area2D
-
+class_name Checkpoint
+extends Area2D
 ## A place where the player respawns if the current scene is reloaded.
 
+const DIALOGUE: DialogueResource = preload(
+	"res://scenes/game_elements/props/checkpoint/components/checkpoint.dialogue"
+)
+
 ## The point where the player will spawn.
 @onready var spawn_point: SpawnPoint = %SpawnPoint
 
+## The sprite displayed when this checkpoint is activated
+@onready var sprite: Sprite2D = %Sprite
+
+## The collision shape for the sprite, when this checkpoint is activated
+@onready var collision_shape: CollisionShape2D = %CollisionShape
+
+@onready var interact_area: InteractArea = %InteractArea
+
 
 func _ready() -> void:
+	sprite.visible = false
 	body_entered.connect(func(_body: Node2D) -> void: self.activate())
+	interact_area.interaction_started.connect(_on_interaction_started)
 
 
 ## Makes this the active checkpoint.
 func activate() -> void:
 	GameState.current_spawn_point = owner.get_path_to(spawn_point)
+	sprite.visible = true
+	collision_shape.set_deferred(&"disabled", false)
+	interact_area.disabled = false
+
+
+func _on_interaction_started(player: Player, _from_right: bool) -> void:
+	DialogueManager.show_dialogue_balloon(DIALOGUE, "", [self, player])
+	await DialogueManager.dialogue_ended
+	interact_area.interaction_ended.emit()
diff --git a/scenes/quests/lore_quests/quest_001/3_stealth_level/stealth_level.tscn b/scenes/quests/lore_quests/quest_001/3_stealth_level/stealth_level.tscn
index 48622599c2..d672085f64 100644
--- a/scenes/quests/lore_quests/quest_001/3_stealth_level/stealth_level.tscn
+++ b/scenes/quests/lore_quests/quest_001/3_stealth_level/stealth_level.tscn
@@ -238,7 +238,7 @@ point_count = 2
 size = Vector2(115, 379)
 
 [sub_resource type="RectangleShape2D" id="RectangleShape2D_xtu6t"]
-size = Vector2(382, 126)
+size = Vector2(254, 212)
 
 [sub_resource type="RectangleShape2D" id="RectangleShape2D_oyx4c"]
 size = Vector2(200, 188.5)
@@ -250,7 +250,7 @@ size = Vector2(96, 190)
 size = Vector2(137, 126)
 
 [sub_resource type="RectangleShape2D" id="RectangleShape2D_n0bwe"]
-size = Vector2(117, 254)
+size = Vector2(212, 254)
 
 [sub_resource type="Resource" id="Resource_idy4y"]
 script = ExtResource("3_idy4y")
@@ -992,12 +992,13 @@ scale = Vector2(0.8, 0.8)
 texture = ExtResource("22_djpoe")
 
 [node name="Checkpoints" type="Node2D" parent="."]
+y_sort_enabled = true
 
 [node name="Checkpoint" parent="Checkpoints" instance=ExtResource("11_idy4y")]
-position = Vector2(1357, 300)
+position = Vector2(1293, 284)
 
 [node name="CollisionShape2D" type="CollisionShape2D" parent="Checkpoints/Checkpoint"]
-position = Vector2(-14, 20.5)
+position = Vector2(-14, 36.5)
 shape = SubResource("RectangleShape2D_idy4y")
 debug_color = Color(0, 0, 0, 0.42)
 
@@ -1005,7 +1006,7 @@ debug_color = Color(0, 0, 0, 0.42)
 position = Vector2(2420, 419)
 
 [node name="CollisionShape2D" type="CollisionShape2D" parent="Checkpoints/Checkpoint2"]
-position = Vector2(13, 95)
+position = Vector2(77, 52)
 shape = SubResource("RectangleShape2D_xtu6t")
 debug_color = Color(0, 0, 0, 0.42)
 
@@ -1038,7 +1039,7 @@ debug_color = Color(0, 0, 0, 0.42)
 position = Vector2(5516, 1184)
 
 [node name="CollisionShape2D" type="CollisionShape2D" parent="Checkpoints/Checkpoint6"]
-position = Vector2(-99.5, -33)
+position = Vector2(-52, -33)
 shape = SubResource("RectangleShape2D_n0bwe")
 debug_color = Color(0, 0, 0, 0.42)
 
diff --git a/scenes/quests/story_quests/template/1_stealth/stealth_template.tscn b/scenes/quests/story_quests/template/1_stealth/stealth_template.tscn
index aa412df789..01f3db35cd 100644
--- a/scenes/quests/story_quests/template/1_stealth/stealth_template.tscn
+++ b/scenes/quests/story_quests/template/1_stealth/stealth_template.tscn
@@ -101,6 +101,7 @@ position = Vector2(1947, 473)
 curve = SubResource("Curve2D_hejm3")
 
 [node name="Checkpoints" type="Node2D" parent="."]
+y_sort_enabled = true
 
 [node name="Checkpoint" parent="Checkpoints" instance=ExtResource("6_yi1jk")]
 position = Vector2(1704, 451)


Reviews:
user: login: JuanFdS
body: I love the idea!, I think in a next PR we could add a small animation for the sprite that makes it "pop up" a bit more.

Also, what do you think about making both the sprite and the dialogue configurable?, so learners can make their own checkpoints in story quests?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T16:23:03Z
user: login: manuq
body: Works well in the template stealth game as well. It would be nice to add an animation when visible, but not this round as the asset is a placeholder anyways. Not merging only because @JuanFdS left a question.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:31:45Z
user: login: wjt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T19:13:39Z
user: login: wjt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T19:18:01Z

Review comments:
diff_hunk: @@ -1,17 +1,27 @@
 # SPDX-FileCopyrightText: The Threadbare Authors
 # SPDX-License-Identifier: MPL-2.0
-class_name Checkpoint extends Area2D
-
+class_name Checkpoint
+extends Area2D
 ## A place where the player respawns if the current scene is reloaded.
 
 ## The point where the player will spawn.
 @onready var spawn_point: SpawnPoint = %SpawnPoint
 
+## The sprite displayed when this checkpoint is activated
+@onready var sprite: Sprite2D = %Sprite
+
+## The collision shape for the sprite, when this checkpoint is activated
+@onready var collision_shape: CollisionShape2D = %CollisionShape
+
 
 func _ready() -> void:
+	sprite.visible = false
+	collision_shape.set_deferred(&"disabled", true)
path: scenes/game_elements/props/checkpoint/components/checkpoint.gd
user: login: JuanFdS
body: Why not disable the collision shape in the saved scene?
created_at: 2025-04-30T16:19:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,17 +1,27 @@
 # SPDX-FileCopyrightText: The Threadbare Authors
 # SPDX-License-Identifier: MPL-2.0
-class_name Checkpoint extends Area2D
-
+class_name Checkpoint
+extends Area2D
 ## A place where the player respawns if the current scene is reloaded.
 
 ## The point where the player will spawn.
 @onready var spawn_point: SpawnPoint = %SpawnPoint
 
+## The sprite displayed when this checkpoint is activated
+@onready var sprite: Sprite2D = %Sprite
+
+## The collision shape for the sprite, when this checkpoint is activated
+@onready var collision_shape: CollisionShape2D = %CollisionShape
+
 
 func _ready() -> void:
+	sprite.visible = false
+	collision_shape.set_deferred(&"disabled", true)
path: scenes/game_elements/props/checkpoint/components/checkpoint.gd
user: login: wjt
body: Good question... I think I wanted it to be visible in the scene.
created_at: 2025-04-30T19:13:39Z
author_association: MEMBER
diff_hunk: @@ -1,17 +1,27 @@
 # SPDX-FileCopyrightText: The Threadbare Authors
 # SPDX-License-Identifier: MPL-2.0
-class_name Checkpoint extends Area2D
-
+class_name Checkpoint
+extends Area2D
 ## A place where the player respawns if the current scene is reloaded.
 
 ## The point where the player will spawn.
 @onready var spawn_point: SpawnPoint = %SpawnPoint
 
+## The sprite displayed when this checkpoint is activated
+@onready var sprite: Sprite2D = %Sprite
+
+## The collision shape for the sprite, when this checkpoint is activated
+@onready var collision_shape: CollisionShape2D = %CollisionShape
+
 
 func _ready() -> void:
+	sprite.visible = false
+	collision_shape.set_deferred(&"disabled", true)
path: scenes/game_elements/props/checkpoint/components/checkpoint.gd
user: login: wjt
body: I adjusted this to set it to disabled in the scene.
created_at: 2025-04-30T19:18:01Z
author_association: MEMBER

PR comments:
user: login: wjt
created_at: 2025-04-30T15:27:18Z
body: https://endlessm.github.io/threadbare/branches/endlessm/visual-checkpoint/#uid://db6vhi7s2f37c
author_association: MEMBER
user: login: wjt
created_at: 2025-04-30T15:28:28Z
body: ![image](https://github.com/user-attachments/assets/2876b5b0-0a00-4a86-8092-95b85cfaa2c2)

![image](https://github.com/user-attachments/assets/765f60dc-ea2b-43f9-a56b-d2225ddb3a8d)

author_association: MEMBER
user: login: wjt
created_at: 2025-04-30T19:13:55Z
body: I left the dialogue and sprite non-configurable for the same reason: they are placeholders.
author_association: MEMBER