Ники, общий рейтинг (топ-10 по картам), таблица Tab, сдача в гонке, таймер Parkour

- Ники: профиль 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
This commit is contained in:
2026-10-29 12:09:33 +05:00
co-authored by Claude Sonnet 5.5
parent d38152ea97
commit 666bebfb60
31 changed files with 2014 additions and 44 deletions
+105
View File
@@ -0,0 +1,105 @@
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