extends Node # Бот для проверки режима «Гонка» (автозагрузка RaceBot во ВРЕМЕННОЙ копии проекта, см. tools/test/race_game_test.sh). # Сам «жмёт R» (действие ready через Input.parse_input_event), на отсчёте пытается идти (проверка заморозки), # в гонке бежит безопасный маршрут race_route.json (Blender-координаты; идея — бот-бегун из Blender models/race_01/tests/runner.gd). # Аргументы (после --): --name=A --delay=<с задержки старта бега> --expect=<сколько игроков ждать перед «R»> # --rounds=<сколько кругов пройти> --quit-race-time=<выйти из игры на этой секунде гонки> # --die-at-gate=: после ворот n умереть (проверка возрождения лицом по трассе) # --fall-at-gate=: после ворот n упасть в пустоту (проверка срыва ниже ворот) # Факты печатаются строками «[имя] …»; сверяет их tools/test/race_game_test.sh. const Data = preload("res://maps/race/race_data.gd") var label := "?" var delay := 0.0 var expect := 2 var rounds := 1 var quit_race_time := -1.0 var die_gate := -1 var fall_gate := -1 var route: Array = [] var wi := 1 var jumped_at := -100 var fr := 0 var rules var p var last_phase := -1 var rounds_done := 0 var last_press := -10.0 var t := 0.0 var freeze_start := Vector3.ZERO var special_done := false var special_wait := 0 var special_expect := Vector3.ZERO var stuck_fr := 0 var spectator_logged := false var music_logged := false var music_cd_logged := false var freeze_logged := false func _ready() -> void: for a in OS.get_cmdline_user_args(): 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)) if a.begins_with("--rounds="): rounds = int(a.substr(9)) if a.begins_with("--quit-race-time="): quit_race_time = float(a.substr(17)) if a.begins_with("--die-at-gate="): die_gate = int(a.substr(14)) if a.begins_with("--fall-at-gate="): fall_gate = int(a.substr(15)) 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]) func key(a: String, down: bool) -> void: if down: Input.action_press(a) else: Input.action_release(a) func ray(from: Vector3, to: Vector3) -> Dictionary: var q := PhysicsRayQueryParameters3D.create(from, to) q.exclude = [p.get_rid()] return p.get_world_3d().direct_space_state.intersect_ray(q) func say(text: String) -> void: print("[%s] %s" % [label, text]) func _physics_process(_delta: float) -> void: var sc := get_tree().current_scene if sc == null or sc.name != "Race": return rules = sc.get_node_or_null("Rules") p = get_tree().get_first_node_in_group("local_player") if rules == null or p == null: return fr += 1 t += _delta var ph: int = rules.phase if ph != last_phase: say("ФАЗА %s, unix=%.3f, место_в_слоте=(%.1f, %.1f, %.1f)" % [rules.Phase.keys()[ph], Time.get_unix_time_from_system(), p.global_position.x, p.global_position.y, p.global_position.z]) if ph == rules.Phase.COUNTDOWN: freeze_start = p.global_position if ph == rules.Phase.WAIT and last_phase == rules.Phase.RESULTS: rounds_done += 1 say("КРУГОВ_ЗАВЕРШЕНО %d" % rounds_done) if rounds_done >= rounds: say("ГОТОВО, выхожу") get_tree().quit() if ph == rules.Phase.RACE: wi = 1 special_done = false music_logged = false music_cd_logged = false freeze_logged = false say("УЧАСТНИК=%s" % rules.participants.has(rules.my_id)) last_phase = ph match ph: rules.Phase.WAIT: _wait() rules.Phase.COUNTDOWN: # пытаемся бежать: заморозка не должна пускать key("move_forward", true) key("sprint", true) key("jump", true) if (Time.get_ticks_msec() + 3000) >= rules.start_tick and not music_cd_logged: music_cd_logged = true say("МУЗЫКА на отсчёте: %s" % _music_playing()) if (Time.get_ticks_msec() + 300) >= rules.start_tick and not freeze_logged: freeze_logged = true say("ЗАМОРОЗКА: сдвиг за отсчёт %.3f м (по горизонтали)" % Vector2(p.global_position.x - freeze_start.x, p.global_position.z - freeze_start.z).length()) rules.Phase.RACE: if not music_logged and rules.race_time() > 1.0: music_logged = true say("МУЗЫКА после старта: играет=%s" % _music_playing()) _race() _: for k in ["move_forward", "sprint", "jump"]: key(k, false) func _wait() -> void: for k in ["move_forward", "sprint", "jump"]: key(k, false) if rules.waiting_for_race(): if not spectator_logged: spectator_logged = true say("ГОНКА_ИДЁТ: жду следующий круг (участник=%s)" % rules.participants.has(rules.my_id)) return if rules.flags_ready.get(rules.my_id, false): return if Net.ghosts.size() < expect - 1 or t - last_press < 1.5: return last_press = t spectator_logged = false var ev := InputEventAction.new() ev.action = "ready" ev.pressed = true Input.parse_input_event(ev) say("жму R, видно игроков: %d" % (Net.ghosts.size() + 1)) func _race() -> void: if not rules.participants.has(rules.my_id): return # бегаем свободно без зачёта — бот просто стоит if quit_race_time >= 0.0 and rules.race_time() >= quit_race_time: say("ВЫХОЖУ из игры в разгар гонки (время %.1f)" % rules.race_time()) get_tree().quit() return if rules.finish.has(rules.my_id): for k in ["move_forward", "sprint", "jump"]: key(k, false) return if rules.race_time() < delay: return # --- проверки возрождения --- var g: int = rules.passed.get(rules.my_id, 0) if not special_done and die_gate >= 0 and g == die_gate and not p.is_dead: special_done = true special_wait = 1 say("УМИРАЮ после ворот %d" % g) p.die() elif not special_done and fall_gate >= 0 and g == fall_gate and not p.is_dead: special_done = true special_wait = 1 say("ПАДАЮ в пустоту после ворот %d" % g) p.global_position.y -= 45.0 p.velocity = Vector3.ZERO if special_wait > 0: if p.is_dead: special_wait = 2 elif special_wait == 2: special_wait = 0 var f: Vector3 = -p.global_transform.basis.z var ex: float = rules.map.gate_yaw(g - 1) var want := Vector3(-sin(ex), 0, -cos(ex)) say("ВОЗРОЖДЕНИЕ у ворот %d: взгляд (%.2f, %.2f), ожидался (%.2f, %.2f), позиция (%.1f, %.1f, %.1f), ворота (%.1f, %.1f, %.1f) -> %s" % [ g, f.x, f.z, want.x, want.z, p.global_position.x, p.global_position.y, p.global_position.z, rules.map.gate_pos(g - 1).x, rules.map.gate_pos(g - 1).y, rules.map.gate_pos(g - 1).z, "ВЕРНО" if Vector2(f.x, f.z).distance_to(Vector2(want.x, want.z)) < 0.05 and p.global_position.distance_to(rules.map.gate_pos(g - 1)) < 1.0 else "ОШИБКА"]) return _drive() func _drive() -> void: 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()]) var tgt := gp(route[wi]) var to := Vector2(tgt.x - pos.x, tgt.z - pos.z) var seg_a := gp(route[wi - 1]) var seg := Vector2(tgt.x - seg_a.x, tgt.z - seg_a.z) var reached := to.length() < 2.5 or (to.dot(seg) < 0.0 and to.length() < 8.0) if _at_gate(tgt): # точка маршрута на воротах: считается взятой, только когда плоскость ворот уже пересечена reached = to.dot(seg) < 0.0 and to.length() < 12.0 if reached and wi < route.size() - 1: wi += 1 tgt = gp(route[wi]) to = Vector2(tgt.x - pos.x, tgt.z - pos.z) if p.is_dead: return key("move_forward", true) key("sprint", true) var dir := to.normalized() p.rotation.y = atan2(-dir.x, -dir.y) # прыжок: край впереди или препятствие var jump := false if p.is_on_floor() and fr - jumped_at > 12: var ahead := Vector3(dir.x, 0, dir.y) * (0.5 + Vector2(p.velocity.x, p.velocity.z).length() * 0.045) var probe := pos + ahead if ray(probe, probe + Vector3(0, -2.6, 0)).is_empty(): jump = true if not ray(pos + Vector3(0, -0.5, 0), pos + Vector3(0, -0.5, 0) + Vector3(dir.x, 0, dir.y) * 1.4).is_empty(): jump = true if jump: jumped_at = fr key("jump", jump or fr - jumped_at < 22) func _music_playing() -> bool: for n in get_tree().current_scene.get_children(): if n is AudioStreamPlayer and n.stream is AudioStreamOggVorbis and n.playing: return true return false func _at_gate(w: Vector3) -> bool: for g in Data.CHECKPOINTS: if Vector2(g[0].x - w.x, g[0].z - w.z).length() < 3.0: return true return false