Ники, общий рейтинг (топ-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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user