Race 2 «Верхние ярусы», фаза 1: карта, вентиляторы, ускоряющие полосы, правила общие с Race
- maps/race2/: сцена, race2.gd (ворота со световыми столбами, лазеры, вентиляторы, полосы, срыв, дождь, гул), данные и модель из Blender models/race_02 (раскладка layout.py -> build_race.py -> bake.py; свет пока запечён грубо, небо и музыка — копии Race) - objects/vent.gd, objects/boost_strip.gd + хуки apply_vent()/apply_boost() в player/player.gd (MAX_SPEED и остальная физика не тронуты) - race_rules.gd берёт данные у карты (общий код правил для Race и Race 2) - портал RACE 2 в хабе, пресет экспорта Race2 + export_all.sh, Net.TIMED_MAPS/MAP_TITLES для race2 (RPC не менялись; серверу нужен deploy_server.sh) - тесты: race_game_test.sh с MAP=race2 (бот проводит игроков телепортами), race2_check.txt - README: раздел «Карта Race 2», результаты ботов по веткам Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://c3hge1rwjutk8"
|
||||
path="res://.godot/imported/music.ogg-f9a618edc60fbea7ca697db82c17868a.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/audio/music.ogg"
|
||||
dest_files=["res://.godot/imported/music.ogg-f9a618edc60fbea7ca697db82c17868a.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -0,0 +1,42 @@
|
||||
shader_type spatial;
|
||||
// Поверхности уровня Parkour: освещение запечено в цвета вершин (Blender, Cycles), динамических источников нет.
|
||||
// Итог = цвет материала × запечённый свет × мелкое «зерно» бетона (по мировым координатам) + слабый блик на скользящих углах.
|
||||
render_mode unshaded;
|
||||
|
||||
uniform vec3 albedo_color = vec3(0.1);
|
||||
uniform float light_scale = 8.0; // цвета вершин сжаты в 8 раз при экспорте (см. bake.py)
|
||||
uniform float exposure = 1.0;
|
||||
uniform sampler2D grain : repeat_enable, filter_linear_mipmap;
|
||||
uniform float grain_scale = 0.3;
|
||||
uniform float grain_amount = 0.45;
|
||||
uniform sampler2D sky_tex : source_color, filter_linear_mipmap; // панорама неба: отражается в «мокрых» горизонтальных поверхностях
|
||||
uniform float wet = 0.6;
|
||||
|
||||
varying vec3 wpos;
|
||||
varying vec3 wnorm;
|
||||
|
||||
void vertex() {
|
||||
wpos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
wnorm = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
}
|
||||
|
||||
vec2 pano_uv(vec3 d) {
|
||||
return vec2(atan(d.x, -d.z) / (2.0 * PI) + 0.5, acos(clamp(d.y, -1.0, 1.0)) / PI);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec3 n = abs(wnorm);
|
||||
n /= (n.x + n.y + n.z + 0.0001);
|
||||
float g = texture(grain, wpos.zy * grain_scale).r * n.x
|
||||
+ texture(grain, wpos.xz * grain_scale).r * n.y
|
||||
+ texture(grain, wpos.xy * grain_scale).r * n.z;
|
||||
float gr = mix(1.0 - grain_amount * 0.5, 1.0 + grain_amount * 0.5, g);
|
||||
vec3 lit = COLOR.rgb * light_scale;
|
||||
vec3 nv = normalize(NORMAL);
|
||||
float ndv = clamp(dot(nv, normalize(VIEW)), 0.0, 1.0);
|
||||
float fres = 0.05 + 0.95 * pow(1.0 - ndv, 4.0);
|
||||
vec3 rw = normalize((INV_VIEW_MATRIX * vec4(reflect(-normalize(VIEW), nv), 0.0)).xyz);
|
||||
float up = smoothstep(0.6, 0.95, wnorm.y); // блестят только горизонтальные поверхности
|
||||
vec3 refl = textureLod(sky_tex, pano_uv(rw), 3.0).rgb;
|
||||
ALBEDO = albedo_color * lit * gr * exposure + refl * fres * wet * up * (0.5 + 0.5 * min(lit.r + lit.g + lit.b, 1.0));
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwrd86827arnc
|
||||
@@ -0,0 +1,396 @@
|
||||
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.11, 0.115, 0.125), "Dark": Color(0.016, 0.017, 0.02), "Steel": Color(0.13, 0.14, 0.15),
|
||||
"PaintY": Color(0.38, 0.30, 0.06), "PaintR": Color(0.26, 0.08, 0.05), "PaintB": Color(0.05, 0.08, 0.11),
|
||||
"PaintO": Color(0.33, 0.14, 0.05),
|
||||
}
|
||||
# Светящиеся (мрачная палитра): Cyan — холодный белёсый, Magenta — тревожный красный, Orange — натриевая лампа
|
||||
const GLOWS := {
|
||||
"NeonCyan": [Color(0.50, 0.86, 0.82), 0.65], "NeonMagenta": [Color(0.95, 0.07, 0.04), 1.2],
|
||||
"NeonOrange": [Color(1.0, 0.42, 0.08), 1.3], "NeonYellow": [Color(0.92, 0.62, 0.10), 1.1],
|
||||
"NeonGreen": [Color(0.45, 0.85, 0.35), 1.0], "NeonBlue": [Color(0.25, 0.40, 0.70), 1.0],
|
||||
"NeonRed": [Color(1.0, 0.05, 0.05), 1.5], "NeonWhite": [Color(1.0, 0.72, 0.42), 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.26, 0.29, 0.33)
|
||||
env.ambient_light_energy = 0.6
|
||||
env.fog_enabled = true
|
||||
env.fog_light_color = Color(0.2, 0.16, 0.13) # грязный дымный воздух
|
||||
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.8
|
||||
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.6, 0.68, 0.75)
|
||||
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.55, 0.30, 0.14) # ещё не очередные: тусклые
|
||||
const GATE_NEXT := Color(1.0, 0.5, 0.15) # очередные: яркий натриевый
|
||||
const GATE_DONE := Color(0.45, 0.85, 0.35) # пройдены: зелёные
|
||||
|
||||
# Угол рыскания из данных (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.04, 0.05), 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.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.7, 0.78, 0.82, 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://djwekkr3vawop
|
||||
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bepdwblao0vt"
|
||||
path="res://.godot/imported/race2.glb-855c5ef0b2a873d4c22fd233e8a67730.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/race2.glb"
|
||||
dest_files=["res://.godot/imported/race2.glb-855c5ef0b2a873d4c22fd233e8a67730.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://maps/race2/race2.gd" id="1_level"]
|
||||
[ext_resource type="PackedScene" path="res://maps/race2/race2.glb" id="2_map"]
|
||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="3_player"]
|
||||
|
||||
[node name="Race2" type="Node3D"]
|
||||
script = ExtResource("1_level")
|
||||
|
||||
[node name="Map" parent="." instance=ExtResource("2_map")]
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("3_player")]
|
||||
@@ -0,0 +1,88 @@
|
||||
# Сгенерировано Blender models/race_02/build_race.py из layout.py — не править руками
|
||||
extends RefCounted
|
||||
|
||||
# Старт: 8 слотов в ряд поперёк трассы, лицом по трассе (-Z); центр капсулы
|
||||
const START_SLOTS := [
|
||||
Vector3(-9.80, 61.25, -28.00),
|
||||
Vector3(-7.00, 61.25, -28.00),
|
||||
Vector3(-4.20, 61.25, -28.00),
|
||||
Vector3(-1.40, 61.25, -28.00),
|
||||
Vector3(1.40, 61.25, -28.00),
|
||||
Vector3(4.20, 61.25, -28.00),
|
||||
Vector3(7.00, 61.25, -28.00),
|
||||
Vector3(9.80, 61.25, -28.00),
|
||||
]
|
||||
|
||||
# Ворота по порядку: [центр капсулы на полу ворот, полуширина поперёк трассы (м), рыскание (град; 0 = -Z, -90 = на +X, 180 = на +Z)]. Последний = ФИНИШ
|
||||
# Ворота стоят там, где ветки (безопасная / рискованная / секретная) сливаются.
|
||||
const CHECKPOINTS := [
|
||||
[Vector3(0.00, 61.20, -70.00), 8.0, 0.0], # cp1
|
||||
[Vector3(0.00, 62.20, -104.00), 17.0, 0.0], # cp2
|
||||
[Vector3(107.00, 62.20, -232.00), 25.0, 0.0], # cp3
|
||||
[Vector3(109.00, 53.00, -352.00), 12.0, 0.0], # cp4
|
||||
[Vector3(110.00, 53.00, -384.00), 20.0, 0.0], # cp5
|
||||
[Vector3(110.00, 53.00, -468.00), 50.0, 0.0], # cp6
|
||||
[Vector3(110.00, 53.00, -508.00), 22.0, 0.0], # cp7
|
||||
[Vector3(110.00, 53.00, -577.00), 26.0, 0.0], # cp8
|
||||
[Vector3(110.00, 53.00, -634.00), 18.0, 0.0], # cp9
|
||||
[Vector3(150.00, 48.00, -652.00), 28.0, -90.0], # cp10
|
||||
[Vector3(200.00, 29.00, -560.00), 12.0, 180.0], # cp11
|
||||
[Vector3(203.00, 25.00, -530.00), 33.0, 180.0], # cp12
|
||||
[Vector3(210.00, 75.00, -488.00), 14.0, 180.0], # cp13
|
||||
]
|
||||
|
||||
# Лазеры: центр, размер, режим (0 — всегда, 1 — пульсирует), период, время включения, сдвиг
|
||||
const LASERS := [
|
||||
[Vector3(108.00, 52.25, -544.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL0
|
||||
[Vector3(112.00, 52.25, -544.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR0
|
||||
[Vector3(108.00, 53.45, -560.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 0.00], # mL1
|
||||
[Vector3(112.00, 53.45, -560.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 1.50], # mR1
|
||||
[Vector3(108.00, 52.25, -576.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL2
|
||||
[Vector3(112.00, 52.25, -576.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR2
|
||||
[Vector3(108.00, 53.45, -592.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 0.00], # mL3
|
||||
[Vector3(112.00, 53.45, -592.25), Vector3(4.00, 0.70, 0.50), 1, 3.00, 1.00, 1.50], # mR3
|
||||
[Vector3(108.00, 52.25, -608.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 0.00], # mL4
|
||||
[Vector3(112.00, 52.25, -608.25), Vector3(4.00, 0.90, 0.50), 1, 3.00, 1.00, 1.50], # mR4
|
||||
]
|
||||
|
||||
# Вентиляционные потоки: центр решётки на полу, сечение (X, Z), высота столба
|
||||
const VENTS := [
|
||||
[Vector3(107.00, 43.80, -450.00), Vector2(6.00, 6.00), 14.00], # v_tn
|
||||
[Vector3(230.00, 23.80, -505.00), Vector2(6.00, 6.00), 15.00], # v1
|
||||
[Vector3(230.00, 36.30, -477.00), Vector2(6.00, 6.00), 15.00], # v2
|
||||
[Vector3(198.00, 48.80, -465.00), Vector2(6.00, 6.00), 15.00], # v3
|
||||
[Vector3(190.00, 61.30, -481.00), Vector2(6.00, 6.00), 15.00], # v4
|
||||
[Vector3(210.00, 23.80, -504.00), Vector2(8.00, 8.00), 53.00], # v_lift
|
||||
]
|
||||
|
||||
# Ускоряющие полосы: центр на полу, размер (ширина, длина), рыскание (направление разгона), скорость
|
||||
const STRIPS := [
|
||||
[Vector3(61.00, 62.00, -105.00), Vector2(6.00, 8.00), -45.0, 22.0], # s_p2a
|
||||
[Vector3(0.00, 60.00, -70.00), Vector2(6.00, 14.00), 0.0, 22.0], # s_b1
|
||||
[Vector3(9.00, 61.00, -105.00), Vector2(6.00, 16.00), -90.0, 22.0], # s_T2
|
||||
[Vector3(79.00, 52.80, -304.00), Vector2(5.00, 8.00), 0.0, 22.0], # s_ruin
|
||||
[Vector3(109.00, 51.80, -335.00), Vector2(6.00, 8.00), 0.0, 22.0], # s_LM
|
||||
[Vector3(107.00, 43.80, -408.00), Vector2(5.00, 10.00), 0.0, 22.0], # s_tn
|
||||
[Vector3(101.00, 48.20, -524.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw524
|
||||
[Vector3(101.00, 48.20, -548.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw548
|
||||
[Vector3(101.00, 48.20, -572.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw572
|
||||
[Vector3(101.00, 48.20, -596.00), Vector2(4.00, 12.00), 0.0, 22.0], # s_cw596
|
||||
[Vector3(149.00, 46.80, -649.00), Vector2(6.00, 14.00), -90.0, 22.0], # s_d1
|
||||
[Vector3(113.00, 51.80, -642.00), Vector2(6.00, 14.00), -90.0, 22.0], # s_q1
|
||||
[Vector3(202.00, 41.80, -652.00), Vector2(6.00, 14.00), 180.0, 22.0], # s_d3
|
||||
[Vector3(202.00, 34.80, -604.00), Vector2(6.00, 10.00), 180.0, 22.0], # s_d5
|
||||
[Vector3(200.00, 23.80, -530.00), Vector2(6.00, 10.00), 180.0, 22.0], # s_f0
|
||||
]
|
||||
|
||||
# Закрытые места без дождя (траншея, шахта): x0, x1, z0, z1 (Godot), пол, потолок
|
||||
const COVERED := [
|
||||
[102.0, 112.0, -408.0, -452.0, 42.8, 47.2], # траншея
|
||||
[204.0, 216.0, -510.0, -500.0, 22.8, 76.8], # шахта-лифт
|
||||
]
|
||||
|
||||
# Портал в хаб за финишем: позиция на полу площадки
|
||||
const PORTAL := Vector3(210.00, 73.80, -476.00)
|
||||
# Длина безопасного маршрута, м
|
||||
const TRACK_LENGTH := 1227.9
|
||||
# Срыв: на столько ниже последнего чекпоинта — возрождение
|
||||
const KILL_DEPTH := 32.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://c01qphr2te2ln
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
@@ -0,0 +1,41 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://fqq72yjnny11"
|
||||
path.s3tc="res://.godot/imported/sky.png-b8c1bf32feecd75c07d68b59edc4fccf.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/race2/sky.png"
|
||||
dest_files=["res://.godot/imported/sky.png-b8c1bf32feecd75c07d68b59edc4fccf.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Reference in New Issue
Block a user