diff --git a/project.godot b/project.godot index b025b66..26e47a6 100644 --- a/project.godot +++ b/project.godot @@ -21,6 +21,7 @@ GameState="*res://scripts/util/game_state.gd" TextureGen="*res://scripts/util/texture_generator.gd" SceneManager="*res://scripts/util/scene_manager.gd" DialogManager="*res://scenes/components/dialog_ui.tscn" +PauseMenu="*res://scenes/components/pause_menu.tscn" [display] diff --git a/scenes/components/pause_menu.tscn b/scenes/components/pause_menu.tscn new file mode 100644 index 0000000..7483612 --- /dev/null +++ b/scenes/components/pause_menu.tscn @@ -0,0 +1,179 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/ui/pause_menu.gd" id="1_pause"] + +[sub_resource type="StyleBoxFlat" id="style_panel"] +bg_color = Color(0.12, 0.13, 0.16, 0.98) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.42, 0.44, 0.5, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_left = 6 +corner_radius_bottom_right = 6 +content_margin_left = 20.0 +content_margin_top = 16.0 +content_margin_right = 20.0 +content_margin_bottom = 18.0 + +[sub_resource type="StyleBoxFlat" id="style_btn"] +bg_color = Color(0.18, 0.19, 0.23, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.4, 0.42, 0.48, 1) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_left = 2 +corner_radius_bottom_right = 2 +content_margin_left = 12.0 +content_margin_top = 4.0 +content_margin_right = 12.0 +content_margin_bottom = 4.0 + +[node name="PauseMenu" type="CanvasLayer"] +layer = 100 +script = ExtResource("1_pause") + +[node name="BG" type="ColorRect" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.65) +mouse_filter = 0 + +[node name="MainPanel" type="PanelContainer" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -110.0 +offset_top = -100.0 +offset_right = 110.0 +offset_bottom = 100.0 +theme_override_styles/panel = SubResource("style_panel") + +[node name="V" type="VBoxContainer" parent="MainPanel"] +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="Title" type="Label" parent="MainPanel/V"] +layout_mode = 2 +text = "ПАУЗА" +horizontal_alignment = 1 +modulate = Color(0.94, 0.71, 0.1, 1) + +[node name="Spacer" type="Control" parent="MainPanel/V"] +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 + +[node name="BtnResume" type="Button" parent="MainPanel/V"] +custom_minimum_size = Vector2(180, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "Продолжить" + +[node name="BtnSettings" type="Button" parent="MainPanel/V"] +custom_minimum_size = Vector2(180, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "Настройки" + +[node name="BtnMainMenu" type="Button" parent="MainPanel/V"] +custom_minimum_size = Vector2(180, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "В главное меню" + +[node name="BtnQuit" type="Button" parent="MainPanel/V"] +custom_minimum_size = Vector2(180, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "Сохранить и выйти" + +[node name="SettingsPanel" type="PanelContainer" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -180.0 +offset_top = -110.0 +offset_right = 180.0 +offset_bottom = 110.0 +theme_override_styles/panel = SubResource("style_panel") + +[node name="V" type="VBoxContainer" parent="SettingsPanel"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Title" type="Label" parent="SettingsPanel/V"] +layout_mode = 2 +text = "Настройки" +horizontal_alignment = 1 +modulate = Color(0.94, 0.71, 0.1, 1) + +[node name="SensRow" type="HBoxContainer" parent="SettingsPanel/V"] +layout_mode = 2 + +[node name="Label" type="Label" parent="SettingsPanel/V/SensRow"] +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +text = "Чувствительность мыши" + +[node name="Slider" type="HSlider" parent="SettingsPanel/V/SensRow"] +custom_minimum_size = Vector2(140, 18) +layout_mode = 2 +min_value = 1.0 +max_value = 10.0 +step = 0.1 +value = 2.5 + +[node name="Value" type="Label" parent="SettingsPanel/V/SensRow"] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +text = "0.0025" + +[node name="VolRow" type="HBoxContainer" parent="SettingsPanel/V"] +layout_mode = 2 + +[node name="Label" type="Label" parent="SettingsPanel/V/VolRow"] +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +text = "Громкость" + +[node name="Slider" type="HSlider" parent="SettingsPanel/V/VolRow"] +custom_minimum_size = Vector2(140, 18) +layout_mode = 2 +min_value = 0.0 +max_value = 100.0 +step = 1.0 +value = 50.0 + +[node name="Value" type="Label" parent="SettingsPanel/V/VolRow"] +custom_minimum_size = Vector2(50, 0) +layout_mode = 2 +text = "50%" + +[node name="Spacer2" type="Control" parent="SettingsPanel/V"] +custom_minimum_size = Vector2(0, 10) +layout_mode = 2 + +[node name="Buttons" type="HBoxContainer" parent="SettingsPanel/V"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 8 + +[node name="Back" type="Button" parent="SettingsPanel/V/Buttons"] +custom_minimum_size = Vector2(100, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "Назад" + +[node name="Apply" type="Button" parent="SettingsPanel/V/Buttons"] +custom_minimum_size = Vector2(100, 28) +layout_mode = 2 +theme_override_styles/normal = SubResource("style_btn") +text = "Применить" diff --git a/scripts/main_menu.gd b/scripts/main_menu.gd index 6de10e4..706f930 100644 --- a/scripts/main_menu.gd +++ b/scripts/main_menu.gd @@ -2,25 +2,41 @@ extends Control @onready var btn_new: Button = $Center/V/BtnNew @onready var btn_continue: Button = $Center/V/BtnContinue +@onready var btn_settings: Button = $Center/V/BtnSettings @onready var btn_quit: Button = $Center/V/BtnQuit func _ready() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE - GameState.reset() DialogManager.set_crosshair_visible(false) DialogManager.set_controls_hint("") DialogManager.set_date_time("", "") DialogManager.set_objective("") + btn_new.pressed.connect(_on_new_game) + btn_continue.pressed.connect(_on_continue) btn_quit.pressed.connect(_on_quit) + btn_continue.disabled = not GameState.has_save() + if not btn_continue.disabled: + btn_continue.text = "Продолжить" btn_new.grab_focus() func _on_new_game() -> void: + GameState.reset() + GameState.delete_save() SceneManager.go_room() +func _on_continue() -> void: + if not GameState.load_game(): + return + var target: String = GameState.last_scene_path + if target == "" or not ResourceLoader.exists(target): + target = SceneManager.SCENE_ACT1_ROOM + SceneManager.change_scene(target) + + func _on_quit() -> void: get_tree().quit() diff --git a/scripts/main_menu.gd.uid b/scripts/main_menu.gd.uid new file mode 100644 index 0000000..999fe3a --- /dev/null +++ b/scripts/main_menu.gd.uid @@ -0,0 +1 @@ +uid://cisntlhe47gsc diff --git a/scripts/player/fpv_controller.gd b/scripts/player/fpv_controller.gd index cfc6b41..1f66fff 100644 --- a/scripts/player/fpv_controller.gd +++ b/scripts/player/fpv_controller.gd @@ -30,10 +30,11 @@ func _release_mouse() -> void: _captured = false -func _input(event: InputEvent) -> void: +func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion and _captured and not _locked: - rotate_y(-event.relative.x * mouse_sensitivity) - camera.rotate_x(-event.relative.y * mouse_sensitivity) + var sens: float = GameState.mouse_sensitivity + rotate_y(-event.relative.x * sens) + camera.rotate_x(-event.relative.y * sens) camera.rotation.x = clamp(camera.rotation.x, -PI * 0.45, PI * 0.45) elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if not _captured: @@ -46,12 +47,8 @@ func _input(event: InputEvent) -> void: if _focused_obj.has_method("interact"): _focused_obj.interact(self) elif event is InputEventKey and event.pressed: - if event.keycode == KEY_ESCAPE: - if _captured: - _release_mouse() - else: - _capture_mouse() - elif event.keycode == KEY_Q: + if event.keycode == KEY_Q: + GameState.save_game(get_tree().current_scene.scene_file_path if get_tree().current_scene else "") get_tree().quit() diff --git a/scripts/ui/pause_menu.gd b/scripts/ui/pause_menu.gd new file mode 100644 index 0000000..5d0865b --- /dev/null +++ b/scripts/ui/pause_menu.gd @@ -0,0 +1,115 @@ +extends CanvasLayer + +@onready var bg: ColorRect = $BG +@onready var main_panel: PanelContainer = $MainPanel +@onready var settings_panel: PanelContainer = $SettingsPanel + +@onready var btn_resume: Button = $MainPanel/V/BtnResume +@onready var btn_settings: Button = $MainPanel/V/BtnSettings +@onready var btn_main: Button = $MainPanel/V/BtnMainMenu +@onready var btn_quit: Button = $MainPanel/V/BtnQuit + +@onready var sens_slider: HSlider = $SettingsPanel/V/SensRow/Slider +@onready var sens_value: Label = $SettingsPanel/V/SensRow/Value +@onready var vol_slider: HSlider = $SettingsPanel/V/VolRow/Slider +@onready var vol_value: Label = $SettingsPanel/V/VolRow/Value +@onready var btn_apply: Button = $SettingsPanel/V/Buttons/Apply +@onready var btn_back: Button = $SettingsPanel/V/Buttons/Back + + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + visible = false + main_panel.visible = true + settings_panel.visible = false + layer = 100 + + btn_resume.pressed.connect(_resume) + btn_settings.pressed.connect(_open_settings) + btn_main.pressed.connect(_to_main_menu) + btn_quit.pressed.connect(_quit) + btn_apply.pressed.connect(_apply_settings) + btn_back.pressed.connect(_close_settings) + sens_slider.value_changed.connect(func(_v: float) -> void: _update_value_labels()) + vol_slider.value_changed.connect(func(_v: float) -> void: _update_value_labels()) + + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: + if visible: + if settings_panel.visible: + _close_settings() + else: + _resume() + get_viewport().set_input_as_handled() + elif _can_pause(): + _open() + get_viewport().set_input_as_handled() + + +func _can_pause() -> bool: + var scene: Node = get_tree().current_scene + if scene == null: + return false + # Не паузим в главном меню + if scene.name == "MainMenu": + return false + return true + + +func _open() -> void: + visible = true + main_panel.visible = true + settings_panel.visible = false + get_tree().paused = true + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + btn_resume.grab_focus() + + +func _resume() -> void: + visible = false + get_tree().paused = false + # Восстановить захват мыши только для 3D-сцен (с FPV игроком) + var scene: Node = get_tree().current_scene + if scene != null and scene.has_node("PlayerFPV"): + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + + +func _open_settings() -> void: + sens_slider.value = GameState.mouse_sensitivity * 1000.0 + vol_slider.value = GameState.master_volume * 100.0 + _update_value_labels() + main_panel.visible = false + settings_panel.visible = true + btn_back.grab_focus() + + +func _close_settings() -> void: + settings_panel.visible = false + main_panel.visible = true + btn_resume.grab_focus() + + +func _update_value_labels() -> void: + sens_value.text = "%.4f" % (sens_slider.value / 1000.0) + vol_value.text = "%d%%" % int(vol_slider.value) + + +func _apply_settings() -> void: + GameState.mouse_sensitivity = sens_slider.value / 1000.0 + GameState.master_volume = vol_slider.value / 100.0 + GameState._apply_audio_volume() + GameState.save_settings() + _close_settings() + + +func _to_main_menu() -> void: + GameState.save_game(get_tree().current_scene.scene_file_path if get_tree().current_scene else "") + visible = false + get_tree().paused = false + SceneManager.go_main() + + +func _quit() -> void: + GameState.save_game(get_tree().current_scene.scene_file_path if get_tree().current_scene else "") + get_tree().quit() diff --git a/scripts/util/game_state.gd b/scripts/util/game_state.gd index 5ebcc84..f0bb47e 100644 --- a/scripts/util/game_state.gd +++ b/scripts/util/game_state.gd @@ -21,8 +21,31 @@ var act2_timezone: String = "UTC+3 (С., страна Р)" var game_date: String = "21 АВГ 2020 · ПТ" var game_time: String = "10:30" -# Ачивки — логируются в консоль, UI на будущее -var achievements: Array[String] = [] +# Ачивки +var achievements: Array = [] + +# Сохранения +var last_scene_path: String = "" + +# Настройки +var mouse_sensitivity: float = 0.0025 +var master_volume: float = 0.5 + +const SAVE_PATH: String = "user://save01.json" +const SETTINGS_PATH: String = "user://settings.json" + + +func _ready() -> void: + load_settings() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_CLOSE_REQUEST: + var scene_path: String = "" + if get_tree() != null and get_tree().current_scene != null: + scene_path = get_tree().current_scene.scene_file_path + save_game(scene_path) + get_tree().quit() func unlock_achievement(ach: String) -> void: @@ -42,3 +65,101 @@ func reset() -> void: act2_license_read = false act2_password_set = false achievements.clear() + game_date = "21 АВГ 2020 · ПТ" + game_time = "10:30" + + +# === SETTINGS === + +func save_settings() -> void: + var data: Dictionary = { + "mouse_sensitivity": mouse_sensitivity, + "master_volume": master_volume, + } + var file: FileAccess = FileAccess.open(SETTINGS_PATH, FileAccess.WRITE) + if file: + file.store_string(JSON.stringify(data)) + file.close() + + +func load_settings() -> void: + if not FileAccess.file_exists(SETTINGS_PATH): + return + var file: FileAccess = FileAccess.open(SETTINGS_PATH, FileAccess.READ) + if not file: + return + var raw: String = file.get_as_text() + file.close() + var data: Variant = JSON.parse_string(raw) + if data == null or not data is Dictionary: + return + mouse_sensitivity = data.get("mouse_sensitivity", 0.0025) + master_volume = data.get("master_volume", 0.5) + _apply_audio_volume() + + +func _apply_audio_volume() -> void: + # Bus 0 = Master + var db: float = linear_to_db(master_volume) if master_volume > 0.0 else -80.0 + AudioServer.set_bus_volume_db(0, db) + + +# === SAVE / LOAD GAME === + +func save_game(current_scene_path: String = "") -> void: + var data: Dictionary = { + "player_name": player_name, + "act1_gift_opened": act1_gift_opened, + "act1_note_read": act1_note_read, + "act1_father_left": act1_father_left, + "act1_drive_taken": act1_drive_taken, + "wifi_password_found": wifi_password_found, + "act2_license_read": act2_license_read, + "act2_password_set": act2_password_set, + "act2_timezone": act2_timezone, + "game_date": game_date, + "game_time": game_time, + "achievements": achievements, + "scene_path": current_scene_path, + } + var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE) + if file: + file.store_string(JSON.stringify(data, " ")) + file.close() + print("[SAVE] %s" % SAVE_PATH) + + +func load_game() -> bool: + if not FileAccess.file_exists(SAVE_PATH): + return false + var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ) + if not file: + return false + var raw: String = file.get_as_text() + file.close() + var data: Variant = JSON.parse_string(raw) + if data == null or not data is Dictionary: + return false + player_name = data.get("player_name", "") + act1_gift_opened = data.get("act1_gift_opened", false) + act1_note_read = data.get("act1_note_read", false) + act1_father_left = data.get("act1_father_left", false) + act1_drive_taken = data.get("act1_drive_taken", false) + wifi_password_found = data.get("wifi_password_found", false) + act2_license_read = data.get("act2_license_read", false) + act2_password_set = data.get("act2_password_set", false) + act2_timezone = data.get("act2_timezone", "UTC+3 (С., страна Р)") + game_date = data.get("game_date", "21 АВГ 2020 · ПТ") + game_time = data.get("game_time", "10:30") + achievements = data.get("achievements", []) + last_scene_path = data.get("scene_path", "") + return true + + +func has_save() -> bool: + return FileAccess.file_exists(SAVE_PATH) + + +func delete_save() -> void: + if FileAccess.file_exists(SAVE_PATH): + DirAccess.remove_absolute(ProjectSettings.globalize_path(SAVE_PATH))