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:
2026-10-29 17:21:07 +05:00
co-authored by Claude Sonnet 5.5
parent b58c30a525
commit c3e0ae40b2
27 changed files with 1017 additions and 11 deletions
+54 -3
View File
@@ -8,9 +8,13 @@ extends Node
# --fall-at-gate=<n>: после ворот n упасть в пустоту (проверка срыва ниже ворот)
# --gu1=<план>, --gu2=<план>: сдача в круге 1 / 2. План: «cancel@1,giveup@2» — когда пройдено столько ворот,
# держать G 1,0 с и отпустить (отмена) / держать до сдачи (проверка race_giveup_test.sh)
# --map=race2: карта «Гонка 2». Бежать по её маршруту без физики бегуна нельзя (вентиляторы, лазеры, стены), поэтому бот
# «проводит» игрока через ворота телепортами по ~0,35 м за кадр (меньше TELEPORT_JUMP: правила считают это бегом); проверяются правила, а не проходимость
# (проходимость карты — боты в Blender models/race_02/tests/).
# Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh.
const Data = preload("res://maps/race/race_data.gd")
var Data = null # данные карты: race_data.gd или (--map=race2) race2_data.gd
var map_id := "race"
var label := "?"
var delay := 0.0
@@ -46,6 +50,7 @@ var gu_after_cancel := false
func _ready() -> void:
for a in OS.get_cmdline_user_args():
if a.begins_with("--map="): map_id = a.substr(6)
if a.begins_with("--name="): label = a.substr(7)
if a.begins_with("--delay="): delay = float(a.substr(8))
if a.begins_with("--expect="): expect = int(a.substr(9))
@@ -57,7 +62,11 @@ func _ready() -> void:
for e in a.substr(6).split(","):
var q := e.split("@")
gu_plan[int(a.substr(4, 1))].append({"mode": q[0], "gate": int(q[1])})
route = JSON.parse_string(FileAccess.get_file_as_string("res://tools/test/race_route.json")).safe
if map_id == "race2":
Data = load("res://maps/race2/race2_data.gd")
else:
Data = load("res://maps/race/race_data.gd")
route = JSON.parse_string(FileAccess.get_file_as_string("res://tools/test/race_route.json")).safe
func gp(w: Array) -> Vector3:
return Vector3(w[0], w[2], -w[1])
@@ -76,7 +85,7 @@ func say(text: String) -> void:
func _physics_process(_delta: float) -> void:
var sc := get_tree().current_scene
if sc == null or sc.name != "Race":
if sc == null or sc.name != ("Race2" if map_id == "race2" else "Race"):
return
rules = sc.get_node_or_null("Rules")
p = get_tree().get_first_node_in_group("local_player")
@@ -220,7 +229,49 @@ func _race() -> void:
return
_drive()
func _drive_tele() -> void:
var g: int = rules.passed.get(rules.my_id, 0)
if g >= Data.CHECKPOINTS.size() or p.is_dead:
return
var gate: Array = Data.CHECKPOINTS[g]
var yaw := deg_to_rad(gate[2])
var fwd := Vector3(-sin(yaw), 0, -cos(yaw))
var tgt: Vector3 = gate[0] + fwd * 1.5
var step := 0.35 # м за кадр (~21 м/с): круг ~60 с, как у настоящей гонки — иначе зритель C не успеет зайти в разгар круга
# лазерный мост: идём медленно (12 м/с) и на ближних подступах к воротам выбираем полосу, где лазер выключен в момент пролёта
var pos: Vector3 = p.global_position
var lane := -1.0
if pos.x > 105.0 and pos.x < 115.0 and pos.z < -535.0 and pos.z > -615.0:
step = 0.2
for l in Data.LASERS:
if l[0].x < 110.0: # левая половина; правая — в паре
var dz: float = pos.z - l[0].z # > 0 — ворота ещё впереди (идём в -Z)
if dz > -1.0 and dz < 8.0:
var t := Time.get_unix_time_from_system() + maxf(dz, 0.0) / 12.0
var left_on: bool = fposmod(t + l[5], l[3]) < l[4] or fposmod(t + 0.4 + l[5], l[3]) < l[4] or fposmod(t - 0.4 + l[5], l[3]) < l[4]
var right_on: bool = fposmod(t + 1.5 + l[5], l[3]) < l[4] or fposmod(t + 1.9 + l[5], l[3]) < l[4] or fposmod(t + 1.1 + l[5], l[3]) < l[4]
lane = 112.0 if left_on else 108.0
if left_on and right_on:
lane = -2.0 # обе полосы заняты — стоим
if lane == -2.0:
return
if lane > 0.0:
p.global_position.x = lane
pos.x = lane
tgt.x = lane
var d: Vector3 = tgt - pos
if tgt.y - pos.y > 2.0: # ворота выше (крыша башни): сначала поднимаемся вертикально — иначе упрёмся в стену
d = Vector3(0, minf(step, tgt.y - pos.y + 1.5), 0)
elif d.length() > step:
d = d.normalized() * step
p.global_position = pos + d
p.velocity = Vector3.ZERO
p.rotation.y = yaw
func _drive() -> void:
if map_id == "race2":
_drive_tele()
return
var pos: Vector3 = p.global_position
if fr % 120 == 0 and OS.get_cmdline_user_args().has("--bot-log"):
say("бег: wi=%d поз=(%.1f, %.1f, %.1f) v=(%.1f, %.1f) пол=%s" % [wi, pos.x, pos.y, pos.z, p.velocity.x, p.velocity.z, p.is_on_floor()])