From cc395a4efb6c62aba54ff913fb59444210982f2b Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:00:51 -0500 Subject: [PATCH 1/8] Checkpoint_void first ver --- .../checkpoint_void/checkpoint_void.tscn | 50 +++++++++ .../components/checkpoint_void.gd | 106 ++++++++++++++++++ .../components/checkpoint_void.gd.uid | 1 + 3 files changed, 157 insertions(+) create mode 100644 scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn create mode 100644 scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd create mode 100644 scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid diff --git a/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn b/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn new file mode 100644 index 0000000000..5c91015ad0 --- /dev/null +++ b/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn @@ -0,0 +1,50 @@ +[gd_scene format=3 uid="uid://cgodc8fruhsmv"] + +[ext_resource type="Script" uid="uid://hq3fcex5wdu2" path="res://scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd" id="1_aylub"] +[ext_resource type="Script" uid="uid://0enyu5v4ra34" path="res://scenes/game_elements/props/spawn_point/components/spawn_point.gd" id="2_vdk03"] +[ext_resource type="SpriteFrames" uid="uid://dmg1egdoye3ns" path="res://scenes/game_elements/props/checkpoint/components/knitwitch_frames_purple.tres" id="3_1fo6i"] +[ext_resource type="Script" uid="uid://du8wfijr35r35" path="res://scenes/game_elements/props/interact_area/interact_area.gd" id="4_sfptc"] +[ext_resource type="Script" uid="uid://edcifob4jc4s" path="res://scenes/game_logic/talk_behavior.gd" id="5_keb2t"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_3xcwf"] +radius = 48.0 + +[node name="Checkpoint" type="Area2D" unique_id=1789892556] +collision_layer = 0 +script = ExtResource("1_aylub") + +[node name="SpawnPoint" type="Marker2D" parent="." unique_id=118546798 groups=["spawn_point"]] +unique_name_in_owner = true +position = Vector2(0, 1) +script = ExtResource("2_vdk03") + +[node name="Sprite" type="AnimatedSprite2D" parent="." unique_id=422090831] +unique_name_in_owner = true +position = Vector2(0, -64) +sprite_frames = ExtResource("3_1fo6i") +animation = &"idle" +autoplay = "idle" + +[node name="InteractArea" type="Area2D" parent="." unique_id=2037902510 node_paths=PackedStringArray("marker")] +unique_name_in_owner = true +collision_layer = 0 +collision_mask = 0 +script = ExtResource("4_sfptc") +marker = NodePath("Marker") +disabled = true +action = "Admire" +metadata/_custom_type_script = "uid://du8wfijr35r35" + +[node name="CollisionShape" type="CollisionShape2D" parent="InteractArea" unique_id=317026295] +position = Vector2(1, -4) +shape = SubResource("CircleShape2D_3xcwf") +debug_color = Color(0.6, 0.545, 0, 0.42) + +[node name="Marker" type="Marker2D" parent="InteractArea" unique_id=1178774098] +position = Vector2(0, -128) + +[node name="TalkBehavior" type="Node" parent="." unique_id=487856562 node_paths=PackedStringArray("interact_area")] +unique_name_in_owner = true +script = ExtResource("5_keb2t") +interact_area = NodePath("../InteractArea") +metadata/_custom_type_script = "uid://edcifob4jc4s" diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd new file mode 100644 index 0000000000..4debea81ce --- /dev/null +++ b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: The Threadbare Authors +# SPDX-License-Identifier: MPL-2.0 +@tool +extends Checkpoint +class_name CheckpointVoid + +# This checkpoint variation will allow some enemies to bypass the scene +# reset after re-spawning in a checkpoint, keeping their tilemap changes +# and last positions + +# Static variables +static var saved_enemy_states: Dictionary = {} +static var saved_consumed_tiles: Array[Vector2i] = [] +static var pending_consumed_tiles: Array[Vector2i] = [] +static var _tracker_instance: Node = null + +# Variables you need to assign in Inspector +## Assign the enemies you want to bypass the reset scene +@export var persistent_enemies: Array[CharacterBody2D] + +## Assign the modified layer +@export var shared_void_layer: Node2D + +const _NEIGHBORS := [ + TileSet.CELL_NEIGHBOR_BOTTOM_SIDE, + TileSet.CELL_NEIGHBOR_LEFT_SIDE, + TileSet.CELL_NEIGHBOR_TOP_SIDE, + TileSet.CELL_NEIGHBOR_RIGHT_SIDE, +] + +func _ready() -> void: + super._ready() + + if Engine.is_editor_hint(): + return + + if _tracker_instance == null or not is_instance_valid(_tracker_instance): + _tracker_instance = self + # Clears pending tiles consumed in case the player doesn't reach the checkpoint + pending_consumed_tiles.clear() + + # Restores the enemy state + for enemy in persistent_enemies: + if not is_instance_valid(enemy): + continue + + var path_key := str(enemy.get_path()) + if saved_enemy_states.has(path_key): + var state_data: Dictionary = saved_enemy_states[path_key] + + if state_data.get("is_defeated", false): + enemy.queue_free() + else: + var saved_pos: Vector2 = state_data.get("position", enemy.position) + enemy.position = saved_pos + # Updates _last_position using set() to avoid wrong particle emissions + enemy.set("_last_position", saved_pos) + + # Restore layers modified by other enemies + if saved_consumed_tiles.size() > 0 and shared_void_layer != null: + if shared_void_layer.has_method("consume_cells"): + shared_void_layer.consume_cells(saved_consumed_tiles) + + +func _process(_delta: float) -> void: + if _tracker_instance != self or shared_void_layer == null: + return + + for enemy in persistent_enemies: + if not is_instance_valid(enemy): + continue + if enemy.get("state") == 3: + continue + + if shared_void_layer.has_method("coord_for") and shared_void_layer.has_method("get_neighbor_cell"): + var coord: Vector2i = shared_void_layer.coord_for(enemy) + var coords: Array[Vector2i] = [coord] + + for neighbor: int in _NEIGHBORS: + coords.append(shared_void_layer.get_neighbor_cell(coord, neighbor)) + + for c: Vector2i in coords: + if not pending_consumed_tiles.has(c) and not saved_consumed_tiles.has(c): + pending_consumed_tiles.append(c) + + +func activate() -> void: + for c: Vector2i in pending_consumed_tiles: + if not saved_consumed_tiles.has(c): + saved_consumed_tiles.append(c) + pending_consumed_tiles.clear() + + for enemy: CharacterBody2D in persistent_enemies: + var path_key := str(enemy.get_path()) + if is_instance_valid(enemy) and enemy.get("state") != 3: + saved_enemy_states[path_key] = { + "position": enemy.position, + "is_defeated": false + } + else: + saved_enemy_states[path_key] = { + "position": Vector2.ZERO, + "is_defeated": true + } + + super.activate() diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid new file mode 100644 index 0000000000..938d9cec5c --- /dev/null +++ b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid @@ -0,0 +1 @@ +uid://hq3fcex5wdu2 From b50de8c9daaeaf96c8eb33f78184aff3a4e99d01 Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:39:01 -0500 Subject: [PATCH 2/8] Update checkpoint_void.tscn --- scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn b/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn index 5c91015ad0..a783573224 100644 --- a/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn +++ b/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn @@ -9,7 +9,7 @@ [sub_resource type="CircleShape2D" id="CircleShape2D_3xcwf"] radius = 48.0 -[node name="Checkpoint" type="Area2D" unique_id=1789892556] +[node name="Checkpoint_void" type="Area2D" unique_id=1789892556] collision_layer = 0 script = ExtResource("1_aylub") From a20d780117c6cc340657583bcb71a3dae989e2af Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:21:48 -0500 Subject: [PATCH 3/8] Update checkpoint_void.gd --- .../components/checkpoint_void.gd | 111 ++++++++++++------ 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd index 4debea81ce..bdfdcfc7cf 100644 --- a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd +++ b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd @@ -3,22 +3,21 @@ @tool extends Checkpoint class_name CheckpointVoid +## A checkpoint that saves and restores the state of specific enemies and the void layer. +## +## This checkpoint extends the base Checkpoint functionality to track specific enemies +## (like guards or void-spreading enemies) and prevents them, along with consumed tiles, +## from resetting when the scene is reloaded. -# This checkpoint variation will allow some enemies to bypass the scene -# reset after re-spawning in a checkpoint, keeping their tilemap changes -# and last positions - -# Static variables static var saved_enemy_states: Dictionary = {} static var saved_consumed_tiles: Array[Vector2i] = [] static var pending_consumed_tiles: Array[Vector2i] = [] static var _tracker_instance: Node = null -# Variables you need to assign in Inspector -## Assign the enemies you want to bypass the reset scene +## Specific enemies that should retain their position and state across scene reloads. @export var persistent_enemies: Array[CharacterBody2D] -## Assign the modified layer +## The tilemap layer that is being consumed by the void. @export var shared_void_layer: Node2D const _NEIGHBORS := [ @@ -33,45 +32,70 @@ func _ready() -> void: if Engine.is_editor_hint(): return - + + # Designate a single instance as the tracker to avoid repeating calculations + # when multiple checkpoints exist in the same level. if _tracker_instance == null or not is_instance_valid(_tracker_instance): _tracker_instance = self - # Clears pending tiles consumed in case the player doesn't reach the checkpoint pending_consumed_tiles.clear() - # Restores the enemy state - for enemy in persistent_enemies: + if saved_consumed_tiles.size() > 0 and shared_void_layer != null: + if shared_void_layer.has_method("consume_cells"): + shared_void_layer.consume_cells(saved_consumed_tiles) + + # Deferring this call ensures that any initialization in the enemy's _ready + # function finishes before overwriting its variables. + call_deferred("_restore_enemies") + + +func _restore_enemies() -> void: + for enemy: CharacterBody2D in persistent_enemies: if not is_instance_valid(enemy): continue var path_key := str(enemy.get_path()) if saved_enemy_states.has(path_key): - var state_data: Dictionary = saved_enemy_states[path_key] + var data: Dictionary = saved_enemy_states[path_key] - if state_data.get("is_defeated", false): + # Only the void-spreading enemies will trigger this, since guards are never saved as defeated. + if data.get("is_defeated", false): enemy.queue_free() - else: - var saved_pos: Vector2 = state_data.get("position", enemy.position) - enemy.position = saved_pos - # Updates _last_position using set() to avoid wrong particle emissions - enemy.set("_last_position", saved_pos) + continue - # Restore layers modified by other enemies - if saved_consumed_tiles.size() > 0 and shared_void_layer != null: - if shared_void_layer.has_method("consume_cells"): - shared_void_layer.consume_cells(saved_consumed_tiles) + enemy.global_position = data["position"] + + # Prevent a massive particle burst upon reload by syncing the last recorded position. + if "_last_position" in enemy: + enemy.set("_last_position", data["position"]) + + # Restore specific patrol variables if the enemy acts as a guard. + if data.get("is_guard", false): + enemy.set("current_patrol_point_idx", data["current_idx"]) + enemy.set("previous_patrol_point_idx", data["prev_idx"]) + enemy.set("state", data["state"]) + + var movement: Node = enemy.get_node_or_null("%GuardMovement") + if movement and movement.has_method("set_destination"): + movement.set_destination(data["movement_dest"]) func _process(_delta: float) -> void: if _tracker_instance != self or shared_void_layer == null: return - for enemy in persistent_enemies: + for enemy: CharacterBody2D in persistent_enemies: if not is_instance_valid(enemy): continue - if enemy.get("state") == 3: + + var is_guard: bool = "current_patrol_point_idx" in enemy + var state: int = enemy.get("state") + + # Ignore defeated enemies. VoidSpreadingEnemy uses state 3 for DEFEATED. + # Guards do not have a defeated state, so we skip this check for them. + if not is_guard and state == 3: continue + # Replicate the void calculation logic for living enemies. if shared_void_layer.has_method("coord_for") and shared_void_layer.has_method("get_neighbor_cell"): var coord: Vector2i = shared_void_layer.coord_for(enemy) var coords: Array[Vector2i] = [coord] @@ -92,15 +116,32 @@ func activate() -> void: for enemy: CharacterBody2D in persistent_enemies: var path_key := str(enemy.get_path()) - if is_instance_valid(enemy) and enemy.get("state") != 3: - saved_enemy_states[path_key] = { - "position": enemy.position, - "is_defeated": false - } - else: - saved_enemy_states[path_key] = { - "position": Vector2.ZERO, - "is_defeated": true - } + if not is_instance_valid(enemy): + continue + + var is_guard: bool = "current_patrol_point_idx" in enemy + var state: int = enemy.get("state") + + # Guards are never defeated, only void-spreading enemies (state 3) can be. + var is_defeated: bool = not is_guard and state == 3 + + # Base dictionary data shared across all supported enemy types. + var enemy_data := { + "position": enemy.global_position, + "is_guard": is_guard, + "is_defeated": is_defeated + } + + # Save additional patrol data if the enemy acts as a guard. + if is_guard and not is_defeated: + var movement: Node = enemy.get_node_or_null("%GuardMovement") + var dest: Vector2 = movement.get("destination") if movement else enemy.global_position + + enemy_data["current_idx"] = enemy.get("current_patrol_point_idx") + enemy_data["prev_idx"] = enemy.get("previous_patrol_point_idx") + enemy_data["state"] = state + enemy_data["movement_dest"] = dest + + saved_enemy_states[path_key] = enemy_data super.activate() From ce24aef6914b49f2344d5c5a1ba6f616abe5aa0e Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:20:59 -0500 Subject: [PATCH 4/8] fix path tracking --- .../components/checkpoint_void.gd | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd index bdfdcfc7cf..4a620c0e82 100644 --- a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd +++ b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd @@ -14,6 +14,9 @@ static var saved_consumed_tiles: Array[Vector2i] = [] static var pending_consumed_tiles: Array[Vector2i] = [] static var _tracker_instance: Node = null +# Global array to combine persistent enemies from ALL checkpoints in the current scene. +static var _all_tracked_enemies: Array[CharacterBody2D] = [] + ## Specific enemies that should retain their position and state across scene reloads. @export var persistent_enemies: Array[CharacterBody2D] @@ -33,23 +36,27 @@ func _ready() -> void: if Engine.is_editor_hint(): return - # Designate a single instance as the tracker to avoid repeating calculations - # when multiple checkpoints exist in the same level. + # Designate a single instance as the tracker and reset global arrays upon scene reload. if _tracker_instance == null or not is_instance_valid(_tracker_instance): _tracker_instance = self pending_consumed_tiles.clear() + _all_tracked_enemies.clear() + + # Compile all persistent enemies from every checkpoint into a single global tracker. + for enemy: CharacterBody2D in persistent_enemies: + if is_instance_valid(enemy) and not _all_tracked_enemies.has(enemy): + _all_tracked_enemies.append(enemy) if saved_consumed_tiles.size() > 0 and shared_void_layer != null: if shared_void_layer.has_method("consume_cells"): shared_void_layer.consume_cells(saved_consumed_tiles) - # Deferring this call ensures that any initialization in the enemy's _ready - # function finishes before overwriting its variables. call_deferred("_restore_enemies") func _restore_enemies() -> void: - for enemy: CharacterBody2D in persistent_enemies: + # Iterate over the global pool to restore everyone. + for enemy: CharacterBody2D in _all_tracked_enemies: if not is_instance_valid(enemy): continue @@ -57,18 +64,15 @@ func _restore_enemies() -> void: if saved_enemy_states.has(path_key): var data: Dictionary = saved_enemy_states[path_key] - # Only the void-spreading enemies will trigger this, since guards are never saved as defeated. if data.get("is_defeated", false): enemy.queue_free() continue enemy.global_position = data["position"] - # Prevent a massive particle burst upon reload by syncing the last recorded position. if "_last_position" in enemy: enemy.set("_last_position", data["position"]) - # Restore specific patrol variables if the enemy acts as a guard. if data.get("is_guard", false): enemy.set("current_patrol_point_idx", data["current_idx"]) enemy.set("previous_patrol_point_idx", data["prev_idx"]) @@ -83,19 +87,17 @@ func _process(_delta: float) -> void: if _tracker_instance != self or shared_void_layer == null: return - for enemy: CharacterBody2D in persistent_enemies: + # The tracker instance now tracks ALL enemies from ALL checkpoints. + for enemy: CharacterBody2D in _all_tracked_enemies: if not is_instance_valid(enemy): continue var is_guard: bool = "current_patrol_point_idx" in enemy var state: int = enemy.get("state") - # Ignore defeated enemies. VoidSpreadingEnemy uses state 3 for DEFEATED. - # Guards do not have a defeated state, so we skip this check for them. if not is_guard and state == 3: continue - # Replicate the void calculation logic for living enemies. if shared_void_layer.has_method("coord_for") and shared_void_layer.has_method("get_neighbor_cell"): var coord: Vector2i = shared_void_layer.coord_for(enemy) var coords: Array[Vector2i] = [coord] @@ -114,25 +116,22 @@ func activate() -> void: saved_consumed_tiles.append(c) pending_consumed_tiles.clear() - for enemy: CharacterBody2D in persistent_enemies: - var path_key := str(enemy.get_path()) + # Save the snapshot of ALL tracked enemies, regardless of which checkpoint is activated. + for enemy: CharacterBody2D in _all_tracked_enemies: if not is_instance_valid(enemy): continue + var path_key := str(enemy.get_path()) var is_guard: bool = "current_patrol_point_idx" in enemy var state: int = enemy.get("state") - - # Guards are never defeated, only void-spreading enemies (state 3) can be. var is_defeated: bool = not is_guard and state == 3 - # Base dictionary data shared across all supported enemy types. var enemy_data := { "position": enemy.global_position, "is_guard": is_guard, "is_defeated": is_defeated } - # Save additional patrol data if the enemy acts as a guard. if is_guard and not is_defeated: var movement: Node = enemy.get_node_or_null("%GuardMovement") var dest: Vector2 = movement.get("destination") if movement else enemy.global_position From b7ad2ca9772d89a9d15db85ccee92db7f7c1e90d Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:23:13 -0500 Subject: [PATCH 5/8] Final checkpoint_void after testing --- .../components/checkpoint_void.gd | 129 ++++++++++++------ 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd index 4a620c0e82..67a1782af9 100644 --- a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd +++ b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd @@ -9,13 +9,13 @@ class_name CheckpointVoid ## (like guards or void-spreading enemies) and prevents them, along with consumed tiles, ## from resetting when the scene is reloaded. +# Store everything including position, paths, and consumed tiles in a single dictionary +# bound strictly to each enemy to prevent synchronization issues. static var saved_enemy_states: Dictionary = {} -static var saved_consumed_tiles: Array[Vector2i] = [] -static var pending_consumed_tiles: Array[Vector2i] = [] -static var _tracker_instance: Node = null -# Global array to combine persistent enemies from ALL checkpoints in the current scene. -static var _all_tracked_enemies: Array[CharacterBody2D] = [] +# Temporary dictionary to track tiles for each enemy individually. +var pending_consumed_tiles: Dictionary = {} +var _is_restoring: bool = false ## Specific enemies that should retain their position and state across scene reloads. @export var persistent_enemies: Array[CharacterBody2D] @@ -36,27 +36,28 @@ func _ready() -> void: if Engine.is_editor_hint(): return - # Designate a single instance as the tracker and reset global arrays upon scene reload. - if _tracker_instance == null or not is_instance_valid(_tracker_instance): - _tracker_instance = self - pending_consumed_tiles.clear() - _all_tracked_enemies.clear() - - # Compile all persistent enemies from every checkpoint into a single global tracker. - for enemy: CharacterBody2D in persistent_enemies: - if is_instance_valid(enemy) and not _all_tracked_enemies.has(enemy): - _all_tracked_enemies.append(enemy) + pending_consumed_tiles.clear() - if saved_consumed_tiles.size() > 0 and shared_void_layer != null: + # Gather all saved tiles from all active enemies assigned to this checkpoint. + var all_saved_tiles: Array[Vector2i] = [] + for path_key: String in saved_enemy_states: + var data: Dictionary = saved_enemy_states[path_key] + if data.has("tiles"): + for c: Vector2i in data["tiles"]: + if not all_saved_tiles.has(c): + all_saved_tiles.append(c) + + # Consume the TileMap cells before placing the enemies to prevent visual glitches. + if all_saved_tiles.size() > 0 and shared_void_layer != null: if shared_void_layer.has_method("consume_cells"): - shared_void_layer.consume_cells(saved_consumed_tiles) + shared_void_layer.consume_cells(all_saved_tiles) + _is_restoring = true call_deferred("_restore_enemies") func _restore_enemies() -> void: - # Iterate over the global pool to restore everyone. - for enemy: CharacterBody2D in _all_tracked_enemies: + for enemy: CharacterBody2D in persistent_enemies: if not is_instance_valid(enemy): continue @@ -68,32 +69,44 @@ func _restore_enemies() -> void: enemy.queue_free() continue + # Restore the global position for the world. enemy.global_position = data["position"] + # Inject the local position instead of the global one. if "_last_position" in enemy: - enemy.set("_last_position", data["position"]) + enemy.set("_last_position", enemy.position) + + if data.has("state"): + enemy.set("state", data["state"]) + + var path_behavior: Node = enemy.get_node_or_null("%PathWalkBehavior") + if path_behavior and data.has("path_behavior"): + var pb_data: Dictionary = data["path_behavior"] + for prop: String in pb_data: + path_behavior.set(prop, pb_data[prop]) if data.get("is_guard", false): enemy.set("current_patrol_point_idx", data["current_idx"]) enemy.set("previous_patrol_point_idx", data["prev_idx"]) - enemy.set("state", data["state"]) var movement: Node = enemy.get_node_or_null("%GuardMovement") if movement and movement.has_method("set_destination"): movement.set_destination(data["movement_dest"]) + + _is_restoring = false -func _process(_delta: float) -> void: - if _tracker_instance != self or shared_void_layer == null: +func _track_tiles() -> void: + if Engine.is_editor_hint() or shared_void_layer == null or _is_restoring: return - # The tracker instance now tracks ALL enemies from ALL checkpoints. - for enemy: CharacterBody2D in _all_tracked_enemies: + for enemy: CharacterBody2D in persistent_enemies: if not is_instance_valid(enemy): continue var is_guard: bool = "current_patrol_point_idx" in enemy - var state: int = enemy.get("state") + var raw_state: Variant = enemy.get("state") + var state: int = raw_state if raw_state != null else -1 if not is_guard and state == 3: continue @@ -105,42 +118,82 @@ func _process(_delta: float) -> void: for neighbor: int in _NEIGHBORS: coords.append(shared_void_layer.get_neighbor_cell(coord, neighbor)) + if not pending_consumed_tiles.has(enemy): + pending_consumed_tiles[enemy] = [] + for c: Vector2i in coords: - if not pending_consumed_tiles.has(c) and not saved_consumed_tiles.has(c): - pending_consumed_tiles.append(c) + if not pending_consumed_tiles[enemy].has(c): + pending_consumed_tiles[enemy].append(c) + + +func _process(_delta: float) -> void: + _track_tiles() + + +func _physics_process(_delta: float) -> void: + _track_tiles() func activate() -> void: - for c: Vector2i in pending_consumed_tiles: - if not saved_consumed_tiles.has(c): - saved_consumed_tiles.append(c) - pending_consumed_tiles.clear() + if _is_restoring: + super.activate() + return + + # Make a copy of the old states before clearing them to retain previous progress. + var old_states: Dictionary = saved_enemy_states.duplicate() + saved_enemy_states.clear() - # Save the snapshot of ALL tracked enemies, regardless of which checkpoint is activated. - for enemy: CharacterBody2D in _all_tracked_enemies: + for enemy: CharacterBody2D in persistent_enemies: + var path_key := str(enemy.get_path()) if not is_instance_valid(enemy): continue - var path_key := str(enemy.get_path()) var is_guard: bool = "current_patrol_point_idx" in enemy - var state: int = enemy.get("state") + var raw_state: Variant = enemy.get("state") + var state: int = raw_state if raw_state != null else -1 var is_defeated: bool = not is_guard and state == 3 + # Retrieve the tiles that the enemy had already destroyed in past lives. + var old_tiles: Array = [] + if old_states.has(path_key): + old_tiles = old_states[path_key].get("tiles", []) + + # Add the newly destroyed tiles from the current run. + var new_tiles: Array = pending_consumed_tiles.get(enemy, []) + var combined_tiles: Array = old_tiles.duplicate() + + for c: Vector2i in new_tiles: + if not combined_tiles.has(c): + combined_tiles.append(c) + + # Save the complete snapshot for this specific enemy. var enemy_data := { "position": enemy.global_position, "is_guard": is_guard, - "is_defeated": is_defeated + "is_defeated": is_defeated, + "state": state, + "tiles": combined_tiles } + var path_behavior: Node = enemy.get_node_or_null("%PathWalkBehavior") + if path_behavior: + var pb_data := {} + for prop: String in ["progress", "progress_ratio", "current_point_index", "current_point", "target_position"]: + if prop in path_behavior: + pb_data[prop] = path_behavior.get(prop) + enemy_data["path_behavior"] = pb_data + if is_guard and not is_defeated: var movement: Node = enemy.get_node_or_null("%GuardMovement") var dest: Vector2 = movement.get("destination") if movement else enemy.global_position enemy_data["current_idx"] = enemy.get("current_patrol_point_idx") enemy_data["prev_idx"] = enemy.get("previous_patrol_point_idx") - enemy_data["state"] = state enemy_data["movement_dest"] = dest saved_enemy_states[path_key] = enemy_data + + # Clear the temporary list once the state is securely saved. + pending_consumed_tiles.clear() super.activate() From 6ac56c7ca07a08e37ab6a3a92d426b9d94253c16 Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:41:30 -0500 Subject: [PATCH 6/8] Delete separate prop Gotta fuse the new code with the og checkpoint in a way that doesn't affect (?) existing ones --- .../checkpoint_void/checkpoint_void.tscn | 50 ----- .../components/checkpoint_void.gd | 199 ------------------ .../components/checkpoint_void.gd.uid | 1 - 3 files changed, 250 deletions(-) delete mode 100644 scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn delete mode 100644 scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd delete mode 100644 scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid diff --git a/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn b/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn deleted file mode 100644 index a783573224..0000000000 --- a/scenes/game_elements/props/checkpoint_void/checkpoint_void.tscn +++ /dev/null @@ -1,50 +0,0 @@ -[gd_scene format=3 uid="uid://cgodc8fruhsmv"] - -[ext_resource type="Script" uid="uid://hq3fcex5wdu2" path="res://scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd" id="1_aylub"] -[ext_resource type="Script" uid="uid://0enyu5v4ra34" path="res://scenes/game_elements/props/spawn_point/components/spawn_point.gd" id="2_vdk03"] -[ext_resource type="SpriteFrames" uid="uid://dmg1egdoye3ns" path="res://scenes/game_elements/props/checkpoint/components/knitwitch_frames_purple.tres" id="3_1fo6i"] -[ext_resource type="Script" uid="uid://du8wfijr35r35" path="res://scenes/game_elements/props/interact_area/interact_area.gd" id="4_sfptc"] -[ext_resource type="Script" uid="uid://edcifob4jc4s" path="res://scenes/game_logic/talk_behavior.gd" id="5_keb2t"] - -[sub_resource type="CircleShape2D" id="CircleShape2D_3xcwf"] -radius = 48.0 - -[node name="Checkpoint_void" type="Area2D" unique_id=1789892556] -collision_layer = 0 -script = ExtResource("1_aylub") - -[node name="SpawnPoint" type="Marker2D" parent="." unique_id=118546798 groups=["spawn_point"]] -unique_name_in_owner = true -position = Vector2(0, 1) -script = ExtResource("2_vdk03") - -[node name="Sprite" type="AnimatedSprite2D" parent="." unique_id=422090831] -unique_name_in_owner = true -position = Vector2(0, -64) -sprite_frames = ExtResource("3_1fo6i") -animation = &"idle" -autoplay = "idle" - -[node name="InteractArea" type="Area2D" parent="." unique_id=2037902510 node_paths=PackedStringArray("marker")] -unique_name_in_owner = true -collision_layer = 0 -collision_mask = 0 -script = ExtResource("4_sfptc") -marker = NodePath("Marker") -disabled = true -action = "Admire" -metadata/_custom_type_script = "uid://du8wfijr35r35" - -[node name="CollisionShape" type="CollisionShape2D" parent="InteractArea" unique_id=317026295] -position = Vector2(1, -4) -shape = SubResource("CircleShape2D_3xcwf") -debug_color = Color(0.6, 0.545, 0, 0.42) - -[node name="Marker" type="Marker2D" parent="InteractArea" unique_id=1178774098] -position = Vector2(0, -128) - -[node name="TalkBehavior" type="Node" parent="." unique_id=487856562 node_paths=PackedStringArray("interact_area")] -unique_name_in_owner = true -script = ExtResource("5_keb2t") -interact_area = NodePath("../InteractArea") -metadata/_custom_type_script = "uid://edcifob4jc4s" diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd deleted file mode 100644 index 67a1782af9..0000000000 --- a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd +++ /dev/null @@ -1,199 +0,0 @@ -# SPDX-FileCopyrightText: The Threadbare Authors -# SPDX-License-Identifier: MPL-2.0 -@tool -extends Checkpoint -class_name CheckpointVoid -## A checkpoint that saves and restores the state of specific enemies and the void layer. -## -## This checkpoint extends the base Checkpoint functionality to track specific enemies -## (like guards or void-spreading enemies) and prevents them, along with consumed tiles, -## from resetting when the scene is reloaded. - -# Store everything including position, paths, and consumed tiles in a single dictionary -# bound strictly to each enemy to prevent synchronization issues. -static var saved_enemy_states: Dictionary = {} - -# Temporary dictionary to track tiles for each enemy individually. -var pending_consumed_tiles: Dictionary = {} -var _is_restoring: bool = false - -## Specific enemies that should retain their position and state across scene reloads. -@export var persistent_enemies: Array[CharacterBody2D] - -## The tilemap layer that is being consumed by the void. -@export var shared_void_layer: Node2D - -const _NEIGHBORS := [ - TileSet.CELL_NEIGHBOR_BOTTOM_SIDE, - TileSet.CELL_NEIGHBOR_LEFT_SIDE, - TileSet.CELL_NEIGHBOR_TOP_SIDE, - TileSet.CELL_NEIGHBOR_RIGHT_SIDE, -] - -func _ready() -> void: - super._ready() - - if Engine.is_editor_hint(): - return - - pending_consumed_tiles.clear() - - # Gather all saved tiles from all active enemies assigned to this checkpoint. - var all_saved_tiles: Array[Vector2i] = [] - for path_key: String in saved_enemy_states: - var data: Dictionary = saved_enemy_states[path_key] - if data.has("tiles"): - for c: Vector2i in data["tiles"]: - if not all_saved_tiles.has(c): - all_saved_tiles.append(c) - - # Consume the TileMap cells before placing the enemies to prevent visual glitches. - if all_saved_tiles.size() > 0 and shared_void_layer != null: - if shared_void_layer.has_method("consume_cells"): - shared_void_layer.consume_cells(all_saved_tiles) - - _is_restoring = true - call_deferred("_restore_enemies") - - -func _restore_enemies() -> void: - for enemy: CharacterBody2D in persistent_enemies: - if not is_instance_valid(enemy): - continue - - var path_key := str(enemy.get_path()) - if saved_enemy_states.has(path_key): - var data: Dictionary = saved_enemy_states[path_key] - - if data.get("is_defeated", false): - enemy.queue_free() - continue - - # Restore the global position for the world. - enemy.global_position = data["position"] - - # Inject the local position instead of the global one. - if "_last_position" in enemy: - enemy.set("_last_position", enemy.position) - - if data.has("state"): - enemy.set("state", data["state"]) - - var path_behavior: Node = enemy.get_node_or_null("%PathWalkBehavior") - if path_behavior and data.has("path_behavior"): - var pb_data: Dictionary = data["path_behavior"] - for prop: String in pb_data: - path_behavior.set(prop, pb_data[prop]) - - if data.get("is_guard", false): - enemy.set("current_patrol_point_idx", data["current_idx"]) - enemy.set("previous_patrol_point_idx", data["prev_idx"]) - - var movement: Node = enemy.get_node_or_null("%GuardMovement") - if movement and movement.has_method("set_destination"): - movement.set_destination(data["movement_dest"]) - - _is_restoring = false - - -func _track_tiles() -> void: - if Engine.is_editor_hint() or shared_void_layer == null or _is_restoring: - return - - for enemy: CharacterBody2D in persistent_enemies: - if not is_instance_valid(enemy): - continue - - var is_guard: bool = "current_patrol_point_idx" in enemy - var raw_state: Variant = enemy.get("state") - var state: int = raw_state if raw_state != null else -1 - - if not is_guard and state == 3: - continue - - if shared_void_layer.has_method("coord_for") and shared_void_layer.has_method("get_neighbor_cell"): - var coord: Vector2i = shared_void_layer.coord_for(enemy) - var coords: Array[Vector2i] = [coord] - - for neighbor: int in _NEIGHBORS: - coords.append(shared_void_layer.get_neighbor_cell(coord, neighbor)) - - if not pending_consumed_tiles.has(enemy): - pending_consumed_tiles[enemy] = [] - - for c: Vector2i in coords: - if not pending_consumed_tiles[enemy].has(c): - pending_consumed_tiles[enemy].append(c) - - -func _process(_delta: float) -> void: - _track_tiles() - - -func _physics_process(_delta: float) -> void: - _track_tiles() - - -func activate() -> void: - if _is_restoring: - super.activate() - return - - # Make a copy of the old states before clearing them to retain previous progress. - var old_states: Dictionary = saved_enemy_states.duplicate() - saved_enemy_states.clear() - - for enemy: CharacterBody2D in persistent_enemies: - var path_key := str(enemy.get_path()) - if not is_instance_valid(enemy): - continue - - var is_guard: bool = "current_patrol_point_idx" in enemy - var raw_state: Variant = enemy.get("state") - var state: int = raw_state if raw_state != null else -1 - var is_defeated: bool = not is_guard and state == 3 - - # Retrieve the tiles that the enemy had already destroyed in past lives. - var old_tiles: Array = [] - if old_states.has(path_key): - old_tiles = old_states[path_key].get("tiles", []) - - # Add the newly destroyed tiles from the current run. - var new_tiles: Array = pending_consumed_tiles.get(enemy, []) - var combined_tiles: Array = old_tiles.duplicate() - - for c: Vector2i in new_tiles: - if not combined_tiles.has(c): - combined_tiles.append(c) - - # Save the complete snapshot for this specific enemy. - var enemy_data := { - "position": enemy.global_position, - "is_guard": is_guard, - "is_defeated": is_defeated, - "state": state, - "tiles": combined_tiles - } - - var path_behavior: Node = enemy.get_node_or_null("%PathWalkBehavior") - if path_behavior: - var pb_data := {} - for prop: String in ["progress", "progress_ratio", "current_point_index", "current_point", "target_position"]: - if prop in path_behavior: - pb_data[prop] = path_behavior.get(prop) - enemy_data["path_behavior"] = pb_data - - if is_guard and not is_defeated: - var movement: Node = enemy.get_node_or_null("%GuardMovement") - var dest: Vector2 = movement.get("destination") if movement else enemy.global_position - - enemy_data["current_idx"] = enemy.get("current_patrol_point_idx") - enemy_data["prev_idx"] = enemy.get("previous_patrol_point_idx") - enemy_data["movement_dest"] = dest - - saved_enemy_states[path_key] = enemy_data - - # Clear the temporary list once the state is securely saved. - pending_consumed_tiles.clear() - - super.activate() diff --git a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid b/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid deleted file mode 100644 index 938d9cec5c..0000000000 --- a/scenes/game_elements/props/checkpoint_void/components/checkpoint_void.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hq3fcex5wdu2 From e4acba883324d49d19796819ff8b3956fdf9104a Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:31:13 -0500 Subject: [PATCH 7/8] Update with game_state --- .../enemies/guard/components/guard.gd | 59 +++++++++++++++++++ .../components/tile_map_cover.gd | 44 ++++++++++++++ .../components/void_spreading_enemy.gd | 56 ++++++++++++++++++ .../props/checkpoint/components/checkpoint.gd | 6 ++ 4 files changed, 165 insertions(+) diff --git a/scenes/game_elements/characters/enemies/guard/components/guard.gd b/scenes/game_elements/characters/enemies/guard/components/guard.gd index 72a9c1bcc3..f950e810b7 100644 --- a/scenes/game_elements/characters/enemies/guard/components/guard.gd +++ b/scenes/game_elements/characters/enemies/guard/components/guard.gd @@ -155,6 +155,7 @@ func _ready() -> void: guard_movement.destination_reached.connect(self._on_destination_reached) guard_movement.still_time_finished.connect(self._on_still_time_finished) guard_movement.path_blocked.connect(self._on_path_blocked) + _init_persistence() func _process(delta: float) -> void: @@ -480,3 +481,61 @@ func _on_detection_area_body_exited(body: Node2D) -> void: if state == State.DETECTING: guard_movement.stop_moving() state = State.INVESTIGATING + +## Establishes the persistence listener. +## The state load is deferred to ensure it overrides the guard's default teleport-to-start logic on initialization. +func _init_persistence() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + + if not GameState.scene.changed.is_connected(_on_checkpoint_activated): + GameState.scene.changed.connect(_on_checkpoint_activated) + + call_deferred("_load_state") + + +## Serializes patrol indices and current destination to maintain the guard's exact route context. +func _on_checkpoint_activated() -> void: + var spawn_path: NodePath = GameState.scene.spawn_point + if spawn_path.is_empty(): + return + + var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) + var checkpoint: Node = node + + while checkpoint and not checkpoint is Checkpoint: + checkpoint = checkpoint.get_parent() + + if not checkpoint or not checkpoint.get("save_void_and_enemies"): + return + + var save_key := str(get_path()) + var data := { + "position": global_position, + "state": state, + "current_idx": current_patrol_point_idx, + "prev_idx": previous_patrol_point_idx + } + + if guard_movement: + data["movement_dest"] = guard_movement.destination + + GameState.scene.facts[save_key] = data + + +## Reconstructs the guard's state and patrol parameters from the global dictionary. +func _load_state() -> void: + var save_key := str(get_path()) + if GameState.scene.facts.has(save_key): + var data: Dictionary = GameState.scene.facts[save_key] + + global_position = data["position"] + if "_last_position" in self: + set("_last_position", data["position"]) + + state = data["state"] + current_patrol_point_idx = data["current_idx"] + previous_patrol_point_idx = data["prev_idx"] + + if guard_movement and data.has("movement_dest"): + guard_movement.set_destination(data["movement_dest"]) diff --git a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd index 7848b8287d..ec2dbb0e24 100644 --- a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd +++ b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd @@ -110,6 +110,7 @@ func _ready() -> void: for coord: Vector2i in get_used_cells(): consume(coord, true) + _init_persistence() ## Cover all [param cells] with [member terrain_name], hiding any nodes in those cells which are @@ -174,3 +175,46 @@ func uncover_all(duration: float) -> void: await tween.finished clear() self.modulate.a = 1.0 + +## Initializes the connection to the global scene state to listen for checkpoint activations. +## Falls back safely if testing the scene in isolation without the GameState singleton. +func _init_persistence() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + + if not GameState.scene.changed.is_connected(_on_checkpoint_activated): + GameState.scene.changed.connect(_on_checkpoint_activated) + + _load_state() + + +## Captures the current state of consumed tiles when the scene spawn point changes. +## Validates the active checkpoint's persistence configuration before committing data. +func _on_checkpoint_activated() -> void: + var spawn_path: NodePath = GameState.scene.spawn_point + if spawn_path.is_empty(): + return + + # Traverse the scene tree upwards from the spawn point to find the parent Checkpoint node. + # NOTE: Not sure on how to access the checkpoints in one scene apart from this method + var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) + var checkpoint: Node = node + + while checkpoint and not checkpoint is Checkpoint: + checkpoint = checkpoint.get_parent() + + # Abort the save process if the checkpoint lacks the persistence flag. + if not checkpoint or not checkpoint.get("save_void_and_enemies"): + return + + var save_key := str(get_path()) + GameState.scene.facts[save_key] = get_used_cells() + + +## Retrieves and applies the historically consumed tiles from the global state. +## Executes immediately without animation to prevent visual glitches upon scene reload. +func _load_state() -> void: + var save_key := str(get_path()) + if GameState.scene.facts.has(save_key): + var saved_tiles: Array = GameState.scene.facts[save_key] + consume_cells(saved_tiles, true) diff --git a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd index a4b4b82946..0d8efd3424 100644 --- a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd +++ b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd @@ -83,6 +83,7 @@ func _ready() -> void: idle_patrol_path = idle_patrol_path state = state _last_position = position + _init_persistence() func start(detected_node: Node2D) -> void: @@ -141,3 +142,58 @@ func _on_player_capture_area_body_entered(body: Node2D) -> void: var player := body as Player player.defeat(true) + + +## Hooks into the global scene state to monitor for spawn point updates. +func _init_persistence() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + + if not GameState.scene.changed.is_connected(_on_checkpoint_activated): + GameState.scene.changed.connect(_on_checkpoint_activated) + + _load_state() + + +## Packages the enemy's positional data and path progress into the global facts dictionary. +## Verifies the persistence flag of the active checkpoint before saving. +func _on_checkpoint_activated() -> void: + var spawn_path: NodePath = GameState.scene.spawn_point + if spawn_path.is_empty(): + return + + var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) + var checkpoint: Node = node + + while checkpoint and not checkpoint is Checkpoint: + checkpoint = checkpoint.get_parent() + + if not checkpoint or not checkpoint.get("save_void_and_enemies"): + return + + var save_key := str(get_path()) + var data := { + "position": global_position, + "last_position": _last_position, + "state": state + } + + if path_walk_behavior: + data["path_progress"] = path_walk_behavior.get("progress_ratio") + + GameState.scene.facts[save_key] = data + + +## Restores the enemy to its exact state from the previous save. +## Overrides the internal last_position variable to prevent massive particle bursts caused by global teleportation. +func _load_state() -> void: + var save_key := str(get_path()) + if GameState.scene.facts.has(save_key): + var data: Dictionary = GameState.scene.facts[save_key] + + global_position = data["position"] + _last_position = data["last_position"] + state = data["state"] + + if path_walk_behavior and data.has("path_progress"): + path_walk_behavior.set("progress_ratio", data["path_progress"]) diff --git a/scenes/game_elements/props/checkpoint/components/checkpoint.gd b/scenes/game_elements/props/checkpoint/components/checkpoint.gd index d10f67bd38..b9fe0c9657 100644 --- a/scenes/game_elements/props/checkpoint/components/checkpoint.gd +++ b/scenes/game_elements/props/checkpoint/components/checkpoint.gd @@ -3,6 +3,7 @@ @tool class_name Checkpoint extends Area2D +signal activated ## A place where the player respawns if the current scene is reloaded. ## ## A checkpoint is initially invisible. It becomes visible when the player enters the area, which @@ -29,6 +30,10 @@ const REQUIRED_ANIMATIONS := [&"idle", &"appear"] ## be able to interact with the checkpoint. @export var dialogue: DialogueResource = preload("uid://bug2aqd47jgyu") +## Determines if the enemies and the void layer should save their state when +## the player activates this checkpoint. +@export var save_void_and_enemies: bool = false + ## The point where the player will spawn. @onready var spawn_point: SpawnPoint = %SpawnPoint @@ -72,6 +77,7 @@ func _ready() -> void: ## Makes this the active checkpoint. func activate() -> void: + activated.emit() GameState.scene.spawn_point = owner.get_path_to(spawn_point) GameState.save() From 80b6aadf198572f1d06b87d22db187d88c9ce2a2 Mon Sep 17 00:00:00 2001 From: NataliaOspinal <226923708+NataliaOspinal@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:11:40 -0500 Subject: [PATCH 8/8] Shift overall responsability to checkpoint --- .../enemies/guard/components/guard.gd | 37 +++++----------- .../components/tile_map_cover.gd | 43 ++++++------------- .../components/void_spreading_enemy.gd | 39 ++++++----------- .../props/checkpoint/components/checkpoint.gd | 5 ++- scenes/globals/game_state/per_scene_state.gd | 3 ++ 5 files changed, 42 insertions(+), 85 deletions(-) diff --git a/scenes/game_elements/characters/enemies/guard/components/guard.gd b/scenes/game_elements/characters/enemies/guard/components/guard.gd index f950e810b7..5224779304 100644 --- a/scenes/game_elements/characters/enemies/guard/components/guard.gd +++ b/scenes/game_elements/characters/enemies/guard/components/guard.gd @@ -155,7 +155,8 @@ func _ready() -> void: guard_movement.destination_reached.connect(self._on_destination_reached) guard_movement.still_time_finished.connect(self._on_still_time_finished) guard_movement.path_blocked.connect(self._on_path_blocked) - _init_persistence() + add_to_group("persistence_listeners") + call_deferred("_load_state") func _process(delta: float) -> void: @@ -482,32 +483,13 @@ func _on_detection_area_body_exited(body: Node2D) -> void: guard_movement.stop_moving() state = State.INVESTIGATING -## Establishes the persistence listener. -## The state load is deferred to ensure it overrides the guard's default teleport-to-start logic on initialization. -func _init_persistence() -> void: - if GameState.scene == null or not "facts" in GameState.scene: - return +## Responds to the checkpoint's activated signal and records patrol parameters. +func _on_checkpoint_activated(checkpoint: Checkpoint) -> void: + if not checkpoint.save_void_and_enemies: + return - if not GameState.scene.changed.is_connected(_on_checkpoint_activated): - GameState.scene.changed.connect(_on_checkpoint_activated) - - call_deferred("_load_state") - - -## Serializes patrol indices and current destination to maintain the guard's exact route context. -func _on_checkpoint_activated() -> void: - var spawn_path: NodePath = GameState.scene.spawn_point - if spawn_path.is_empty(): + if GameState.scene == null or not "facts" in GameState.scene: return - - var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) - var checkpoint: Node = node - - while checkpoint and not checkpoint is Checkpoint: - checkpoint = checkpoint.get_parent() - - if not checkpoint or not checkpoint.get("save_void_and_enemies"): - return var save_key := str(get_path()) var data := { @@ -523,8 +505,11 @@ func _on_checkpoint_activated() -> void: GameState.scene.facts[save_key] = data -## Reconstructs the guard's state and patrol parameters from the global dictionary. +## Reconstructs the guard's patrol route context upon scene load. func _load_state() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + var save_key := str(get_path()) if GameState.scene.facts.has(save_key): var data: Dictionary = GameState.scene.facts[save_key] diff --git a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd index ec2dbb0e24..fa4726d6cd 100644 --- a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd +++ b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/tile_map_cover.gd @@ -110,7 +110,8 @@ func _ready() -> void: for coord: Vector2i in get_used_cells(): consume(coord, true) - _init_persistence() + add_to_group("persistence_listeners") + _load_state() ## Cover all [param cells] with [member terrain_name], hiding any nodes in those cells which are @@ -176,44 +177,24 @@ func uncover_all(duration: float) -> void: clear() self.modulate.a = 1.0 -## Initializes the connection to the global scene state to listen for checkpoint activations. -## Falls back safely if testing the scene in isolation without the GameState singleton. -func _init_persistence() -> void: +## Responds to the checkpoint's activated signal and saves consumed tiles if permitted. +func _on_checkpoint_activated(checkpoint: Checkpoint) -> void: + if not checkpoint.save_void_and_enemies: + return + if GameState.scene == null or not "facts" in GameState.scene: return - if not GameState.scene.changed.is_connected(_on_checkpoint_activated): - GameState.scene.changed.connect(_on_checkpoint_activated) - - _load_state() - - -## Captures the current state of consumed tiles when the scene spawn point changes. -## Validates the active checkpoint's persistence configuration before committing data. -func _on_checkpoint_activated() -> void: - var spawn_path: NodePath = GameState.scene.spawn_point - if spawn_path.is_empty(): - return - - # Traverse the scene tree upwards from the spawn point to find the parent Checkpoint node. - # NOTE: Not sure on how to access the checkpoints in one scene apart from this method - var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) - var checkpoint: Node = node - - while checkpoint and not checkpoint is Checkpoint: - checkpoint = checkpoint.get_parent() - - # Abort the save process if the checkpoint lacks the persistence flag. - if not checkpoint or not checkpoint.get("save_void_and_enemies"): - return - var save_key := str(get_path()) GameState.scene.facts[save_key] = get_used_cells() -## Retrieves and applies the historically consumed tiles from the global state. -## Executes immediately without animation to prevent visual glitches upon scene reload. +## Reconstructs the holes in the TileMap immediately on scene load. +## Executes without animation to prevent visual glitches upon respawn. func _load_state() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + var save_key := str(get_path()) if GameState.scene.facts.has(save_key): var saved_tiles: Array = GameState.scene.facts[save_key] diff --git a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd index 0d8efd3424..372bfd81ab 100644 --- a/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd +++ b/scenes/game_elements/characters/enemies/void_spreading_enemy/components/void_spreading_enemy.gd @@ -83,7 +83,8 @@ func _ready() -> void: idle_patrol_path = idle_patrol_path state = state _last_position = position - _init_persistence() + add_to_group("persistence_listeners") + _load_state() func start(detected_node: Node2D) -> void: @@ -144,33 +145,14 @@ func _on_player_capture_area_body_entered(body: Node2D) -> void: player.defeat(true) -## Hooks into the global scene state to monitor for spawn point updates. -func _init_persistence() -> void: +## Responds to the checkpoint's activated signal and records position and path progress. +func _on_checkpoint_activated(checkpoint: Checkpoint) -> void: + if not checkpoint.save_void_and_enemies: + return + if GameState.scene == null or not "facts" in GameState.scene: return - if not GameState.scene.changed.is_connected(_on_checkpoint_activated): - GameState.scene.changed.connect(_on_checkpoint_activated) - - _load_state() - - -## Packages the enemy's positional data and path progress into the global facts dictionary. -## Verifies the persistence flag of the active checkpoint before saving. -func _on_checkpoint_activated() -> void: - var spawn_path: NodePath = GameState.scene.spawn_point - if spawn_path.is_empty(): - return - - var node: Node = get_tree().current_scene.get_node_or_null(spawn_path) - var checkpoint: Node = node - - while checkpoint and not checkpoint is Checkpoint: - checkpoint = checkpoint.get_parent() - - if not checkpoint or not checkpoint.get("save_void_and_enemies"): - return - var save_key := str(get_path()) var data := { "position": global_position, @@ -184,9 +166,12 @@ func _on_checkpoint_activated() -> void: GameState.scene.facts[save_key] = data -## Restores the enemy to its exact state from the previous save. -## Overrides the internal last_position variable to prevent massive particle bursts caused by global teleportation. +## Reconstructs the enemy's exact position and path progress on scene load. +## Overrides the internal last_position variable to prevent massive particle bursts caused by teleportation. func _load_state() -> void: + if GameState.scene == null or not "facts" in GameState.scene: + return + var save_key := str(get_path()) if GameState.scene.facts.has(save_key): var data: Dictionary = GameState.scene.facts[save_key] diff --git a/scenes/game_elements/props/checkpoint/components/checkpoint.gd b/scenes/game_elements/props/checkpoint/components/checkpoint.gd index b9fe0c9657..929a09fea7 100644 --- a/scenes/game_elements/props/checkpoint/components/checkpoint.gd +++ b/scenes/game_elements/props/checkpoint/components/checkpoint.gd @@ -77,7 +77,10 @@ func _ready() -> void: ## Makes this the active checkpoint. func activate() -> void: - activated.emit() + for listener: Node in get_tree().get_nodes_in_group("persistence_listeners"): + if listener.has_method("_on_checkpoint_activated") and not activated.is_connected(listener._on_checkpoint_activated): + activated.connect(listener._on_checkpoint_activated) + activated.emit(self) GameState.scene.spawn_point = owner.get_path_to(spawn_point) GameState.save() diff --git a/scenes/globals/game_state/per_scene_state.gd b/scenes/globals/game_state/per_scene_state.gd index eabb13dcdf..dbb73b508a 100644 --- a/scenes/globals/game_state/per_scene_state.gd +++ b/scenes/globals/game_state/per_scene_state.gd @@ -24,6 +24,9 @@ signal lights_changed(lights_on: bool, immediate: bool) ## Set when any introductory dialogue has been played for the current scene. @export var intro_dialogue_shown: bool +## Dictionary to store arbitrary persistent scene facts, such as enemy states and consumed tiles. +@export var facts: Dictionary = {} + ## Current state of artificial lights. Set with [member set_lights_on]. This is ## not saved to disk since the lights are controlled programmatically. var lights_on: bool