Ники, общий рейтинг (топ-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
+107
View File
@@ -0,0 +1,107 @@
class_name LbView
extends VBoxContainer
# Таблица рейтинга карты (топ-10 + «Вы: N-й»), данные берутся из Net.boards и обновляются сами по сигналу.
# Одна и та же таблица в панели Tab, в итогах гонки, итогах Parkour и будущей карты Metro: LbView.create("metro").
# Ники выводятся обычными Label (не RichText), поэтому «[b]» в чужом нике оформление не ломает.
const TEXT := Color(0.92, 0.88, 0.8)
const DIM := Color(0.62, 0.6, 0.56)
const MINE := Color(0.96, 0.76, 0.44)
const GOOD := Color(0.5, 0.85, 0.4)
var map_id := ""
var font_size := 18
var banner := false # сверху пометка «НОВЫЙ РЕКОРД КАРТЫ / ЛИЧНЫЙ РЕКОРД» (после submit_record)
var name_width := 190.0
static func create(id: String, size := 18, with_banner := false) -> LbView:
var v := LbView.new()
v.map_id = id
v.font_size = size
v.banner = with_banner
v.add_theme_constant_override("separation", 6)
return v
static func make_label(text: String, size: int, color: Color) -> Label:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", color)
l.add_theme_constant_override("outline_size", 3)
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.85))
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
return l
# Тёмная полупрозрачная подложка в мрачном стиле
static func panel_style(margin := 20) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.035, 0.04, 0.045, 0.86)
sb.border_color = Color(0.45, 0.30, 0.16, 0.9)
sb.set_border_width_all(2)
sb.set_content_margin_all(margin)
return sb
func _ready() -> void:
Net.leaderboard_updated.connect(_on_updated)
Net.record_result.connect(_on_record)
render()
func set_map(id: String) -> void:
map_id = id
render()
func _on_updated(id: String) -> void:
if id == map_id:
render()
func _on_record(id: String, _t: float, _rank: int, _pb: bool, _mr: bool) -> void:
if id == map_id:
render()
func render() -> void:
for c in get_children():
remove_child(c)
c.queue_free()
if banner:
var rec = Net.last_record.get(map_id)
if rec != null:
var txt := ""
if rec.map_record:
txt = "НОВЫЙ РЕКОРД КАРТЫ"
elif rec.personal:
txt = "НОВЫЙ ЛИЧНЫЙ РЕКОРД"
if txt != "":
var b := make_label(txt, font_size + 4, GOOD)
b.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(b)
var board = Net.boards.get(map_id)
if board == null:
add_child(make_label("—" if Net.connected else "Нет связи с сервером", font_size, DIM))
return
var top: Array = board.top
if top.is_empty():
add_child(make_label("Пока нет результатов", font_size, DIM))
var grid := GridContainer.new()
grid.columns = 3
grid.add_theme_constant_override("h_separation", 16)
grid.add_theme_constant_override("v_separation", 2)
add_child(grid)
for i in top.size():
var mine: bool = board.my_rank == i + 1
var col := MINE if mine else TEXT
var place := make_label("%d" % (i + 1), font_size, col if mine else DIM)
place.custom_minimum_size.x = font_size * 1.4
place.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
grid.add_child(place)
var nm := make_label(str(top[i][0]), font_size, col)
nm.custom_minimum_size.x = name_width
nm.clip_text = true
nm.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
grid.add_child(nm)
var tm := make_label(Net.fmt_time(float(top[i][1])), font_size, col)
tm.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
grid.add_child(tm)
if board.my_rank > top.size():
add_child(make_label("Вы: %d-й, %s" % [board.my_rank, Net.fmt_time(board.my_time)], font_size, MINE))
elif board.my_rank == 0 and not top.is_empty():
add_child(make_label("Вы: нет результата", font_size, DIM))