extends Node # Автолоад «Settings»: пользовательские настройки (мышь, стик, FOV, громкость) и раскладка геймпада Xbox. # Геймпад добавляется в InputMap кодом (а не в project.godot): раскладка собрана в одном месте и видна целиком. # Сохраняется в user://settings.cfg, но только когда игрок что-то менял (save()). # # Раскладка под паркур и гонки (правый большой палец всегда остаётся на стике обзора): # левый стик — движение, правый стик — обзор # A / LB — прыжок (подтягивание, прыжок от стены, двойной прыжок) RT / L3 — спринт (держать) # X / RB — рывок B — присед (держать); RT + B на бегу — подкат # LT / R3 — зум Y — «готов» (гонка) / забег заново (Parkour) # Back — таблица лидеров крестовина вниз (держать) — сдаться в гонке # Start — пауза signal changed const PATH := "user://settings.cfg" const DEFAULT_FOV := 60.0 const STICK_DEADZONE := 0.22 const STICK_LOOK_SPEED := 3.6 # рад/с при полном отклонении стика и множителе 1.0 (~205°/с) var mouse_sens := 1.0 # множитель к player.MOUSE_SENSITIVITY var stick_sens := 1.0 # множитель к STICK_LOOK_SPEED var fov := DEFAULT_FOV # базовое поле зрения var volume := 1.0 # общая громкость 0..1 var invert_y := false # инверсия вертикали (мышь и стик) var gamepad_used := false # игрок трогал геймпад (для подсказок) func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS _setup_gamepad() load_settings() apply() func _input(event: InputEvent) -> void: if event is InputEventJoypadButton or (event is InputEventJoypadMotion and absf(event.axis_value) > 0.5): gamepad_used = true elif event is InputEventKey or event is InputEventMouseButton: gamepad_used = false # ---------- сохранение ---------- func load_settings() -> void: var cfg := ConfigFile.new() if cfg.load(PATH) != OK: return mouse_sens = clampf(float(cfg.get_value("input", "mouse_sens", mouse_sens)), 0.2, 3.0) stick_sens = clampf(float(cfg.get_value("input", "stick_sens", stick_sens)), 0.3, 2.5) invert_y = bool(cfg.get_value("input", "invert_y", invert_y)) fov = clampf(float(cfg.get_value("video", "fov", fov)), 60.0, 100.0) volume = clampf(float(cfg.get_value("audio", "volume", volume)), 0.0, 1.0) func save() -> void: var cfg := ConfigFile.new() cfg.set_value("input", "mouse_sens", mouse_sens) cfg.set_value("input", "stick_sens", stick_sens) cfg.set_value("input", "invert_y", invert_y) cfg.set_value("video", "fov", fov) cfg.set_value("audio", "volume", volume) cfg.save(PATH) func apply() -> void: var bus := AudioServer.get_bus_index("Master") AudioServer.set_bus_mute(bus, volume <= 0.001) AudioServer.set_bus_volume_db(bus, linear_to_db(maxf(volume, 0.001))) changed.emit() func reset_defaults() -> void: mouse_sens = 1.0 stick_sens = 1.0 fov = DEFAULT_FOV volume = 1.0 invert_y = false apply() # ---------- геймпад ---------- func _setup_gamepad() -> void: # Стики: двигаем и смотрим _axis("move_left", JOY_AXIS_LEFT_X, -1.0) _axis("move_right", JOY_AXIS_LEFT_X, 1.0) _axis("move_forward", JOY_AXIS_LEFT_Y, -1.0) _axis("move_back", JOY_AXIS_LEFT_Y, 1.0) for a in ["move_left", "move_right", "move_forward", "move_back"]: InputMap.action_set_deadzone(a, STICK_DEADZONE) _new_action("look_left", STICK_DEADZONE) _new_action("look_right", STICK_DEADZONE) _new_action("look_up", STICK_DEADZONE) _new_action("look_down", STICK_DEADZONE) _axis("look_left", JOY_AXIS_RIGHT_X, -1.0) _axis("look_right", JOY_AXIS_RIGHT_X, 1.0) _axis("look_up", JOY_AXIS_RIGHT_Y, -1.0) _axis("look_down", JOY_AXIS_RIGHT_Y, 1.0) # Действия _button("jump", JOY_BUTTON_A) _button("jump", JOY_BUTTON_LEFT_SHOULDER) # прыжок на плече: правый палец не уходит со стика обзора _axis("sprint", JOY_AXIS_TRIGGER_RIGHT, 1.0) _button("sprint", JOY_BUTTON_LEFT_STICK) _button("crouch", JOY_BUTTON_B) _button("dash", JOY_BUTTON_X) _button("dash", JOY_BUTTON_RIGHT_SHOULDER) _new_action("zoom", 0.3) _axis("zoom", JOY_AXIS_TRIGGER_LEFT, 1.0) _button("zoom", JOY_BUTTON_RIGHT_STICK) _button("ready", JOY_BUTTON_Y) _button("leaderboard", JOY_BUTTON_BACK) _button("giveup", JOY_BUTTON_DPAD_DOWN) # Навигация по меню: A — выбрать, B — назад (в этом проекте у ui_accept/ui_cancel кнопок геймпада нет) _button_once("ui_accept", JOY_BUTTON_A) _button_once("ui_cancel", JOY_BUTTON_B) _button_once("ui_up", JOY_BUTTON_DPAD_UP) _button_once("ui_down", JOY_BUTTON_DPAD_DOWN) _button_once("ui_left", JOY_BUTTON_DPAD_LEFT) _button_once("ui_right", JOY_BUTTON_DPAD_RIGHT) # Пауза: Esc и Start _new_action("pause", 0.5) var esc := InputEventKey.new() esc.physical_keycode = KEY_ESCAPE InputMap.action_add_event("pause", esc) _button("pause", JOY_BUTTON_START) func _new_action(action: String, deadzone: float) -> void: if not InputMap.has_action(action): InputMap.add_action(action, deadzone) func _axis(action: String, axis: JoyAxis, value: float) -> void: _new_action(action, 0.5) var e := InputEventJoypadMotion.new() e.axis = axis e.axis_value = value InputMap.action_add_event(action, e) func _button_once(action: String, button: JoyButton) -> void: for e in InputMap.action_get_events(action): if e is InputEventJoypadButton and e.button_index == button: return _button(action, button) func _button(action: String, button: JoyButton) -> void: _new_action(action, 0.5) var e := InputEventJoypadButton.new() e.button_index = button InputMap.action_add_event(action, e)