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 карты в рейтинге сервера 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