Files
my-world/maps/race/race_rules.gd
T
aibrashandClaude Sonnet 5.5 666bebfb60 Ники, общий рейтинг (топ-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
2026-10-29 12:09:33 +05:00

733 lines
28 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
# Правила режима «Гонка»: ожидание -> отсчёт -> гонка -> итоги -> снова ожидание, HUD и обмен событиями.
# Сервер про гонку ничего не знает, он только пересылает события (Net.RACE_*, submit_race/recv_race) внутри комнаты карты.
# Каждый клиент ведёт собственную копию состояния и приходит к одному результату, потому что:
# - старт даёт единственный отправитель (наименьший id в комнате) и все считают время от одного момента;
# - ворота засчитывает сам игрок и сообщает всем (CP/FINISH), порядок мест вычисляется одинаково у всех;
# - раз в секунду каждый шлёт «маяк» (RACE_STATE) — по нему догоняют потерявшие событие и пришедшие позже.
# Биты маяка (a): 0..7 — пройдено ворот; 8 — готов; 9..10 — фаза отправителя (Phase); 11 — сдался.
# Сдача (клавиша G, удержание): игрока возвращает на стартовую площадку, время не пишется ни в итоги, ни в рейтинг;
# сдавшиеся считаются завершившими для конца круга. Рейтинг: финиш -> Net.submit_my_record(rating_id, время).
const Data = preload("res://maps/race/race_data.gd")
enum Phase { WAIT, COUNTDOWN, RACE, RESULTS }
const COUNTDOWN_TIME := 6.0 # от START до старта: секунда на «приготовьтесь» и 5…1
const CLOCK_TOLERANCE := 0.4 # на сколько unix-время старта может разойтись с ожидаемым, прежде чем считаем часы чужими
const MAX_SLOTS := 8
const EXTRA_ROW := 3.0 # запасные места: ряды позади стартовых слотов (на столько метров назад)
const AFTER_FIRST_FINISH := 45.0 # гонка кончается через столько секунд после первого финиша
const RESULTS_TIME := 8.0
const BEACON_PERIOD := 1.0
const GIVEUP_HOLD := 1.5 # сколько секунд держать G, чтобы сдаться
const GATE_HEIGHT := 5.0 # диапазон высот, в котором пересечение плоскости ворот засчитывается
const GATE_MARGIN := 1.0 # запас по ширине сверх полуширины ворот
const TELEPORT_JUMP := 4.0 # смещение за кадр больше этого — телепорт (возрождение), не пересечение ворот
var map: Node3D # race.gd: gate_pos/gate_yaw/gate_half/set_gate_progress
var player: Node3D
var phase := Phase.WAIT
var my_id := 1
var flags_ready := {} # id -> готов (только в ожидании)
var passed := {} # id -> пройдено ворот (участники круга)
var finish := {} # id -> время финиша, с
var peer_phase := {} # id -> фаза по последнему маяку
var known := {} # id -> true: этого игрока видели в комнате
var participants: Array = [] # отсортированные id участников текущего круга
var start_tick := 0 # Time.get_ticks_msec() момента старта
var start_sender := 0
var results: Array = [] # итоги круга: {id, place, time, gates}
var results_end_tick := 0
var splits: Array = [] # свои сплиты круга
var best_time := -1.0
var best_splits: Array = []
var gave_up := {} # id -> true: сдался в этом круге
var rating_id := "race" # id карты в рейтинге сервера (Metro: map.rating_id = "metro")
var debug := false # --race-log: факты в консоль (тесты)
var _beacon_timer := 0.0
var _prev_pos := Vector3.ZERO
var _have_prev := false
var _go_until := 0
var _split_until := 0
var _new_record := false
var _music_on := false
var _giveup_hold := 0.0
# HUD
var _timer_label: Label
var _best_label: Label
var _place_label: Label
var _gates_label: Label
var _center_label: Label
var _split_label: Label
var _hint_label: Label
var _results_panel: PanelContainer
var _giveup_label: Label
var _giveup_bar: ProgressBar
func setup(m: Node3D) -> void:
map = m
player = m.player
if m.get("rating_id") != null:
rating_id = m.rating_id
func _ready() -> void:
debug = OS.get_cmdline_user_args().has("--race-log")
my_id = _current_id()
known[my_id] = true
_load_best()
_build_hud()
Net.race_event.connect(_on_race_event)
map.set_gate_progress(0)
_log("карта готова, id=%d, ворот=%d" % [my_id, Data.CHECKPOINTS.size()])
func _log(text: String) -> void:
if debug:
print("[RACE %d] %s" % [my_id, text])
# ---------- участники ----------
func _current_id() -> int:
return multiplayer.get_unique_id() if Net.connected else 1
# Все игроки комнаты, включая нас (чужие — по призракам Net.ghosts)
func _room_ids() -> Array:
var ids: Array = [my_id]
for id in Net.ghosts.keys():
if is_instance_valid(Net.ghosts[id]):
ids.append(id)
ids.sort()
return ids
func _refresh_room() -> void:
var now := _current_id()
if now != my_id: # подключились к серверу: у нас другой id — начинаем с чистого листа
my_id = now
_reset_all()
var ids := _room_ids()
for id in ids:
known[id] = true
for id in known.keys():
if not ids.has(id):
_forget(id)
func _reset_all() -> void:
flags_ready.clear()
passed.clear()
finish.clear()
gave_up.clear()
peer_phase.clear()
known.clear()
known[my_id] = true
participants.clear()
_set_phase(Phase.WAIT)
player.input_frozen = false
map.set_gate_progress(0)
# Игрок вышел: убираем из расчётов
func _forget(id: int) -> void:
known.erase(id)
flags_ready.erase(id)
passed.erase(id)
finish.erase(id)
gave_up.erase(id)
peer_phase.erase(id)
participants.erase(id)
_log("игрок %d вышел" % id)
func _name_of(id: int) -> String:
return Net.nick_of(id) # как над призраком (net/ghost.gd)
func _color_of(id: int) -> Color:
return Color.from_hsv(fmod(id * 0.618034, 1.0), 0.7, 1.0) # как у призрака
func _pos_of(id: int) -> Vector3:
if id == my_id:
return player.global_position
var g = Net.ghosts.get(id)
if g != null and is_instance_valid(g):
return g.position
return Vector3(1e6, 1e6, 1e6)
# ---------- сеть ----------
func _send(kind: int, a: int, b: float) -> void:
Net.send_race(kind, a, b)
func _beacon_bits() -> int:
var bits := mini(passed.get(my_id, 0), 255)
if flags_ready.get(my_id, false):
bits |= 1 << 8
bits |= int(phase) << 9
if gave_up.has(my_id):
bits |= 1 << 11
return bits
func _send_beacon() -> void:
_beacon_timer = 0.0
_send(Net.RACE_STATE, _beacon_bits(), finish.get(my_id, -1.0))
func _set_phase(p: Phase) -> void:
if p != phase:
phase = p
_log("фаза -> %s" % Phase.keys()[p])
_send_beacon()
func _on_race_event(id: int, kind: int, a: int, b: float) -> void:
if id == my_id:
return
match kind:
Net.RACE_READY:
if phase == Phase.WAIT:
flags_ready[id] = a != 0
Net.RACE_START:
_on_start(id, b)
Net.RACE_CP:
if participants.has(id):
passed[id] = maxi(passed.get(id, 0), a + 1)
Net.RACE_FINISH:
if participants.has(id):
finish[id] = b
passed[id] = Data.CHECKPOINTS.size()
Net.RACE_GIVEUP:
if participants.has(id) and not finish.has(id):
gave_up[id] = true
_log("игрок %d сдался" % id)
Net.RACE_STATE:
_on_beacon(id, a, b)
func _on_beacon(id: int, a: int, b: float) -> void:
var ph := (a >> 9) & 3
peer_phase[id] = ph
if phase == Phase.WAIT and ph == Phase.WAIT:
flags_ready[id] = ((a >> 8) & 1) == 1
if participants.has(id) and (phase == Phase.RACE or phase == Phase.RESULTS) and (ph == Phase.RACE or ph == Phase.RESULTS):
passed[id] = maxi(passed.get(id, 0), a & 255)
if b >= 0.0 and not finish.has(id):
finish[id] = b
if ((a >> 11) & 1) == 1 and not finish.has(id):
gave_up[id] = true
# Идёт чужой круг, в который мы не попали (зашли позже): бегаем свободно и ждём следующего
func waiting_for_race() -> bool:
if phase != Phase.WAIT:
return false
for id in peer_phase:
if known.has(id) and (peer_phase[id] == Phase.COUNTDOWN or peer_phase[id] == Phase.RACE):
return true
return false
# ---------- ожидание: готовность ----------
func _unhandled_input(event: InputEvent) -> void:
if Net.input_blocked:
return
if event.is_action_pressed("ready") and not event.is_echo():
toggle_ready()
func toggle_ready() -> void:
if phase != Phase.WAIT or waiting_for_race():
return
flags_ready[my_id] = not flags_ready.get(my_id, false)
_log("готов=%s" % flags_ready[my_id])
_send(Net.RACE_READY, 1 if flags_ready[my_id] else 0, 0.0)
# Все ли в комнате готовы; старт даёт наименьший id (если двое стали «последними готовыми», старт всё равно один)
func _check_all_ready() -> void:
if phase != Phase.WAIT or waiting_for_race() or not flags_ready.get(my_id, false):
return
var ids := _room_ids()
if ids[0] != my_id:
return
for id in ids:
if not flags_ready.get(id, false):
return
var t := Time.get_unix_time_from_system() + COUNTDOWN_TIME
_log("все готовы (%d), даю старт" % ids.size())
_send(Net.RACE_START, 0, t)
_on_start(my_id, t)
# ---------- отсчёт и старт ----------
func _slot_pos(i: int) -> Vector3:
var base: Vector3 = Data.START_SLOTS[i % MAX_SLOTS]
return base + Vector3(0, 0, EXTRA_ROW * (i / MAX_SLOTS)) # лишние — рядами позади
func _on_start(sender: int, unix_start: float) -> void:
if phase == Phase.RACE or (phase == Phase.COUNTDOWN and sender >= start_sender):
return # второй START (двое решили одновременно): остаётся от наименьшего id
start_sender = sender
var remaining := COUNTDOWN_TIME
if sender != my_id:
remaining = unix_start - Time.get_unix_time_from_system()
if absf(remaining - COUNTDOWN_TIME) > CLOCK_TOLERANCE: # часы отправителя расходятся с нашими: считаем от получения
_log("часы расходятся на %.2f с, считаю от получения" % (remaining - COUNTDOWN_TIME))
remaining = COUNTDOWN_TIME - 0.1
start_tick = Time.get_ticks_msec() + int(remaining * 1000.0)
flags_ready.clear()
participants = _room_ids()
passed.clear()
finish.clear()
gave_up.clear()
splits.clear()
_new_record = false
for id in participants:
passed[id] = 0
var slot := participants.find(my_id)
player.place(Transform3D(Basis.IDENTITY, _slot_pos(slot)))
player.input_frozen = true
_have_prev = false
map.set_gate_progress(0)
_set_phase(Phase.COUNTDOWN)
_log("старт от %d: слот %d из %d, через %.2f с, участники %s" % [sender, slot, participants.size(), remaining, participants])
func _begin_race() -> void:
player.input_frozen = false
_go_until = Time.get_ticks_msec() + 1200
_set_phase(Phase.RACE)
func race_time() -> float:
return (Time.get_ticks_msec() - start_tick) / 1000.0
# ---------- гонка ----------
func _physics_process(_delta: float) -> void:
if phase != Phase.RACE or not participants.has(my_id) or finish.has(my_id) or gave_up.has(my_id) or player.is_dead:
_have_prev = false
return
var cur: Vector3 = player.global_position
if _have_prev:
var idx: int = passed.get(my_id, 0)
if (cur - _prev_pos).length() < TELEPORT_JUMP and _crossed(idx, _prev_pos, cur):
_pass_gate(idx)
_prev_pos = cur
_have_prev = true
# Отрезок prev->cur пересёк плоскость ворот i по ходу трассы в пределах ворот?
func _crossed(i: int, prev: Vector3, cur: Vector3) -> bool:
var p: Vector3 = map.gate_pos(i)
var yaw: float = map.gate_yaw(i)
var fwd := Vector3(-sin(yaw), 0, -cos(yaw)) # rotation.y = yaw поворачивает -Z именно сюда
var side := Vector3(cos(yaw), 0, -sin(yaw))
var d0 := (prev - p).dot(fwd)
var d1 := (cur - p).dot(fwd)
if not (d0 < 0.0 and d1 >= 0.0):
return false
var q := prev.lerp(cur, d0 / (d0 - d1))
if absf((q - p).dot(side)) > float(map.gate_half(i)) + GATE_MARGIN:
return false
var dy := q.y - p.y
return dy > -3.0 and dy < GATE_HEIGHT + 4.0
func _pass_gate(idx: int) -> void:
var t := race_time()
var n: int = Data.CHECKPOINTS.size()
passed[my_id] = idx + 1
splits.append(t)
map.set_gate_progress(idx + 1)
# возрождаемся на последних пройденных воротах, лицом по трассе
player.spawn_transform = Transform3D(Basis(Vector3.UP, map.gate_yaw(idx)), map.gate_pos(idx))
var diff := ""
if best_splits.size() > idx:
var d: float = t - best_splits[idx]
diff = " %s%.2f" % ["+" if d >= 0.0 else "-", absf(d)]
_split_label.add_theme_color_override("font_color", Color(0.9, 0.45, 0.35) if d >= 0.0 else Color(0.5, 0.85, 0.4))
else:
_split_label.add_theme_color_override("font_color", Color(0.92, 0.88, 0.8))
if idx == n - 1:
finish[my_id] = t
_send(Net.RACE_FINISH, 0, t)
Net.submit_my_record(rating_id, t) # источник истины для рейтинга — сервер
_new_record = best_time < 0.0 or t < best_time
if _new_record:
best_time = t
best_splits = splits.duplicate()
_save_best()
_split_label.text = "ФИНИШ %s%s%s" % [_fmt(t), diff, " НОВЫЙ РЕКОРД" if _new_record else ""]
_log("ФИНИШ %.3f (место %d)" % [t, _my_place()])
else:
_split_label.text = "ВОРОТА %d %s%s" % [idx + 1, _fmt(t), diff]
_send(Net.RACE_CP, idx, t)
_log("ворота %d: %.3f" % [idx + 1, t])
_split_until = Time.get_ticks_msec() + 3500
_send_beacon()
# ---------- места ----------
func _dist_next(id: int) -> float:
var g: int = passed.get(id, 0)
if g >= Data.CHECKPOINTS.size():
return 0.0
return _pos_of(id).distance_to(map.gate_pos(g))
# Финишировавшие по времени, затем остальные: больше ворот, ближе к следующим
func ranking() -> Array:
var ids: Array = participants.duplicate()
ids.sort_custom(func(a: int, b: int) -> bool:
var fa := finish.has(a)
var fb := finish.has(b)
if fa and fb:
return finish[a] < finish[b] or (finish[a] == finish[b] and a < b)
if fa != fb:
return fa
var ga := gave_up.has(a)
var gb := gave_up.has(b)
if ga != gb:
return gb # сдавшиеся — ниже остальных
var pa: int = passed.get(a, 0)
var pb: int = passed.get(b, 0)
if pa != pb:
return pa > pb
var da := _dist_next(a)
var db := _dist_next(b)
if not is_equal_approx(da, db):
return da < db
return a < b)
return ids
func _my_place() -> int:
return ranking().find(my_id) + 1
func _check_race_end() -> void:
if participants.is_empty():
return
var first := -1.0 # время первого финиша
var all_done := true
for id in participants:
if finish.has(id):
if first < 0.0 or finish[id] < first:
first = finish[id]
elif not gave_up.has(id):
all_done = false
if all_done or (first >= 0.0 and race_time() >= first + AFTER_FIRST_FINISH):
_end_race()
func _end_race() -> void:
Net.request_board(rating_id) # к показу итогов — свежий рейтинг (другие могли финишировать позже нас)
results.clear()
var rank := ranking()
for i in rank.size():
var id: int = rank[i]
results.append({"id": id, "place": i + 1, "time": finish.get(id, -1.0), "gates": passed.get(id, 0), "gaveup": gave_up.has(id)})
results_end_tick = Time.get_ticks_msec() + int(RESULTS_TIME * 1000.0)
_set_phase(Phase.RESULTS)
_show_results()
for r in results:
_log("итог: место %d игрок %d время %.3f ворот %d сдался=%s" % [r.place, r.id, r.time, r.gates, r.gaveup])
func _back_to_wait() -> void:
participants.clear()
passed.clear()
finish.clear()
gave_up.clear()
flags_ready.clear()
_results_panel.visible = false
player.input_frozen = false
player.spawn_transform = Transform3D(Basis.IDENTITY, Data.START_SLOTS[0])
map.set_gate_progress(0)
_have_prev = false
_set_phase(Phase.WAIT)
# Сдача: удерживаем G, пока не наполнится полоска; отпустил раньше — отмена
func _update_giveup(delta: float) -> void:
var can := phase == Phase.RACE and participants.has(my_id) and not finish.has(my_id) and not gave_up.has(my_id) and not Net.input_blocked
if can and Input.is_action_pressed("giveup"):
_giveup_hold += delta
if _giveup_hold >= GIVEUP_HOLD:
_giveup_hold = 0.0
_give_up()
else:
_giveup_hold = 0.0
func _give_up() -> void:
gave_up[my_id] = true
_send(Net.RACE_GIVEUP, 0, race_time())
player.place(Transform3D(Basis.IDENTITY, Data.START_SLOTS[0])) # назад на стартовую площадку, дальше бегаем свободно
map.set_gate_progress(0)
_have_prev = false
_split_label.text = "ВЫ СДАЛИСЬ"
_split_label.add_theme_color_override("font_color", DIM)
_split_until = Time.get_ticks_msec() + 3500
_send_beacon()
_log("СДАЛСЯ (время гонки %.2f)" % race_time())
func _process(delta: float) -> void:
_refresh_room()
_update_giveup(delta)
_beacon_timer += delta
if _beacon_timer >= BEACON_PERIOD:
_send_beacon()
var now := Time.get_ticks_msec()
match phase:
Phase.WAIT:
_check_all_ready()
Phase.COUNTDOWN:
if now >= start_tick:
_begin_race()
Phase.RACE:
_check_race_end()
Phase.RESULTS:
if now >= results_end_tick:
_back_to_wait()
_update_hud()
# музыка: от «СТАРТ!» до возврата в ожидание; зашедший в разгар чужого круга тоже слышит её
var want := phase == Phase.RACE or phase == Phase.RESULTS
if phase == Phase.WAIT:
for id in peer_phase:
if known.has(id) and peer_phase[id] == Phase.RACE:
want = true
if want != _music_on:
_music_on = want
map.set_music(want)
# ---------- личный рекорд ----------
func _best_path() -> String:
for arg in OS.get_cmdline_user_args():
if arg.begins_with("--race-best="): # для тестов: не трогаем настоящий рекорд
return arg.substr("--race-best=".length())
return "user://race_best.cfg"
func _load_best() -> void:
var cf := ConfigFile.new()
if cf.load(_best_path()) == OK:
best_time = cf.get_value("race", "best", -1.0)
best_splits = cf.get_value("race", "splits", [])
func _save_best() -> void:
var cf := ConfigFile.new()
cf.set_value("race", "best", best_time)
cf.set_value("race", "splits", best_splits)
cf.save(_best_path())
# ---------- HUD ----------
const TEXT := Color(0.92, 0.88, 0.8)
const DIM := Color(0.66, 0.64, 0.6)
func _fmt(t: float) -> String:
t = maxf(t, 0.0)
return "%d:%05.2f" % [int(t / 60.0), fmod(t, 60.0)]
func _label(size: int, color: Color, parent: Control) -> Label:
var l := Label.new()
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", color)
l.add_theme_constant_override("outline_size", maxi(size / 5, 4))
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(l)
return l
func _build_hud() -> void:
var layer := CanvasLayer.new()
layer.layer = 12
add_child(layer)
var root := Control.new()
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
layer.add_child(root)
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_timer_label = _label(40, TEXT, root)
_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_best_label = _label(20, DIM, root)
_best_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_place_label = _label(30, TEXT, root)
_gates_label = _label(22, DIM, root)
_center_label = _label(160, TEXT, root)
_center_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_center_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_split_label = _label(28, TEXT, root)
_split_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hint_label = _label(24, TEXT, root)
_hint_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_timer_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP, Control.PRESET_MODE_MINSIZE, 0)
for l in [_timer_label, _best_label, _split_label, _hint_label]:
l.anchor_left = 0.0
l.anchor_right = 1.0
l.offset_left = 0.0
l.offset_right = 0.0
_timer_label.anchor_top = 0.0
_timer_label.offset_top = 14.0
_best_label.anchor_top = 0.0
_best_label.offset_top = 64.0
_split_label.anchor_top = 0.0
_split_label.offset_top = 100.0
_hint_label.anchor_top = 1.0
_hint_label.anchor_bottom = 1.0
_hint_label.offset_top = -80.0
_hint_label.offset_bottom = -40.0
_place_label.position = Vector2(64, 18)
_gates_label.position = Vector2(66, 58)
_center_label.anchor_left = 0.0
_center_label.anchor_right = 1.0
_center_label.anchor_top = 0.2
_center_label.anchor_bottom = 0.5
_center_label.offset_left = 0.0
_center_label.offset_right = 0.0
# индикатор сдачи: внизу по центру, полоска наполняется за GIVEUP_HOLD секунд
_giveup_label = _label(18, DIM, root)
_giveup_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_giveup_label.anchor_left = 0.0
_giveup_label.anchor_right = 1.0
_giveup_label.anchor_top = 1.0
_giveup_label.anchor_bottom = 1.0
_giveup_label.offset_top = -112.0
_giveup_label.offset_bottom = -84.0
_giveup_bar = ProgressBar.new()
_giveup_bar.show_percentage = false
_giveup_bar.min_value = 0.0
_giveup_bar.max_value = GIVEUP_HOLD
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.03, 0.03, 0.035, 0.85)
var fg := StyleBoxFlat.new()
fg.bg_color = Color(0.78, 0.30, 0.20)
_giveup_bar.add_theme_stylebox_override("background", bg)
_giveup_bar.add_theme_stylebox_override("fill", fg)
_giveup_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
root.add_child(_giveup_bar)
_giveup_bar.anchor_left = 0.5
_giveup_bar.anchor_right = 0.5
_giveup_bar.anchor_top = 1.0
_giveup_bar.anchor_bottom = 1.0
_giveup_bar.offset_left = -110.0
_giveup_bar.offset_right = 110.0
_giveup_bar.offset_top = -80.0
_giveup_bar.offset_bottom = -72.0
_giveup_bar.visible = false
_results_panel = PanelContainer.new()
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.04, 0.045, 0.05, 0.84)
sb.border_color = Color(0.45, 0.30, 0.16, 0.9)
sb.set_border_width_all(2)
sb.set_content_margin_all(26)
_results_panel.add_theme_stylebox_override("panel", sb)
_results_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
_results_panel.visible = false
root.add_child(_results_panel)
_results_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
_results_panel.grow_horizontal = Control.GROW_DIRECTION_BOTH
_results_panel.grow_vertical = Control.GROW_DIRECTION_BOTH
func _show_results() -> void:
for c in _results_panel.get_children():
c.queue_free()
var cols := HBoxContainer.new() # слева итоги круга, справа общий рейтинг сервера
cols.add_theme_constant_override("separation", 44)
_results_panel.add_child(cols)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 10)
cols.add_child(box)
var title := _label(36, TEXT, box)
title.text = "ИТОГИ"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var grid := GridContainer.new()
grid.columns = 3
grid.add_theme_constant_override("h_separation", 34)
grid.add_theme_constant_override("v_separation", 6)
box.add_child(grid)
for r in results:
var c := _color_of(r.id)
var mine: bool = r.id == my_id
var place := _label(26, c if mine else TEXT, grid)
place.text = "%d" % r.place
var nm := _label(26, c, grid)
nm.text = _name_of(r.id) + (" (вы)" if mine else "")
var tm := _label(26, TEXT if r.time >= 0.0 else DIM, grid)
if r.gaveup:
tm.text = "сдался"
else:
tm.text = _fmt(r.time) if r.time >= 0.0 else "не финишировал (%d/%d)" % [r.gates, Data.CHECKPOINTS.size()]
if best_time >= 0.0:
var rec := _label(20, DIM, box)
rec.text = "Личный рекорд: %s%s" % [_fmt(best_time), " (новый!)" if _new_record else ""]
rec.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var side := VBoxContainer.new()
side.add_theme_constant_override("separation", 8)
cols.add_child(side)
var st := _label(26, TEXT, side)
st.text = "ОБЩИЙ РЕЙТИНГ"
st.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
side.add_child(LbView.create(rating_id, 18, true))
_results_panel.visible = true
func _update_hud() -> void:
var now := Time.get_ticks_msec()
var racing := participants.has(my_id) and (phase == Phase.RACE or phase == Phase.RESULTS)
_timer_label.visible = racing
_place_label.visible = racing
_gates_label.visible = racing
_best_label.visible = racing or phase == Phase.COUNTDOWN
_best_label.text = "РЕКОРД %s" % (_fmt(best_time) if best_time >= 0.0 else "—")
_split_label.visible = now < _split_until and racing
if racing:
var n: int = Data.CHECKPOINTS.size()
if gave_up.has(my_id):
_timer_label.text = "СДАЛСЯ"
_timer_label.add_theme_color_override("font_color", DIM)
elif finish.has(my_id):
_timer_label.text = _fmt(finish[my_id])
_timer_label.add_theme_color_override("font_color", Color(0.5, 0.85, 0.4))
else:
_timer_label.text = _fmt(race_time())
_timer_label.add_theme_color_override("font_color", TEXT)
_place_label.text = "МЕСТО %d/%d" % [_my_place(), participants.size() - gave_up.size()]
_place_label.visible = not gave_up.has(my_id)
_gates_label.visible = not gave_up.has(my_id)
_gates_label.text = "ВОРОТА %d/%d" % [passed.get(my_id, 0), n]
# центр: отсчёт, «СТАРТ!»
_center_label.visible = false
if phase == Phase.COUNTDOWN:
var rem := (start_tick - now) / 1000.0
_center_label.visible = true
if rem > COUNTDOWN_TIME - 1.0:
_center_label.add_theme_font_size_override("font_size", 56)
_center_label.text = "ПРИГОТОВЬТЕСЬ"
else:
_center_label.add_theme_font_size_override("font_size", 170)
_center_label.text = "%d" % maxi(ceili(rem), 1)
elif phase == Phase.RACE and now < _go_until:
_center_label.visible = true
_center_label.add_theme_font_size_override("font_size", 110)
_center_label.text = "СТАРТ!"
# сдача: подсказка первые секунды гонки, при удержании G — «СДАЮСЬ…» и полоска
var can_give := phase == Phase.RACE and participants.has(my_id) and not finish.has(my_id) and not gave_up.has(my_id)
_giveup_bar.visible = can_give and _giveup_hold > 0.0
_giveup_bar.value = _giveup_hold
_giveup_label.visible = can_give and (_giveup_hold > 0.0 or race_time() < 12.0)
if _giveup_hold > 0.0:
_giveup_label.text = "СДАЮСЬ…"
_giveup_label.add_theme_font_size_override("font_size", 24)
_giveup_label.add_theme_color_override("font_color", TEXT)
else:
_giveup_label.text = "G (удерживать) — сдаться"
_giveup_label.add_theme_font_size_override("font_size", 16)
_giveup_label.add_theme_color_override("font_color", DIM)
# подсказка внизу
_hint_label.visible = true
if phase == Phase.WAIT:
if waiting_for_race():
_hint_label.text = "ГОНКА ИДЁТ — дождитесь следующего круга"
else:
var ids := _room_ids()
var n_ready := 0
for id in ids:
if flags_ready.get(id, false):
n_ready += 1
if flags_ready.get(my_id, false):
_hint_label.text = "ГОТОВ (%d/%d) · R — отменить" % [n_ready, ids.size()]
else:
_hint_label.text = "R — готов (%d/%d)" % [n_ready, ids.size()]
elif phase == Phase.RACE and not participants.has(my_id):
_hint_label.text = "ГОНКА ИДЁТ"
else:
_hint_label.visible = false