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)