Геймпад Xbox, меню паузы с настройками, новый хаб с музыкой

- Геймпад: раскладка под паркур и гонки, обзор правым стиком, навигация по меню (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>
This commit is contained in:
2026-10-31 18:36:50 +05:00
co-authored by Claude Sonnet 5.5
parent 0ffba7311f
commit 2d347d132b
27 changed files with 1220 additions and 41 deletions
+32 -9
View File
@@ -207,7 +207,8 @@ func _ready() -> void:
add_to_group("local_player")
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
head_base_pos = head.position
base_fov = head.fov
base_fov = Settings.fov
head.fov = base_fov
# Тело видно в редакторе и отбрасывает тень, но своя камера его не рисует (слой 2)
for mi: MeshInstance3D in $Model.find_children("*", "MeshInstance3D", true, false):
@@ -245,8 +246,7 @@ func _setup_vignette() -> void:
rect.material = vignette_material
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
# Esc / Start теперь открывают меню паузы (core/pause_menu.gd), оно само освобождает мышь
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not Net.input_blocked:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # (панель ника открыта — мышь нужна на экране)
@@ -255,10 +255,7 @@ func _unhandled_input(event: InputEvent) -> void:
return
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
var zoom_factor := head.fov / base_fov
rotate_y(-event.relative.x * MOUSE_SENSITIVITY * zoom_factor)
head.rotate_x(-event.relative.y * MOUSE_SENSITIVITY * zoom_factor)
head.rotation.x = clamp(head.rotation.x, deg_to_rad(-80), deg_to_rad(80))
_mouse_look(event.relative)
if input_frozen or Net.input_blocked:
return
@@ -268,6 +265,32 @@ func _unhandled_input(event: InputEvent) -> void:
elif event.is_action_pressed("dash"):
_try_dash()
# Обзор мышью: смещение в пикселях -> поворот с учётом чувствительности из настроек и зума
func _mouse_look(relative: Vector2) -> void:
var k := MOUSE_SENSITIVITY * Settings.mouse_sens * head.fov / base_fov
_look(relative.x * k, relative.y * k)
# Поворот обзора (радианы за событие/кадр); вертикаль можно инвертировать в настройках
func _look(dx: float, dy: float) -> void:
if Settings.invert_y:
dy = -dy
rotate_y(-dx)
head.rotate_x(-dy)
head.rotation.x = clamp(head.rotation.x, deg_to_rad(-80), deg_to_rad(80))
# Обзор правым стиком геймпада (плавно, каждый кадр); кривая 1.6 даёт точность у центра и быстрый разворот на краю
func _process(delta: float) -> void:
base_fov = Settings.fov
if is_dead or Net.input_blocked: # открыто меню/панель ника (на отсчёте гонки обзор разрешён, как мышью)
return
var v := Input.get_vector("look_left", "look_right", "look_up", "look_down")
if v == Vector2.ZERO:
return
var strength := pow(minf(v.length(), 1.0), 1.6)
v = v.normalized() * strength
var k := Settings.STICK_LOOK_SPEED * Settings.stick_sens * delta * head.fov / base_fov
_look(v.x * k, v.y * k)
# Битовые флаги состояния для сетевой синхронизации (см. Net.FLAG_*)
func get_net_flags() -> int:
var flags := 0
@@ -887,8 +910,8 @@ func _update_camera(delta: float, input_dir: Vector2) -> void:
head.rotation.z = current_roll + randf_range(-1.0, 1.0) * shake_strength * 0.3
# Зум на ЛКМ
is_zooming = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) \
and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
is_zooming = (Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED) \
or (Input.is_action_pressed("zoom") and not Net.input_blocked)
# FOV: зум > рывок > слайд > стена > скорость бега
var target_fov := base_fov + sprint_factor * SPRINT_FOV_BONUS