Ники, общий рейтинг (топ-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
+99 -1
View File
@@ -12,7 +12,7 @@ const PORTALS := [
const ROOM_WIDTH := 28.0
const ROOM_DEPTH := 18.0
const ROOM_HEIGHT := 6.0
const ROOM_HEIGHT := 7.0 # +1 м: над порталами доски рейтинга, над ними — «CHOOSE A MAP»
const WALL := 0.5
const PORTAL_SPACING := 4.5
@@ -35,6 +35,9 @@ func _ready() -> void:
_build_room()
_build_portals()
_build_environment()
_build_boards()
if not Net.nick_asked:
Overlay.open_nick.call_deferred(true) # первый запуск: спрашиваем ник
func _build_room() -> void:
var body := StaticBody3D.new()
@@ -101,6 +104,101 @@ func _build_portals() -> void:
label.position = Vector3(0, ROOM_HEIGHT - 0.35, -ROOM_DEPTH * 0.5 + 0.1)
add_child(label)
# ---------- доски рейтинга над порталами карт «на время» ----------
const BOARD_REFRESH := 60.0 # раз в столько секунд обновляем доски
const BOARD_ROWS := 3
const BOARD_SIZE := Vector2(4.1, 1.45)
const BOARD_Y := 5.5 # центр доски; заголовок портала (Label3D) ниже, «CHOOSE A MAP» выше
var _boards := {} # map_id -> {"names": Label3D, "times": Label3D}
func _build_boards() -> void:
var z := -ROOM_DEPTH * 0.5 + 0.06
for i in PORTALS.size():
var info: Dictionary = PORTALS[i]
if not Net.TIMED_MAPS.has(info.map):
continue # песочница и Backrooms: рейтинга нет
var x := (i - (PORTALS.size() - 1) * 0.5) * PORTAL_SPACING
var border := MeshInstance3D.new()
var bq := QuadMesh.new()
bq.size = BOARD_SIZE + Vector2(0.1, 0.1)
bq.material = _unshaded(Color(0.30, 0.20, 0.11))
border.mesh = bq
border.position = Vector3(x, BOARD_Y, z)
add_child(border)
var back := MeshInstance3D.new()
var q := QuadMesh.new()
q.size = BOARD_SIZE
q.material = _unshaded(Color(0.025, 0.028, 0.032))
back.mesh = q
back.position = Vector3(x, BOARD_Y, z + 0.005)
add_child(back)
var title := _board_label(Net.MAP_TITLES.get(info.map, info.title).to_upper() + " · ТОП-3", 36, Color(0.96, 0.76, 0.44), HORIZONTAL_ALIGNMENT_CENTER)
title.position = Vector3(x, BOARD_Y + 0.52, z + 0.012)
add_child(title)
var names := _board_label("", 32, Color(0.92, 0.88, 0.8), HORIZONTAL_ALIGNMENT_LEFT)
names.position = Vector3(x - BOARD_SIZE.x * 0.5 + 0.12, BOARD_Y + 0.36, z + 0.012)
names.vertical_alignment = VERTICAL_ALIGNMENT_TOP
add_child(names)
var times := _board_label("", 32, Color(0.92, 0.88, 0.8), HORIZONTAL_ALIGNMENT_RIGHT)
times.position = Vector3(x + BOARD_SIZE.x * 0.5 - 0.12, BOARD_Y + 0.36, z + 0.012)
times.vertical_alignment = VERTICAL_ALIGNMENT_TOP
add_child(times)
_boards[info.map] = {"names": names, "times": times}
if _boards.is_empty():
return
Net.leaderboard_updated.connect(_on_board_updated)
var timer := Timer.new()
timer.wait_time = 2.0 # быстрая проверка: ждём подключения, потом запрашиваем раз в BOARD_REFRESH
timer.autostart = true
timer.timeout.connect(_request_boards)
add_child(timer)
for m in _boards:
_on_board_updated(m)
Net.request_board(m) # при входе в хаб — свежие данные
func _unshaded(c: Color) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.albedo_color = c
return m
func _board_label(text: String, size: int, color: Color, align: HorizontalAlignment) -> Label3D:
var l := Label3D.new()
l.text = text
l.font_size = size
l.pixel_size = 0.0056
l.modulate = color
l.outline_size = 6
l.outline_modulate = Color(0, 0, 0, 0.9)
l.horizontal_alignment = align
l.shaded = false
return l
func _request_boards() -> void:
for m in _boards:
Net.request_board(m, int(BOARD_REFRESH * 1000.0))
func _on_board_updated(map_id: String) -> void:
if not _boards.has(map_id):
return
var board = Net.boards.get(map_id)
var n_lines := []
var t_lines := []
for i in BOARD_ROWS:
if board != null and i < board.top.size():
var nick := str(board.top[i][0])
if nick.length() > 13:
nick = nick.substr(0, 12) + "…"
n_lines.append("%d %s" % [i + 1, nick])
t_lines.append(Net.fmt_time(float(board.top[i][1])))
else:
n_lines.append("%d —" % (i + 1))
t_lines.append("—")
_boards[map_id].names.text = "\n".join(n_lines)
_boards[map_id].times.text = "\n".join(t_lines)
func _build_environment() -> void:
var env := Environment.new()
env.background_mode = Environment.BG_COLOR