From 94beee112efdc9c52f37b95eadc04237e9d066e9 Mon Sep 17 00:00:00 2001 From: SchimmelSpreu83 Date: Mon, 22 Jun 2026 01:52:04 +0200 Subject: [PATCH] Some script improvements, clean-up and refactoring - Refactored VibrationComponent (RumbleComponent) -- Now supports multiple vibrations and adjustable curves. -- Inspired by the RumblePak addon. - Added a new startup scene. --- .../ayuroo/maestro/_mo_chase_test.tscn | 18 +- .../refactor_dialogue/refactor_dialogue.gd | 15 +- .../refactor_dialogue/refactor_dialogue.tscn | 60 +-- .../shaders/rhythm/beating_lightbeam.gdshader | 4 +- game/project.godot | 5 + game/src/core/aabb_gizmo.gd | 5 +- game/src/core/autoloads/input_manager.gd | 4 +- game/src/core/autoloads/settings_handler.gd | 4 +- game/src/core/autoloads/shader_globals.gd | 9 +- .../rumble_component.gd} | 46 +- .../rumble_component.gd.uid} | 0 .../controller_rumble/rumble_conductor.gd | 64 +++ .../controller_rumble/rumble_conductor.gd.uid | 1 + .../core/controller_rumble/rumble_config.gd | 22 + .../controller_rumble/rumble_config.gd.uid | 1 + game/src/core/instance_persister.gd | 3 +- game/src/core/interactive_loader.gd | 2 +- game/src/core/rhythm/rhythm_listener.gd | 16 +- game/src/core/rhythm/rhythm_player.gd | 8 +- game/src/core/rhythm/tempo_map.gd | 3 +- game/src/core/signal_group.gd | 6 +- game/src/core/utils.gd | 3 +- .../level/debug_aabb_ingame_material.tres | 2 - game/src/core/world/level/level_streamer.gd | 51 +- game/src/core/world/level/world_proxy.gd | 2 +- game/src/core/world/world.gd | 9 +- game/src/game.gd | 88 ++-- game/src/game.tscn | 2 +- game/src/gameplay/characters/player/head.gd | 2 +- game/src/gameplay/characters/player/hud.gd | 11 +- .../characters/player/player_character.gd | 12 +- .../objects/stagelight/stagelight.tscn | 2 +- .../gameplay/systems/flashlight/flashlight.gd | 5 +- .../systems/flashlight/flashlight_manager.gd | 2 +- .../systems/flashlight/flashlight_trigger.gd | 6 +- .../gameplay/systems/power_box/fuse_pickup.gd | 2 + .../gameplay/systems/power_box/power_box.gd | 6 +- game/src/ui/main_menu/main_menu.gd | 19 +- game/src/ui/settings_menu/settings_menu.gd | 12 +- game/src/ui/settings_menu/slider_label.gd | 3 +- .../setup/controller/controller_setup_menu.gd | 41 +- .../controller/controller_setup_menu.tscn | 18 +- game/src/ui/setup/initial_setup_menu.gd | 22 +- game/src/ui/setup/initial_setup_menu.tscn | 39 +- game/src/ui/setup/rhythm/rhythm_setup_menu.gd | 10 +- .../ui/setup/rhythm/rhythm_setup_menu.tscn | 32 +- game/src/ui/startup/fog_overlay.gdshader | 30 ++ game/src/ui/startup/fog_overlay.gdshader.uid | 1 + game/src/ui/startup/startup_scene.gd | 52 ++ game/src/ui/startup/startup_scene.gd.uid | 1 + game/src/ui/startup/startup_scene.tscn | 491 ++++++++++++++++++ game/src/ui/startup/title.pixi | Bin 0 -> 29638 bytes game/src/ui/startup/title.png | Bin 0 -> 27639 bytes game/src/ui/startup/title.png.import | 40 ++ game/tools/animation_player_editor_calls.gd | 7 +- game/tools/cutscene_animation_player.gd | 1 - game/tools/debug_outline_material.tres | 2 - game/tools/time_music_animation_player.gd | 107 ++-- 58 files changed, 1144 insertions(+), 285 deletions(-) rename game/src/core/{vibration_component.gd => controller_rumble/rumble_component.gd} (80%) rename game/src/core/{vibration_component.gd.uid => controller_rumble/rumble_component.gd.uid} (100%) create mode 100644 game/src/core/controller_rumble/rumble_conductor.gd create mode 100644 game/src/core/controller_rumble/rumble_conductor.gd.uid create mode 100644 game/src/core/controller_rumble/rumble_config.gd create mode 100644 game/src/core/controller_rumble/rumble_config.gd.uid create mode 100644 game/src/ui/startup/fog_overlay.gdshader create mode 100644 game/src/ui/startup/fog_overlay.gdshader.uid create mode 100644 game/src/ui/startup/startup_scene.gd create mode 100644 game/src/ui/startup/startup_scene.gd.uid create mode 100644 game/src/ui/startup/startup_scene.tscn create mode 100644 game/src/ui/startup/title.pixi create mode 100644 game/src/ui/startup/title.png create mode 100644 game/src/ui/startup/title.png.import diff --git a/game/_development/ayuroo/maestro/_mo_chase_test.tscn b/game/_development/ayuroo/maestro/_mo_chase_test.tscn index 55c17c1..45f5185 100644 --- a/game/_development/ayuroo/maestro/_mo_chase_test.tscn +++ b/game/_development/ayuroo/maestro/_mo_chase_test.tscn @@ -7,7 +7,8 @@ [ext_resource type="AnimationLibrary" uid="uid://dhcvtj55pdbi0" path="res://_development/ayuroo/maestro/_mo_anim.tres" id="4_q2x38"] [ext_resource type="Script" uid="uid://cx1nws4t8hs2u" path="res://src/core/rhythm/tempo_section_info.gd" id="4_we4r8"] [ext_resource type="Material" uid="uid://c38215ysnknyk" path="res://assets/dev/dark/dark_01.tres" id="7_6lemm"] -[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/vibration_component.gd" id="9_yut28"] +[ext_resource type="Script" uid="uid://dwwingj0dsxdg" path="res://src/core/controller_rumble/rumble_config.gd" id="9_51o3i"] +[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/controller_rumble/rumble_component.gd" id="9_yut28"] [sub_resource type="Resource" id="Resource_gvx8b"] script = ExtResource("3_7lyik") @@ -30,6 +31,19 @@ height = 0.3 radius = 0.15 height = 0.3 +[sub_resource type="Resource" id="Resource_51o3i"] +script = ExtResource("9_51o3i") +duration = 0.1 +weak_intensity = 0.575 +strong_intensity = 0.75 + +[sub_resource type="Resource" id="Resource_ploqb"] +script = ExtResource("9_51o3i") +duration = 0.175 +weak_intensity = 0.375 +strong_intensity = 0.0 +metadata/_custom_type_script = "uid://dwwingj0dsxdg" + [sub_resource type="BoxShape3D" id="BoxShape3D_gsfaw"] size = Vector3(14, 5, 1) @@ -96,6 +110,7 @@ material = ExtResource("7_6lemm") [node name="HeavyBeatVibration" type="Node" parent="." unique_id=1136863204] script = ExtResource("9_yut28") +data = SubResource("Resource_51o3i") duration = 0.1 sync_to_audio = true weak_magnitude = 0.575 @@ -103,6 +118,7 @@ strong_magnitude = 0.75 [node name="LightBeatVibration" type="Node" parent="." unique_id=2006943016] script = ExtResource("9_yut28") +data = SubResource("Resource_ploqb") duration = 0.175 sync_to_audio = true weak_magnitude = 0.375 diff --git a/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd b/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd index a0e294f..99e57cd 100644 --- a/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd +++ b/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd @@ -101,24 +101,24 @@ func _on_action(action: Action): var node = anim_player\ .get_node(anim_player.root_node)\ .get_node_or_null(_current_info.path) - + if node: track_path = node.name var property := _current_info.path.get_concatenated_subnames() if not property.is_empty(): track_path += ":" + property - + var msg = 'Delete all tracks with the path "%s"?' % track_path - + if _current_info.type == EditInfo.Type.NODE: msg = 'Delete tracks belonging to the node "%s"?' % track_path - + _show_confirmation(msg, _remove) func _render_edit_dialogue(): var info := _current_info - + if info.type == EditInfo.Type.METHOD_TRACK: is_full_path = false edit_full_path_toggle.disabled = true @@ -126,7 +126,7 @@ func _render_edit_dialogue(): else: edit_full_path_toggle.disabled = false edit_full_path_toggle.visible = true - + if is_full_path: edit_dialogue_input.text = info.path else: @@ -161,7 +161,7 @@ func _on_full_path_toggled(pressed: bool): ## Callback on rename func _on_rename_confirmed(_arg0 = null): var new := edit_dialogue_input.text - + edit_dialogue.hide() if not _anim_player or not _anim_player is AnimationPlayer: push_error("AnimationPlayer is null or invalid") @@ -232,4 +232,3 @@ func _show_confirmation(text: String, on_confirmed: Callable): confirmation_dialogue.confirmed.connect(on_confirmed, CONNECT_ONE_SHOT) confirmation_dialogue.popup_centered() confirmation_dialogue.dialog_text = text - diff --git a/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.tscn b/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.tscn index cc1dae8..3f0331a 100644 --- a/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.tscn +++ b/game/addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.tscn @@ -1,16 +1,16 @@ -[gd_scene load_steps=4 format=3 uid="uid://cyfxysds8uhnx"] +[gd_scene format=3 uid="uid://cyfxysds8uhnx"] -[ext_resource type="Script" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd" id="1_nkqdl"] -[ext_resource type="Script" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/components/anim_player_tree.gd" id="2_7pqfs"] -[ext_resource type="Script" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/components/node_select.gd" id="3_87x4i"] +[ext_resource type="Script" uid="uid://ft8r8earybjw" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/refactor_dialogue.gd" id="1_nkqdl"] +[ext_resource type="Script" uid="uid://bk2ganon2sasp" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/components/anim_player_tree.gd" id="2_7pqfs"] +[ext_resource type="Script" uid="uid://4alsx2krjeo8" path="res://addons/anim_player_refactor/scenes/refactor_dialogue/components/node_select.gd" id="3_87x4i"] -[node name="RefactorDialogue" type="AcceptDialog"] +[node name="RefactorDialogue" type="AcceptDialog" unique_id=480531508] title = "Refactor Animations" size = Vector2i(400, 599) ok_button_text = "Close" script = ExtResource("1_nkqdl") -[node name="VBoxContainer" type="VBoxContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=960944020] offset_left = 8.0 offset_top = 8.0 offset_right = 392.0 @@ -19,20 +19,20 @@ size_flags_horizontal = 3 size_flags_vertical = 3 theme_override_constants/separation = 8 -[node name="TreeContainer" type="VBoxContainer" parent="VBoxContainer"] +[node name="TreeContainer" type="VBoxContainer" parent="VBoxContainer" unique_id=1854530831] layout_mode = 2 -[node name="Label" type="Label" parent="VBoxContainer/TreeContainer"] +[node name="Label" type="Label" parent="VBoxContainer/TreeContainer" unique_id=524900720] layout_mode = 2 text = "Properties:" -[node name="FilterInput" type="LineEdit" parent="VBoxContainer/TreeContainer"] +[node name="FilterInput" type="LineEdit" parent="VBoxContainer/TreeContainer" unique_id=309777513] layout_mode = 2 placeholder_text = "Filter..." caret_blink = true caret_blink_interval = 0.5 -[node name="AnimPlayerTree" type="Tree" parent="VBoxContainer/TreeContainer"] +[node name="AnimPlayerTree" type="Tree" parent="VBoxContainer/TreeContainer" unique_id=222752454] unique_name_in_owner = true custom_minimum_size = Vector2(300, 400) layout_mode = 2 @@ -41,95 +41,95 @@ hide_root = true scroll_horizontal_enabled = false script = ExtResource("2_7pqfs") -[node name="RootNodeContainer" type="VBoxContainer" parent="VBoxContainer"] +[node name="RootNodeContainer" type="VBoxContainer" parent="VBoxContainer" unique_id=646228580] layout_mode = 2 -[node name="Label" type="Label" parent="VBoxContainer/RootNodeContainer"] +[node name="Label" type="Label" parent="VBoxContainer/RootNodeContainer" unique_id=1112656841] layout_mode = 2 text = "Root Node" -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/RootNodeContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/RootNodeContainer" unique_id=1988478185] layout_mode = 2 -[node name="ChangeRoot" type="Button" parent="VBoxContainer/RootNodeContainer/HBoxContainer"] +[node name="ChangeRoot" type="Button" parent="VBoxContainer/RootNodeContainer/HBoxContainer" unique_id=162815316] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 text = "Change Root" -[node name="EditDialogue" type="ConfirmationDialog" parent="."] +[node name="EditDialogue" type="ConfirmationDialog" parent="." unique_id=2018114141] unique_name_in_owner = true title = "Renaming" position = Vector2i(0, 36) size = Vector2i(230, 239) -[node name="VBoxContainer" type="VBoxContainer" parent="EditDialogue"] +[node name="VBoxContainer" type="VBoxContainer" parent="EditDialogue" unique_id=1119827862] offset_left = 8.0 offset_top = 8.0 offset_right = 222.0 offset_bottom = 190.0 -[node name="HBoxContainer" type="HBoxContainer" parent="EditDialogue/VBoxContainer"] +[node name="HBoxContainer" type="HBoxContainer" parent="EditDialogue/VBoxContainer" unique_id=935198969] layout_mode = 2 -[node name="EditDialogueButton" type="Button" parent="EditDialogue/VBoxContainer/HBoxContainer"] +[node name="EditDialogueButton" type="Button" parent="EditDialogue/VBoxContainer/HBoxContainer" unique_id=456759932] unique_name_in_owner = true layout_mode = 2 focus_mode = 0 -[node name="EditInput" type="LineEdit" parent="EditDialogue/VBoxContainer"] +[node name="EditInput" type="LineEdit" parent="EditDialogue/VBoxContainer" unique_id=2101598468] unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 0 -[node name="HBoxContainer2" type="HBoxContainer" parent="EditDialogue/VBoxContainer"] +[node name="HBoxContainer2" type="HBoxContainer" parent="EditDialogue/VBoxContainer" unique_id=1105990044] layout_mode = 2 -[node name="Label" type="Label" parent="EditDialogue/VBoxContainer/HBoxContainer2"] +[node name="Label" type="Label" parent="EditDialogue/VBoxContainer/HBoxContainer2" unique_id=1241184059] layout_mode = 2 text = "Used in:" -[node name="EditFullPathToggle" type="CheckButton" parent="EditDialogue/VBoxContainer/HBoxContainer2"] +[node name="EditFullPathToggle" type="CheckButton" parent="EditDialogue/VBoxContainer/HBoxContainer2" unique_id=1158699283] unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 10 text = "Edit full path" -[node name="MarginContainer" type="MarginContainer" parent="EditDialogue/VBoxContainer"] +[node name="MarginContainer" type="MarginContainer" parent="EditDialogue/VBoxContainer" unique_id=1578839664] custom_minimum_size = Vector2(0, 100) layout_mode = 2 -[node name="ColorRect" type="ColorRect" parent="EditDialogue/VBoxContainer/MarginContainer"] +[node name="ColorRect" type="ColorRect" parent="EditDialogue/VBoxContainer/MarginContainer" unique_id=1203243085] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 color = Color(0, 0, 0, 0.211765) -[node name="ScrollContainer" type="ScrollContainer" parent="EditDialogue/VBoxContainer/MarginContainer"] +[node name="ScrollContainer" type="ScrollContainer" parent="EditDialogue/VBoxContainer/MarginContainer" unique_id=1073527380] layout_mode = 2 size_flags_vertical = 3 horizontal_scroll_mode = 0 -[node name="MarginContainer" type="MarginContainer" parent="EditDialogue/VBoxContainer/MarginContainer/ScrollContainer"] +[node name="MarginContainer" type="MarginContainer" parent="EditDialogue/VBoxContainer/MarginContainer/ScrollContainer" unique_id=1889816102] layout_mode = 2 theme_override_constants/margin_left = 2 theme_override_constants/margin_top = 2 theme_override_constants/margin_right = 2 theme_override_constants/margin_bottom = 2 -[node name="EditAnimationList" type="Label" parent="EditDialogue/VBoxContainer/MarginContainer/ScrollContainer/MarginContainer"] +[node name="EditAnimationList" type="Label" parent="EditDialogue/VBoxContainer/MarginContainer/ScrollContainer/MarginContainer" unique_id=1056061212] unique_name_in_owner = true layout_mode = 2 text = "Test Test 2" -[node name="NodeSelectDialogue" type="ConfirmationDialog" parent="."] +[node name="NodeSelectDialogue" type="ConfirmationDialog" parent="." unique_id=1598902309] unique_name_in_owner = true title = "Select a node..." size = Vector2i(616, 557) ok_button_text = "Change" -[node name="NodeSelect" type="Tree" parent="NodeSelectDialogue"] +[node name="NodeSelect" type="Tree" parent="NodeSelectDialogue" unique_id=1495313424] unique_name_in_owner = true custom_minimum_size = Vector2(600, 500) offset_left = 8.0 @@ -139,7 +139,7 @@ offset_bottom = 508.0 scroll_horizontal_enabled = false script = ExtResource("3_87x4i") -[node name="ConfirmationDialog" type="ConfirmationDialog" parent="."] +[node name="ConfirmationDialog" type="ConfirmationDialog" parent="." unique_id=585191931] unique_name_in_owner = true size = Vector2i(300, 200) ok_button_text = "Delete" diff --git a/game/assets/shaders/rhythm/beating_lightbeam.gdshader b/game/assets/shaders/rhythm/beating_lightbeam.gdshader index 1500ed1..5b1a6f8 100644 --- a/game/assets/shaders/rhythm/beating_lightbeam.gdshader +++ b/game/assets/shaders/rhythm/beating_lightbeam.gdshader @@ -15,10 +15,10 @@ void vertex() { void fragment() { float phase = get_beat_phase(); vec4 tex = texture(albedo, UV); - + float edge = dot(NORMAL, VIEW); edge = clamp(pow(edge, mix(8.0, 2.0, edge_fade)), 0.0, 1.0); - + ALBEDO = mix(tex.rgb, color.rgb, 0.5); ALPHA = max(tex.a * ease(1.0 - phase, flash_exponent) * alpha_multiplier * edge, 0.0); } diff --git a/game/project.godot b/game/project.godot index 6da6e04..ad97b9f 100644 --- a/game/project.godot +++ b/game/project.godot @@ -29,6 +29,7 @@ ControllerIcons="*uid://bdosbfkp568je" ShaderGlobals="*uid://d2lr860r1ysrm" LimboConsole="*uid://dyxornv8vwibg" SettingsHandler="*uid://dj1t6rc6fpiti" +RumbleConductor="*uid://by4n7qykdds0o" [debug] @@ -239,3 +240,7 @@ time={ "type": "float", "value": 0.0 } +is_editor_hint={ +"type": "bool", +"value": false +} diff --git a/game/src/core/aabb_gizmo.gd b/game/src/core/aabb_gizmo.gd index 69ad5c2..f2c472c 100644 --- a/game/src/core/aabb_gizmo.gd +++ b/game/src/core/aabb_gizmo.gd @@ -34,7 +34,6 @@ func _set_handle(id: int, secondary: bool, camera: Camera3D, point: Vector2) -> var position: Vector3 = _aabb.position - func _redraw() -> void: clear() @@ -88,7 +87,7 @@ func _box_commit_handle( func _box_get_handle_name(id: int) -> String: match id: - 0,2,4: + 0, 2, 4: return "" 1: return "Size X" @@ -102,7 +101,7 @@ func _box_get_handle_name(id: int) -> String: func _box_set_handle(segment: Vector3, id: int, size: Vector3, position: Vector3) -> void: var axis: int = id / 2 - var sign = id % 2 * -2 + 1 + var sign: float = id % 2 * -2 + 1 var _initial_size: Vector3 = initial_value var neg_end: float = _initial_size[axis] * -0.5 var pos_end: float = _initial_size[axis] * 0.5 diff --git a/game/src/core/autoloads/input_manager.gd b/game/src/core/autoloads/input_manager.gd index 23ef0c9..bb2cec4 100644 --- a/game/src/core/autoloads/input_manager.gd +++ b/game/src/core/autoloads/input_manager.gd @@ -45,7 +45,9 @@ func release_mouse() -> void: func is_keyboard_or_mouse_event(event: InputEvent) -> bool: return ( - event is InputEventMouseButton or event is InputEventMouseMotion or event is InputEventKey + event is InputEventMouseButton + or event is InputEventMouseMotion + or event is InputEventKey ) diff --git a/game/src/core/autoloads/settings_handler.gd b/game/src/core/autoloads/settings_handler.gd index af6894c..fcf735f 100644 --- a/game/src/core/autoloads/settings_handler.gd +++ b/game/src/core/autoloads/settings_handler.gd @@ -69,8 +69,8 @@ func apply_settings() -> void: if not Engine.is_embedded_in_editor(): DisplayServer.window_set_mode(get_setting("video", "window_mode", DisplayServer.WindowMode.WINDOW_MODE_FULLSCREEN)) # Controls - VibrationComponent.min_weak_magnitude_threshold = get_setting("controls", "vibration_min_weak_threshold", 0.0) - VibrationComponent.min_strong_magnitude_threshold = get_setting("controls", "vibration_min_strong_threshold", 0.0) + RumbleConductor.min_weak_magnitude_threshold = get_setting("controls", "vibration_min_weak_threshold", 0.0) + RumbleConductor.min_strong_magnitude_threshold = get_setting("controls", "vibration_min_strong_threshold", 0.0) func commit_settings() -> void: diff --git a/game/src/core/autoloads/shader_globals.gd b/game/src/core/autoloads/shader_globals.gd index 423a0f0..678d26c 100644 --- a/game/src/core/autoloads/shader_globals.gd +++ b/game/src/core/autoloads/shader_globals.gd @@ -12,9 +12,16 @@ var user_offset_ms: float = 0.0 func _notification(what: int) -> void: + # Just in case. if what == NOTIFICATION_EDITOR_PRE_SAVE: - # Just in case. RenderingServer.global_shader_parameter_set(&"time", 0.0) + RenderingServer.global_shader_parameter_set(&"is_editor_hint", false) + elif what == NOTIFICATION_EDITOR_POST_SAVE: + RenderingServer.global_shader_parameter_set(&"is_editor_hint", Engine.is_editor_hint()) + + +func _ready() -> void: + RenderingServer.global_shader_parameter_set(&"is_editor_hint", Engine.is_editor_hint()) func _process(delta: float) -> void: diff --git a/game/src/core/vibration_component.gd b/game/src/core/controller_rumble/rumble_component.gd similarity index 80% rename from game/src/core/vibration_component.gd rename to game/src/core/controller_rumble/rumble_component.gd index 62dfb95..81bd976 100644 --- a/game/src/core/vibration_component.gd +++ b/game/src/core/controller_rumble/rumble_component.gd @@ -1,5 +1,5 @@ @tool -class_name VibrationComponent +class_name RumbleComponent extends Node ## A small helper node to quickly perform controller vibrations. @@ -10,10 +10,13 @@ extends Node #static var total_weak_vibration: float = 0.0 #static var total_strong_vibration: float = 0.0 #static var _vibration_handled: bool = false -static var min_weak_magnitude_threshold: float = 0.0 -static var min_strong_magnitude_threshold: float = 0.0 +#static var min_weak_magnitude_threshold: float = 0.0 +#static var min_strong_magnitude_threshold: float = 0.0 #static var min_duration_threshold: float = 0.0 + +@export var data: RumbleConfig + ## If [code]false[/code], calling [method vibrate] will not start a controller vibration. @export var enabled: bool = true: get = is_enabled, @@ -28,8 +31,7 @@ static var min_strong_magnitude_threshold: float = 0.0 @export var device: int = 0: get = get_device, set = set_device -## Delay the vibration by this amount. -@export var delay: float = 0.0 + @export var sync_to_audio: bool = false #region Editor tooling @@ -65,18 +67,26 @@ var animated_strong_magnitude: float = 0.0: animated_strong_magnitude = clampf(value, 0.0, 1.0) -func _process(delta: float) -> void: +func _init() -> void: + if not is_instance_valid(data): + data = RumbleConfig.new() + + +func _physics_process(delta: float) -> void: if ( - can_vibrate() - and not is_zero_approx(animated_weak_magnitude + animated_strong_magnitude) + can_vibrate() + and not is_zero_approx(animated_weak_magnitude + animated_strong_magnitude) ): - var _weak_magnitude: float = animated_weak_magnitude - var _strong_magnitude: float = animated_strong_magnitude + var config := RumbleConfig.new() + config.weak_intensity = animated_weak_magnitude + config.strong_intensity = animated_strong_magnitude + config.duration = delta - _weak_magnitude = remap(_weak_magnitude, 0.0, 1.0, min_weak_magnitude_threshold * float(_weak_magnitude > 0.0), 1.0) - _strong_magnitude = remap(_strong_magnitude, 0.0, 1.0, min_strong_magnitude_threshold * float(_strong_magnitude > 0.0), 1.0) + #_weak_magnitude = remap(_weak_magnitude, 0.0, 1.0, min_weak_magnitude_threshold * float(_weak_magnitude > 0.0), 1.0) + #_strong_magnitude = remap(_strong_magnitude, 0.0, 1.0, min_strong_magnitude_threshold * float(_strong_magnitude > 0.0), 1.0) - Input.start_joy_vibration.call_deferred(device, _weak_magnitude, _strong_magnitude, delta) + RumbleConductor.add_rumble(config, device) + #Input.start_joy_vibration.call_deferred(device, _weak_magnitude, _strong_magnitude, delta) animated_weak_magnitude = 0.0 animated_strong_magnitude = 0.0 @@ -87,20 +97,20 @@ func vibrate() -> void: if not can_vibrate(): return - if delay > 0.0: - await get_tree().create_timer(delay).timeout - if sync_to_audio: var last_mix_time: float = AudioServer.get_time_since_last_mix() var output_latency: float = AudioServer.get_output_latency() #SPrint.print_msg("Audio Delay: %s" % (last_mix_time + output_latency)) await get_tree().create_timer(last_mix_time + output_latency).timeout + data.rumble(device) + return + var _weak_magnitude: float = clampf(weak_magnitude * magnitude_multiplier, 0.0, 1.0) var _strong_magnitude: float = clampf(strong_magnitude * magnitude_multiplier, 0.0, 1.0) - _weak_magnitude = remap(_weak_magnitude, 0.0, 1.0, min_weak_magnitude_threshold * float(_weak_magnitude > 0.0), 1.0) - _strong_magnitude = remap(_strong_magnitude, 0.0, 1.0, min_strong_magnitude_threshold * float(_strong_magnitude > 0.0), 1.0) + #_weak_magnitude = remap(_weak_magnitude, 0.0, 1.0, min_weak_magnitude_threshold * float(_weak_magnitude > 0.0), 1.0) + #_strong_magnitude = remap(_strong_magnitude, 0.0, 1.0, min_strong_magnitude_threshold * float(_strong_magnitude > 0.0), 1.0) #duration = maxf(duration, min_duration_threshold) diff --git a/game/src/core/vibration_component.gd.uid b/game/src/core/controller_rumble/rumble_component.gd.uid similarity index 100% rename from game/src/core/vibration_component.gd.uid rename to game/src/core/controller_rumble/rumble_component.gd.uid diff --git a/game/src/core/controller_rumble/rumble_conductor.gd b/game/src/core/controller_rumble/rumble_conductor.gd new file mode 100644 index 0000000..5227161 --- /dev/null +++ b/game/src/core/controller_rumble/rumble_conductor.gd @@ -0,0 +1,64 @@ +@tool +extends Node + +# Inspired by the RumblePak addon by Elliptical +# (https://store.godotengine.org/asset/elliptical/rumblepak/) + +static var min_weak_magnitude_threshold: float = 0.0 +static var min_strong_magnitude_threshold: float = 0.0 + +var vibrations: Array[RumbleData] = [] + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + for device: int in Input.get_connected_joypads(): + var total_rumble := Vector2.ZERO + + for i: int in range(vibrations.size() - 1, -1, -1): + var rumble_data: RumbleData = vibrations.get(i) + + if rumble_data.device != device: + continue + + var config: RumbleConfig = rumble_data.config + var time_elapsed: float = rumble_data.time_elapsed + + if not is_instance_valid(config) or time_elapsed > config.duration: + vibrations.remove_at(i) + continue + + var rumble := Vector2(config.weak_intensity, config.strong_intensity) + var curve_offset: Vector2 + + if is_instance_valid(config.weak_curve): + curve_offset.x = config.weak_curve.max_domain * (time_elapsed / config.duration) + rumble.x = config.weak_curve.sample_baked(curve_offset.x) * config.weak_intensity + + if is_instance_valid(config.strong_curve): + curve_offset.y = config.strong_curve.max_domain * (time_elapsed / config.duration) + rumble.y = config.strong_curve.sample_baked(curve_offset.y) * config.strong_intensity + + rumble.x = remap(rumble.x, 0.0, 1.0, min_weak_magnitude_threshold * float(rumble.x > 0.0), 1.0) + rumble.y = remap(rumble.y, 0.0, 1.0, min_strong_magnitude_threshold * float(rumble.y > 0.0), 1.0) + + rumble *= config.intensity + + total_rumble = (total_rumble + rumble).clampf(0.0, 1.0) + + rumble_data.time_elapsed += delta + + Input.start_joy_vibration(device, total_rumble.x, total_rumble.y) + + +func add_rumble(rumble_config: RumbleConfig, device: int = 0) -> void: + var rumble_data := RumbleData.new() + rumble_data.config = rumble_config + rumble_data.device = device + vibrations.append(rumble_data) + + +class RumbleData: + var config: RumbleConfig + var time_elapsed: float = 0.0 + var device: int = 0 diff --git a/game/src/core/controller_rumble/rumble_conductor.gd.uid b/game/src/core/controller_rumble/rumble_conductor.gd.uid new file mode 100644 index 0000000..70e2669 --- /dev/null +++ b/game/src/core/controller_rumble/rumble_conductor.gd.uid @@ -0,0 +1 @@ +uid://by4n7qykdds0o diff --git a/game/src/core/controller_rumble/rumble_config.gd b/game/src/core/controller_rumble/rumble_config.gd new file mode 100644 index 0000000..453c0ef --- /dev/null +++ b/game/src/core/controller_rumble/rumble_config.gd @@ -0,0 +1,22 @@ +@tool +class_name RumbleConfig +extends Resource + +@warning_ignore("unused_private_class_variable") +@export_tool_button("Test Rumble", "Noise") +var _editor_test_vibration: Callable = rumble + +@export_range(0.0, 1.0) var intensity: float = 1.0 +@export_range(0.0, 1.0, 0.001, "or_greater", "suffix:s") var duration: float = 0.15 + +@export_group("Weak Magnitude", "weak_") +@export_range(0.0, 1.0) var weak_intensity: float = 1.0 +@export var weak_curve: Curve + +@export_group("Strong Magnitude", "strong_") +@export_range(0.0, 1.0) var strong_intensity: float = 1.0 +@export var strong_curve: Curve + + +func rumble(device: int = 0) -> void: + RumbleConductor.add_rumble(self, device) diff --git a/game/src/core/controller_rumble/rumble_config.gd.uid b/game/src/core/controller_rumble/rumble_config.gd.uid new file mode 100644 index 0000000..5cdbfa7 --- /dev/null +++ b/game/src/core/controller_rumble/rumble_config.gd.uid @@ -0,0 +1 @@ +uid://dwwingj0dsxdg diff --git a/game/src/core/instance_persister.gd b/game/src/core/instance_persister.gd index 2b71267..8e9292c 100644 --- a/game/src/core/instance_persister.gd +++ b/game/src/core/instance_persister.gd @@ -6,7 +6,8 @@ var _node_hash: String = "" func _init(node: Node, world: World) -> void: - assert(is_instance_valid(world), "world must be valid. Are you sure you have defined set the reference?") + assert(is_instance_valid(world), + "'world' must be valid. Are you sure you have defined/set the reference correctly?") _node = node _node_hash = str(world.get_path_to(node).hash()) _world = world diff --git a/game/src/core/interactive_loader.gd b/game/src/core/interactive_loader.gd index f3a1b5d..7bfeab2 100644 --- a/game/src/core/interactive_loader.gd +++ b/game/src/core/interactive_loader.gd @@ -26,7 +26,7 @@ func load_threaded(path: String) -> Resource: while ResourceLoader.load_threaded_get_status(path) != ResourceLoader.THREAD_LOAD_LOADED: match ResourceLoader.load_threaded_get_status(path, load_progress): - ResourceLoader.THREAD_LOAD_FAILED || ResourceLoader.THREAD_LOAD_INVALID_RESOURCE: + ResourceLoader.THREAD_LOAD_FAILED or ResourceLoader.THREAD_LOAD_INVALID_RESOURCE: #SPrint.print_msg("[Interactive Loader] load failed.", -1.0, SPrint.WARNING) return null diff --git a/game/src/core/rhythm/rhythm_listener.gd b/game/src/core/rhythm/rhythm_listener.gd index 119220a..0c558a0 100644 --- a/game/src/core/rhythm/rhythm_listener.gd +++ b/game/src/core/rhythm/rhythm_listener.gd @@ -2,8 +2,8 @@ class_name RhythmListener extends Node -signal beat_tick(beat_index: int) -signal bar_tick(bar_index: int) +signal beat_ticked(beat_index: int) +signal bar_ticked(bar_index: int) @export_range(0.0, 1.0, 0.001, "or_greater", "or_less") var phase_shift: float = 0.0 @export_range(0.0, 1.0, 0.001, "or_greater", "or_less") var phase_multiplier: float = 1.0 @@ -29,12 +29,12 @@ func _process(_delta: float) -> void: var bar_index: int = floori(bar) if _last_beat_index != beat_index: - on_beat_tick(beat_index) + on_beat_ticked(beat_index) _last_beat_index = beat_index _last_beat_time = RhythmPlayer.song_time + phase_shift if _last_bar_index != bar_index: - on_bar_tick(bar_index) + on_bar_ticked(bar_index) _last_bar_index = bar_index _last_bar_time = RhythmPlayer.song_time + phase_shift @@ -57,11 +57,11 @@ func get_time_to_next_bar() -> float: # -func on_beat_tick(beat_index: int) -> void: +func on_beat_ticked(beat_index: int) -> void: #SPrint.print_msg("Beat received %s" % beat_index, 0.75) - beat_tick.emit(beat_index) + beat_ticked.emit(beat_index) -func on_bar_tick(bar_index: int) -> void: +func on_bar_ticked(bar_index: int) -> void: #SPrint.print_msg("Bar received %s" % bar_index, 1.0) - bar_tick.emit(bar_index) + bar_ticked.emit(bar_index) diff --git a/game/src/core/rhythm/rhythm_player.gd b/game/src/core/rhythm/rhythm_player.gd index bcfc12d..73fb734 100644 --- a/game/src/core/rhythm/rhythm_player.gd +++ b/game/src/core/rhythm/rhythm_player.gd @@ -1,8 +1,8 @@ class_name RhythmPlayer extends Node -signal beat_tick(beat_index: int) -signal bar_tick(bar_index: int) +signal beat_ticked(beat_index: int) +signal bar_ticked(bar_index: int) signal finished static var user_offset_ms: float = 0.0: set = set_user_offset_ms @@ -172,11 +172,11 @@ func _update_playback() -> void: #SPrint.print_msgf("Bar: %s (%s) bar on beat: %s" % [bar_to_signature(bar_index, _bars), bar_index, bar_index % _bars == 0]) if _last_beat != beat_index: - beat_tick.emit(beat_index) + beat_ticked.emit(beat_index) _last_beat = beat_index if _last_bar != bar_index: - bar_tick.emit(bar_index) + bar_ticked.emit(bar_index) _last_bar = bar_index diff --git a/game/src/core/rhythm/tempo_map.gd b/game/src/core/rhythm/tempo_map.gd index 6473262..a6bf244 100644 --- a/game/src/core/rhythm/tempo_map.gd +++ b/game/src/core/rhythm/tempo_map.gd @@ -136,7 +136,8 @@ func recalculate_times() -> void: var seconds: float = beat_delta * 60.0 / previous.bpm current.time = previous.time + seconds - print_rich("- [b]%s[/b] [i]bpm[/i] ([b]1/%s[/b]) at [b]%s[/b] [i]seconds[/i] (beat delta: [b]%s[/b], seconds delta: [b]%s[/b])." % [current.bpm, current.beats_per_bar, current.time, beat_delta, seconds]) + print_rich("- [b]%s[/b] [i]bpm[/i] ([b]1/%s[/b]) at [b]%s[/b] [i]seconds[/i] (beat delta: [b]%s[/b], seconds delta: [b]%s[/b])." + % [current.bpm, current.beats_per_bar, current.time, beat_delta, seconds]) func get_beat_at_time(time: float) -> float: diff --git a/game/src/core/signal_group.gd b/game/src/core/signal_group.gd index f3a59b3..d7b4bc9 100644 --- a/game/src/core/signal_group.gd +++ b/game/src/core/signal_group.gd @@ -2,7 +2,7 @@ class_name SignalGroup ## @tutorial(Small Modified Version from ShaggyDev): https://shaggydev.com/2025/06/12/godot-awaiting-signals/ -signal _all_complete +signal _all_completed var _counter: int = 0 #var _watched_signals: Array[Signal] = [] @@ -36,7 +36,7 @@ func all(signals: Array[Signal], custom_count: int = -1) -> Array[Signal]: _on_signal_complete(sig) ), CONNECT_ONE_SHOT) - await _all_complete + await _all_completed return _signals_in_order @@ -52,7 +52,7 @@ func _on_signal_complete(sig: Signal) -> void: _signals_in_order.append(sig) if _counter == 0: - _all_complete.emit() + _all_completed.emit() #class Probe extends RefCounted: diff --git a/game/src/core/utils.gd b/game/src/core/utils.gd index 4e20b8f..fb3e229 100644 --- a/game/src/core/utils.gd +++ b/game/src/core/utils.gd @@ -99,8 +99,7 @@ static func full_rotation_float( static func get_uid(path: String) -> int: - var uid: int = ResourceUID.text_to_id(ResourceUID.path_to_uid(path)) - return uid + return ResourceUID.text_to_id(ResourceUID.path_to_uid(path)) ## Converts any given uid (as an [int] or [code]uid://[/code] path) diff --git a/game/src/core/world/level/debug_aabb_ingame_material.tres b/game/src/core/world/level/debug_aabb_ingame_material.tres index afa73bb..f08b482 100644 --- a/game/src/core/world/level/debug_aabb_ingame_material.tres +++ b/game/src/core/world/level/debug_aabb_ingame_material.tres @@ -8,5 +8,3 @@ shader = ExtResource("1_1ceck") shader_parameter/base_color = Color(0.3, 1, 0.3, 0.15) shader_parameter/edge_color = Color(1, 1, 0.3, 0.8) shader_parameter/edge_power = 3.0 -shader_parameter/box_center = Vector3(0, 0, 0) -shader_parameter/box_extents = Vector3(0, 0, 0) diff --git a/game/src/core/world/level/level_streamer.gd b/game/src/core/world/level/level_streamer.gd index 3d6e984..ce0d3fe 100644 --- a/game/src/core/world/level/level_streamer.gd +++ b/game/src/core/world/level/level_streamer.gd @@ -9,18 +9,28 @@ signal loading_finished @export var enabled: bool = true: set = set_enabled @export var threaded_loading: bool = true @export var prevent_unloading: bool = false -@export var unload_delay: float = 3.0 +@export var handles_level_visibility: bool = true +@export_range(0.0, 10.0, 0.001, "or_greater", "suffix:s") var unload_delay: float = 3.0 @export var level_id: StringName = &"": set(value): level_id = value update_configuration_warnings() + @export_file("*.tscn", "*.scn") var scene_path: String: set(value): scene_path = value update_configuration_warnings() -@export_tool_button("Edit Level", "Edit") var editor_edit_level: Callable = _editor_edit_level -@export_tool_button("Load Level", "Load") var editor_load_level: Callable = _editor_load_level -@export_tool_button("Unload Level", "Clear") var editor_unload_level: Callable = _editor_unload_level + +@export_tool_button("Edit Level", "Edit") +var editor_edit_level: Callable = _editor_edit_level + +# alternative icons: 'Load', 'Override' +@export_tool_button("Load Level", "FileAccess") +var editor_load_level: Callable = _editor_load_level + +@export_tool_button("Unload Level", "Clear") +var editor_unload_level: Callable = _editor_unload_level + @export var load_aabbs: Array[AABB] = []: set(value): load_aabbs = value @@ -33,11 +43,16 @@ signal loading_finished #clear_gizmos() #for aabb: AABB in load_aabbs: #add_gizmo.call_deferred(AABBGizmo.new(aabb, self)) -@export_tool_button("Auto Generate AABB", "CSGBox3D") var editor_auto_gen_aabb: Callable = _editor_auto_gen_aabb -@export var editor_visualize_aabbs_with_box: bool = true: set = _editor_set_visualize_aabbs_with_box +@export_tool_button("Auto Generate AABB", "CSGBox3D") +var editor_auto_gen_aabb: Callable = _editor_auto_gen_aabb + +@export var editor_visualize_aabbs_with_box: bool = true: + set = _editor_set_visualize_aabbs_with_box var loaded_level: Node var is_loading_level: bool = false +var editor_force_draw_debug: bool = false + var _unload_time_left: float = 0.0 var _precomputed_aabb: AABB # Big bounding box covering all [_precomputed_aabbs] for faster checking. var _precomputed_aabbs: Array[AABB] @@ -49,8 +64,8 @@ static func get_node_aabb(node: Node3D = null, ignore_top_level: bool = true, bo var aabb: AABB var _transform: Transform3D - # We are going down the child chain, - # we want the aabb of each subsequent node to be on the same axis as the parent. + # We are going down the child chain, + # we want the aabb of each subsequent node to be on the same axis as the parent. if bounds_transform.is_equal_approx(Transform3D()): _transform = node.global_transform else: @@ -128,7 +143,10 @@ func _process(delta: float) -> void: elif not prevent_unloading and is_level_loaded(): _unload_time_left = maxf(_unload_time_left - delta, 0.0) - if _unload_time_left == 0.0: + if handles_level_visibility: + loaded_level.hide() + + if is_zero_approx(_unload_time_left): unload_level() @@ -152,14 +170,20 @@ func get_precomputed_aabbs() -> Array[AABB]: func load_level() -> void: _unload_time_left = unload_delay - if is_level_loaded() or is_loading_level: + if is_loading_level: + return + + if is_level_loaded(): + loaded_level.show() return is_loading_level = true + assert(ResourceLoader.exists(scene_path, "PackedScene"), "'scene_path' is invalid or not a PackedScene.") + var scene: PackedScene = await load_scene_threaded() if threaded_loading else load(scene_path) loaded_level = scene.instantiate() - add_child(loaded_level) + add_child(loaded_level, true) is_loading_level = false loading_finished.emit() @@ -175,7 +199,7 @@ func is_level_loaded() -> bool: func load_scene_threaded() -> PackedScene: - var interactive_loader := InteractiveLoader.create_oneshot(self) + var interactive_loader := InteractiveLoader.create_oneshot_and_bind(self) var resource: Resource = await interactive_loader.load_threaded(scene_path) return resource as PackedScene @@ -183,6 +207,7 @@ func load_scene_threaded() -> PackedScene: func get_camera() -> Camera3D: var camera: Camera3D = get_viewport().get_camera_3d() + # also prevent godot's override in-game camera from affecting the loading. if not is_instance_valid(camera) or camera.name == &"OverrideCamera3D": return _previous_camera @@ -259,7 +284,7 @@ func _editor_unload_level() -> void: func _editor_process_gizmo() -> void: var editor_selection: Object = Engine.get_singleton(&"EditorInterface").get_selection() - if editor_selection.get_selected_nodes().has(self): + if editor_selection.get_selected_nodes().has(self) or editor_force_draw_debug: _draw_debug() return diff --git a/game/src/core/world/level/world_proxy.gd b/game/src/core/world/level/world_proxy.gd index fb6cb2b..5ff116d 100644 --- a/game/src/core/world/level/world_proxy.gd +++ b/game/src/core/world/level/world_proxy.gd @@ -1,7 +1,7 @@ class_name WorldProxy extends Node -signal world_changed +signal world_changed(new_world: World) static var world_proxies: Array[WorldProxy] = [] diff --git a/game/src/core/world/world.gd b/game/src/core/world/world.gd index 0ef80d2..85337ed 100644 --- a/game/src/core/world/world.gd +++ b/game/src/core/world/world.gd @@ -22,6 +22,7 @@ signal world_unload_requested(do_save: bool) @export var debug_load_levels_based_on_player_position: bool = true var world_state: WorldState +var game: Game func _exit_tree() -> void: @@ -31,7 +32,7 @@ func _exit_tree() -> void: func _ready() -> void: if debug_enabled: if get_tree().current_scene == self: - _setup_world_debug() + _setup_debug_world_state() if is_instance_valid(world_state): await apply_world_state() @@ -83,8 +84,8 @@ func apply_world_state() -> void: if is_instance_valid(player): player.process_mode = Node.PROCESS_MODE_DISABLED - if is_instance_valid(player) and player.has_method(&"apply_orientation"): - player.apply_orientation(world_state.player_transform) + if player.has_method(&"apply_orientation"): + player.apply_orientation(world_state.player_transform) await _load_levels() @@ -210,7 +211,7 @@ func _load_levels() -> void: await SignalGroup.await_signals(signals) -func _setup_world_debug() -> void: +func _setup_debug_world_state() -> void: world_state = debug_world_state if is_instance_valid(debug_world_state) else WorldState.new() if is_instance_valid(debug_player_position): diff --git a/game/src/game.gd b/game/src/game.gd index 9b1aaa1..6033554 100644 --- a/game/src/game.gd +++ b/game/src/game.gd @@ -4,8 +4,8 @@ extends Node ## TODO: sicherungskasten minigames (Kabel verbinden, oder draufhauen und geht wieder) - -const SAVE_DIR: String = "user://saves/%s/" +const USER_SETTINGS_PATH: String = "user://settings.ini" +const SAVE_DIR: String = "user://saves/%d/" # TODO "user://ch1/saves/%d/ # Chapter 1 const SAVE_PATH: String = SAVE_DIR + "%s" const INITIAL_SAVE_DATA: SaveData = preload("uid://b8ojagpq5pxr2") @@ -14,12 +14,14 @@ static var is_debug: bool = true @export_file("*.tscn", "*.scn") var initial_setup_path: String = "uid://8bxv3c5f8d2j" @export_file("*.tscn", "*.scn") var main_menu_path: String = "uid://7v62dybcabgw" -var current_save_slot: int = 0 -var current_save_data: SaveData = INITIAL_SAVE_DATA.duplicate() +var save_slot: int = 0 +var save_data: SaveData = INITIAL_SAVE_DATA.duplicate() var world: World var main_menu: MainMenu -@onready var world_container: Node = %WorldContainer +var _os_dialog_window_result: int = -1 + +@onready var world_holder: Node = %WorldHolder @onready var loading_screen: LoadingScreen = %LoadingScreen @@ -28,39 +30,46 @@ static func _static_init() -> void: static func debug_draw(draw_shape: String, args: Array) -> void: - if is_debug and Engine.has_singleton(&"DebugDraw3D"): - Engine.get_singleton(&"DebugDraw3D").callv("draw_" + draw_shape, args) + const DD3D: StringName = &"DebugDraw3D" + if is_debug and Engine.has_singleton(DD3D): + Engine.get_singleton(DD3D).callv("draw_" + draw_shape, args) +## Turns res://example.tscn into example.tres (and .scn -> .res) and uid://1234567 into 1234567.tres. +static func convert_path_to_resource_filename(path: String) -> String: + path = ResourceUID.path_to_uid(path) + + if path.begins_with("res://"): + return path.get_file().replace(".tscn", ".tres").replace(".scn", ".res") + + return path.replace("uid://", "") + ".tres" func _ready() -> void: if not Engine.is_editor_hint(): - if needs_game_setup(): - load_initial_setup() - else: - load_main_menu() - + load_startup() register_commands() -func load_game(save_data: SaveData, save_slot: int) -> void: - current_save_data = save_data.duplicate() - current_save_slot = save_slot +func load_game(_save_data: SaveData, _save_slot: int) -> void: + if not await check_computer_hash(save_data.com_hash): + return + + save_data = _save_data.duplicate() + save_slot = _save_slot InputManager.capture_mouse() - load_world(current_save_data.current_world, false) + load_world(save_data.current_world, false) func save_game() -> Error: if not is_instance_valid(world): return ERR_DOES_NOT_EXIST - var save_slot: String = str(current_save_slot) var file_name: String = "save_data.tres" if is_instance_valid(world): - current_save_data.current_world = ResourceUID.path_to_uid(world.scene_file_path) + save_data.current_world = ResourceUID.path_to_uid(world.scene_file_path) DirAccess.make_dir_recursive_absolute(SAVE_DIR % save_slot) - return ResourceSaver.save(current_save_data, SAVE_PATH % [save_slot, file_name]) + return ResourceSaver.save(save_data, SAVE_PATH % [save_slot, file_name]) func load_world(world_path: String, save_previous: bool = true, load_from_save: bool = true) -> Error: @@ -87,7 +96,7 @@ func load_world(world_path: String, save_previous: bool = true, load_from_save: world.change_world_requested.connect(load_world) world.world_unload_requested.connect(_on_unload_world_request) - world_container.add_child.call_deferred(world) + world_holder.add_child.call_deferred(world) await world.loaded loading_screen.fade_out() return OK @@ -110,8 +119,7 @@ func save_world_state() -> Error: world.update_world_state() var world_state: WorldState = world.world_state - var save_slot: String = str(current_save_slot) - var file_name: String = _get_world_save_file_name_from_world_path(world_state.world_path) + var file_name: String = convert_path_to_resource_filename(world_state.world_path) print("File name: ", file_name) print(SAVE_PATH % [save_slot, file_name]) DirAccess.make_dir_recursive_absolute(SAVE_DIR % save_slot) @@ -120,8 +128,7 @@ func save_world_state() -> Error: func load_world_state(world_path: String) -> WorldState: - var save_slot: String = str(current_save_slot) - var file_name: String = _get_world_save_file_name_from_world_path(world_path) + var file_name: String = convert_path_to_resource_filename(world_path) var file_path: String = SAVE_PATH % [save_slot, file_name] if not ResourceLoader.exists(file_path): return null @@ -136,29 +143,18 @@ func load_main_menu() -> void: _main_menu.load_game_request.connect(load_game) _main_menu.quit_request.connect(quit_game) main_menu = _main_menu - world_container.add_child(main_menu) + world_holder.add_child(main_menu) -func load_initial_setup() -> void: - var setup_menu: InitialSetupMenu = load(initial_setup_path).instantiate() - setup_menu.setup_finished.connect(func() -> void: - setup_menu.queue_free() - var config: ConfigFile = Utils.load_config(MainMenu.USER_SETTINGS_PATH) - config.set_value("save", "needs_setup", false) - config.save(MainMenu.USER_SETTINGS_PATH) +func load_startup() -> void: + var startup: StartupScene = load(startup_scene).instantiate() + startup.startup_finished.connect(func() -> void: + startup.queue_free() await loading_screen.fade_in(0.0) load_main_menu() loading_screen.fade_out() ) - add_child(setup_menu) - - -func needs_game_setup() -> bool: - var config := ConfigFile.new() - if config.load(MainMenu.USER_SETTINGS_PATH) != OK: - return true - - return config.get_value("save", "needs_setup", true) + add_child(startup) func unload_main_menu() -> void: @@ -194,13 +190,3 @@ func _on_unload_world_request(do_save: bool) -> void: await load_main_menu() loading_screen.fade_out() - - -func _get_world_save_file_name_from_world_path(world_path: String) -> String: - world_path = ResourceUID.path_to_uid(world_path) - print("World path: ", world_path) - - if world_path.begins_with("res://"): - return world_path.get_file().replace(".tscn", ".tres").replace(".scn", ".res") - - return world_path.replace("uid://", "") + ".tres" diff --git a/game/src/game.tscn b/game/src/game.tscn index 2129739..b0294ad 100644 --- a/game/src/game.tscn +++ b/game/src/game.tscn @@ -7,7 +7,7 @@ script = ExtResource("1_7uq6d") metadata/_custom_type_script = "uid://cl1u038dbrou2" -[node name="WorldContainer" type="Node" parent="." unique_id=1835125942] +[node name="WorldHolder" type="Node" parent="." unique_id=1835125942] unique_name_in_owner = true [node name="CanvasLayer" type="CanvasLayer" parent="." unique_id=919699501] diff --git a/game/src/gameplay/characters/player/head.gd b/game/src/gameplay/characters/player/head.gd index 8a372d7..8d1a340 100644 --- a/game/src/gameplay/characters/player/head.gd +++ b/game/src/gameplay/characters/player/head.gd @@ -103,7 +103,7 @@ func _process_input(delta: float) -> void: if not using_controller and mouse_acceleration: rotational_velocity = _lerp_rotational_velocity(get_sensitivity() / 10, mouse_friction, delta) elif using_controller and controller_acceleration: - rotational_velocity = _lerp_rotational_velocity(get_sensitivity(), controller_friction,delta) + rotational_velocity = _lerp_rotational_velocity(get_sensitivity(), controller_friction, delta) apply_rotation(rotational_velocity) else: rotational_velocity = Vector2.ZERO diff --git a/game/src/gameplay/characters/player/hud.gd b/game/src/gameplay/characters/player/hud.gd index 0c51011..e0964ab 100644 --- a/game/src/gameplay/characters/player/hud.gd +++ b/game/src/gameplay/characters/player/hud.gd @@ -1,6 +1,9 @@ class_name Hud extends CanvasLayer +const _CROSSHAIR_ANIM_TO_DOT: StringName = &"to_dot" +const _CROSSHAIR_ANIM_TO_HOLLOW: StringName = &"to_hollow" + @export var interaction_ray: InteractionRay: set = set_interaction_ray @onready var interaction_prompt: RichTextLabel = $InteractionPrompt @@ -8,7 +11,7 @@ extends CanvasLayer func _ready() -> void: - crosshair.play(&"to_dot", 1.0, true) + crosshair.play(_CROSSHAIR_ANIM_TO_DOT, 1.0, true) func set_interaction_ray(ray: InteractionRay) -> void: @@ -27,15 +30,15 @@ func _on_interaction_ray_focused(area: InteractionArea) -> void: if is_instance_valid(area): area.hint_prompts_changed.connect(_populate_focused_hints.bind(area)) _populate_focused_hints(area) - _play_crosshair_anim(&"to_hollow") + _play_crosshair_anim(_CROSSHAIR_ANIM_TO_HOLLOW) else: interaction_prompt.clear() - _play_crosshair_anim(&"to_dot") + _play_crosshair_anim(_CROSSHAIR_ANIM_TO_DOT) func _on_interaction_ray_focus_lost(area: InteractionArea) -> void: interaction_prompt.clear() - _play_crosshair_anim(&"to_dot") + _play_crosshair_anim(_CROSSHAIR_ANIM_TO_DOT) if is_instance_valid(area): area.hint_prompts_changed.disconnect(_populate_focused_hints) diff --git a/game/src/gameplay/characters/player/player_character.gd b/game/src/gameplay/characters/player/player_character.gd index 289702e..bd96d39 100644 --- a/game/src/gameplay/characters/player/player_character.gd +++ b/game/src/gameplay/characters/player/player_character.gd @@ -10,8 +10,6 @@ const ACTION_MOVE_FORWARD: StringName = &"move_forward" const ACTION_MOVE_BACKWARD: StringName = &"move_backward" const ACTION_INTERACT: StringName = &"interact" -static var player: PlayerCharacter - @export var flashlight_manager: FlashlightManager @export_group("Crouching") @@ -41,10 +39,6 @@ var _flying: bool = false @onready var flashlight: Flashlight = %Flashlight -func _init() -> void: - player = self - - func _exit_tree() -> void: LimboConsole.unregister_command("player_fly") LimboConsole.remove_alias("noclip") @@ -112,13 +106,11 @@ func _physics_process(delta: float) -> void: SPrint.print_msgf( "Player on floor: %s (was on floor: %s)\nPlayer Horizontal-Velocity: %s\nPlayer Vertical-Velocity: %s" - % [is_on_floor(), was_on_floor, (velocity * Utils.VEC3_HOR).length(), velocity.y], - true, - ) + % [is_on_floor(), was_on_floor, (velocity * Utils.VEC3_HOR).length(), velocity.y], true) func align(with_node: Node3D, reset_velocity: bool = true) -> void: - assert(is_instance_valid(with_node), "with_node is invalid.") + assert(is_instance_valid(with_node), "'with_node' is invalid.") apply_orientation(with_node.global_transform) if reset_velocity: diff --git a/game/src/gameplay/objects/stagelight/stagelight.tscn b/game/src/gameplay/objects/stagelight/stagelight.tscn index 6e6ab38..4c5d4d9 100644 --- a/game/src/gameplay/objects/stagelight/stagelight.tscn +++ b/game/src/gameplay/objects/stagelight/stagelight.tscn @@ -61,7 +61,7 @@ light_surface_material = SubResource("ShaderMaterial_nqu5b") [node name="StudioSpotLight" parent="." unique_id=449918207 instance=ExtResource("1_3cilv")] -[node name="Cylinder" parent="StudioSpotLight" index="0" unique_id=142845931] +[node name="Cylinder" parent="StudioSpotLight" index="0" unique_id=35052532] surface_material_override/1 = SubResource("ShaderMaterial_nqu5b") [node name="StudioSpotLightFraming01" parent="." unique_id=2007355874 instance=ExtResource("3_otype")] diff --git a/game/src/gameplay/systems/flashlight/flashlight.gd b/game/src/gameplay/systems/flashlight/flashlight.gd index 4a8528f..55301da 100644 --- a/game/src/gameplay/systems/flashlight/flashlight.gd +++ b/game/src/gameplay/systems/flashlight/flashlight.gd @@ -30,9 +30,8 @@ func _unhandled_input(event: InputEvent) -> void: func try_turn_on() -> void: - if is_instance_valid(manager): - if manager.is_drained(): - return + if is_instance_valid(manager) and manager.is_drained(): + return turn_on() diff --git a/game/src/gameplay/systems/flashlight/flashlight_manager.gd b/game/src/gameplay/systems/flashlight/flashlight_manager.gd index 0e15148..54b5936 100644 --- a/game/src/gameplay/systems/flashlight/flashlight_manager.gd +++ b/game/src/gameplay/systems/flashlight/flashlight_manager.gd @@ -42,7 +42,7 @@ func _exit_tree() -> void: func _ready() -> void: world = await _world_proxy.wait_for_world() - _instance_persister= InstancePersister.new(self, world) + _instance_persister = InstancePersister.new(self, world) power = _instance_persister.get_property(&"power", initial_power) is_flashlight_active = _instance_persister.get_property(&"is_flashlight_active", is_flashlight_active) diff --git a/game/src/gameplay/systems/flashlight/flashlight_trigger.gd b/game/src/gameplay/systems/flashlight/flashlight_trigger.gd index 3342cbd..2a31ce9 100644 --- a/game/src/gameplay/systems/flashlight/flashlight_trigger.gd +++ b/game/src/gameplay/systems/flashlight/flashlight_trigger.gd @@ -10,14 +10,14 @@ enum DrainModes { @export var allowed_bodies: Array[Node3D] @export var trigger_once: bool = true -var world: World -var _world_proxy: WorldProxy - @export var drain_mode := DrainModes.SET @export var drain_value: float = 0.3 @export var change_manager_drain_mode: bool = false @export var manager_drain_mode := FlashlightManager.DrainModes.MANUAL +var world: World +var _world_proxy: WorldProxy + func _enter_tree() -> void: _world_proxy = WorldProxy.new() diff --git a/game/src/gameplay/systems/power_box/fuse_pickup.gd b/game/src/gameplay/systems/power_box/fuse_pickup.gd index 2acc872..b77febe 100644 --- a/game/src/gameplay/systems/power_box/fuse_pickup.gd +++ b/game/src/gameplay/systems/power_box/fuse_pickup.gd @@ -1,6 +1,7 @@ class_name FusePickup extends Node3D +signal picked_up @export var power_box: PowerBox @@ -23,4 +24,5 @@ func _ready() -> void: func _on_picked_up() -> void: power_box.on_fuse_picked_up() InstancePersister.new(self, world).set_property(&"is_collected", true) + picked_up.emit() queue_free() diff --git a/game/src/gameplay/systems/power_box/power_box.gd b/game/src/gameplay/systems/power_box/power_box.gd index cbb8cb8..779d5e8 100644 --- a/game/src/gameplay/systems/power_box/power_box.gd +++ b/game/src/gameplay/systems/power_box/power_box.gd @@ -80,10 +80,10 @@ func _on_interaction_interacted() -> void: #start_minigame() pass - if not is_active: - turn_on() - else: + if is_active: turn_off() + else: + turn_on() # TODO diff --git a/game/src/ui/main_menu/main_menu.gd b/game/src/ui/main_menu/main_menu.gd index 71d7821..03b13a6 100644 --- a/game/src/ui/main_menu/main_menu.gd +++ b/game/src/ui/main_menu/main_menu.gd @@ -4,9 +4,8 @@ extends Control signal load_game_request(save_data: SaveData, slot_index: int) signal quit_request -const USER_SETTINGS_PATH: String = "user://settings.ini" const SAVES_DIR: String = "user://saves/" -const SAVE_DATA_PATH: String = SAVES_DIR + "%s/save_data.tres" +const SAVE_DATA_PATH: String = SAVES_DIR + "%d/save_data.tres" @onready var continue_button: Button = %ContinueButton @onready var load_button: Button = %LoadButton @@ -27,7 +26,7 @@ static func get_save_slots() -> PackedInt32Array: var index: int = 0 while index < dirs.size(): - var dir_name: String = dirs[index] + var dir_name: String = dirs.get(index) if dir_name.is_valid_int() and not (dir_name.begins_with("+") or dir_name.begins_with("-")): index += 1 else: @@ -73,7 +72,7 @@ func _ready() -> void: func has_last_save_slot() -> bool: - var config: ConfigFile = Utils.load_config(USER_SETTINGS_PATH) + var config: ConfigFile = Utils.load_config(Game.USER_SETTINGS_PATH) return config.has_section_key("save", "last_slot") @@ -88,7 +87,7 @@ func populate_save_entries() -> void: func _on_continue_pressed() -> void: - var config: ConfigFile = Utils.load_config(USER_SETTINGS_PATH) + var config: ConfigFile = Utils.load_config(Game.USER_SETTINGS_PATH) var slot: int = config.get_value("save", "last_slot", 0) var path: String = get_save_path(slot) @@ -100,7 +99,7 @@ func _on_continue_pressed() -> void: return config.set_value("save", "last_slot", slot) - config.save(USER_SETTINGS_PATH) + config.save(Game.USER_SETTINGS_PATH) var save_data: SaveData = load(path) load_game_request.emit(save_data, slot) @@ -116,9 +115,9 @@ func _on_new_game_pressed() -> void: var slot_index: int = get_unique_save_slot_index() load_game_request.emit(save_data, slot_index) - var config: ConfigFile = Utils.load_config(USER_SETTINGS_PATH) + var config: ConfigFile = Utils.load_config(Game.USER_SETTINGS_PATH) config.set_value("save", "last_slot", slot_index) - config.save(USER_SETTINGS_PATH) + config.save(Game.USER_SETTINGS_PATH) func _on_options_pressed() -> void: @@ -130,9 +129,9 @@ func _on_quit_pressed() -> void: func _on_save_entry_pressed(save_slot: int) -> void: - var config: ConfigFile = Utils.load_config(USER_SETTINGS_PATH) + var config: ConfigFile = Utils.load_config(Game.USER_SETTINGS_PATH) config.set_value("save", "last_slot", save_slot) - config.save(USER_SETTINGS_PATH) + config.save(Game.USER_SETTINGS_PATH) var save_path: String = get_save_path(save_slot) diff --git a/game/src/ui/settings_menu/settings_menu.gd b/game/src/ui/settings_menu/settings_menu.gd index 41e7331..18c539b 100644 --- a/game/src/ui/settings_menu/settings_menu.gd +++ b/game/src/ui/settings_menu/settings_menu.gd @@ -60,7 +60,11 @@ func _ready() -> void: controller_setup_menu.confirmed.connect(_on_vibration_config_confirmed) controller_vibrations.toggled.connect(_on_controller_vibrations_toggled) - visibility_changed.connect(func() -> void: if is_visible_in_tree(): update_controls_from_settings()) + visibility_changed.connect( + func() -> void: + if is_visible_in_tree(): + update_controls_from_settings() + ) set_process(false) @@ -82,8 +86,10 @@ func close() -> void: func update_controls_from_settings() -> void: # Gameplay - headbobbing.set_pressed_no_signal(SettingsHandler.get_setting("gameplay", "headbobbing", true)) - headbobbing_slider.set_value_no_signal(SettingsHandler.get_setting("gameplay", "headbobbing_multiplier", 1.0)) + var enabled: bool = SettingsHandler.get_setting("gameplay", "headbobbing", true) + var multiplier: float = SettingsHandler.get_setting("gameplay", "headbobbing_multiplier", 1.0) + headbobbing.set_pressed_no_signal(enabled) + headbobbing_slider.set_value_no_signal(multiplier) # Localization match SettingsHandler.get_setting("localization", "language", "en_US"): diff --git a/game/src/ui/settings_menu/slider_label.gd b/game/src/ui/settings_menu/slider_label.gd index eb13a85..36cb014 100644 --- a/game/src/ui/settings_menu/slider_label.gd +++ b/game/src/ui/settings_menu/slider_label.gd @@ -41,7 +41,8 @@ func set_slider(new_slider: Slider) -> void: func _on_value_changed(new_value: float) -> void: var value: float = new_value if do_remap: - value = remap(value, remap_initial_min, remap_initial_max, remap_output_min, remap_output_max) + value = remap(value, remap_initial_min, remap_initial_max, + remap_output_min, remap_output_max) var new_text: String = str(value).pad_zeros(pad_zeros).pad_decimals(pad_decimals) set_text(str(prefix, positive_prefix if value >= 0.0 else negative_prefix, new_text, suffix)) diff --git a/game/src/ui/setup/controller/controller_setup_menu.gd b/game/src/ui/setup/controller/controller_setup_menu.gd index bb22e4a..90c7ddf 100644 --- a/game/src/ui/setup/controller/controller_setup_menu.gd +++ b/game/src/ui/setup/controller/controller_setup_menu.gd @@ -10,7 +10,7 @@ var is_testing_weak: bool = false var is_testing_strong: bool = false var _duration_idx: int = 0 -@onready var vibration_component: VibrationComponent = %VibrationComponent +@onready var rumble_component: RumbleComponent = %RumbleComponent @onready var repeat_vibration_timer: Timer = %RepeatVibrationTimer @onready var min_duration_slider: HSlider = %MinDurationSlider @@ -22,9 +22,6 @@ var _duration_idx: int = 0 func _ready() -> void: - _on_visibility_changed() - visibility_changed.connect(_on_visibility_changed) - repeat_vibration_timer.timeout.connect(_test_vibration) min_weak_magnitude.focus_entered.connect(_on_weak_slider_focused) @@ -32,25 +29,25 @@ func _ready() -> void: min_strong_magnitude.focus_entered.connect(_on_strong_slider_focused) min_strong_magnitude.focus_exited.connect(_on_strong_slider_unfocused) - min_weak_magnitude.grab_focus() - _on_weak_slider_focused() - min_weak_magnitude.value_changed.connect(_on_min_weak_magnitude_value_changed) min_strong_magnitude.value_changed.connect(_on_min_strong_magnitude_value_changed) confirm_button.pressed.connect(confirm) + visibility_changed.connect(_on_visibility_changed) + _on_visibility_changed() + func confirm() -> void: SettingsHandler.set_setting( "controls", "vibration_min_weak_threshold", - VibrationComponent.min_weak_magnitude_threshold, + RumbleConductor.min_weak_magnitude_threshold, false ) SettingsHandler.set_setting( "controls", "vibration_min_strong_threshold", - VibrationComponent.min_strong_magnitude_threshold, + RumbleConductor.min_strong_magnitude_threshold, true ) @@ -58,27 +55,30 @@ func confirm() -> void: func _test_vibration() -> void: - vibration_component.weak_magnitude = MIN_WEAK_MAGNITUDE * float(is_testing_weak) - vibration_component.strong_magnitude = MIN_STRONG_MAGNITUDE * float(is_testing_strong) + # If is_testing_weak and is_testing_strong are both false, vibrate both together. + var weak_enabled: bool = is_testing_weak or (not is_testing_weak and not is_testing_strong) + var strong_enabled: bool = is_testing_strong or (not is_testing_weak and not is_testing_strong) + rumble_component.data.weak_intensity = MIN_WEAK_MAGNITUDE * float(weak_enabled) + rumble_component.data.strong_intensity = MIN_STRONG_MAGNITUDE * float(strong_enabled) match _duration_idx: 5: - vibration_component.duration = 0.75 + rumble_component.data.duration = 0.75 _: - vibration_component.duration = 0.15 + rumble_component.data.duration = 0.15 #1: - #vibration_component.duration = 0.35 + #rumble_component.duration = 0.35 #2: - #vibration_component.duration = 0.75 + #rumble_component.duration = 0.75 _duration_idx = posmod(_duration_idx + 1, 6) - vibration_component.vibrate() + rumble_component.vibrate() _vibrate_texture() func _vibrate_texture() -> void: - var tween: Tween = create_tween().set_loops(maxi(int(vibration_component.duration / 0.1), 1)) + var tween: Tween = create_tween().set_loops(maxi(int(rumble_component.data.duration / 0.1), 1)) tween.tween_property(vibrate_texture, ^"rotation_degrees", -5.0, 0.05) tween.tween_property(vibrate_texture, ^"rotation_degrees", 5.0, 0.05) await tween.finished @@ -91,17 +91,20 @@ func _on_visibility_changed() -> void: repeat_vibration_timer.start() process_mode = Node.PROCESS_MODE_INHERIT min_weak_magnitude.grab_focus() + + min_weak_magnitude.set_value_no_signal(RumbleConductor.min_weak_magnitude_threshold) + min_strong_magnitude.set_value_no_signal(RumbleConductor.min_strong_magnitude_threshold) else: repeat_vibration_timer.stop() process_mode = Node.PROCESS_MODE_DISABLED func _on_min_weak_magnitude_value_changed(value: float) -> void: - VibrationComponent.min_weak_magnitude_threshold = value + RumbleConductor.min_weak_magnitude_threshold = value func _on_min_strong_magnitude_value_changed(value: float) -> void: - VibrationComponent.min_strong_magnitude_threshold = value + RumbleConductor.min_strong_magnitude_threshold = value func _on_weak_slider_focused() -> void: diff --git a/game/src/ui/setup/controller/controller_setup_menu.tscn b/game/src/ui/setup/controller/controller_setup_menu.tscn index 73c1a37..5487e87 100644 --- a/game/src/ui/setup/controller/controller_setup_menu.tscn +++ b/game/src/ui/setup/controller/controller_setup_menu.tscn @@ -1,8 +1,12 @@ [gd_scene format=3 uid="uid://dak6f2xw3mgjj"] [ext_resource type="Script" uid="uid://cs2yn6o8vsp4a" path="res://src/ui/setup/controller/controller_setup_menu.gd" id="1_ee61x"] -[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/vibration_component.gd" id="2_6beo4"] +[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/controller_rumble/rumble_component.gd" id="2_6beo4"] [ext_resource type="Texture2D" uid="uid://g85advbc1vrw" path="res://addons/controller_icons/assets/xboxseries/diagram_simple.png" id="3_6beo4"] +[ext_resource type="Script" uid="uid://dwwingj0dsxdg" path="res://src/core/controller_rumble/rumble_config.gd" id="3_sa2mc"] + +[sub_resource type="Resource" id="Resource_gtqq5"] +script = ExtResource("3_sa2mc") [node name="ControllerSetupMenu" type="Control" unique_id=1563422179] layout_mode = 3 @@ -14,9 +18,10 @@ grow_vertical = 2 script = ExtResource("1_ee61x") metadata/_custom_type_script = "uid://cs2yn6o8vsp4a" -[node name="VibrationComponent" type="Node" parent="." unique_id=193091150] +[node name="RumbleComponent" type="Node" parent="." unique_id=193091150] unique_name_in_owner = true script = ExtResource("2_6beo4") +data = SubResource("Resource_gtqq5") weak_magnitude = 0.05 strong_magnitude = 0.05 metadata/_custom_type_script = "uid://bbwtct3hoxwws" @@ -25,10 +30,13 @@ metadata/_custom_type_script = "uid://bbwtct3hoxwws" unique_name_in_owner = true [node name="Label" type="Label" parent="." unique_id=684594410] -layout_mode = 0 -offset_right = 384.0 -offset_bottom = 49.0 +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 23.0 +grow_horizontal = 2 text = "Adjust the sliders, until you can bearly feel the vibrations." +horizontal_alignment = 1 [node name="RichTextLabel" type="RichTextLabel" parent="." unique_id=1446444531] visible = false diff --git a/game/src/ui/setup/initial_setup_menu.gd b/game/src/ui/setup/initial_setup_menu.gd index 33f226d..0e8d03c 100644 --- a/game/src/ui/setup/initial_setup_menu.gd +++ b/game/src/ui/setup/initial_setup_menu.gd @@ -7,12 +7,25 @@ signal setup_finished @onready var controller_recommended: Control = %ControllerRecommended @onready var rhythm_setup_menu: RhythmSetupMenu = %RhythmSetupMenu @onready var controller_setup_menu: ControllerSetupMenu = %ControllerSetupMenu +@onready var configurable_notice: Label = %ConfigurableNotice +@onready var fade_control: Control = %FadeControl func _ready() -> void: rhythm_setup_menu.confirmed.connect(_on_rythm_setup_confirmed) controller_setup_menu.confirmed.connect(_on_controller_setup_confirmed) + visibility_changed.connect(_on_visibility_changed) + _on_visibility_changed() + + InputManager.input_method_changed.connect(_on_input_method_changed) + + +func _on_visibility_changed() -> void: + print("On Visibility Changed. Is Visible: ", is_visible_in_tree()) + if not is_visible_in_tree(): + return + await get_tree().create_timer(0.5).timeout controller_recommended.show() fade_out() @@ -21,23 +34,24 @@ func _ready() -> void: controller_recommended.hide() controller_setup_menu.show() + configurable_notice.show() fade_out() - InputManager.input_method_changed.connect(_on_input_method_changed) - func fade_in() -> void: get_viewport().gui_release_focus() foreground_color.mouse_filter = Control.MOUSE_FILTER_STOP var tween: Tween = create_tween() - tween.tween_property(foreground_color, ^"color:a", 1.0, 3.0) + #tween.tween_property(foreground_color, ^"color:a", 1.0, 3.0) + tween.tween_property(fade_control, ^"modulate:a", 0.0, 3.0) await tween.finished func fade_out() -> void: foreground_color.mouse_filter = Control.MOUSE_FILTER_IGNORE var tween: Tween = create_tween() - tween.tween_property(foreground_color, ^"color:a", 0.0, 3.0) + #tween.tween_property(foreground_color, ^"color:a", 0.0, 3.0) + tween.tween_property(fade_control, ^"modulate:a", 1.0, 3.0) func _on_rythm_setup_confirmed() -> void: diff --git a/game/src/ui/setup/initial_setup_menu.tscn b/game/src/ui/setup/initial_setup_menu.tscn index eed1f4a..6b9d7ac 100644 --- a/game/src/ui/setup/initial_setup_menu.tscn +++ b/game/src/ui/setup/initial_setup_menu.tscn @@ -24,7 +24,17 @@ grow_horizontal = 2 grow_vertical = 2 color = Color(0, 0, 0, 1) -[node name="ControllerRecommended" type="Control" parent="." unique_id=167704832] +[node name="FadeControl" type="Control" parent="." unique_id=2135836694] +unique_name_in_owner = true +modulate = Color(1, 1, 1, 0) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ControllerRecommended" type="Control" parent="FadeControl" unique_id=167704832] unique_name_in_owner = true visible = false layout_mode = 1 @@ -34,7 +44,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="VBoxContainer" type="VBoxContainer" parent="ControllerRecommended" unique_id=684870312] +[node name="VBoxContainer" type="VBoxContainer" parent="FadeControl/ControllerRecommended" unique_id=684870312] layout_mode = 1 anchors_preset = -1 anchor_left = 0.15 @@ -44,17 +54,17 @@ anchor_bottom = 0.97 grow_horizontal = 2 grow_vertical = 2 -[node name="Label" type="Label" parent="ControllerRecommended/VBoxContainer" unique_id=1590844875] +[node name="Label" type="Label" parent="FadeControl/ControllerRecommended/VBoxContainer" unique_id=1590844875] layout_mode = 2 theme_override_font_sizes/font_size = 42 text = "Use of a controller recommended" horizontal_alignment = 1 -[node name="Control" type="Control" parent="ControllerRecommended/VBoxContainer" unique_id=595986704] +[node name="Control" type="Control" parent="FadeControl/ControllerRecommended/VBoxContainer" unique_id=595986704] layout_mode = 2 size_flags_vertical = 3 -[node name="TextureRect" type="TextureRect" parent="ControllerRecommended/VBoxContainer/Control" unique_id=1190977067] +[node name="TextureRect" type="TextureRect" parent="FadeControl/ControllerRecommended/VBoxContainer/Control" unique_id=1190977067] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -68,20 +78,35 @@ texture = ExtResource("4_sr5uw") expand_mode = 1 stretch_mode = 5 -[node name="RhythmSetupMenu" parent="." unique_id=552044082 instance=ExtResource("2_2gigy")] +[node name="RhythmSetupMenu" parent="FadeControl" unique_id=552044082 instance=ExtResource("2_2gigy")] unique_name_in_owner = true process_mode = 4 visible = false layout_mode = 1 -[node name="ControllerSetupMenu" parent="." unique_id=1563422179 instance=ExtResource("3_sr5uw")] +[node name="ControllerSetupMenu" parent="FadeControl" unique_id=1563422179 instance=ExtResource("3_sr5uw")] unique_name_in_owner = true process_mode = 4 visible = false layout_mode = 1 +[node name="ConfigurableNotice" type="Label" parent="FadeControl" unique_id=2047433102] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -23.0 +grow_horizontal = 2 +grow_vertical = 0 +text = "You can change this later in the settings." +horizontal_alignment = 1 + [node name="ForegroundColor" type="ColorRect" parent="." unique_id=1692094271] unique_name_in_owner = true +visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 diff --git a/game/src/ui/setup/rhythm/rhythm_setup_menu.gd b/game/src/ui/setup/rhythm/rhythm_setup_menu.gd index af612e5..6d174e5 100644 --- a/game/src/ui/setup/rhythm/rhythm_setup_menu.gd +++ b/game/src/ui/setup/rhythm/rhythm_setup_menu.gd @@ -32,6 +32,13 @@ func close() -> void: confirmed.emit() +func fade_out_and_stop_setup(duration: float) -> void: + var tween: Tween = create_tween() + tween.tween_property(rhythm_beat, ^"volume_linear", 0.0, duration) + tween.tween_callback(stop_setup) + await tween.finished + + func stop_setup() -> void: rhythm_player.stop() _restore_song_playback_data() @@ -55,7 +62,8 @@ func _on_visibility_changed() -> void: if not rhythm_beat.playing: _store_song_playback_data() - user_offset_slider.set_value_no_signal(SettingsHandler.get_setting("audio", "user_offset_ms", 0.0)) + var user_offset: float = SettingsHandler.get_setting("audio", "user_offset_ms", 0.0) + user_offset_slider.set_value_no_signal(user_offset) user_offset_slider.grab_focus() rhythm_player.play() diff --git a/game/src/ui/setup/rhythm/rhythm_setup_menu.tscn b/game/src/ui/setup/rhythm/rhythm_setup_menu.tscn index 15be745..20bdb63 100644 --- a/game/src/ui/setup/rhythm/rhythm_setup_menu.tscn +++ b/game/src/ui/setup/rhythm/rhythm_setup_menu.tscn @@ -4,13 +4,24 @@ [ext_resource type="Script" uid="uid://bfpr421kg4s" path="res://src/ui/settings_menu/slider_label.gd" id="2_0yond"] [ext_resource type="Script" uid="uid://bdi06itcm6wfp" path="res://src/core/rhythm/rhythm_player.gd" id="2_jsobq"] [ext_resource type="AudioStream" uid="uid://cwpx80o5yaauf" path="res://src/ui/setup/rhythm/120bpm_beat.ogg" id="2_uupbh"] -[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/vibration_component.gd" id="2_yr7ls"] +[ext_resource type="Script" uid="uid://bbwtct3hoxwws" path="res://src/core/controller_rumble/rumble_component.gd" id="2_yr7ls"] [ext_resource type="Script" uid="uid://co7j2qtqpud6b" path="res://src/core/rhythm/rhythm_listener.gd" id="3_o47te"] +[ext_resource type="Script" uid="uid://dwwingj0dsxdg" path="res://src/core/controller_rumble/rumble_config.gd" id="3_xnyx0"] [ext_resource type="Script" uid="uid://c5mqtmsvgt4e8" path="res://src/core/rhythm/song_info.gd" id="4_7a0hf"] [ext_resource type="Script" uid="uid://cx1nws4t8hs2u" path="res://src/core/rhythm/tempo_section_info.gd" id="5_cribi"] [ext_resource type="Texture2D" uid="uid://t6ydwif1bo1c" path="res://godot_icon.svg" id="6_cribi"] [ext_resource type="Script" uid="uid://bvmfdeypbeqsn" path="res://src/core/rhythm/rhythm_property_setter.gd" id="7_0yond"] +[sub_resource type="Resource" id="Resource_ej02t"] +script = ExtResource("3_xnyx0") +weak_intensity = 0.5 +strong_intensity = 0.0 + +[sub_resource type="Resource" id="Resource_sp7no"] +script = ExtResource("3_xnyx0") +weak_intensity = 0.9 +strong_intensity = 0.5 + [sub_resource type="Resource" id="Resource_0yond"] script = ExtResource("4_7a0hf") audio_stream = ExtResource("2_uupbh") @@ -27,14 +38,16 @@ mouse_filter = 2 script = ExtResource("1_la56f") metadata/_custom_type_script = "uid://bvs4uvpewolwc" -[node name="BeatVibration" type="Node" parent="." unique_id=1560105761] +[node name="BeatRumble" type="Node" parent="." unique_id=1560105761] script = ExtResource("2_yr7ls") +data = SubResource("Resource_ej02t") weak_magnitude = 0.5 strong_magnitude = 0.0 metadata/_custom_type_script = "uid://bbwtct3hoxwws" -[node name="BarVibration" type="Node" parent="." unique_id=1199048244] +[node name="BarRumble" type="Node" parent="." unique_id=1199048244] script = ExtResource("2_yr7ls") +data = SubResource("Resource_sp7no") weak_magnitude = 0.9 strong_magnitude = 0.5 metadata/_custom_type_script = "uid://bbwtct3hoxwws" @@ -155,5 +168,14 @@ texture = ExtResource("6_cribi") layout_mode = 2 texture = ExtResource("6_cribi") -[connection signal="bar_tick" from="RhythmListener" to="BarVibration" method="vibrate" flags=3 unbinds=1] -[connection signal="beat_tick" from="RhythmListener" to="BeatVibration" method="vibrate" unbinds=1] +[node name="Label" type="Label" parent="." unique_id=1207688688] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 23.0 +grow_horizontal = 2 +text = "Adjust the slider so that everything syncs up with the beat." +horizontal_alignment = 1 + +[connection signal="bar_ticked" from="RhythmListener" to="BarRumble" method="vibrate" flags=3 unbinds=1] +[connection signal="beat_ticked" from="RhythmListener" to="BeatRumble" method="vibrate" unbinds=1] diff --git a/game/src/ui/startup/fog_overlay.gdshader b/game/src/ui/startup/fog_overlay.gdshader new file mode 100644 index 0000000..0b3d391 --- /dev/null +++ b/game/src/ui/startup/fog_overlay.gdshader @@ -0,0 +1,30 @@ +shader_type canvas_item; + +uniform sampler2D color_ramp; +uniform float fog_amount = 1.0; +uniform sampler2D fog_texture : repeat_enable; +uniform sampler2D fog_overlay_texture : repeat_enable; + +global uniform float time; + +void vertex() { + // Called for every vertex the material is visible on. +} + +void fragment() { + // Called for every pixel the material is visible on. + if (UV.y > 0.5) + { + vec4 fog = texture(fog_texture, vec2(UV.x + time * 0.02, UV.y)); + vec4 fog_overlay = texture(fog_overlay_texture, vec2(UV.x + time * -0.015, UV.y + time * 0.0025)); + fog = mix(fog, fog_overlay, 0.5); + vec3 sampled_color = texture(color_ramp, vec2(fog.r, 0.0)).rgb; + fog = mix(fog, vec4(sampled_color, 0.0), 1.0); + COLOR += fog * ((abs(0.5 - UV.x) * 2.0) * (abs(0.5 - UV.y) * 2.0)) * fog_amount; + } +} + +//void light() { +// // Called for every pixel for every light affecting the CanvasItem. +// // Uncomment to replace the default light processing function with this one. +//} diff --git a/game/src/ui/startup/fog_overlay.gdshader.uid b/game/src/ui/startup/fog_overlay.gdshader.uid new file mode 100644 index 0000000..4a0fef5 --- /dev/null +++ b/game/src/ui/startup/fog_overlay.gdshader.uid @@ -0,0 +1 @@ +uid://cx2yv2l8vbubl diff --git a/game/src/ui/startup/startup_scene.gd b/game/src/ui/startup/startup_scene.gd new file mode 100644 index 0000000..100b0b8 --- /dev/null +++ b/game/src/ui/startup/startup_scene.gd @@ -0,0 +1,52 @@ +class_name StartupScene +extends Control + +signal startup_finished + +@onready var menu_animations: AnimationPlayer = %MenuAnimations +@onready var initial_setup_menu: InitialSetupMenu = %InitialSetupMenu +@onready var splash_screen_button: Button = %SplashScreenButton + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + splash_screen_button.pressed.connect(_on_splash_confirmed) + + menu_animations.play(&"fade_splash_in") + + +func needs_game_setup() -> bool: + var config := ConfigFile.new() + if config.load(Game.USER_SETTINGS_PATH) != OK: + return true + + return config.get_value("save", "needs_initial_setup", true) + + +func show_initial_setup_menu() -> void: + initial_setup_menu.setup_finished.connect(_on_initial_setup_menu_setup_finished) + initial_setup_menu.show() + + +func _on_splash_confirmed() -> void: + splash_screen_button.disabled = true + + + if needs_game_setup(): + menu_animations.play(&"minimize_splash") + await menu_animations.animation_finished + show_initial_setup_menu() + else: + menu_animations.play(&"fade_out") + await menu_animations.animation_finished + startup_finished.emit() + + +func _on_initial_setup_menu_setup_finished() -> void: + var config: ConfigFile = Utils.load_config(Game.USER_SETTINGS_PATH) + config.set_value("save", "needs_initial_setup", false) + config.save(Game.USER_SETTINGS_PATH) + + menu_animations.play(&"fade_out") + await menu_animations.animation_finished + startup_finished.emit() diff --git a/game/src/ui/startup/startup_scene.gd.uid b/game/src/ui/startup/startup_scene.gd.uid new file mode 100644 index 0000000..35da319 --- /dev/null +++ b/game/src/ui/startup/startup_scene.gd.uid @@ -0,0 +1 @@ +uid://o3b1qkysue8d diff --git a/game/src/ui/startup/startup_scene.tscn b/game/src/ui/startup/startup_scene.tscn new file mode 100644 index 0000000..fdca02b --- /dev/null +++ b/game/src/ui/startup/startup_scene.tscn @@ -0,0 +1,491 @@ +[gd_scene format=3 uid="uid://0kd77tbv1p3s"] + +[ext_resource type="Texture2D" uid="uid://vs1sgal1rlr3" path="res://src/ui/startup/title.png" id="1_8mufd"] +[ext_resource type="Script" uid="uid://o3b1qkysue8d" path="res://src/ui/startup/startup_scene.gd" id="1_b5a2f"] +[ext_resource type="PackedScene" uid="uid://8bxv3c5f8d2j" path="res://src/ui/setup/initial_setup_menu.tscn" id="1_xv1lf"] +[ext_resource type="Shader" uid="uid://cx2yv2l8vbubl" path="res://src/ui/startup/fog_overlay.gdshader" id="2_3kk5a"] + +[sub_resource type="Gradient" id="Gradient_b5a2f"] +colors = PackedColorArray(0, 0, 0, 1, 0.18414839, 0.22214463, 0.27734375, 1) + +[sub_resource type="GradientTexture1D" id="GradientTexture1D_b5a2f"] +gradient = SubResource("Gradient_b5a2f") + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_b5a2f"] +seed = 1 + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_3kk5a"] +generate_mipmaps = false +noise = SubResource("FastNoiseLite_b5a2f") +seamless = true + +[sub_resource type="FastNoiseLite" id="FastNoiseLite_8mufd"] + +[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_xv1lf"] +generate_mipmaps = false +noise = SubResource("FastNoiseLite_8mufd") +seamless = true + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_qhbau"] +resource_local_to_scene = true +shader = ExtResource("2_3kk5a") +shader_parameter/color_ramp = SubResource("GradientTexture1D_b5a2f") +shader_parameter/fog_amount = 1.0 +shader_parameter/fog_texture = SubResource("NoiseTexture2D_xv1lf") +shader_parameter/fog_overlay_texture = SubResource("NoiseTexture2D_3kk5a") + +[sub_resource type="Animation" id="Animation_xv1lf"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("LogoSplash/Title:modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Color(1, 1, 1, 1)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("LogoSplash/Label:offset_transform_position_ratio") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(0, 9)] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("LogoSplash/Label:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Color(1, 1, 1, 1)] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("LogoSplash/Title:anchor_left") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("LogoSplash/Title:anchor_top") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} +tracks/5/type = "value" +tracks/5/imported = false +tracks/5/enabled = true +tracks/5/path = NodePath("LogoSplash/Title:anchor_right") +tracks/5/interp = 1 +tracks/5/loop_wrap = true +tracks/5/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/6/type = "value" +tracks/6/imported = false +tracks/6/enabled = true +tracks/6/path = NodePath("LogoSplash/Title:anchor_bottom") +tracks/6/interp = 1 +tracks/6/loop_wrap = true +tracks/6/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/7/type = "value" +tracks/7/imported = false +tracks/7/enabled = true +tracks/7/path = NodePath("LogoSplash/Label/SplashScreenButton:disabled") +tracks/7/interp = 1 +tracks/7/loop_wrap = true +tracks/7/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [true] +} +tracks/8/type = "value" +tracks/8/imported = false +tracks/8/enabled = true +tracks/8/path = NodePath("BackgroundRect:material:shader_parameter/fog_amount") +tracks/8/interp = 1 +tracks/8/loop_wrap = true +tracks/8/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/9/type = "value" +tracks/9/imported = false +tracks/9/enabled = true +tracks/9/path = NodePath("LogoSplash:modulate") +tracks/9/interp = 1 +tracks/9/loop_wrap = true +tracks/9/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Color(1, 1, 1, 1)] +} + +[sub_resource type="Animation" id="Animation_3kk5a"] +resource_name = "fade_out" +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("LogoSplash:modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Color(1, 1, 1, 1), Color(1, 1, 1, 0)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("BackgroundRect:material:shader_parameter/fog_amount") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [1.0, 0.0] +} + +[sub_resource type="Animation" id="Animation_8mufd"] +resource_name = "fade_splash_in" +length = 8.5 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("LogoSplash/Title:modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(4, 6), +"transitions": PackedFloat32Array(4.4382753, 1), +"update": 0, +"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("LogoSplash/Label:offset_transform_position_ratio") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(0, 9)] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("LogoSplash/Label:modulate") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(7.5, 8.5), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Color(1, 1, 1, 0), Color(1, 1, 1, 1)] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("LogoSplash/Title:anchor_left") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("LogoSplash/Title:anchor_top") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [0.0] +} +tracks/5/type = "value" +tracks/5/imported = false +tracks/5/enabled = true +tracks/5/path = NodePath("LogoSplash/Title:anchor_right") +tracks/5/interp = 1 +tracks/5/loop_wrap = true +tracks/5/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/6/type = "value" +tracks/6/imported = false +tracks/6/enabled = true +tracks/6/path = NodePath("LogoSplash/Title:anchor_bottom") +tracks/6/interp = 1 +tracks/6/loop_wrap = true +tracks/6/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [1.0] +} +tracks/7/type = "value" +tracks/7/imported = false +tracks/7/enabled = true +tracks/7/path = NodePath("LogoSplash/Label/SplashScreenButton:disabled") +tracks/7/interp = 1 +tracks/7/loop_wrap = true +tracks/7/keys = { +"times": PackedFloat32Array(0, 7.71), +"transitions": PackedFloat32Array(1, 1), +"update": 1, +"values": [true, false] +} +tracks/8/type = "value" +tracks/8/imported = false +tracks/8/enabled = true +tracks/8/path = NodePath("BackgroundRect:material:shader_parameter/fog_amount") +tracks/8/interp = 1 +tracks/8/loop_wrap = true +tracks/8/keys = { +"times": PackedFloat32Array(0, 4), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [0.0, 1.0] +} + +[sub_resource type="Animation" id="Animation_b5a2f"] +resource_name = "minimize_splash" +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("LogoSplash/Title:modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Color(1, 1, 1, 1), Color(1, 1, 1, 0.8039216)] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("LogoSplash/Title:anchor_left") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [0.0, 0.0] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("LogoSplash/Title:anchor_top") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [0.0, 0.0] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("LogoSplash/Title:anchor_right") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [1.0, 1.0] +} +tracks/4/type = "value" +tracks/4/imported = false +tracks/4/enabled = true +tracks/4/path = NodePath("LogoSplash/Title:anchor_bottom") +tracks/4/interp = 1 +tracks/4/loop_wrap = true +tracks/4/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(0.5, 1), +"update": 0, +"values": [1.0, 0.35] +} +tracks/5/type = "value" +tracks/5/imported = false +tracks/5/enabled = true +tracks/5/path = NodePath("LogoSplash/Label:modulate") +tracks/5/interp = 1 +tracks/5/loop_wrap = true +tracks/5/keys = { +"times": PackedFloat32Array(0, 0.375), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [Color(1, 1, 1, 1), Color(1, 1, 1, 0)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_b5a2f"] +_data = { +&"RESET": SubResource("Animation_xv1lf"), +&"fade_out": SubResource("Animation_3kk5a"), +&"fade_splash_in": SubResource("Animation_8mufd"), +&"minimize_splash": SubResource("Animation_b5a2f") +} + +[sub_resource type="InputEventAction" id="InputEventAction_mffen"] +action = &"ui_accept" + +[sub_resource type="Shortcut" id="Shortcut_qhbau"] +events = [SubResource("InputEventAction_mffen")] + +[node name="StartupScene" type="Control" unique_id=40531563] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_b5a2f") + +[node name="BackgroundRect" type="ColorRect" parent="." unique_id=9977493] +material = SubResource("ShaderMaterial_qhbau") +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 1) + +[node name="MenuAnimations" type="AnimationPlayer" parent="." unique_id=571760447] +unique_name_in_owner = true +libraries/ = SubResource("AnimationLibrary_b5a2f") + +[node name="LogoSplash" type="Control" parent="." unique_id=1956225183] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Label" type="RichTextLabel" parent="LogoSplash" unique_id=73966910] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -152.5 +offset_top = -11.5 +offset_right = 152.5 +offset_bottom = 11.5 +grow_horizontal = 2 +grow_vertical = 2 +offset_transform_enabled = true +offset_transform_position_ratio = Vector2(0, 9) +offset_transform_visual_only = false +bbcode_enabled = true +text = "Press [img width=1em]uid://bi8jxa3gcbfdn[/img] to start" +horizontal_alignment = 1 + +[node name="SplashScreenButton" type="Button" parent="LogoSplash/Label" unique_id=1618938357] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +disabled = true +shortcut = SubResource("Shortcut_qhbau") +flat = true + +[node name="Title" type="TextureRect" parent="LogoSplash" unique_id=959861830] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("1_8mufd") +expand_mode = 1 +stretch_mode = 5 + +[node name="Label2" type="Label" parent="LogoSplash" unique_id=430972999] +visible = false +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -334.0 +offset_top = -68.5 +offset_right = 334.0 +offset_bottom = 68.5 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_font_sizes/font_size = 100 +text = "Magic N' Stuff" +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="InitialSetupMenu" parent="." unique_id=1302993740 instance=ExtResource("1_xv1lf")] +unique_name_in_owner = true +visible = false +layout_mode = 1 +anchors_preset = -1 +anchor_top = 0.3 + +[node name="ColorRect" parent="InitialSetupMenu" index="0" unique_id=1648891710] +visible = false + +[editable path="InitialSetupMenu"] diff --git a/game/src/ui/startup/title.pixi b/game/src/ui/startup/title.pixi new file mode 100644 index 0000000000000000000000000000000000000000..53bfd15f088758c65bed3921143cee659ae1dbca GIT binary patch literal 29638 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 Cv~?#=i~WU?vMN9am|DC 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 T@(j45LZE+e{r=GrT=G8wVT0!Y literal 0 HcmV?d00001 diff --git a/game/src/ui/startup/title.png.import b/game/src/ui/startup/title.png.import new file mode 100644 index 0000000..c4a1f32 --- /dev/null +++ b/game/src/ui/startup/title.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vs1sgal1rlr3" +path="res://.godot/imported/title.png-f2a8960a23c861534baa8f4e869c6614.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/ui/startup/title.png" +dest_files=["res://.godot/imported/title.png-f2a8960a23c861534baa8f4e869c6614.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/game/tools/animation_player_editor_calls.gd b/game/tools/animation_player_editor_calls.gd index a6297f2..31883bb 100644 --- a/game/tools/animation_player_editor_calls.gd +++ b/game/tools/animation_player_editor_calls.gd @@ -7,10 +7,9 @@ extends AnimationPlayer &"free", ] -var EDITOR_PATH: String: - get: return str(get_parent().get_path()) + "/" - var function_calls: Array[Dictionary] = [] +var _editor_path: String: + get: return str(get_parent().get_path()) + "/" func _ready() -> void: @@ -57,7 +56,7 @@ func animation_started_func(_dismiss: Variant) -> void: if animation.track_get_type(track_index) != Animation.TYPE_METHOD: continue - var path: String = EDITOR_PATH + str(animation.track_get_path(track_index)) + var path: String = _editor_path + str(animation.track_get_path(track_index)) var track_key_count: int = animation.track_get_key_count(track_index) for key_index: int in track_key_count: var time: float = animation.track_get_key_time(track_index, key_index) diff --git a/game/tools/cutscene_animation_player.gd b/game/tools/cutscene_animation_player.gd index b627a0f..af17c73 100644 --- a/game/tools/cutscene_animation_player.gd +++ b/game/tools/cutscene_animation_player.gd @@ -22,7 +22,6 @@ func play_cutscene() -> void: disable_flashlight() - func align_player_with_end_alignment() -> void: if not is_instance_valid(player) or not is_instance_valid(end_alignment): return diff --git a/game/tools/debug_outline_material.tres b/game/tools/debug_outline_material.tres index 7b100b2..134b68a 100644 --- a/game/tools/debug_outline_material.tres +++ b/game/tools/debug_outline_material.tres @@ -8,5 +8,3 @@ shader = ExtResource("1_3pgam") shader_parameter/base_color = Color(0.3, 1, 0.3, 0.15) shader_parameter/edge_color = Color(1, 1, 0.3, 0.8) shader_parameter/edge_power = 3.0 -shader_parameter/box_center = Vector3(0, 0, 0) -shader_parameter/box_extents = Vector3(0, 0, 0) diff --git a/game/tools/time_music_animation_player.gd b/game/tools/time_music_animation_player.gd index 633169b..9824f50 100644 --- a/game/tools/time_music_animation_player.gd +++ b/game/tools/time_music_animation_player.gd @@ -19,12 +19,15 @@ extends AnimationPlayerEditorCalls ## is bigger than the [member max_catchup_threshold], ## or lower than the [method AudioServer.get_output_latency]. -enum CatchupBehaviour { - NONE, ## Don't catch up slowsly. +enum CatchUpBehaviour { + NONE, ## Don't catch up slowly. SPEED_SCALE_ANIMATION, ## Adjusts the [member AnimationPlayer.speed_scale] property when desynced. - PITCH_SCALE_MUSIC ## Adjusts the [member AudioStreamPlayer.pitch_scale] property when desynced. + PITCH_SCALE_MUSIC, ## Adjusts the [member AudioStreamPlayer.pitch_scale] property when desynced. } +## If this is set, it start's the [RhythmPlayer] instead of the [member audio_player].[br] +## [member audio_player] should still be set to the same node as in the [member rhythm_player]. +@export var rhythm_player: RhythmPlayer: set = set_rhythm_player ## The [AudioStreamPlayer], [AudioStreamPlayer2D] ## or [AudioStreamPlayer3D] the music should be played on.[br] ## [b]Should be set to the same player as defined in your animation.[/b] @@ -35,30 +38,33 @@ var audio_player: NodePath = ^"": set = set_audio_player @export var music_anim_track_index: int = 0 # The offset from 0.0 seconds when the music starts in the animation. #@export var music_anim_offset: float = 0.0 -## @deprecated: Actually reduntant, because this should always happen if above [member max_catchup_threshold]. +## @deprecated: Actually reduntant. This should always happen if above [member max_catchup_threshold]. ## The max difference audio and animation can have to each other, ## until the audio is snapped back to the animation.[br] ## However, [member catch_up_behaviour] takes priority, if this is set to value lower than that. -@export var max_error: float = 0.375 # 0.02175 +@export var max_error: float = 0.375 +## Helper variable in case you want to modify the [member AudioStreamPlayer.pitch_scale] property. +@export var audio_pitch_scale: float = 1.0 +## Helper variable in case you want to modify the [member AnimationPlayer.speed_scale] property. +@export var anim_speed_scale: float = 1.0 @export_group("Catching up") -## Defines in what way we want to sync the audio and animation back up. -@export var catch_up_behaviour := CatchupBehaviour.SPEED_SCALE_ANIMATION #@export var min_catchup_threshold: float = 0.0015 ## Should be set to a value above- or equal to [member max_error]. @export var max_catchup_threshold: float = 0.375 -@export_subgroup("Pitch Scale") +## Defines in what way we want to sync the audio and animation back up. +@export var catch_up_behaviour := CatchUpBehaviour.SPEED_SCALE_ANIMATION: + set(value): + catch_up_behaviour = value + notify_property_list_changed() +#@export_subgroup("Pitch Scale") ## The maximum pitch scale change the audio can be set to, when they are desynced. @export var max_pitch_scale_difference: float = 1.01 -## Helper variable in case you want to modify the [member AudioStreamPlayer.pitch_scale] property. -@export var custom_pitch_scale: float = 1.0 -@export_subgroup("Speed Scale") +#@export_subgroup("Speed Scale") ## The maximum speed scale change the animation can be set to, when they are desynced. @export var max_speed_scale_difference: float = 1.15 -## Helper variable in case you want to modify the [member AnimationPlayer.speed_scale] property. -@export var custom_speed_scale: float = 1.0 var _audio_player: Node -var _timer := Timer.new() +var _audio_playback_delay_timer := Timer.new() var _track_time: float = 0.0 @@ -66,8 +72,8 @@ func _ready() -> void: super() if not Engine.is_editor_hint(): - add_child(_timer) - _timer.one_shot = false + add_child(_audio_playback_delay_timer) + _audio_playback_delay_timer.one_shot = false set_audio_player(audio_player) animation_started.connect(_anim_started) @@ -85,6 +91,10 @@ func _process(delta: float) -> void: var latency: float = AudioServer.get_output_latency() var audio_position: float = _audio_player.get_playback_position() var anim_position: float = current_animation_position + AudioServer.get_time_since_last_mix() + + if anim_position < _track_time: + return + var difference: float = anim_position - (audio_position + _track_time) var abs_difference: float = absf(difference) @@ -95,22 +105,29 @@ func _process(delta: float) -> void: SPrint.print_msgf("Audio-Pitch: %s\nLatency: %s" % [_audio_player.pitch_scale, latency]) if ( - not catch_up_behaviour == CatchupBehaviour.NONE + not catch_up_behaviour == CatchUpBehaviour.NONE and abs_difference < max_catchup_threshold - #and abs_difference > latency #min_catchup_threshold: ): match catch_up_behaviour: - CatchupBehaviour.SPEED_SCALE_ANIMATION: + CatchUpBehaviour.SPEED_SCALE_ANIMATION: _sync_anim_speed_scale(difference, delta) - CatchupBehaviour.PITCH_SCALE_MUSIC: + CatchUpBehaviour.PITCH_SCALE_MUSIC: _sync_music_pitch_scale(latency, difference, delta) elif abs_difference > max_error: - #_audio_player.seek(difference) - _audio_player.play(anim_position + _track_time + latency) - SPrint.print_msg("Snapped Audio to: %s" % [_audio_player.get_playback_position()]) + _audio_player.play(anim_position - _track_time + latency) + SPrint.print_msg("Snapped Audio to: %s" % _audio_player.get_playback_position()) else: - _audio_player.pitch_scale = custom_pitch_scale - speed_scale = custom_speed_scale + _audio_player.pitch_scale = audio_pitch_scale + speed_scale = anim_speed_scale + + +func _validate_property(property: Dictionary) -> void: + if property.name == &"max_pitch_scale_difference": + if catch_up_behaviour != CatchUpBehaviour.PITCH_SCALE_MUSIC: + property.usage = PROPERTY_USAGE_NO_EDITOR + elif property.name == &"max_speed_scale_difference": + if catch_up_behaviour != CatchUpBehaviour.SPEED_SCALE_ANIMATION: + property.usage = PROPERTY_USAGE_NO_EDITOR func set_audio_player(path: NodePath) -> void: @@ -120,6 +137,10 @@ func set_audio_player(path: NodePath) -> void: _audio_player = node if Utils.is_node_audioplayer(node) else null +func set_rhythm_player(player: RhythmPlayer) -> void: + rhythm_player = player + + #func play_timed( #anim_name: StringName, #from_marker: StringName = &"", @@ -129,13 +150,14 @@ func set_audio_player(path: NodePath) -> void: #from_end: bool = false #) -> void: #_anim_started(anim_name) - #play_section_with_markers(anim_name, from_marker, end_marker, custom_blend, custom_speed, from_end) + #play_section_with_markers(anim_name, from_marker, + #end_marker, custom_blend, custom_speed, from_end) func _sync_anim_speed_scale(difference: float, delta: float) -> void: var max_speed_scale: float = max_speed_scale_difference - var min_speed_scale: float = custom_speed_scale + (custom_speed_scale - max_speed_scale) - var speed: float = custom_speed_scale - difference + var min_speed_scale: float = anim_speed_scale + (anim_speed_scale - max_speed_scale) + var speed: float = anim_speed_scale - difference speed = clampf(speed, min_speed_scale, max_speed_scale) @@ -143,19 +165,20 @@ func _sync_anim_speed_scale(difference: float, delta: float) -> void: speed = move_toward(speed_scale, speed, delta * 0.01) #else: #speed = lerp(speed_scale, speed, 1.0 - pow(0.5, delta)) #0.001 + #speed = clampf(speed, min_speed_scale, max_speed_scale) - speed_scale = speed#clampf(speed, min_speed_scale, max_speed_scale) + speed_scale = speed * _audio_player.pitch_scale func _sync_music_pitch_scale(latency: float, difference: float, delta: float) -> void: var max_pitch_scale: float = max_pitch_scale_difference - var min_pitch_scale: float = custom_pitch_scale + (custom_pitch_scale - max_pitch_scale) + var min_pitch_scale: float = audio_pitch_scale + (audio_pitch_scale - max_pitch_scale) - var pitch: float = custom_pitch_scale + difference + latency + var pitch: float = audio_pitch_scale + difference + latency pitch = clampf(pitch, min_pitch_scale, max_pitch_scale) pitch = lerp(_audio_player.pitch_scale, pitch, 1.0 - pow(0.5, delta)) - _audio_player.pitch_scale = clampf(pitch, min_pitch_scale, max_pitch_scale) + _audio_player.pitch_scale = clampf(pitch, min_pitch_scale, max_pitch_scale) * anim_speed_scale func _anim_started(anim_name: StringName) -> void: @@ -170,24 +193,30 @@ func _anim_started(anim_name: StringName) -> void: var duration: float = _track_time - current_animation_position if duration > 0.0: - _timer.start(duration) + _audio_playback_delay_timer.start(duration) var _signals: Array[Signal] = await SignalGroup.await_signals( - [_timer.timeout, animation_finished, animation_changed], 1 + [_audio_playback_delay_timer.timeout, animation_finished, animation_changed], 1 ) - if not _signals.front() == _timer.timeout: + if not _signals.front() == _audio_playback_delay_timer.timeout: return - #var latency: float = AudioServer.get_output_latency() - AudioServer.get_time_to_next_mix() - _audio_player.stream = animation.audio_track_get_key_stream(music_anim_track_index, 0) - _audio_player.play(current_animation_position - _track_time)# - latency) + var playback_position: float = current_animation_position - _track_time + + if is_instance_valid(rhythm_player): + rhythm_player.play(playback_position) + elif is_instance_valid(_audio_player): + _audio_player.stream = animation.audio_track_get_key_stream(music_anim_track_index, 0) + _audio_player.play(playback_position) func _anim_finished(_anim_name: StringName) -> void: if Engine.is_editor_hint(): return - if is_instance_valid(_audio_player): + if is_instance_valid(rhythm_player): + rhythm_player.stop() + elif is_instance_valid(_audio_player): _audio_player.stop() _audio_player.stream = null