Ники, общий рейтинг (топ-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:
+99
-1
@@ -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
|
||||
|
||||
@@ -9,10 +9,30 @@ const KILL_DEPTH := 32.0 # Ниже последнего чекпоинт
|
||||
const GATE_HEIGHT := 5.0
|
||||
const GATE_HALF_WIDTH := 3.0
|
||||
|
||||
# Забег на время. Старт — пересечение линии на конце стартовой платформы (Blender y = 14 -> Godot z = -14, см. layout.py:
|
||||
# slab('start', -12, 12, -14, 14, ...)). Финиш — вершина: площадка summit (18x18 м) перед порталом; её центр в Godot —
|
||||
# (PORTAL.x, верх, PORTAL.z + 3), потому что PORTAL = (SUM.x, SUM.y + 3) в Blender. Смерть таймер не останавливает.
|
||||
const START_LINE_Z := -14.0
|
||||
const FINISH_SIZE := Vector3(14.0, 5.0, 14.0)
|
||||
const RESULTS_TIME := 8.0
|
||||
const RATING_ID := "parkour"
|
||||
const TEXT := Color(0.92, 0.88, 0.8)
|
||||
const DIM := Color(0.62, 0.6, 0.56)
|
||||
const GOOD := Color(0.5, 0.85, 0.4)
|
||||
|
||||
var player: Node3D
|
||||
var _lasers: Array = [] # {area, mat, mesh, mode, period, on, phase}
|
||||
var _hud_label: Label
|
||||
var _hud_timer := 0.0
|
||||
var run_active := false # таймер идёт
|
||||
var run_done := false # финишировали, ждём R
|
||||
var run_start_tick := 0
|
||||
var run_time := 0.0 # время финиша
|
||||
var _prev_z := 0.0
|
||||
var _timer_label: Label
|
||||
var _results_panel: PanelContainer
|
||||
var _results_end := 0
|
||||
var _cp_items: Array = [] # {mat, sheet, label} — чтобы вернуть ворота в исходный цвет при перезапуске
|
||||
|
||||
func _ready() -> void:
|
||||
Net.enter_map("parkour")
|
||||
@@ -31,6 +51,8 @@ func _ready() -> void:
|
||||
_add_ambience()
|
||||
_add_music()
|
||||
_add_exit()
|
||||
_add_finish()
|
||||
_prev_z = player.global_position.z
|
||||
|
||||
# Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин
|
||||
const SURFACES := {
|
||||
@@ -94,6 +116,7 @@ func _physics_process(delta: float) -> void:
|
||||
break
|
||||
_update_lasers()
|
||||
_update_rain()
|
||||
_update_run()
|
||||
if _hud_timer > 0.0:
|
||||
_hud_timer -= delta
|
||||
_hud_label.modulate.a = clampf(_hud_timer / 0.5, 0.0, 1.0)
|
||||
@@ -147,6 +170,38 @@ func _build_hud() -> void:
|
||||
layer.add_child(_hud_label)
|
||||
_hud_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
|
||||
_hud_label.position.y = 90.0
|
||||
# таймер забега: небольшой, сверху по центру
|
||||
_timer_label = Label.new()
|
||||
_timer_label.text = Net.fmt_time(0.0)
|
||||
_timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_timer_label.add_theme_font_size_override("font_size", 30)
|
||||
_timer_label.add_theme_color_override("font_color", DIM)
|
||||
_timer_label.add_theme_constant_override("outline_size", 6)
|
||||
_timer_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
_timer_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layer.add_child(_timer_label)
|
||||
_timer_label.anchor_left = 0.0
|
||||
_timer_label.anchor_right = 1.0
|
||||
_timer_label.offset_top = 14.0
|
||||
var hint := Label.new()
|
||||
hint.text = "R — заново"
|
||||
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
hint.add_theme_font_size_override("font_size", 14)
|
||||
hint.add_theme_color_override("font_color", DIM)
|
||||
hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layer.add_child(hint)
|
||||
hint.anchor_left = 0.0
|
||||
hint.anchor_right = 1.0
|
||||
hint.offset_top = 52.0
|
||||
# итоги: время и общий рейтинг
|
||||
_results_panel = PanelContainer.new()
|
||||
_results_panel.add_theme_stylebox_override("panel", LbView.panel_style(24))
|
||||
_results_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_results_panel.visible = false
|
||||
layer.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 _emissive(color: Color, energy: float, alpha := 1.0) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
@@ -196,6 +251,7 @@ func _add_checkpoint(pos: Vector3) -> void:
|
||||
cs.shape = shape
|
||||
area.add_child(cs)
|
||||
root.add_child(area)
|
||||
_cp_items.append({"mat": mat, "sheet": sheet_mat, "label": label})
|
||||
area.body_entered.connect(func(body: Node3D) -> void:
|
||||
if body != player or player.spawn_transform.origin.is_equal_approx(pos):
|
||||
return
|
||||
@@ -247,6 +303,91 @@ func _update_lasers() -> void:
|
||||
l.mat.emission_energy_multiplier = 2.4 if active else 0.06
|
||||
l.mesh.visible = active or l.mode == 1
|
||||
|
||||
# ---------- забег на время ----------
|
||||
|
||||
func _add_finish() -> void:
|
||||
var area := Area3D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 1
|
||||
area.position = Data.PORTAL + Vector3(0.0, FINISH_SIZE.y * 0.5, 3.0)
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = FINISH_SIZE
|
||||
cs.shape = shape
|
||||
area.add_child(cs)
|
||||
add_child(area)
|
||||
area.body_entered.connect(func(body: Node3D) -> void:
|
||||
if body == player:
|
||||
_finish_run())
|
||||
|
||||
func run_elapsed() -> float:
|
||||
return (Time.get_ticks_msec() - run_start_tick) / 1000.0
|
||||
|
||||
# Старт: пересечение линии START_LINE_Z по ходу трассы (тот, кто телепортирован за линию, тоже стартует)
|
||||
func _update_run() -> void:
|
||||
var z: float = player.global_position.z
|
||||
if not run_active and not run_done and _prev_z >= START_LINE_Z and z < START_LINE_Z:
|
||||
run_active = true
|
||||
run_start_tick = Time.get_ticks_msec()
|
||||
_timer_label.add_theme_color_override("font_color", TEXT)
|
||||
_prev_z = z
|
||||
if run_active:
|
||||
_timer_label.text = Net.fmt_time(run_elapsed())
|
||||
if _results_panel.visible and Time.get_ticks_msec() >= _results_end:
|
||||
_results_panel.visible = false
|
||||
|
||||
func _finish_run() -> void:
|
||||
if not run_active:
|
||||
return
|
||||
run_active = false
|
||||
run_done = true
|
||||
run_time = run_elapsed()
|
||||
_timer_label.text = Net.fmt_time(run_time)
|
||||
_timer_label.add_theme_color_override("font_color", GOOD)
|
||||
Net.submit_my_record(RATING_ID, run_time) # сервер сам проверит и ответит рейтингом
|
||||
Net.request_board(RATING_ID)
|
||||
_show_results()
|
||||
|
||||
func _show_results() -> void:
|
||||
for c in _results_panel.get_children():
|
||||
c.queue_free()
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 8)
|
||||
_results_panel.add_child(box)
|
||||
var t := LbView.make_label("ВЕРШИНА", 30, TEXT)
|
||||
t.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(t)
|
||||
var tm := LbView.make_label(Net.fmt_time(run_time), 36, GOOD)
|
||||
tm.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(tm)
|
||||
var rt := LbView.make_label("ОБЩИЙ РЕЙТИНГ", 18, DIM)
|
||||
rt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(rt)
|
||||
box.add_child(LbView.create(RATING_ID, 18, true))
|
||||
_results_panel.visible = true
|
||||
_results_end = Time.get_ticks_msec() + int(RESULTS_TIME * 1000.0)
|
||||
|
||||
# R: забег заново — на старт, таймер сброшен, чекпоинты снова не пройдены
|
||||
func restart_run() -> void:
|
||||
run_active = false
|
||||
run_done = false
|
||||
run_time = 0.0
|
||||
_results_panel.visible = false
|
||||
_timer_label.text = Net.fmt_time(0.0)
|
||||
_timer_label.add_theme_color_override("font_color", DIM)
|
||||
for c in _cp_items:
|
||||
c.mat.emission = Color(1.0, 0.5, 0.15)
|
||||
c.sheet.emission = Color(1.0, 0.5, 0.15)
|
||||
c.label.modulate = Color(1.0, 0.62, 0.3)
|
||||
player.place(Transform3D(Basis.IDENTITY, Data.START))
|
||||
_prev_z = Data.START.z
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if Net.input_blocked:
|
||||
return
|
||||
if event.is_action_pressed("ready") and not event.is_echo():
|
||||
restart_run()
|
||||
|
||||
# ---------- выход ----------
|
||||
|
||||
func _add_exit() -> void:
|
||||
|
||||
+127
-10
@@ -5,7 +5,9 @@ extends Node
|
||||
# - старт даёт единственный отправитель (наименьший id в комнате) и все считают время от одного момента;
|
||||
# - ворота засчитывает сам игрок и сообщает всем (CP/FINISH), порядок мест вычисляется одинаково у всех;
|
||||
# - раз в секунду каждый шлёт «маяк» (RACE_STATE) — по нему догоняют потерявшие событие и пришедшие позже.
|
||||
# Биты маяка (a): 0..7 — пройдено ворот; 8 — готов; 9..10 — фаза отправителя (Phase).
|
||||
# Биты маяка (a): 0..7 — пройдено ворот; 8 — готов; 9..10 — фаза отправителя (Phase); 11 — сдался.
|
||||
# Сдача (клавиша G, удержание): игрока возвращает на стартовую площадку, время не пишется ни в итоги, ни в рейтинг;
|
||||
# сдавшиеся считаются завершившими для конца круга. Рейтинг: финиш -> Net.submit_my_record(rating_id, время).
|
||||
|
||||
const Data = preload("res://maps/race/race_data.gd")
|
||||
|
||||
@@ -18,6 +20,7 @@ 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 # смещение за кадр больше этого — телепорт (возрождение), не пересечение ворот
|
||||
@@ -39,6 +42,8 @@ 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
|
||||
@@ -48,6 +53,7 @@ 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
|
||||
@@ -58,10 +64,14 @@ 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")
|
||||
@@ -107,6 +117,7 @@ func _reset_all() -> void:
|
||||
flags_ready.clear()
|
||||
passed.clear()
|
||||
finish.clear()
|
||||
gave_up.clear()
|
||||
peer_phase.clear()
|
||||
known.clear()
|
||||
known[my_id] = true
|
||||
@@ -121,12 +132,13 @@ func _forget(id: int) -> void:
|
||||
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 "Игрок %d" % (id % 1000) # как у призрака (net/ghost.gd)
|
||||
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) # как у призрака
|
||||
@@ -149,6 +161,8 @@ func _beacon_bits() -> int:
|
||||
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:
|
||||
@@ -177,6 +191,10 @@ func _on_race_event(id: int, kind: int, a: int, b: float) -> void:
|
||||
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)
|
||||
|
||||
@@ -189,6 +207,8 @@ func _on_beacon(id: int, a: int, b: float) -> void:
|
||||
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:
|
||||
@@ -202,6 +222,8 @@ func waiting_for_race() -> bool:
|
||||
# ---------- ожидание: готовность ----------
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if Net.input_blocked:
|
||||
return
|
||||
if event.is_action_pressed("ready") and not event.is_echo():
|
||||
toggle_ready()
|
||||
|
||||
@@ -248,6 +270,7 @@ func _on_start(sender: int, unix_start: float) -> void:
|
||||
participants = _room_ids()
|
||||
passed.clear()
|
||||
finish.clear()
|
||||
gave_up.clear()
|
||||
splits.clear()
|
||||
_new_record = false
|
||||
for id in participants:
|
||||
@@ -271,7 +294,7 @@ func race_time() -> float:
|
||||
# ---------- гонка ----------
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if phase != Phase.RACE or not participants.has(my_id) or finish.has(my_id) or player.is_dead:
|
||||
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
|
||||
@@ -316,6 +339,7 @@ func _pass_gate(idx: int) -> void:
|
||||
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
|
||||
@@ -348,6 +372,10 @@ func ranking() -> Array:
|
||||
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:
|
||||
@@ -371,27 +399,29 @@ func _check_race_end() -> void:
|
||||
if finish.has(id):
|
||||
if first < 0.0 or finish[id] < first:
|
||||
first = finish[id]
|
||||
else:
|
||||
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)})
|
||||
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" % [r.place, r.id, r.time, r.gates])
|
||||
_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
|
||||
@@ -400,8 +430,32 @@ func _back_to_wait() -> void:
|
||||
_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()
|
||||
@@ -515,6 +569,37 @@ func _build_hud() -> void:
|
||||
_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)
|
||||
@@ -532,9 +617,12 @@ func _build_hud() -> void:
|
||||
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)
|
||||
_results_panel.add_child(box)
|
||||
cols.add_child(box)
|
||||
var title := _label(36, TEXT, box)
|
||||
title.text = "ИТОГИ"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
@@ -551,11 +639,21 @@ func _show_results() -> void:
|
||||
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)
|
||||
tm.text = _fmt(r.time) if r.time >= 0.0 else "не финишировал (%d/%d)" % [r.gates, Data.CHECKPOINTS.size()]
|
||||
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:
|
||||
@@ -569,13 +667,18 @@ func _update_hud() -> void:
|
||||
_split_label.visible = now < _split_until and racing
|
||||
if racing:
|
||||
var n: int = Data.CHECKPOINTS.size()
|
||||
if finish.has(my_id):
|
||||
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()]
|
||||
_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]
|
||||
|
||||
# центр: отсчёт, «СТАРТ!»
|
||||
@@ -594,6 +697,20 @@ func _update_hud() -> void:
|
||||
_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:
|
||||
|
||||
Reference in New Issue
Block a user