Skip to content
11 changes: 11 additions & 0 deletions scenes/dev/level_design/void_persistence/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://28abe4vgj3ul"]

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

[resource]
script = ExtResource("1_ikuea")
title = "Void Persistence"
description = "Learn how to set up your checkpoints for persistent enemies!"
first_scene = "uid://dwgvjv2wxnr0k"
threads_to_collect = 0
metadata/_custom_type_script = "uid://dts1hwdy3phin"
309 changes: 309 additions & 0 deletions scenes/dev/level_design/void_persistence/void_persistence_test.tscn

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func _ready() -> void:
return

for parent: Node in consumable_node_holders:
if parent == null:
continue
for child: Node in parent.get_children():
if child is Node2D:
var coord := coord_for(child)
Expand All @@ -110,6 +112,12 @@ func _ready() -> void:

for coord: Vector2i in get_used_cells():
consume(coord, true)
if (
GameState.scene != null
and not GameState.scene.checkpoint_activated.is_connected(_on_checkpoint_activated)
):
GameState.scene.checkpoint_activated.connect(_on_checkpoint_activated)
_load_state()


## Cover all [param cells] with [member terrain_name], hiding any nodes in those cells which are
Expand Down Expand Up @@ -176,5 +184,28 @@ func uncover_all(duration: float, animate_modulate: bool = false) -> void:

await tween.finished
clear()
if animate_modulate:
self.modulate.a = 1.0
self.modulate.a = 1.0


## 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

var save_key := str(get_path())
GameState.scene.facts[save_key] = get_used_cells()


## 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]
consume_cells(saved_tiles, true)
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ func _ready() -> void:
idle_patrol_path = idle_patrol_path
state = state
_last_position = position
if (
GameState.scene != null
and not GameState.scene.checkpoint_activated.is_connected(_on_checkpoint_activated)
):
GameState.scene.checkpoint_activated.connect(_on_checkpoint_activated)
_load_state()

if Engine.is_editor_hint():
set_process(false)
Expand Down Expand Up @@ -203,3 +209,38 @@ func _on_player_capture_area_body_entered(body: Node2D) -> void:

var player := body as Player
player.defeat(true)


## Responds to the checkpoint's activated signal and records position and path progress.
func _on_checkpoint_activated(checkpoint: Checkpoint) -> void:
if not checkpoint.get("save_void_and_enemies"):
return

if GameState.scene == null or not "facts" in GameState.scene:
return

var save_key := str(get_path())
var data := {"position": global_position, "last_position": _last_position, "state": state}

if path_walk_behavior:
data["path_direction"] = path_walk_behavior.direction

GameState.scene.facts[save_key] = data


## Reconstructs the enemy's exact position and path progress on scene load.
## Prevents 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]

global_position = data["position"]
_last_position = data["last_position"]
state = data["state"]

if path_walk_behavior and data.has("path_direction"):
path_walk_behavior.direction = data["path_direction"]
2 changes: 1 addition & 1 deletion scenes/game_elements/props/checkpoint/checkpoint.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ 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.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=1178774098]
position = Vector2(0, -128)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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

Expand Down Expand Up @@ -72,6 +76,8 @@ func _ready() -> void:

## Makes this the active checkpoint.
func activate() -> void:
if GameState.scene != null:
GameState.scene.checkpoint_activated.emit(self)
GameState.scene.spawn_point = owner.get_path_to(spawn_point)
GameState.save()

Expand Down
7 changes: 7 additions & 0 deletions scenes/globals/game_state/per_scene_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ extends Resource
## lights should turn on gradually.
signal lights_changed(lights_on: bool, immediate: bool)

## Emitted when the player activates a checkpoint.
@warning_ignore("unused_signal")
signal checkpoint_activated(checkpoint: Area2D)

## The path to the current scene.
@export var path: String

Expand All @@ -24,6 +28,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 = {}
Comment on lines +31 to +32

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Interesting! We already have 2 other generic facts, one global GameState.global.facts, and one per-quest GameState.quest.facts.

For collected buttons, we use those because we don't want the buttons to re-appear when replaying the scene. Now I think for the Void enemy position and consumed tiles, it makes sense to add a new per-scene generic facts. Adding this here will make them reset when the scene changes. Seems to be what we want.


## 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ limit_target = NodePath("../../../TileMapLayers/Water")
metadata/_custom_type_script = "uid://bhexx6mj1xv3q"

[node name="VoidSpreadingEnemy" parent="OnTheGround" unique_id=1727069559 node_paths=PackedStringArray("void_layer", "idle_patrol_path") instance=ExtResource("6_wtoj6")]
position = Vector2(735, 3148)
position = Vector2(736, 3151)
void_layer = NodePath("../../TileMapLayers/CanvasGroup/Void")
idle_patrol_path = NodePath("../VoidSpreadingEnemyPatrolPath")

Expand Down
Loading