- Геймпад: раскладка под паркур и гонки, обзор правым стиком, навигация по меню (A/B/крестовина) - Меню паузы (Esc/Start): выход в хаб, настройки (мышь, стик, FOV, громкость, инверсия), справка по управлению - Хаб 48x32x10 м: колонны, окна с городом, голограмма, лавки; музыка «Local Forecast - Elevator» (Kevin MacLeod, CC-BY) - Тесты: pause_test, pad_menu_test, hub_tour; README обновлён Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
148 lines
6.2 KiB
GDScript
148 lines
6.2 KiB
GDScript
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)
|