extends Node3D # Карта «Гонка 2» — «Верхние ярусы» (RACE 2): забег по крышам и мостам мрачного города: старт на верхней платформе, мосты между # башнями, бег по фасадам, площадь с краном, пульсирующие лазерные мосты, спуск по крышам и подъём по спирали вентиляторов # на финальную башню. На каждом участке три пути: безопасный, рискованный и секретный (см. README, «Карта Race 2»). # Модель — race2.glb, данные — race2_data.gd (оба генерирует `Blender models/race_02/build_race.py` из layout.py). # Здесь: внешний вид (материалы по именам из GLB, окружение, дождь, гул), ворота, лазеры, вентиляторы, ускоряющие полосы, срыв, # возрождение на последних воротах. Правила гонки (ожидание, отсчёт, место, итоги, сеть) — общие с картой Race: maps/race/race_rules.gd # (ему передаётся data = race2_data.gd и rating_id); сервер про них ничего не знает. const Data = preload("res://maps/race2/race2_data.gd") const Rules = preload("res://maps/race/race_rules.gd") const RATING_ID := "race2" # id карты в рейтинге сервера (Net.TIMED_MAPS) const KILL_DEPTH := 32.0 # Ниже последних пройденных ворот на столько — срыв (то же значение в race_data.gd) const GATE_HEIGHT := 5.0 const FLOOR_TO_CENTER := 1.2 # Центр капсулы над полом в данных ворот и слотов var player: Node3D var rules: Node var data := Data # для race_rules.gd (общий код правил) var rating_id := RATING_ID var _lasers: Array = [] # {area, mat, mesh, mode, period, on, phase} var _gates: Array = [] # {root, mat, sheet, label} — визуал ворот по индексам CHECKPOINTS func _ready() -> void: Net.enter_map("race2") player = get_tree().get_first_node_in_group("local_player") var start: Vector3 = Data.START_SLOTS[0] player.global_position = start player.spawn_transform = Transform3D(Basis.IDENTITY, start) player.death_label.text = "СОРВАЛСЯ!" _prepare_map() _build_environment() for i in Data.CHECKPOINTS.size(): _add_gate(i) for l in Data.LASERS: _add_laser(l) for v in Data.VENTS: _add_vent(v) for st in Data.STRIPS: _add_strip(st) _add_rain() _add_ambience() _add_exit() rules = Rules.new() rules.name = "Rules" rules.setup(self) add_child(rules) # Цвета поверхностей — те же значения, что в Blender (kit.py: SOLID); освещение уже запечено в цветах вершин const SURFACES := { "Concrete": Color(0.095, 0.105, 0.15), "Dark": Color(0.012, 0.013, 0.024), "Steel": Color(0.115, 0.125, 0.185), "PaintY": Color(0.30, 0.10, 0.36), "PaintR": Color(0.22, 0.05, 0.16), "PaintB": Color(0.04, 0.06, 0.18), "PaintO": Color(0.14, 0.09, 0.30), } # Светящиеся (мрачная палитра): Cyan — холодный белёсый, Magenta — тревожный красный, Orange — натриевая лампа const GLOWS := { "NeonCyan": [Color(0.30, 0.85, 0.98), 0.7], "NeonMagenta": [Color(0.85, 0.08, 0.65), 1.2], "NeonOrange": [Color(0.55, 0.22, 1.0), 1.3], "NeonYellow": [Color(0.72, 0.84, 1.0), 1.0], "NeonGreen": [Color(0.25, 0.95, 0.75), 1.0], "NeonBlue": [Color(0.28, 0.32, 0.92), 1.0], "NeonRed": [Color(1.0, 0.06, 0.28), 1.5], "NeonWhite": [Color(0.72, 0.82, 1.0), 0.9], } # Коллизию создаёт импорт (узел Collision-colonly), здесь — внешний вид: материалы по именам из Blender func _prepare_map() -> void: var shader := load("res://maps/race2/baked.gdshader") as Shader var noise := FastNoiseLite.new() noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH noise.frequency = 0.04 noise.fractal_octaves = 4 var grain := NoiseTexture2D.new() grain.noise = noise grain.width = 512 grain.height = 512 grain.seamless = true var sky_tex := load("res://maps/race2/sky.png") as Texture2D var cache := {} for mi: MeshInstance3D in $Map.find_children("*", "MeshInstance3D", true, false): mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF for s in mi.mesh.get_surface_count(): var src := mi.mesh.surface_get_material(s) var key: String = src.resource_name if src != null else "" if not cache.has(key): if SURFACES.has(key): var m := ShaderMaterial.new() m.shader = shader m.set_shader_parameter("albedo_color", Vector3(SURFACES[key].r, SURFACES[key].g, SURFACES[key].b)) m.set_shader_parameter("grain", grain) m.set_shader_parameter("sky_tex", sky_tex) m.set_shader_parameter("exposure", 2.1) cache[key] = m elif GLOWS.has(key): cache[key] = _emissive(GLOWS[key][0], GLOWS[key][1]) else: cache[key] = null if cache[key] != null: mi.set_surface_override_material(s, cache[key]) func _physics_process(_delta: float) -> void: if player == null: return if not player.is_dead: if player.global_position.y < player.spawn_transform.origin.y - KILL_DEPTH: player.die() else: for l in _lasers: if _laser_active(l) and l.area.overlaps_body(player): player.die() break _update_lasers() _update_rain() # ---------- окружение ---------- func _build_environment() -> void: var sky_mat := PanoramaSkyMaterial.new() sky_mat.panorama = load("res://maps/race2/sky.png") var sky := Sky.new() sky.sky_material = sky_mat var env := Environment.new() env.background_mode = Environment.BG_SKY env.sky = sky env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # свет на самом игроке/призраках; уровень освещён запечённо env.ambient_light_color = Color(0.20, 0.22, 0.40) env.ambient_light_energy = 0.6 env.fog_enabled = true env.fog_light_color = Color(0.10, 0.09, 0.24) # холодный индиго-дым высоких ярусов env.fog_density = 0.011 env.fog_sky_affect = 0.45 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC env.glow_enabled = true env.glow_intensity = 0.55 env.glow_bloom = 0.1 env.glow_hdr_threshold = 1.0 env.adjustment_enabled = true env.adjustment_contrast = 1.15 env.adjustment_saturation = 0.9 var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() # подсветка только для персонажей sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0) sun.light_color = Color(0.5, 0.58, 0.95) sun.light_energy = 0.4 add_child(sun) func _emissive(color: Color, energy: float, alpha := 1.0) -> StandardMaterial3D: var m := StandardMaterial3D.new() m.albedo_color = Color(0, 0, 0, alpha) m.emission_enabled = true m.emission = color m.emission_energy_multiplier = energy if alpha < 1.0: m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.cull_mode = BaseMaterial3D.CULL_DISABLED return m # ---------- ворота: высокие светящиеся рамы поперёк трассы ---------- const GATE_OFF := Color(0.30, 0.26, 0.55) # ещё не очередные: тусклый индиго const GATE_NEXT := Color(1.0, 0.18, 0.85) # очередные: яркая маджента const GATE_DONE := Color(0.25, 0.95, 0.95) # пройдены: ледяной циан # Угол рыскания из данных (0 = вдоль -Z, -90 = на +X) — это ровно rotation.y в Godot func gate_yaw(i: int) -> float: return deg_to_rad(Data.CHECKPOINTS[i][2]) func gate_pos(i: int) -> Vector3: return Data.CHECKPOINTS[i][0] func gate_half(i: int) -> float: return Data.CHECKPOINTS[i][1] func _add_gate(i: int) -> void: var last: bool = i == Data.CHECKPOINTS.size() - 1 var half: float = gate_half(i) var mat := _emissive(GATE_OFF, 0.9) var sheet := _emissive(GATE_OFF, 0.3, 0.07) var root := Node3D.new() root.position = gate_pos(i) - Vector3(0, FLOOR_TO_CENTER, 0) # в данных — центр капсулы, ворота ставим на пол root.rotation.y = gate_yaw(i) # локальный -Z ворот смотрит по трассе add_child(root) var post_h := GATE_HEIGHT for s in [-1.0, 1.0]: var bm := BoxMesh.new() bm.size = Vector3(0.4, post_h, 0.4) root.add_child(_part(bm, mat, Vector3(s * half, post_h * 0.5, 0))) var top := BoxMesh.new() top.size = Vector3(half * 2 + 0.4, 0.4, 0.4) root.add_child(_part(top, mat, Vector3(0, post_h, 0))) var beams: Array = [] # световые столбы над стойками: ворота видно издалека for s in [-1.0, 1.0]: var bm2 := BoxMesh.new() bm2.size = Vector3(0.7, 70.0, 0.7) var bmat := _emissive(GATE_OFF, 1.0, 0.10) root.add_child(_part(bm2, bmat, Vector3(s * half, post_h + 35.0, 0))) beams.append(bmat) var qm := QuadMesh.new() # слабая светящаяся «плёнка» в проёме qm.size = Vector2(half * 2, post_h) root.add_child(_part(qm, sheet, Vector3(0, post_h * 0.5, 0))) var label := Label3D.new() label.text = "FINISH" if last else "CHECKPOINT %d/%d" % [i + 1, Data.CHECKPOINTS.size()] label.billboard = BaseMaterial3D.BILLBOARD_ENABLED label.pixel_size = 0.022 label.modulate = GATE_OFF label.outline_modulate = Color.BLACK label.position = Vector3(0, post_h + 1.2, 0) root.add_child(label) _gates.append({"root": root, "mat": mat, "sheet": sheet, "label": label, "beams": beams}) # Раскраска ворот по прогрессу игрока: пройденные зелёные, очередные яркие, остальные тусклые func set_gate_progress(passed: int) -> void: for i in _gates.size(): var g: Dictionary = _gates[i] var c := GATE_DONE if i < passed else (GATE_NEXT if i == passed else GATE_OFF) g.mat.emission = c g.sheet.emission = c g.label.modulate = c for b in g.beams: b.emission = c g.mat.emission_energy_multiplier = 1.6 if i == passed else 0.9 func _part(mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D: var mi := MeshInstance3D.new() mi.mesh = mesh mi.material_override = mat mi.position = pos mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF return mi # ---------- лазеры ---------- func _add_laser(d: Array) -> void: var area := Area3D.new() area.collision_layer = 0 area.collision_mask = 1 area.position = d[0] var cs := CollisionShape3D.new() var shape := BoxShape3D.new() shape.size = d[1] cs.shape = shape area.add_child(cs) var bm := BoxMesh.new() bm.size = d[1] var mat := _emissive(Color(1.0, 0.05, 0.22), 2.4, 0.35 if d[1].y > 3.0 else 1.0) var mesh := _part(bm, mat, Vector3.ZERO) area.add_child(mesh) add_child(area) _lasers.append({"area": area, "mat": mat, "mesh": mesh, "mode": d[2], "period": d[3], "on": d[4], "phase": d[5]}) # Пульсирующие лазеры считают фазу по системным часам — у всех игроков примерно одинаково func _laser_active(l: Dictionary) -> bool: if l.mode == 0: return true return fposmod(Time.get_unix_time_from_system() + l.phase, l.period) < l.on func _update_lasers() -> void: for l in _lasers: var active := _laser_active(l) l.mat.emission_energy_multiplier = 2.4 if active else 0.3 l.mesh.visible = active or l.mode == 1 # ---------- вентиляторы и ускоряющие полосы (objects/vent.gd, objects/boost_strip.gd) ---------- func _add_vent(d: Array) -> void: var a := Area3D.new() a.set_script(load("res://objects/vent.gd")) a.size = d[1] a.height = d[2] a.hidden = d[3] a.position = d[0] add_child(a) func _add_strip(d: Array) -> void: var a := Area3D.new() a.set_script(load("res://objects/boost_strip.gd")) a.size = d[1] a.speed = d[3] a.position = d[0] a.rotation.y = deg_to_rad(d[2]) add_child(a) # ---------- выход ---------- func _add_exit() -> void: var portal := Area3D.new() portal.set_script(load("res://objects/portal.gd")) portal.target_map = "hub" portal.target_scene = "res://maps/hub/hub.tscn" portal.title = "HUB" portal.color = Color(0.3, 1, 0.45) portal.position = Data.PORTAL add_child(portal) # ---------- дождь и звук ---------- var _rain: GPUParticles3D # Ливень вокруг игрока: тонкие вертикальные штрихи, летящие по ветру; в закрытых местах (COVERED из race2_data.gd) не идёт func _add_rain() -> void: _rain = GPUParticles3D.new() _rain.amount = 2200 _rain.lifetime = 0.8 _rain.local_coords = false _rain.visibility_aabb = AABB(Vector3(-30, -30, -30), Vector3(60, 60, 60)) var pm := ParticleProcessMaterial.new() pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX pm.emission_box_extents = Vector3(26.0, 0.5, 26.0) pm.direction = Vector3(0.12, -1.0, 0.05) pm.spread = 2.0 pm.initial_velocity_min = 34.0 pm.initial_velocity_max = 40.0 pm.gravity = Vector3.ZERO _rain.process_material = pm var quad := QuadMesh.new() quad.size = Vector2(0.018, 0.7) var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color = Color(0.62, 0.7, 0.95, 0.3) mat.billboard_mode = BaseMaterial3D.BILLBOARD_FIXED_Y mat.cull_mode = BaseMaterial3D.CULL_DISABLED quad.material = mat _rain.draw_pass_1 = quad _rain.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF add_child(_rain) func _update_rain() -> void: if _rain == null or player == null: return _rain.global_position = player.global_position + Vector3(0, 12, 0) var p := player.global_position var covered := false for c in Data.COVERED: # траншея и шахта: x0, x1, z0, z1, пол, потолок (z0 > z1: по Z расстояние со знаком минус) if p.x > c[0] and p.x < c[1] and p.z < c[2] and p.z > c[3] and p.y > c[4] and p.y < c[5]: covered = true _rain.emitting = not covered # Низкий промышленный гул и шум дождя: 3 секунды без щелчка на стыке петли (края сведены перекрёстным затуханием) func _add_ambience() -> void: const RATE := 22050 const FADE := 4410 var n := RATE * 3 var rng := RandomNumberGenerator.new() rng.seed = 13 var raw := PackedFloat32Array() raw.resize(n + FADE) var lp := 0.0 for i in n + FADE: lp = lerpf(lp, rng.randf_range(-1.0, 1.0), 0.55) # мягкий «шипящий» шум raw[i] = lp var data := PackedByteArray() data.resize(n * 2) for i in n: var v := raw[i] if i < FADE: var t := float(i) / FADE v = raw[i] * sin(t * PI * 0.5) + raw[n + i] * cos(t * PI * 0.5) var tt := float(i) / RATE var hum := 0.16 * sin(TAU * 42.0 * tt) + 0.08 * sin(TAU * 63.0 * tt) + 0.05 * sin(TAU * 21.0 * tt) data.encode_s16(i * 2, int(clampf(v * 0.45 + hum, -1.0, 1.0) * 14000.0)) var wav := AudioStreamWAV.new() wav.format = AudioStreamWAV.FORMAT_16_BITS wav.mix_rate = RATE wav.stereo = false wav.loop_mode = AudioStreamWAV.LOOP_FORWARD wav.loop_begin = 0 wav.loop_end = n wav.data = data var ap := AudioStreamPlayer.new() ap.stream = wav ap.volume_db = -13.0 ap.autoplay = true add_child(ap) # Песня гонки (пока та же, что у Race; своя музыка — фаза 2): Ogg Vorbis, моно 32 кГц (~0,6 МБ). Включается в момент «СТАРТ!» (race_rules.gd), плавно затихает при возврате в ожидание const MUSIC_DB := -9.0 var _music: AudioStreamPlayer var _music_tween: Tween func set_music(on: bool) -> void: if _music == null: var stream := load("res://maps/race2/audio/music.ogg") as AudioStreamOggVorbis stream.loop = true _music = AudioStreamPlayer.new() _music.stream = stream _music.volume_db = MUSIC_DB add_child(_music) if _music_tween: _music_tween.kill() if on: if not _music.playing: _music.volume_db = MUSIC_DB _music.play() # каждый круг — с начала elif _music.playing: _music_tween = create_tween() _music_tween.tween_property(_music, "volume_db", -60.0, 0.8) _music_tween.tween_callback(_music.stop)