Геймпад 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:
+1
-1
@@ -1,7 +1,7 @@
|
||||
extends Node
|
||||
# Тестовый бот (автозагрузка во ВРЕМЕННОЙ копии проекта, см. tools/test/make_test_project.sh).
|
||||
# Пережидает смену сцен, по расписанию телепортирует игрока и раз в секунду печатает, что видит.
|
||||
# godot --headless --path <копия> res://maps/hub/hub.tscn -- --client --name=A --tp=2:-2.25,1,-7.5 --tp=9:0,1,5
|
||||
# godot --headless --path <копия> res://maps/hub/hub.tscn -- --client --name=A --tp=2:-12,1,-15 --tp=9:0,1,11
|
||||
# --tp=<секунда>:<x>,<y>,<z> — телепорт игрока на этой секунде (например, прямо в портал).
|
||||
# Остальные аргументы пользователя (--server-url=..., --pack-url=...) читают Net и MapLoader.
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
extends Node
|
||||
# Облёт хаба для кадров: телепорт игрока по точкам, кадры пишет Godot (--write-movie).
|
||||
# godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/h.png --quit-after 45 res://tools/test/hub_tour.tscn -- --client --server-url=ws://127.0.0.1:1
|
||||
# Меню паузы: добавьте `--menu=main|settings|controls` (тогда в кадре меню на хабе, остальной облёт отключён).
|
||||
# Кадры: h00000005.png и т. д. (каждые 5 кадров — следующая точка из spots).
|
||||
var level
|
||||
var p
|
||||
var frame := 0
|
||||
# позиция центра капсулы, рыскание (0 = на север, к порталам), наклон головы (градусы)
|
||||
var spots := [
|
||||
[Vector3(0, 1.25, 12), 0, 4], [Vector3(-10, 1.25, -6), 90, 8], [Vector3(10, 1.25, 8), -90, 6],
|
||||
[Vector3(0, 1.25, -9), 0, 12], [Vector3(0, 1.25, -5), 180, 10], [Vector3(14, 1.25, -9), 0, 8],
|
||||
[Vector3(-20, 1.25, 2), -90, 0], [Vector3(0, 6.0, 13), 0, -18],
|
||||
]
|
||||
func _ready() -> void:
|
||||
Net.nick_asked = true # без панели «Введите ник»
|
||||
level = (load("res://maps/hub/hub.tscn") as PackedScene).instantiate()
|
||||
add_child(level)
|
||||
p = level.get_node("Player")
|
||||
p.set_physics_process(false)
|
||||
func _process(_d: float) -> void:
|
||||
frame += 1
|
||||
for a in OS.get_cmdline_user_args():
|
||||
if a.begins_with("--menu="):
|
||||
if frame == 3:
|
||||
PauseMenu.open_menu()
|
||||
PauseMenu._show_page(a.substr(7))
|
||||
return
|
||||
var i := int(frame / 5)
|
||||
if i < spots.size() and frame % 5 == 1:
|
||||
var s = spots[i]
|
||||
p.global_position = s[0]
|
||||
p.rotation.y = deg_to_rad(s[1])
|
||||
p.head.rotation.x = deg_to_rad(s[2])
|
||||
@@ -0,0 +1 @@
|
||||
uid://dndxmq38i6wyg
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/test/hub_tour.gd" id="1"]
|
||||
|
||||
[node name="T" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,33 @@
|
||||
extends Node
|
||||
# Геймпад в меню паузы: фокус, A выбирает, крестовина двигает.
|
||||
func frames(n: int) -> void:
|
||||
for i in n:
|
||||
await get_tree().process_frame
|
||||
func btn(b: JoyButton, pressed: bool) -> void:
|
||||
var e := InputEventJoypadButton.new()
|
||||
e.button_index = b
|
||||
e.pressed = pressed
|
||||
Input.parse_input_event(e)
|
||||
func _ready() -> void:
|
||||
Net.nick_asked = true
|
||||
add_child((load("res://maps/hub/hub.tscn") as PackedScene).instantiate())
|
||||
await frames(5)
|
||||
PauseMenu.open_menu()
|
||||
await frames(5)
|
||||
var f := get_viewport().gui_get_focus_owner()
|
||||
print("фокус: ", f, " текст: ", f.text if f else "—")
|
||||
btn(JOY_BUTTON_DPAD_DOWN, true); await frames(2); btn(JOY_BUTTON_DPAD_DOWN, false); await frames(2)
|
||||
f = get_viewport().gui_get_focus_owner()
|
||||
print("после вниз: ", f.text if f else "—")
|
||||
btn(JOY_BUTTON_DPAD_UP, true); await frames(2); btn(JOY_BUTTON_DPAD_UP, false); await frames(2)
|
||||
btn(JOY_BUTTON_A, true); await frames(2); btn(JOY_BUTTON_A, false); await frames(3)
|
||||
print("после A на «Продолжить»: меню открыто = ", PauseMenu.open)
|
||||
PauseMenu.open_menu(); await frames(5)
|
||||
btn(JOY_BUTTON_DPAD_DOWN, true); await frames(2); btn(JOY_BUTTON_DPAD_DOWN, false); await frames(2)
|
||||
btn(JOY_BUTTON_DPAD_DOWN, true); await frames(2); btn(JOY_BUTTON_DPAD_DOWN, false); await frames(2)
|
||||
btn(JOY_BUTTON_DPAD_DOWN, true); await frames(2); btn(JOY_BUTTON_DPAD_DOWN, false); await frames(2)
|
||||
f = get_viewport().gui_get_focus_owner()
|
||||
print("фокус перед A: ", f.text if f else "—")
|
||||
btn(JOY_BUTTON_A, true); await frames(2); btn(JOY_BUTTON_A, false); await frames(3)
|
||||
print("страница после A: ", PauseMenu._page)
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cgonoxtgka01j
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/test/pad_menu_test.gd" id="1"]
|
||||
|
||||
[node name="T" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -1,6 +1,6 @@
|
||||
# Проверка карты Parkour «по существу» после прохода через портал с настоящими пакетами.
|
||||
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
|
||||
# и запусти: godot --headless --main-pack <site>/index.pck -- --client --name=T --pack-url=http://127.0.0.1:<порт>/ --tp=2:4.5,1,-8
|
||||
# и запусти: godot --headless --main-pack <site>/index.pck -- --client --name=T --pack-url=http://127.0.0.1:<порт>/ --tp=2:0,1,-15
|
||||
# Ожидание (после рейтинга: таймер=true итоги=true рейтинг_класс=true): скрипт=true, шейдерных_мешей>=1, небо_панорама=true, музыка_играет=true, лазеров_и_ворот=10.
|
||||
# Если в пакете не хватает файла, вместо этого будут false/0 (а имя сцены всё равно «Parkour»).
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
# Проверка меню паузы, настроек и геймпада без окна:
|
||||
# godot --headless --path . res://tools/test/pause_test.tscn -- --client --server-url=ws://127.0.0.1:1
|
||||
# Печатает строки OK/FAIL и «ИТОГО». Хаб загружается как сцена-потомок; последний шаг проверяет «Выйти в хаб».
|
||||
var level
|
||||
var p
|
||||
var fails := 0
|
||||
|
||||
func check(name: String, cond: bool) -> void:
|
||||
print(("OK " if cond else "FAIL ") + name)
|
||||
if not cond:
|
||||
fails += 1
|
||||
|
||||
func act(action: String, pressed := true) -> void:
|
||||
var e := InputEventAction.new()
|
||||
e.action = action
|
||||
e.pressed = pressed
|
||||
Input.parse_input_event(e)
|
||||
|
||||
func frames(n: int) -> void:
|
||||
for i in n:
|
||||
await get_tree().process_frame
|
||||
|
||||
func _ready() -> void:
|
||||
Net.nick_asked = true
|
||||
level = (load("res://maps/hub/hub.tscn") as PackedScene).instantiate()
|
||||
add_child(level)
|
||||
p = level.get_node("Player")
|
||||
await frames(5)
|
||||
await _run()
|
||||
print("ИТОГО: %s" % ("всё хорошо" if fails == 0 else "провалов %d" % fails))
|
||||
get_tree().quit(fails)
|
||||
|
||||
func _run() -> void:
|
||||
# --- раскладка геймпада в InputMap ---
|
||||
var want := {"jump": 2, "sprint": 2, "crouch": 1, "dash": 2, "zoom": 2, "ready": 1, "leaderboard": 1, "giveup": 1, "pause": 2,
|
||||
"move_forward": 2, "look_left": 1, "look_down": 1}
|
||||
for a in want:
|
||||
var pads := 0
|
||||
for e in InputMap.action_get_events(a):
|
||||
if e is InputEventJoypadButton or e is InputEventJoypadMotion:
|
||||
pads += 1
|
||||
check("геймпад: %s привязан (%d)" % [a, pads], pads >= 1 and (a != "jump" and a != "dash" or pads == 2))
|
||||
|
||||
# --- пауза ---
|
||||
check("меню закрыто в начале", not PauseMenu.open)
|
||||
act("pause")
|
||||
await frames(3)
|
||||
act("pause", false)
|
||||
check("Esc/Start открывает меню", PauseMenu.open)
|
||||
check("игрок заблокирован в меню", Net.input_blocked)
|
||||
check("в хабе кнопки «Выйти в хаб» нет", not PauseMenu._hub_btn.visible)
|
||||
act("pause")
|
||||
await frames(3)
|
||||
act("pause", false)
|
||||
check("повторный Esc закрывает меню", not PauseMenu.open)
|
||||
check("после закрытия игрок свободен", not Net.input_blocked)
|
||||
|
||||
# --- настройки: мышь, инверсия, FOV ---
|
||||
Settings.mouse_sens = 1.0
|
||||
Settings.invert_y = false
|
||||
p.rotation.y = 0.0
|
||||
p.head.rotation.x = 0.0
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
var m := Vector2(100, 50) # мышь в headless не «захвачена», поэтому зовём расчёт обзора напрямую
|
||||
p._mouse_look(m)
|
||||
var yaw1: float = absf(p.rotation.y)
|
||||
var pitch1: float = p.head.rotation.x
|
||||
Settings.mouse_sens = 2.0
|
||||
p.rotation.y = 0.0
|
||||
p.head.rotation.x = 0.0
|
||||
p._mouse_look(m)
|
||||
check("чувствительность ×2 удваивает поворот (%.3f -> %.3f)" % [yaw1, absf(p.rotation.y)], absf(absf(p.rotation.y) / yaw1 - 2.0) < 0.05)
|
||||
Settings.invert_y = true
|
||||
p.head.rotation.x = 0.0
|
||||
p._mouse_look(m)
|
||||
check("инверсия меняет знак наклона (%.3f и %.3f)" % [pitch1, p.head.rotation.x], pitch1 != 0.0 and signf(p.head.rotation.x) == -signf(pitch1))
|
||||
Settings.invert_y = false
|
||||
Settings.mouse_sens = 1.0
|
||||
Settings.fov = 90.0
|
||||
await frames(3)
|
||||
check("FOV из настроек применяется (%.0f)" % p.base_fov, is_equal_approx(p.base_fov, 90.0))
|
||||
Settings.fov = Settings.DEFAULT_FOV
|
||||
|
||||
# --- обзор правым стиком ---
|
||||
p.rotation.y = 0.0
|
||||
Input.action_press("look_right", 1.0)
|
||||
await frames(10)
|
||||
Input.action_release("look_right")
|
||||
check("правый стик поворачивает обзор (%.2f рад)" % p.rotation.y, p.rotation.y < -0.1)
|
||||
var before: float = p.rotation.y
|
||||
Net.input_blocked = true
|
||||
Input.action_press("look_right", 1.0)
|
||||
await frames(5)
|
||||
Input.action_release("look_right")
|
||||
Net.input_blocked = false
|
||||
check("в меню стик обзор не крутит", is_equal_approx(p.rotation.y, before))
|
||||
|
||||
# --- кнопки геймпада доходят до действий ---
|
||||
var b := InputEventJoypadButton.new()
|
||||
b.button_index = JOY_BUTTON_A
|
||||
b.pressed = true
|
||||
Input.parse_input_event(b)
|
||||
await frames(2)
|
||||
check("кнопка A = прыжок", Input.is_action_pressed("jump"))
|
||||
b.pressed = false
|
||||
Input.parse_input_event(b)
|
||||
var t := InputEventJoypadMotion.new()
|
||||
t.axis = JOY_AXIS_TRIGGER_RIGHT
|
||||
t.axis_value = 1.0
|
||||
Input.parse_input_event(t)
|
||||
await frames(2)
|
||||
check("правый курок = спринт", Input.is_action_pressed("sprint"))
|
||||
t.axis_value = 0.0
|
||||
Input.parse_input_event(t)
|
||||
|
||||
# --- сохранение настроек ---
|
||||
Settings.mouse_sens = 1.7
|
||||
Settings.save()
|
||||
Settings.mouse_sens = 1.0
|
||||
Settings.load_settings()
|
||||
check("настройки читаются из файла", is_equal_approx(Settings.mouse_sens, 1.7))
|
||||
Settings.reset_defaults()
|
||||
Settings.save()
|
||||
|
||||
# --- выход в хаб с другой карты ---
|
||||
level.reparent(get_tree().root) # хаб — текущая сцена, чтобы смена сцены не убила сам тест
|
||||
get_tree().current_scene = level
|
||||
Net.current_map = "race"
|
||||
act("pause")
|
||||
await frames(3)
|
||||
act("pause", false)
|
||||
check("не в хабе кнопка «Выйти в хаб» видна", PauseMenu._hub_btn.visible)
|
||||
PauseMenu._go_hub()
|
||||
await frames(10)
|
||||
check("«Выйти в хаб» закрыло меню", not PauseMenu.open and not Net.input_blocked)
|
||||
check("«Выйти в хаб» открыло хаб (%s)" % get_tree().current_scene, get_tree().current_scene != null and get_tree().current_scene.name == "Hub")
|
||||
check("карта сменилась на hub", Net.current_map == "hub")
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck5rnfgqt7ndb
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tools/test/pause_test.gd" id="1"]
|
||||
|
||||
[node name="T" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -1,7 +1,7 @@
|
||||
# Проверка карты Race 2 «по существу» после прохода через портал RACE 2 с настоящими пакетами (по образцу parkour_check.txt).
|
||||
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
|
||||
# Пакеты: tools/export_all.sh /tmp/site (НЕ в grid-world), затем (cd /tmp/site && python3 -m http.server 8777 --bind 127.0.0.1 &) и
|
||||
# godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:9.0,1,-7.5
|
||||
# godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:12,1,-15
|
||||
# (портал RACE 2 пятый в ряду: x = (4 - 2) * 4.5 = 9.0). Сервер в этом тесте не нужен.
|
||||
# Ожидание: скрипт=true, правила=true, шейдерных_мешей>=1, небо_панорама=true, ворот=13, лазеров=10, вентиляторов=6, полос=15 (см. race2_data.gd), музыка_в_файле=true, сдача=true.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Проверка карты Race «по существу» после прохода через портал RACE с настоящими пакетами (по образцу parkour_check.txt).
|
||||
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
|
||||
# Пакеты: tools/export_all.sh /tmp/site (НЕ в grid-world), затем (cd /tmp/site && python3 -m http.server 8777 --bind 127.0.0.1 &) и
|
||||
# godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:6.75,1,-7.5
|
||||
# godot --headless --main-pack /tmp/site/index.pck -- --client --name=T --pack-url=http://127.0.0.1:8777/ --tp=2:6,1,-15
|
||||
# (портал RACE четвёртый в ряду: x = (3 - 1.5) * 4.5 = 6.75). Сервер в этом тесте не нужен.
|
||||
# Ожидание: скрипт=true, правила=true, шейдерных_мешей>=1, небо_панорама=true, ворот=8, лазеров=2, музыка_в_файле=true, сдача=true.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user