From 7957a472438c2a0be02526ee2bd270b1290daf71 Mon Sep 17 00:00:00 2001 From: Rob Kelly Date: Tue, 22 Apr 2025 12:08:09 -0600 Subject: [PATCH] Progress persistence --- levels/mechanic_test/prop_test.tscn | 10 ++- project.godot | 1 + src/game/game.tscn | 80 +++++++++--------- src/player/player.gd | 14 ++++ src/player/player.tscn | 2 +- src/props/bulkhead/bulkhead.gd | 36 +++++++++ src/props/bulkhead/bulkhead.tscn | 2 +- src/props/retinal_scanner/retinal_scanner.gd | 9 +++ .../retinal_scanner/retinal_scanner.tscn | 2 +- src/props/wall_switch/wall_switch.gd | 11 +++ src/props/wall_switch/wall_switch.tscn | 2 +- src/world/gunk_body/gunk_body.gd | 19 ++++- src/world/gunk_body/gunk_body.tscn | 2 +- src/world/gunk_node/grunk_nodule.tscn | 2 +- src/world/gunk_node/gunk_node.gd | 14 ++++ src/world/mechanics/alarm/gunk_alarm.tscn | 2 +- src/world/mechanics/heart/gunk_heart.tscn | 2 +- src/world/mechanics/listener/listener.tscn | 2 +- src/world/mechanics/relay/gunk_relay.tscn | 2 +- src/world/mechanics/trigger/gunk_trigger.tscn | 2 +- src/world/save_state.gd | 81 +++++++++++++++++++ src/world/save_state.gd.uid | 1 + src/world/spook_manager/spook_manager.gd | 7 +- src/world/world.gd | 40 +++++---- src/world/world.tscn | 4 +- src/world/world_item/world_item.gd | 10 +++ src/world/world_item/world_item.tscn | 2 +- 27 files changed, 283 insertions(+), 78 deletions(-) create mode 100644 src/world/save_state.gd create mode 100644 src/world/save_state.gd.uid diff --git a/levels/mechanic_test/prop_test.tscn b/levels/mechanic_test/prop_test.tscn index 7f3b781..edeb2d4 100644 --- a/levels/mechanic_test/prop_test.tscn +++ b/levels/mechanic_test/prop_test.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=12 format=4 uid="uid://cfqirm2o3uo4k"] +[gd_scene load_steps=13 format=4 uid="uid://cfqirm2o3uo4k"] [ext_resource type="PackedScene" uid="uid://c2omlx4ptrc01" path="res://src/world/gunk_body/gunk_body.tscn" id="1_cr8wn"] +[ext_resource type="Texture2D" uid="uid://8cm835h4gxwe" path="res://assets/debug_mask.png" id="2_7477u"] [ext_resource type="Shader" uid="uid://ckxc0ngd37rtk" path="res://src/shaders/gunk.gdshader" id="2_lrgpr"] [ext_resource type="FastNoiseLite" uid="uid://cnlvdtx68giv6" path="res://assets/materials/gunk_noise.tres" id="3_7477u"] @@ -57,7 +58,7 @@ seamless = true seamless_blend_skirt = 0.5 noise = ExtResource("3_7477u") -[sub_resource type="ShaderMaterial" id="ShaderMaterial_qjnj2"] +[sub_resource type="ShaderMaterial" id="ShaderMaterial_lrgpr"] resource_local_to_scene = true render_priority = 0 shader = ExtResource("2_lrgpr") @@ -69,6 +70,7 @@ shader_parameter/time_pixellation = 30.0 shader_parameter/roughness = 0.15 shader_parameter/specular_contribution = 0.8 shader_parameter/emission_strength = 0.02 +shader_parameter/normal_scale = 1.0 shader_parameter/uv_scale = Vector2(4, 4) shader_parameter/time_scale = 0.2 shader_parameter/edge_bleed = 0.25 @@ -79,6 +81,7 @@ shader_parameter/jitter_magnitude = 0.0 shader_parameter/jitter_time_scale = 0.1 shader_parameter/vertex_inflation = 0.0 shader_parameter/inflation_pixellation = 10.0 +shader_parameter/overlay_emission_scale = 1.0 [sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_x2vho"] data = PackedVector3Array(-1, 1, 0.1, -1, -0.8, 0.1, -1, 1, -0.1, -1, 1, -0.1, -1, -0.8, 0.1, -1, -0.8, -0.1, -1, -1, 0.1, -1, -0.8, -0.1, -1, -0.8, 0.1, -1, -1, -2.3, -1, -0.8, -0.1, -1, -1, 0.1, -1, -0.8, -2.3, -1, -0.8, -0.1, -1, -1, -2.3, -1, -0.8, -2.1, -1, -0.8, -0.1, -1, -0.8, -2.3, -1, -0.8, -2.1, -1, -0.8, -2.3, -1, 1, -2.1, -1, 1, -2.1, -1, -0.8, -2.3, -1, 1, -2.3, -1, 1, -0.1, -1, -0.8, -0.1, 1, 1, -0.1, 1, 1, -0.1, -1, -0.8, -0.1, 1, -0.8, -0.1, 1, 1, -0.1, 1, -0.8, -0.1, 1, 1, 0.1, 1, 1, 0.1, 1, -0.8, -0.1, 1, -0.8, 0.1, 1, -0.8, -0.1, 1, -1, 0.1, 1, -0.8, 0.1, 1, -0.8, -2.1, 1, -1, 0.1, 1, -0.8, -0.1, 1, -0.8, -2.3, 1, -1, 0.1, 1, -0.8, -2.1, 1, -1, -2.3, 1, -1, 0.1, 1, -0.8, -2.3, 1, -0.8, -2.3, 1, -0.8, -2.1, 1, 1, -2.3, 1, 1, -2.3, 1, -0.8, -2.1, 1, 1, -2.1, 1, 1, 0.1, 1, -0.8, 0.1, -1, 1, 0.1, -1, 1, 0.1, 1, -0.8, 0.1, -1, -0.8, 0.1, 1, -0.8, 0.1, 1, -1, 0.1, -1, -0.8, 0.1, -1, -0.8, 0.1, 1, -1, 0.1, -1, -1, 0.1, -1, -1, -2.3, -1, -1, 0.1, 1, -1, -2.3, 1, -1, -2.3, -1, -1, 0.1, 1, -1, 0.1, -1, 1, -0.1, 1, 1, -0.1, -1, 1, 0.1, -1, 1, 0.1, 1, 1, -0.1, 1, 1, 0.1, 1, -0.8, -0.1, -1, -0.8, -0.1, 1, -0.8, -2.1, 1, -0.8, -2.1, -1, -0.8, -0.1, -1, -0.8, -2.1, -1, -0.8, -2.3, -1, -1, -2.3, 1, -0.8, -2.3, 1, -0.8, -2.3, -1, -1, -2.3, 1, -1, -2.3, -1, -0.8, -2.3, 1, -0.8, -2.3, -1, 1, -2.3, -1, 1, -2.3, 1, -0.8, -2.3, 1, 1, -2.3, 1, 1, -2.1, -1, 1, -2.1, 1, 1, -2.3, 1, 1, -2.3, -1, 1, -2.1, -1, 1, -2.3, 1, -0.8, -2.1, -1, -0.8, -2.1, 1, 1, -2.1, 1, 1, -2.1, -1, -0.8, -2.1, -1, 1, -2.1) @@ -92,7 +95,8 @@ skeleton = NodePath("GunkBody") [node name="GunkBody" parent="Parallel" instance=ExtResource("1_cr8wn")] mask_dim = 128 -source_gunk_material = SubResource("ShaderMaterial_qjnj2") +initial_mask = ExtResource("2_7477u") +source_gunk_material = SubResource("ShaderMaterial_lrgpr") [node name="CollisionShape3D" type="CollisionShape3D" parent="Parallel/GunkBody"] shape = SubResource("ConcavePolygonShape3D_x2vho") diff --git a/project.godot b/project.godot index f4fcf71..1558793 100644 --- a/project.godot +++ b/project.godot @@ -88,6 +88,7 @@ MeetSpookSource="meet-spook event sources" LurkPoint="Point which a lurking beast may wander to." BeastSpawnPoint="Spawn point for a grunkbeast" GrunkBeast="GrunkBeast instances." +Persistent="Nodes which implement save and load methods" [importer_defaults] diff --git a/src/game/game.tscn b/src/game/game.tscn index 8331e18..d17cb34 100644 --- a/src/game/game.tscn +++ b/src/game/game.tscn @@ -10,46 +10,6 @@ Label/constants/outline_size = 16 Label/font_sizes/font_size = 32 Label/fonts/font = ExtResource("2_s6lek") -[sub_resource type="Animation" id="Animation_s6lek"] -resource_name = "initial_display" -step = 0.05 -tracks/0/type = "value" -tracks/0/imported = false -tracks/0/enabled = true -tracks/0/path = NodePath("MarginContainer/HBoxContainer/Loading:visible_ratio") -tracks/0/interp = 1 -tracks/0/loop_wrap = true -tracks/0/keys = { -"times": PackedFloat32Array(0, 0.8, 1), -"transitions": PackedFloat32Array(1, 1, 1), -"update": 0, -"values": [0.0, 0.0, 1.0] -} -tracks/1/type = "value" -tracks/1/imported = false -tracks/1/enabled = true -tracks/1/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters") -tracks/1/interp = 1 -tracks/1/loop_wrap = true -tracks/1/keys = { -"times": PackedFloat32Array(0), -"transitions": PackedFloat32Array(1), -"update": 1, -"values": [0] -} -tracks/2/type = "value" -tracks/2/imported = false -tracks/2/enabled = true -tracks/2/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible") -tracks/2/interp = 1 -tracks/2/loop_wrap = true -tracks/2/keys = { -"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8), -"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), -"update": 1, -"values": [false, true, false, true, false, true, false, true, false] -} - [sub_resource type="Animation" id="Animation_kgj8g"] length = 0.001 tracks/0/type = "value" @@ -131,6 +91,46 @@ tracks/2/keys = { "values": [0, 1, 2, 3] } +[sub_resource type="Animation" id="Animation_s6lek"] +resource_name = "initial_display" +step = 0.05 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("MarginContainer/HBoxContainer/Loading:visible_ratio") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.8, 1), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [0.0, 0.0, 1.0] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [false, true, false, true, false, true, false, true, false] +} + [sub_resource type="AnimationLibrary" id="AnimationLibrary_kgj8g"] _data = { &"RESET": SubResource("Animation_kgj8g"), diff --git a/src/player/player.gd b/src/player/player.gd index 2764715..1f416d3 100644 --- a/src/player/player.gd +++ b/src/player/player.gd @@ -264,4 +264,18 @@ func _physics_process(delta: float) -> void: move_and_slide() + +#endregion + + +#region Persistence +func serialize() -> Dictionary: + return { + "inventory": inventory, + } + + +func deserialize(data: Dictionary) -> void: + @warning_ignore("unsafe_cast") + inventory.assign(data["inventory"] as Dictionary) #endregion diff --git a/src/player/player.tscn b/src/player/player.tscn index 3a0226f..f95b7cf 100644 --- a/src/player/player.tscn +++ b/src/player/player.tscn @@ -592,7 +592,7 @@ _data = { [sub_resource type="PlaneMesh" id="PlaneMesh_p6grl"] -[node name="Player" type="CharacterBody3D"] +[node name="Player" type="CharacterBody3D" groups=["Persistent"]] collision_layer = 8 collision_mask = 33 script = ExtResource("1_npueo") diff --git a/src/props/bulkhead/bulkhead.gd b/src/props/bulkhead/bulkhead.gd index ac6c48b..bf4c8f8 100644 --- a/src/props/bulkhead/bulkhead.gd +++ b/src/props/bulkhead/bulkhead.gd @@ -1,5 +1,8 @@ extends Node3D +@export var start_open := false + +@export_category("Editor Tools") @export var debug_open: bool: set(value): open() @@ -15,6 +18,28 @@ extends Node3D @onready var nav_link: NavigationLink3D = %NavLink +func _ready() -> void: + if start_open: + _instant_open() + + +func _instant_open() -> void: + nav_link.enabled = true + animation.play("open") + animation.advance(100) + + +func _instant_close() -> void: + nav_link.enabled = false + animation.play("open") + animation.advance(0) + animation.stop() + + +func is_open() -> bool: + return nav_link.enabled + + func open() -> void: nav_link.enabled = true animation.play("open") @@ -27,3 +52,14 @@ func close() -> void: # TODO bespoke close anim? animation.play_backwards("open") nav_link.enabled = false + + +func serialize() -> Dictionary: + return {"open": is_open()} + + +func deserialize(state: Dictionary) -> void: + if state["open"]: + _instant_open() + else: + _instant_close() # unneccessary? diff --git a/src/props/bulkhead/bulkhead.tscn b/src/props/bulkhead/bulkhead.tscn index d7994cb..ac0a68e 100644 --- a/src/props/bulkhead/bulkhead.tscn +++ b/src/props/bulkhead/bulkhead.tscn @@ -281,7 +281,7 @@ _data = { &"spray": SubResource("Animation_88qrs") } -[node name="Bulkhead" instance=ExtResource("1_77udb")] +[node name="Bulkhead" groups=["Persistent"] instance=ExtResource("1_77udb")] script = ExtResource("2_hknvo") [node name="Frame" parent="." index="0"] diff --git a/src/props/retinal_scanner/retinal_scanner.gd b/src/props/retinal_scanner/retinal_scanner.gd index 61a3aff..4044eee 100644 --- a/src/props/retinal_scanner/retinal_scanner.gd +++ b/src/props/retinal_scanner/retinal_scanner.gd @@ -40,3 +40,12 @@ func _activate() -> void: func _on_interactive_selected() -> void: if enabled: interactive.enabled = _has_item() + + +func serialize() -> Dictionary: + return {"enabled": enabled} + + +func deserialize(state: Dictionary) -> void: + if state["enabled"]: + enable() diff --git a/src/props/retinal_scanner/retinal_scanner.tscn b/src/props/retinal_scanner/retinal_scanner.tscn index 9203c36..dc52cfc 100644 --- a/src/props/retinal_scanner/retinal_scanner.tscn +++ b/src/props/retinal_scanner/retinal_scanner.tscn @@ -9,7 +9,7 @@ size = Vector3(0.475, 0.65, 0.2) [sub_resource type="BoxShape3D" id="BoxShape3D_5bfyo"] size = Vector3(0.475, 0.65, 0.2) -[node name="RetinalScanner" type="Node3D"] +[node name="RetinalScanner" type="Node3D" groups=["Persistent"]] script = ExtResource("1_c71b5") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] diff --git a/src/props/wall_switch/wall_switch.gd b/src/props/wall_switch/wall_switch.gd index 2f458d2..839ec8a 100644 --- a/src/props/wall_switch/wall_switch.gd +++ b/src/props/wall_switch/wall_switch.gd @@ -85,3 +85,14 @@ func _on_gunk_body_clear_total_updated(clear_total: float) -> void: func _on_action_delay_timeout() -> void: activated.emit() + + +func serialize() -> Dictionary: + return {"enabled": enabled} + + +func deserialize(state: Dictionary) -> void: + if state["enabled"]: + enable() + else: + disable() diff --git a/src/props/wall_switch/wall_switch.tscn b/src/props/wall_switch/wall_switch.tscn index f9f5d30..59c85c8 100644 --- a/src/props/wall_switch/wall_switch.tscn +++ b/src/props/wall_switch/wall_switch.tscn @@ -189,7 +189,7 @@ size = Vector3(0.475, 0.65, 0.2) [sub_resource type="SphereShape3D" id="SphereShape3D_mxsyy"] radius = 3.0 -[node name="WallSwitch" instance=ExtResource("2_whafo")] +[node name="WallSwitch" groups=["Persistent"] instance=ExtResource("2_whafo")] script = ExtResource("2_kfvqd") enabled = true label = "INTERACTIVE_SWITCH_LABEL" diff --git a/src/world/gunk_body/gunk_body.gd b/src/world/gunk_body/gunk_body.gd index c8b16a1..015bbfa 100644 --- a/src/world/gunk_body/gunk_body.gd +++ b/src/world/gunk_body/gunk_body.gd @@ -61,8 +61,7 @@ func _ready() -> void: # Overlay mesh with gunk material mesh_instance.material_overlay = mat_instance - if initial_mask: - mask_texture.texture = initial_mask + _deferred_init.call_deferred() # Initialize meshtool meshtool.create_from_surface(mesh_instance.mesh as ArrayMesh, 0) @@ -71,6 +70,12 @@ func _ready() -> void: _thread.start(_async_compute_clear_total) +func _deferred_init() -> void: + if initial_mask: + mask_texture.texture = initial_mask + mask_texture.visible = true + + func _trigger_recompute_deferred() -> void: _mutex.lock() _mask_tx = mask_viewport.get_texture() @@ -286,3 +291,13 @@ func _process(_delta: float) -> void: func _on_mask_painted() -> void: # XXX any problem with posting each frame? _trigger_recompute_deferred.call_deferred() + + +func serialize() -> Dictionary: + var state := {"mask": mask_viewport.get_texture().get_image()} + return state + + +func deserialize(state: Dictionary) -> void: + @warning_ignore("unsafe_cast") + initial_mask = ImageTexture.create_from_image(state["mask"] as Image) diff --git a/src/world/gunk_body/gunk_body.tscn b/src/world/gunk_body/gunk_body.tscn index 38608d1..4c466ac 100644 --- a/src/world/gunk_body/gunk_body.tscn +++ b/src/world/gunk_body/gunk_body.tscn @@ -5,7 +5,7 @@ [ext_resource type="Script" uid="uid://bom5qysgfvap1" path="res://src/world/gunk_body/draw_controller.gd" id="2_kkcjw"] [ext_resource type="Script" uid="uid://ba7480ara8eo" path="res://levels/sandbox/debug_draw.gd" id="3_m8wx4"] -[node name="GunkBody" type="StaticBody3D"] +[node name="GunkBody" type="StaticBody3D" groups=["Persistent"]] collision_layer = 5 collision_mask = 0 script = ExtResource("1_qqbpr") diff --git a/src/world/gunk_node/grunk_nodule.tscn b/src/world/gunk_node/grunk_nodule.tscn index 989d35f..9ae37b1 100644 --- a/src/world/gunk_node/grunk_nodule.tscn +++ b/src/world/gunk_node/grunk_nodule.tscn @@ -19,7 +19,7 @@ stream_1/stream = ExtResource("5_omayi") stream_2/stream = ExtResource("6_yg8lg") stream_3/stream = ExtResource("7_4kci5") -[node name="GrunkNodule" type="StaticBody3D"] +[node name="GrunkNodule" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_iyr82") diff --git a/src/world/gunk_node/gunk_node.gd b/src/world/gunk_node/gunk_node.gd index 06af623..64ee6e5 100644 --- a/src/world/gunk_node/gunk_node.gd +++ b/src/world/gunk_node/gunk_node.gd @@ -20,6 +20,10 @@ var _sustained_damage := 0.0 var _hit_this_frame := false +func _enter_tree() -> void: + add_to_group("Persistent", true) + + ## Called each frame this node takes a hit. ## ## Derived types should override `_hit()` as a lifecycle method. @@ -68,3 +72,13 @@ func destroy() -> void: func _destroy() -> void: pass # Implemented in derived type + + +func serialize() -> Dictionary: + # Nothing to serialize, but we need a placeholder value to show we haven't been destroyed. + return {} + + +func deserialize(_state: Dictionary) -> void: + # Nothing to deserialize, but we won't be freed! + pass diff --git a/src/world/mechanics/alarm/gunk_alarm.tscn b/src/world/mechanics/alarm/gunk_alarm.tscn index 068ccb6..7101dab 100644 --- a/src/world/mechanics/alarm/gunk_alarm.tscn +++ b/src/world/mechanics/alarm/gunk_alarm.tscn @@ -243,7 +243,7 @@ _data = { &"pulse": SubResource("Animation_vokcn") } -[node name="GunkAlarm" type="StaticBody3D"] +[node name="GunkAlarm" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_piaxx") diff --git a/src/world/mechanics/heart/gunk_heart.tscn b/src/world/mechanics/heart/gunk_heart.tscn index 3a1cc68..2777a33 100644 --- a/src/world/mechanics/heart/gunk_heart.tscn +++ b/src/world/mechanics/heart/gunk_heart.tscn @@ -99,7 +99,7 @@ _data = { &"pulse": SubResource("Animation_eu6st") } -[node name="GunkHeart" type="StaticBody3D"] +[node name="GunkHeart" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_ftym0") diff --git a/src/world/mechanics/listener/listener.tscn b/src/world/mechanics/listener/listener.tscn index e549b23..221378f 100644 --- a/src/world/mechanics/listener/listener.tscn +++ b/src/world/mechanics/listener/listener.tscn @@ -53,7 +53,7 @@ _data = { &"trigger": SubResource("Animation_htscg") } -[node name="Listener" type="StaticBody3D"] +[node name="Listener" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_htscg") diff --git a/src/world/mechanics/relay/gunk_relay.tscn b/src/world/mechanics/relay/gunk_relay.tscn index 7fab607..3053cf1 100644 --- a/src/world/mechanics/relay/gunk_relay.tscn +++ b/src/world/mechanics/relay/gunk_relay.tscn @@ -111,7 +111,7 @@ _data = { &"trigger": SubResource("Animation_rdv5j") } -[node name="GunkRelay" type="StaticBody3D"] +[node name="GunkRelay" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_rdv5j") diff --git a/src/world/mechanics/trigger/gunk_trigger.tscn b/src/world/mechanics/trigger/gunk_trigger.tscn index fbd2d6d..5238831 100644 --- a/src/world/mechanics/trigger/gunk_trigger.tscn +++ b/src/world/mechanics/trigger/gunk_trigger.tscn @@ -13,7 +13,7 @@ emission_energy_multiplier = 0.0 [sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_t1c4j"] data = PackedVector3Array(0.7236, -0.4472, 0.5257, 0, -1, 0, -0.2764, -0.4472, 0.8506, 0, -1, 0, 0.7236, -0.4472, 0.5257, 0.7236, -0.4472, -0.5257, -0.2764, -0.4472, 0.8506, 0, -1, 0, -0.8944, -0.4472, 0, -0.8944, -0.4472, 0, 0, -1, 0, -0.2764, -0.4472, -0.8506, -0.2764, -0.4472, -0.8506, 0, -1, 0, 0.7236, -0.4472, -0.5257, 0.7236, -0.4472, -0.5257, 0.7236, -0.4472, 0.5257, 0.8944, 0.4472, 0, 0.7236, -0.4472, 0.5257, -0.2764, -0.4472, 0.8506, 0.2764, 0.4472, 0.8506, -0.2764, -0.4472, 0.8506, -0.8944, -0.4472, 0, -0.7236, 0.4472, 0.5257, -0.8944, -0.4472, 0, -0.2764, -0.4472, -0.8506, -0.7236, 0.4472, -0.5257, -0.2764, -0.4472, -0.8506, 0.7236, -0.4472, -0.5257, 0.2764, 0.4472, -0.8506, 0.8944, 0.4472, 0, 0.7236, -0.4472, 0.5257, 0.2764, 0.4472, 0.8506, 0.2764, 0.4472, 0.8506, -0.2764, -0.4472, 0.8506, -0.7236, 0.4472, 0.5257, -0.7236, 0.4472, 0.5257, -0.8944, -0.4472, 0, -0.7236, 0.4472, -0.5257, -0.7236, 0.4472, -0.5257, -0.2764, -0.4472, -0.8506, 0.2764, 0.4472, -0.8506, 0.2764, 0.4472, -0.8506, 0.7236, -0.4472, -0.5257, 0.8944, 0.4472, 0, 0.8944, 0.4472, 0, 0.2764, 0.4472, 0.8506, 0, 1, 0, 0.2764, 0.4472, 0.8506, -0.7236, 0.4472, 0.5257, 0, 1, 0, -0.7236, 0.4472, 0.5257, -0.7236, 0.4472, -0.5257, 0, 1, 0, -0.7236, 0.4472, -0.5257, 0.2764, 0.4472, -0.8506, 0, 1, 0, 0.2764, 0.4472, -0.8506, 0.8944, 0.4472, 0, 0, 1, 0) -[node name="GunkTrigger" type="StaticBody3D"] +[node name="GunkTrigger" type="StaticBody3D" groups=["Persistent"]] collision_layer = 36 collision_mask = 0 script = ExtResource("1_t1c4j") diff --git a/src/world/save_state.gd b/src/world/save_state.gd new file mode 100644 index 0000000..3b82f38 --- /dev/null +++ b/src/world/save_state.gd @@ -0,0 +1,81 @@ +class_name SaveState extends Resource +## Serializable container for gameplay state. + +const CURRENT_VERSION := 0 +const PERSISTENT_GROUP := "Persistent" +const SERIALIZE_METHOD := "serialize" +const DESERIALIZE_METHOD := "deserialize" + +@export var save_version := CURRENT_VERSION + +@export var level_path: String + +@export var persistent_state: Dictionary[String, Dictionary] = {} + +@export_group("WorldManager State") +@export var grunk_tank_limit: int +@export var mp3_player_unlocked: bool +@export var toothbrush_unlocked: bool +@export var stickers_unlocked: bool + +@export var grunk_tank: float +@export var grunk_vault: float +@export var alert_level: int + + +static func node_key(node: Node, world: World) -> String: + return str(world.level_root.get_path_to(node)) + + +func load_to_world(world: World) -> void: + if level_path != world.current_level.resource_path: + push_warning( + "This save is for ", + level_path, + " but the loaded level is for ", + world.current_level.resource_path + ) + + world.manager.grunk_tank_limit = grunk_tank_limit + world.manager.mp3_player_unlocked = mp3_player_unlocked + world.manager.toothbrush_unlocked = toothbrush_unlocked + world.manager.stickers_unlocked = stickers_unlocked + world.manager.grunk_tank = grunk_tank + world.manager.grunk_vault = grunk_vault + world.manager.alert_level = alert_level + + var persistent := world.get_tree().get_nodes_in_group(PERSISTENT_GROUP) + + for node: Node in persistent: + var key := SaveState.node_key(node, world) + if key in persistent_state: + # Node is in our persistent state, so load it with data. + Callable(node, DESERIALIZE_METHOD).call(persistent_state[key]) + else: + # Node isn't in our persistent state, so it must have been destroyed. + node.queue_free() + + +static func serialize(world: World) -> SaveState: + var save := SaveState.new() + + save.level_path = world.current_level.resource_path + + save.grunk_tank_limit = world.manager.grunk_tank_limit + save.mp3_player_unlocked = world.manager.mp3_player_unlocked + save.toothbrush_unlocked = world.manager.toothbrush_unlocked + save.stickers_unlocked = world.manager.stickers_unlocked + save.grunk_tank = world.manager.grunk_tank + save.grunk_vault = world.manager.grunk_vault + save.alert_level = world.manager.alert_level + + # NOTE: I'm assuming that `persistent` will have the same order ever time the world is loaded. + # This may not be the case. If so, we need to find a different way to uniquely identify nodes. + var persistent := world.get_tree().get_nodes_in_group(PERSISTENT_GROUP) + + for node: Node in persistent: + var key := SaveState.node_key(node, world) + var data: Dictionary = Callable(node, SERIALIZE_METHOD).call() + save.persistent_state[key] = data + + return save diff --git a/src/world/save_state.gd.uid b/src/world/save_state.gd.uid new file mode 100644 index 0000000..7fe81e5 --- /dev/null +++ b/src/world/save_state.gd.uid @@ -0,0 +1 @@ +uid://dsyyk7muaabf5 diff --git a/src/world/spook_manager/spook_manager.gd b/src/world/spook_manager/spook_manager.gd index 07d741b..a889e84 100644 --- a/src/world/spook_manager/spook_manager.gd +++ b/src/world/spook_manager/spook_manager.gd @@ -51,8 +51,11 @@ func on_alert_raised(new_level: int) -> void: # Set up meet-spook. # Get closest MeetSpook point to player. if Player.instance: - var closest := SceneTools.closest_in_group(Player.instance, MeetSpook.GROUP) - (closest as MeetSpook).prepare() + var closest := ( + SceneTools.closest_in_group(Player.instance, MeetSpook.GROUP) as MeetSpook + ) + if closest: + closest.prepare() 2: # LEVEL 2: AGGRESSIVE # Beast pursues player on sight. diff --git a/src/world/world.gd b/src/world/world.gd index 559e8c5..1bdd771 100644 --- a/src/world/world.gd +++ b/src/world/world.gd @@ -2,7 +2,7 @@ class_name World extends Node ## Access and flow control for the game world. @export var pause_enabled := true -@export var save_path := "user://saved_game.tscn" +@export var save_path := "user://save_state.res" @export var manager: WorldManager @export var spook_manager: SpookManager @@ -46,11 +46,19 @@ func unpause() -> void: get_tree().paused = false -func load_level(level: PackedScene) -> void: +func load_level(level: PackedScene, save: SaveState = null) -> void: for c: Node in level_root.get_children(): c.queue_free() + level_root.remove_child(c) current_level = level - level_root.add_child(level.instantiate()) + print("Instantiating level from ", level.resource_path) + var level_instance := level.instantiate() + #if save: + #level_instance.ready.connect(func() -> void: save.load_to_world(self), CONNECT_ONE_SHOT) + level_root.add_child(level_instance) + if save: + save.load_to_world(self) + print("Done!") func on_player_death() -> void: @@ -59,23 +67,21 @@ func on_player_death() -> void: ui_root.add_child(kill_screen) +func _reload_saved(save: SaveState) -> void: + load_level(current_level, save) + + func on_game_over() -> void: - # TODO: reload from last checkpoint - # in the mean time, just reload the level - Game.instance.queue_scene(save_path) - - -func _reown_tree(node: Node) -> void: - for c: Node in node.get_children(): - c.set_owner(self) - _reown_tree(c) + # reload the level from the last save + if ResourceLoader.exists(save_path): + Game.instance.queue_load(save_path).finally(_reload_saved) + else: + load_level(current_level) func save_progress() -> void: - # No way this works, right? - print("Preparing world for save...") - var save := PackedScene.new() - _reown_tree(self) - save.pack(self) + print("Preparing save state...") + var save := SaveState.serialize(self) print("Writing save to ", save_path) ResourceSaver.save(save, save_path) + print("Done!") diff --git a/src/world/world.tscn b/src/world/world.tscn index 2559234..71f53a9 100644 --- a/src/world/world.tscn +++ b/src/world/world.tscn @@ -3,15 +3,15 @@ [ext_resource type="Script" uid="uid://cgqmhtemibxc5" path="res://src/world/world.gd" id="1_1k4gi"] [ext_resource type="Resource" uid="uid://tgac5tnfx56r" path="res://src/world/world_manager.tres" id="2_5kmgb"] [ext_resource type="PackedScene" uid="uid://byvjsvavbg5xe" path="res://src/ui/menus/pause_menu/pause_menu.tscn" id="2_6fy3g"] -[ext_resource type="PackedScene" uid="uid://bov4ok76woyc" path="res://levels/ghost_ship/ghost_ship.tscn" id="2_jte2u"] [ext_resource type="Resource" uid="uid://0i72bf8ip1lx" path="res://src/world/spook_manager.tres" id="3_l0av5"] +[ext_resource type="PackedScene" uid="uid://b8rv6dg4tgaeb" path="res://src/world/gunk_node/mechanic_test.tscn" id="4_l0av5"] [ext_resource type="PackedScene" uid="uid://c0uitm5cg88h1" path="res://src/ui/menus/kill_screen/kill_screen.tscn" id="6_l0av5"] [node name="World" type="Node"] script = ExtResource("1_1k4gi") manager = ExtResource("2_5kmgb") spook_manager = ExtResource("3_l0av5") -initial_level = ExtResource("2_jte2u") +initial_level = ExtResource("4_l0av5") pause_scene = ExtResource("2_6fy3g") kill_screen_scene = ExtResource("6_l0av5") diff --git a/src/world/world_item/world_item.gd b/src/world/world_item/world_item.gd index ce267d4..bf84f07 100644 --- a/src/world/world_item/world_item.gd +++ b/src/world/world_item/world_item.gd @@ -32,3 +32,13 @@ func _on_interactive_activated() -> void: Player.instance.add_item(item) # TODO: animation, sfx on collect? queue_free() + + +func serialize() -> Dictionary: + # Nothing to serialize, but we need a placeholder value to show we haven't been destroyed. + return {} + + +func deserialize(_state: Dictionary) -> void: + # Nothing to deserialize, but we won't be freed! + pass diff --git a/src/world/world_item/world_item.tscn b/src/world/world_item/world_item.tscn index 2716a8e..6c43c0f 100644 --- a/src/world/world_item/world_item.tscn +++ b/src/world/world_item/world_item.tscn @@ -6,7 +6,7 @@ [sub_resource type="SphereShape3D" id="SphereShape3D_0mein"] radius = 0.25 -[node name="WorldItem" type="MeshInstance3D"] +[node name="WorldItem" type="MeshInstance3D" groups=["Persistent"]] script = ExtResource("1_sptcj") [node name="Interactive" type="StaticBody3D" parent="."]