- Ники: профиль 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
220 lines
8.6 KiB
GDScript
220 lines
8.6 KiB
GDScript
extends Node3D
|
|
# Стартовая комната: ничего тяжёлого, только пол, стены и порталы на карты. Сами карты качаются
|
|
# отдельными пакетами при входе в портал (World/map_loader.gd) — поэтому игра стартует быстро.
|
|
# Чтобы добавить карту — допиши запись в PORTALS: порталы встанут в ряд по центру (влезает 6).
|
|
|
|
const PORTALS := [
|
|
{"title": "SANDBOX", "map": "world", "scene": "res://maps/sandbox/sandbox.tscn", "pack": "world.pck", "color": Color(0.2, 0.9, 1.0)},
|
|
{"title": "BACKROOMS", "map": "backrooms", "scene": "res://maps/backrooms/backrooms.tscn", "pack": "backrooms.pck", "color": Color(1.0, 0.85, 0.25)},
|
|
{"title": "PARKOUR", "map": "parkour", "scene": "res://maps/parkour/parkour.tscn", "pack": "parkour.pck", "color": Color(1.0, 0.2, 0.65)},
|
|
{"title": "RACE", "map": "race", "scene": "res://maps/race/race.tscn", "pack": "race.pck", "color": Color(1.0, 0.55, 0.12)},
|
|
]
|
|
|
|
const ROOM_WIDTH := 28.0
|
|
const ROOM_DEPTH := 18.0
|
|
const ROOM_HEIGHT := 7.0 # +1 м: над порталами доски рейтинга, над ними — «CHOOSE A MAP»
|
|
const WALL := 0.5
|
|
const PORTAL_SPACING := 4.5
|
|
|
|
const GRID_SHADER := """
|
|
shader_type spatial;
|
|
varying vec3 wpos;
|
|
void vertex() { wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
|
|
void fragment() {
|
|
vec2 cell = wpos.xz / 2.0;
|
|
vec2 g = abs(fract(cell - 0.5) - 0.5) / fwidth(cell);
|
|
float line = 1.0 - min(min(g.x, g.y), 1.0);
|
|
ALBEDO = mix(vec3(0.03, 0.04, 0.06), vec3(0.1, 0.65, 0.85), line * 0.8);
|
|
EMISSION = vec3(0.04, 0.3, 0.4) * line;
|
|
ROUGHNESS = 0.9;
|
|
}
|
|
"""
|
|
|
|
func _ready() -> void:
|
|
Net.enter_map("hub")
|
|
_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()
|
|
body.name = "Room"
|
|
add_child(body)
|
|
|
|
var floor_mat := ShaderMaterial.new()
|
|
floor_mat.shader = Shader.new()
|
|
floor_mat.shader.code = GRID_SHADER
|
|
var wall_mat := StandardMaterial3D.new()
|
|
wall_mat.albedo_color = Color(0.09, 0.1, 0.14)
|
|
wall_mat.roughness = 1.0
|
|
var ceiling_mat := StandardMaterial3D.new()
|
|
ceiling_mat.albedo_color = Color(0.05, 0.055, 0.08)
|
|
|
|
var hw := ROOM_WIDTH * 0.5
|
|
var hd := ROOM_DEPTH * 0.5
|
|
_add_box(body, Vector3(ROOM_WIDTH + WALL * 2, 0.5, ROOM_DEPTH + WALL * 2), Vector3(0, -0.25, 0), floor_mat)
|
|
_add_box(body, Vector3(ROOM_WIDTH + WALL * 2, 0.5, ROOM_DEPTH + WALL * 2), Vector3(0, ROOM_HEIGHT + 0.25, 0), ceiling_mat)
|
|
_add_box(body, Vector3(ROOM_WIDTH + WALL * 2, ROOM_HEIGHT, WALL), Vector3(0, ROOM_HEIGHT * 0.5, -hd - WALL * 0.5), wall_mat)
|
|
_add_box(body, Vector3(ROOM_WIDTH + WALL * 2, ROOM_HEIGHT, WALL), Vector3(0, ROOM_HEIGHT * 0.5, hd + WALL * 0.5), wall_mat)
|
|
_add_box(body, Vector3(WALL, ROOM_HEIGHT, ROOM_DEPTH), Vector3(-hw - WALL * 0.5, ROOM_HEIGHT * 0.5, 0), wall_mat)
|
|
_add_box(body, Vector3(WALL, ROOM_HEIGHT, ROOM_DEPTH), Vector3(hw + WALL * 0.5, ROOM_HEIGHT * 0.5, 0), wall_mat)
|
|
|
|
func _add_box(body: StaticBody3D, size: Vector3, pos: Vector3, mat: Material) -> void:
|
|
var shape := BoxShape3D.new()
|
|
shape.size = size
|
|
var cs := CollisionShape3D.new()
|
|
cs.shape = shape
|
|
cs.position = pos
|
|
body.add_child(cs)
|
|
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = size
|
|
mesh.material = mat
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = mesh
|
|
mi.position = pos
|
|
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
add_child(mi)
|
|
|
|
# Порталы вдоль северной стены лицом в комнату, по центру
|
|
func _build_portals() -> void:
|
|
var portal_script := load("res://objects/portal.gd")
|
|
var z := -ROOM_DEPTH * 0.5 + 1.0
|
|
for i in PORTALS.size():
|
|
var info: Dictionary = PORTALS[i]
|
|
var portal := Area3D.new()
|
|
portal.set_script(portal_script)
|
|
portal.target_map = info.map
|
|
portal.target_scene = info.scene
|
|
portal.pack_name = info.pack
|
|
portal.title = info.title
|
|
portal.color = info.color
|
|
portal.position = Vector3((i - (PORTALS.size() - 1) * 0.5) * PORTAL_SPACING, 0.0, z)
|
|
add_child(portal)
|
|
|
|
var label := Label3D.new()
|
|
label.text = "CHOOSE A MAP"
|
|
label.pixel_size = 0.012
|
|
label.font_size = 64
|
|
label.outline_size = 12
|
|
label.modulate = Color(0.7, 0.9, 1.0)
|
|
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
|
|
env.background_color = Color(0.02, 0.025, 0.04)
|
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
|
env.ambient_light_color = Color(0.5, 0.58, 0.75)
|
|
env.ambient_light_energy = 0.55
|
|
var we := WorldEnvironment.new()
|
|
we.environment = env
|
|
add_child(we)
|
|
|
|
var lamp := OmniLight3D.new()
|
|
lamp.position = Vector3(0, ROOM_HEIGHT - 0.6, 2.0)
|
|
lamp.omni_range = 22.0
|
|
lamp.light_energy = 0.8
|
|
lamp.light_color = Color(0.8, 0.9, 1.0)
|
|
lamp.shadow_enabled = false
|
|
add_child(lamp)
|