- Ники: профиль user://profile.cfg (client_id + ник), панель при первом запуске и по N; Net.nick_of(id); ники над призраками, в HUD/итогах гонки и таблицах - Рейтинг на сервере: лучшее время каждого игрока по картам race/parkour/metro, топ-10, user://leaderboard.json (атомарная запись, лимиты, проверка карты/комнаты/времени) - Таблица лидеров по Tab, доски топ-3 над порталами в хабе, итоги гонки с общим рейтингом - Гонка: сдача (G, удержание 1,5 с), RACE_GIVEUP; Parkour: таймер, финиш на вершине, R — перезапуск - Новые RPC (submit_profile, recv_nick, submit_record, recv_record_result, request_leaderboard, recv_leaderboard): клиент и сервер выкладываются вместе - Тесты: nicks_lb_test, race_giveup_test, parkour_run_test, nick_ui_test, ui_tour; README обновлён Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
106 lines
5.1 KiB
GDScript
106 lines
5.1 KiB
GDScript
extends Node
|
||
# Забег Parkour «по существу»: настоящая карта, игрок, сервер (запускает parkour_run_test.py).
|
||
# Старт-линия -> таймер идёт, смерть его не останавливает, финиш у вершины, слишком быстрое время сервер не принимает,
|
||
# R перезапускает забег, нормальное время (старт сдвинут на 45 с назад — иначе пришлось бы бежать ~минуту) попадает в рейтинг.
|
||
var ok := true
|
||
var level
|
||
var p
|
||
|
||
func check(cond: bool, text: String) -> void:
|
||
print((" ДА " if cond else " НЕТ ") + text)
|
||
if not cond:
|
||
ok = false
|
||
|
||
func wait(s: float) -> void:
|
||
await get_tree().create_timer(s).timeout
|
||
|
||
func _ready() -> void:
|
||
_run.call_deferred()
|
||
|
||
func press_r() -> void:
|
||
var ev := InputEventKey.new()
|
||
ev.physical_keycode = KEY_R
|
||
ev.keycode = KEY_R
|
||
ev.pressed = true
|
||
Input.parse_input_event(ev)
|
||
await get_tree().process_frame
|
||
ev = ev.duplicate()
|
||
ev.pressed = false
|
||
Input.parse_input_event(ev)
|
||
|
||
func _run() -> void:
|
||
level = (load("res://maps/parkour/parkour.tscn") as PackedScene).instantiate()
|
||
add_child(level)
|
||
await wait(0.5)
|
||
p = get_tree().get_first_node_in_group("local_player")
|
||
check(await _wait_ready(), "подключились к серверу, вошли в комнату parkour")
|
||
check(not level.run_active and not level.run_done, "до линии таймер не идёт")
|
||
await wait(1.0)
|
||
check(not level.run_active, "стоим на старте — таймер не идёт")
|
||
p.global_position = Vector3(0, 1.25, -11.0)
|
||
p.velocity = Vector3.ZERO
|
||
p.rotation.y = 0.0
|
||
await wait(0.3)
|
||
Input.action_press("move_forward")
|
||
Input.action_press("sprint")
|
||
var t0 := Time.get_ticks_msec()
|
||
while not level.run_active and Time.get_ticks_msec() - t0 < 4000:
|
||
await get_tree().physics_frame
|
||
Input.action_release("move_forward")
|
||
Input.action_release("sprint")
|
||
check(level.run_active and p.global_position.z < level.START_LINE_Z + 1.0, "пересекли линию z=-14 -> таймер пошёл (z=%.1f)" % p.global_position.z)
|
||
await wait(1.2)
|
||
var e1: float = level.run_elapsed()
|
||
check(e1 > 1.0 and level._timer_label.text != "00:00.000", "таймер считает: %s" % level._timer_label.text)
|
||
p.die()
|
||
await wait(3.2)
|
||
check(level.run_active and not p.is_dead and level.run_elapsed() > e1 + 2.5, "смерть и возрождение таймер не останавливают (%.1f с)" % level.run_elapsed())
|
||
# слишком быстрый финиш: таймер остановится, сервер время не примет
|
||
var D = level.Data
|
||
p.global_position = D.PORTAL + Vector3(0, 1.4, 3.0)
|
||
p.velocity = Vector3.ZERO
|
||
await wait(1.0)
|
||
check(level.run_done and not level.run_active and level._results_panel.visible, "у вершины финиш: таймер встал на %s, показаны итоги" % level._timer_label.text)
|
||
await wait(0.8)
|
||
check(not Net.last_record.has("parkour"), "время %.1f с короче допустимого сервер отверг (ответа нет)" % level.run_time)
|
||
var n_before: int = Net.boards["parkour"].top.size() if Net.boards.has("parkour") else 0
|
||
# R: заново
|
||
await press_r()
|
||
await wait(0.4)
|
||
check(not level.run_active and not level.run_done and level._timer_label.text == "00:00.000" and p.global_position.distance_to(D.START) < 1.0,
|
||
"R: телепорт на старт, таймер сброшен")
|
||
# второй забег: линия, затем «состарим» старт на 45 с и финишируем
|
||
p.global_position = Vector3(0, 1.25, -11.0)
|
||
p.velocity = Vector3.ZERO
|
||
await wait(0.3)
|
||
Input.action_press("move_forward")
|
||
Input.action_press("sprint")
|
||
t0 = Time.get_ticks_msec()
|
||
while not level.run_active and Time.get_ticks_msec() - t0 < 4000:
|
||
await get_tree().physics_frame
|
||
Input.action_release("move_forward")
|
||
Input.action_release("sprint")
|
||
check(level.run_active, "после R линия снова запускает таймер")
|
||
level.run_start_tick -= 45000
|
||
p.global_position = D.PORTAL + Vector3(0, 1.4, 3.0)
|
||
p.velocity = Vector3.ZERO
|
||
await wait(1.5)
|
||
var rec = Net.last_record.get("parkour")
|
||
check(rec != null and absf(rec.time - level.run_time) < 0.01 and rec.rank >= 1 and rec.personal, "сервер принял %.2f с: %s" % [level.run_time, str(rec)])
|
||
var b = Net.boards.get("parkour")
|
||
check(b != null and b.my_rank >= 1 and absf(b.my_time - level.run_time) < 0.01, "запись в рейтинге Parkour: место %s, время %.2f" % [str(b.my_rank) if b else "?", b.my_time if b else 0.0])
|
||
check(level._results_panel.visible, "панель итогов с рейтингом показана")
|
||
await wait(8.5)
|
||
check(not level._results_panel.visible, "итоги скрылись через ~8 с, дальше свободная игра")
|
||
print("\nПАРКУР ПРОЙДЕН" if ok else "\nПАРКУР ПРОВАЛЕН")
|
||
get_tree().quit(0 if ok else 1)
|
||
|
||
func _wait_ready() -> bool:
|
||
var t := 0.0
|
||
while not (Net.connected and Net.map_ready):
|
||
await wait(0.1)
|
||
t += 0.1
|
||
if t > 8.0:
|
||
return false
|
||
return true
|