diff --git a/assets/text/text.csv b/assets/text/text.csv index 77cc43e..9e23762 100644 --- a/assets/text/text.csv +++ b/assets/text/text.csv @@ -9,6 +9,7 @@ UI_OFF,Off UI_BACK,"⏎ Back" UI_LOCKED,Locked UI_QUIT,Quit +UI_LOADING,Loading , PAUSE_HEADING,Paused PAUSE_RESUME,Resume diff --git a/project.godot b/project.godot index e2e6cbe..f4fcf71 100644 --- a/project.godot +++ b/project.godot @@ -16,7 +16,7 @@ warnings/threads/thread_model=2 [application] config/name="Grunk" -run/main_scene="uid://884jqafhtrv0" +run/main_scene="uid://qpq2cm1hgeha" config/project_settings_override="user://settings.godot" config/features=PackedStringArray("4.4", "Forward Plus") run/max_fps=60 diff --git a/src/game/game.gd b/src/game/game.gd index 26cdf14..35c3f2b 100644 --- a/src/game/game.gd +++ b/src/game/game.gd @@ -1,8 +1,12 @@ class_name Game extends Node ## Interface to the game as an application. -@export_category("Game Scenes") -@export var world_scene: PackedScene +@export_file("*.tscn") var start_scene: String + +var _loading_resources: Dictionary[String, Promise] = {} + +@onready var content: Node = %Content +@onready var loading_screen: Control = %LoadingScreen ## Handy typed singleton access. static var settings: GameSettingsType: @@ -16,5 +20,91 @@ static var runtime: GameRuntimeType: static var instance: Game +class Promise: + var _callbacks: Array[Callable] = [] + var _end_callbacks: Array[Callable] = [] + + func then(fn: Callable) -> Promise: + _callbacks.push_back(fn) + return self + + func finally(fn: Callable) -> Promise: + _end_callbacks.push_back(fn) + return self + + func resolve(res: Variant) -> void: + for fn: Callable in _callbacks + _end_callbacks: + fn.call(res) + + +class ScenePromise: + extends Promise + + func resolve(res: Variant) -> void: + @warning_ignore("unsafe_cast") + var instance: Node = (res as PackedScene).instantiate() + super.resolve(instance) + + func _ready() -> void: Game.instance = self + _initial_load.call_deferred() + + +func _initial_load() -> void: + queue_scene(start_scene) + + +## Unload the running scene & queue up a new scene to be loaded in the background. +## +## The loading screen will be shown until the scene is loaded. +func queue_scene(path: String) -> Promise: + return queue_load(path, ScenePromise.new(), "PackedScene").finally(_finish_scene_load) + + +## Queue a resource to be loaded in the background. +## +## Returns a `Promise` which can be used to attach callbacks +## which will be called with the resource after it is loaded. +func queue_load(path: String, promise: Promise = null, type_hint: String = "") -> Promise: + if not promise: + promise = Promise.new() + _loading_resources[path] = promise + ResourceLoader.load_threaded_request(path, type_hint) + return promise + + +func _unload_content() -> void: + for child: Node in content.get_children(): + child.queue_free() + + +func _finish_scene_load(scene_instance: Node) -> void: + # Unpause in case the previous scene was paused. + get_tree().paused = false + # Reset time scale in case it's been changed. + Engine.time_scale = 1.0 + + content.add_child(scene_instance) + scene_instance.reparent(content) + + +func _process(_delta: float) -> void: + if _loading_resources: + loading_screen.visible = true + + for key: String in _loading_resources.keys(): + match ResourceLoader.load_threaded_get_status(key): + ResourceLoader.THREAD_LOAD_LOADED: + _loading_resources[key].resolve(ResourceLoader.load_threaded_get(key)) + _loading_resources.erase(key) + ResourceLoader.THREAD_LOAD_FAILED: + assert(false, "Failed loading resource: " + key) + ResourceLoader.THREAD_LOAD_INVALID_RESOURCE: + assert(false, "Can't load invalid resource: " + key) + _: + # Continue loading + pass + + if not _loading_resources: + loading_screen.visible = false diff --git a/src/game/game.tscn b/src/game/game.tscn index 1d32396..8331e18 100644 --- a/src/game/game.tscn +++ b/src/game/game.tscn @@ -1,8 +1,236 @@ -[gd_scene load_steps=3 format=3 uid="uid://qpq2cm1hgeha"] +[gd_scene load_steps=14 format=3 uid="uid://qpq2cm1hgeha"] [ext_resource type="Script" uid="uid://dxl25lkyped4" path="res://src/game/game.gd" id="1_qnjlk"] -[ext_resource type="PackedScene" uid="uid://884jqafhtrv0" path="res://src/world/world.tscn" id="2_s6lek"] +[ext_resource type="FontFile" uid="uid://oq8ue2qrfijg" path="res://assets/fonts/Silkscreen/Silkscreen-Regular.ttf" id="2_s6lek"] +[ext_resource type="Script" uid="uid://ctf1if4ly6nun" path="res://src/game/loading_screen.gd" id="3_kgj8g"] + +[sub_resource type="Theme" id="Theme_s6lek"] +Label/colors/font_color = Color(0.137255, 0.984314, 0.34902, 1) +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" +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), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.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), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [false] +} + +[sub_resource type="Animation" id="Animation_l80un"] +resource_name = "ellipsis_loop" +length = 1.2 +loop_mode = 1 +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), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("MarginContainer/HBoxContainer/Blinker:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [false] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("MarginContainer/HBoxContainer/Ellipsis:visible_characters") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 0.3, 0.6, 0.9), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_kgj8g"] +_data = { +&"RESET": SubResource("Animation_kgj8g"), +&"ellipsis_loop": SubResource("Animation_l80un"), +&"initial_display": SubResource("Animation_s6lek") +} + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_s6lek"] +animation = &"ellipsis_loop" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_kgj8g"] +animation = &"initial_display" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_l80un"] +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_xptat"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_l80un"] +states/ellipsis_loop/node = SubResource("AnimationNodeAnimation_s6lek") +states/ellipsis_loop/position = Vector2(630, 100) +states/initial_display/node = SubResource("AnimationNodeAnimation_kgj8g") +states/initial_display/position = Vector2(399, 100) +transitions = ["Start", "initial_display", SubResource("AnimationNodeStateMachineTransition_l80un"), "initial_display", "ellipsis_loop", SubResource("AnimationNodeStateMachineTransition_xptat")] [node name="Game" type="Node"] script = ExtResource("1_qnjlk") -world_scene = ExtResource("2_s6lek") +start_scene = "uid://884jqafhtrv0" + +[node name="Content" type="Node" parent="."] +unique_name_in_owner = true + +[node name="LoadingScreen" type="Control" parent="."] +unique_name_in_owner = true +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = SubResource("Theme_s6lek") +script = ExtResource("3_kgj8g") + +[node name="ColorRect" type="ColorRect" parent="LoadingScreen"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.0196078, 0.0431373, 0.0627451, 1) + +[node name="MarginContainer" type="MarginContainer" parent="LoadingScreen"] +layout_mode = 1 +anchors_preset = 2 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_top = -40.0 +offset_right = 40.0 +grow_vertical = 0 +theme_override_constants/margin_left = 32 +theme_override_constants/margin_bottom = 32 + +[node name="HBoxContainer" type="HBoxContainer" parent="LoadingScreen/MarginContainer"] +layout_mode = 2 + +[node name="Prompt" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"] +layout_mode = 2 +text = ">" + +[node name="Blinker" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"] +visible = false +layout_mode = 2 +text = "_" + +[node name="Loading" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"] +layout_mode = 2 +text = "UI_LOADING" +visible_characters = 0 +visible_characters_behavior = 1 +visible_ratio = 0.0 + +[node name="Ellipsis" type="Label" parent="LoadingScreen/MarginContainer/HBoxContainer"] +layout_mode = 2 +text = "..." +visible_characters = 0 +visible_characters_behavior = 1 +visible_ratio = 0.0 + +[node name="LoaderAnimation" type="AnimationPlayer" parent="LoadingScreen"] +libraries = { +&"": SubResource("AnimationLibrary_kgj8g") +} + +[node name="LoaderTree" type="AnimationTree" parent="LoadingScreen/LoaderAnimation"] +unique_name_in_owner = true +root_node = NodePath("%LoaderTree/../..") +tree_root = SubResource("AnimationNodeStateMachine_l80un") +anim_player = NodePath("..") + +[connection signal="visibility_changed" from="LoadingScreen" to="LoadingScreen" method="_on_visibility_changed"] diff --git a/src/game/loading_screen.gd b/src/game/loading_screen.gd new file mode 100644 index 0000000..f720134 --- /dev/null +++ b/src/game/loading_screen.gd @@ -0,0 +1,9 @@ +extends Control + +@onready var loader_tree: AnimationTree = %LoaderTree +@onready var state_machine: AnimationNodeStateMachinePlayback = loader_tree["parameters/playback"] + + +func _on_visibility_changed() -> void: + if state_machine: + state_machine.start("initial_display", true) diff --git a/src/game/loading_screen.gd.uid b/src/game/loading_screen.gd.uid new file mode 100644 index 0000000..abe8f25 --- /dev/null +++ b/src/game/loading_screen.gd.uid @@ -0,0 +1 @@ +uid://ctf1if4ly6nun diff --git a/src/player/camera_controller.gd b/src/player/camera_controller.gd index f1dad75..859e324 100644 --- a/src/player/camera_controller.gd +++ b/src/player/camera_controller.gd @@ -4,14 +4,12 @@ const PITCH_LIMIT := deg_to_rad(85.0) const FOCUS_SENSITIVITY := 0.2 const FOCUS_ACCELERATION := 8 -@onready var player: Player = owner - @onready var _target := Vector2(rotation.x, rotation.y) func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: - if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and player.look_enabled: + if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED and Player.instance.look_enabled: camera_motion((event as InputEventMouseMotion).relative) elif event is InputEventMouseButton: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) @@ -21,7 +19,7 @@ func camera_motion(motion: Vector2) -> void: var x_sensitivity: float = Game.settings.mouse_sensitivity_x var y_sensitivity: float = Game.settings.mouse_sensitivity_y var invert_pitch: bool = Game.settings.invert_pitch - if player.firing: + if Player.instance.firing: # Focus movement when firing # Game mechanic, should not be user-configurable. x_sensitivity = FOCUS_SENSITIVITY @@ -42,7 +40,7 @@ func reset_pitch(tween_duration: float) -> void: func _physics_process(delta: float) -> void: var mouse_accel: float = Game.settings.mouse_acceleration - if player.firing: + if Player.instance.firing: mouse_accel = FOCUS_ACCELERATION var weight := 1 - exp(-mouse_accel * delta) diff --git a/src/player/footsteps/footstep_controller.gd b/src/player/footsteps/footstep_controller.gd index 736f52c..07ac4b7 100644 --- a/src/player/footsteps/footstep_controller.gd +++ b/src/player/footsteps/footstep_controller.gd @@ -6,7 +6,6 @@ const VELOCITY_FACTOR := 2.0 var _on_right_foot := false -@onready var player: Player = owner @onready var left_foot: FootController = %LeftFoot @onready var right_foot: FootController = %RightFoot @@ -16,12 +15,12 @@ var _on_right_foot := false func play_footstep() -> void: - if player.sneaking: + if Player.instance.sneaking: return var foot := right_foot if _on_right_foot else left_foot - var relative_speed := player.velocity.length() - MUTE_VELOCITY + var relative_speed := Player.instance.velocity.length() - MUTE_VELOCITY if relative_speed < 0: return diff --git a/src/player/headbob_controller.gd b/src/player/headbob_controller.gd index 77b2633..cb78321 100644 --- a/src/player/headbob_controller.gd +++ b/src/player/headbob_controller.gd @@ -16,13 +16,11 @@ var timescale: float: get: return self["parameters/timescale/scale"] -@onready var player: Player = owner - func _process(delta: float) -> void: - var speed := player.velocity.length() + var speed := Player.instance.velocity.length() var weight := 1 - exp(-BLEND_ACCELERATION * delta) - if player.is_on_floor(): + if Player.instance.is_on_floor(): var timescale_target := speed * VELOCITY_TIMESCALE_FACTOR timescale = lerpf(timescale, timescale_target, weight) else: @@ -30,7 +28,7 @@ func _process(delta: float) -> void: if Game.settings.enable_head_bob: var blend_target := 0.0 - if player.is_on_floor(): + if Player.instance.is_on_floor(): blend_target = speed * VELOCITY_BLEND_FACTOR blend = lerpf(blend, blend_target, weight) diff --git a/src/player/player.gd b/src/player/player.gd index 48c465f..2764715 100644 --- a/src/player/player.gd +++ b/src/player/player.gd @@ -183,6 +183,12 @@ func _signal_death() -> void: func _physics_process(delta: float) -> void: + # REMOVEME + if Input.is_action_just_pressed("ui_page_down"): + get_grabbed() + if Input.is_action_just_pressed("ui_page_up"): + World.instance.save_progress() + # Will be null if no valid interactor is selected. var interactive: Interactive = interact_ray.get_collider() as Interactive hud.select_interactive(interactive) diff --git a/src/ui/hud/player_hud.tscn b/src/ui/hud/player_hud.tscn index 17ef328..231d120 100644 --- a/src/ui/hud/player_hud.tscn +++ b/src/ui/hud/player_hud.tscn @@ -344,10 +344,10 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -401.486 -offset_top = -302.289 -offset_right = -401.486 -offset_bottom = -302.289 +offset_left = -402.339 +offset_top = -299.253 +offset_right = -402.339 +offset_bottom = -299.253 grow_horizontal = 2 grow_vertical = 2 script = ExtResource("4_ud8na") diff --git a/src/world/world.gd b/src/world/world.gd index 9095fd2..559e8c5 100644 --- a/src/world/world.gd +++ b/src/world/world.gd @@ -2,6 +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 manager: WorldManager @export var spook_manager: SpookManager @@ -61,4 +62,20 @@ func on_player_death() -> void: func on_game_over() -> void: # TODO: reload from last checkpoint # in the mean time, just reload the level - load_level(current_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) + + +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("Writing save to ", save_path) + ResourceSaver.save(save, save_path)