Ники, общий рейтинг (топ-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
+44 -1
View File
@@ -6,6 +6,8 @@ extends Node
# --rounds=<сколько кругов пройти> --quit-race-time=<выйти из игры на этой секунде гонки>
# --die-at-gate=<n>: после ворот n умереть (проверка возрождения лицом по трассе)
# --fall-at-gate=<n>: после ворот n упасть в пустоту (проверка срыва ниже ворот)
# --gu1=<план>, --gu2=<план>: сдача в круге 1 / 2. План: «cancel@1,giveup@2» — когда пройдено столько ворот,
# держать G 1,0 с и отпустить (отмена) / держать до сдачи (проверка race_giveup_test.sh)
# Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh.
const Data = preload("res://maps/race/race_data.gd")
@@ -36,6 +38,11 @@ var spectator_logged := false
var music_logged := false
var music_cd_logged := false
var freeze_logged := false
var gu_plan := {1: [], 2: []}
var gu_hold := -1.0
var gu_mode := ""
var gu_done := {}
var gu_after_cancel := false
func _ready() -> void:
for a in OS.get_cmdline_user_args():
@@ -46,6 +53,10 @@ func _ready() -> void:
if a.begins_with("--quit-race-time="): quit_race_time = float(a.substr(17))
if a.begins_with("--die-at-gate="): die_gate = int(a.substr(14))
if a.begins_with("--fall-at-gate="): fall_gate = int(a.substr(15))
if a.begins_with("--gu1=") or a.begins_with("--gu2="):
for e in a.substr(6).split(","):
var q := e.split("@")
gu_plan[int(a.substr(4, 1))].append({"mode": q[0], "gate": int(q[1])})
route = JSON.parse_string(FileAccess.get_file_as_string("res://tools/test/race_route.json")).safe
func gp(w: Array) -> Vector3:
@@ -117,7 +128,7 @@ func _physics_process(_delta: float) -> void:
key(k, false)
func _wait() -> void:
for k in ["move_forward", "sprint", "jump"]:
for k in ["move_forward", "sprint", "jump", "giveup"]:
key(k, false)
if rules.waiting_for_race():
if not spectator_logged:
@@ -143,12 +154,44 @@ func _race() -> void:
say("ВЫХОЖУ из игры в разгар гонки (время %.1f)" % rules.race_time())
get_tree().quit()
return
if rules.gave_up.has(rules.my_id):
for k in ["move_forward", "sprint", "jump", "giveup"]:
key(k, false)
if gu_hold >= 0.0: # только что сдались: фиксируем, где мы и заморожены ли
gu_hold = -1.0
var d := Vector2(p.global_position.x - Data.START_SLOTS[0].x, p.global_position.z - Data.START_SLOTS[0].z).length()
say("СДАЛСЯ_БОТ unix=%.3f на_старте=%s заморожен=%s" % [Time.get_unix_time_from_system(), d < 2.0, p.input_frozen])
return
if rules.finish.has(rules.my_id):
for k in ["move_forward", "sprint", "jump"]:
key(k, false)
return
if rules.race_time() < delay:
return
# --- сдача ---
var cur_round := rounds_done + 1
if gu_after_cancel:
gu_after_cancel = false
say("ПОСЛЕ_ОТМЕНЫ hold=%.2f сдался=%s" % [rules._giveup_hold, rules.gave_up.has(rules.my_id)])
if gu_hold < 0.0 and gu_plan.has(cur_round):
for e in gu_plan[cur_round]:
var gk := "%d%s%d" % [cur_round, e.mode, e.gate]
if not gu_done.has(gk) and rules.passed.get(rules.my_id, 0) == e.gate and not p.is_dead:
gu_done[gk] = true
gu_hold = 0.0
gu_mode = e.mode
say("УДЕРЖИВАЮ G (%s) после ворот %d" % [e.mode, e.gate])
if gu_hold >= 0.0:
for k in ["move_forward", "sprint", "jump"]:
key(k, false)
key("giveup", true)
gu_hold += get_physics_process_delta_time()
if gu_mode == "cancel" and gu_hold >= 1.0:
say("ОТМЕНА_УДЕРЖАНИЯ: отпускаю на %.2f с, шкала=%.2f сдался=%s" % [gu_hold, rules._giveup_hold, rules.gave_up.has(rules.my_id)])
key("giveup", false)
gu_hold = -1.0
gu_after_cancel = true
return
# --- проверки возрождения ---
var g: int = rules.passed.get(rules.my_id, 0)
if not special_done and die_gate >= 0 and g == die_gate and not p.is_dead: