Files
my-world/maps/hub/hub.gd
T
aibrashandClaude Sonnet 5.5 2d347d132b Геймпад Xbox, меню паузы с настройками, новый хаб с музыкой
- Геймпад: раскладка под паркур и гонки, обзор правым стиком, навигация по меню (A/B/крестовина)
- Меню паузы (Esc/Start): выход в хаб, настройки (мышь, стик, FOV, громкость, инверсия), справка по управлению
- Хаб 48x32x10 м: колонны, окна с городом, голограмма, лавки; музыка «Local Forecast - Elevator» (Kevin MacLeod, CC-BY)
- Тесты: pause_test, pad_menu_test, hub_tour; README обновлён

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 18:36:50 +05:00

255 lines
9.8 KiB
GDScript

extends Node3D
# Стартовая комната: ничего тяжёлого, только пол, стены и порталы на карты. Сами карты качаются
# отдельными пакетами при входе в портал (World/map_loader.gd) — поэтому игра стартует быстро.
# Чтобы добавить карту — допиши запись в PORTALS: порталы встанут в ряд по центру (влезает 7).
# Оформление (колонны, окна, голограмма, лавки) — hub_decor.gd; музыка «как в лифте» играет по кругу.
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)},
{"title": "RACE 2", "map": "race2", "scene": "res://maps/race2/race2.tscn", "pack": "race2.pck", "color": Color(0.45, 0.85, 0.35)},
]
const ROOM_WIDTH := 48.0
const ROOM_DEPTH := 32.0
const ROOM_HEIGHT := 10.0 # над порталами доски рейтинга, над ними — «CHOOSE A MAP»
const WALL := 0.5
const PORTAL_SPACING := 6.0
const MUSIC_PATH := "res://maps/hub/audio/music.ogg"
const MUSIC_DB := -9.0
const DecorScript := preload("res://maps/hub/hub_decor.gd")
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()
_build_decor()
_start_music()
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 decor := DecorScript.new()
decor.name = "Decor"
add_child(decor)
var floor_mat := decor.floor_material()
var wall_mat := StandardMaterial3D.new()
wall_mat.albedo_color = Color(0.12, 0.13, 0.18)
wall_mat.metallic = 0.3
wall_mat.roughness = 0.6
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, 7.2, -ROOM_DEPTH * 0.5 + 0.1)
label.pixel_size = 0.016
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 = 1.0
env.glow_enabled = true # неон и светильники светятся (в Compatibility glow работает)
env.glow_intensity = 0.8
env.glow_bloom = 0.1
var we := WorldEnvironment.new()
we.environment = env
add_child(we)
var lamp := OmniLight3D.new()
lamp.position = Vector3(0, ROOM_HEIGHT - 0.8, -6.0)
lamp.omni_range = 30.0
lamp.light_energy = 0.9
lamp.light_color = Color(0.8, 0.9, 1.0)
lamp.shadow_enabled = false
add_child(lamp)
# ---------- оформление и музыка ----------
func _build_decor() -> void:
var decor := get_node("Decor")
decor.build(ROOM_WIDTH, ROOM_DEPTH, ROOM_HEIGHT)
func _start_music() -> void:
var stream = load(MUSIC_PATH)
if stream == null:
return
if stream is AudioStreamOggVorbis:
stream.loop = true
var player := AudioStreamPlayer.new()
player.name = "Music"
player.stream = stream
player.volume_db = -40.0
player.autoplay = true
add_child(player)
create_tween().tween_property(player, "volume_db", MUSIC_DB, 3.0) # плавно нарастает