- maps/race: трасса 633 м из Blender (три развилки: срез по контейнерам, каньон со стенами против нижней дороги, труба с лазерами против коридора), запечённый свет, небо, дождь, гул; фоновая песня (Ogg Vorbis, ~0,6 МБ) включается на «СТАРТ!» - Правила (race_rules.gd): R — готов; когда готовы все — отсчёт 6 с, заморозка в слотах, ворота строго по порядку, сплиты, место, итоги, личный рекорд (user://race_best.cfg) - Сеть: Net.submit_race / recv_race / race_event (RACE_READY, START, CP, FINISH, STATE). ВНИМАНИЕ: набор RPC изменён — клиент и сервер выкладываются вместе, старый клиент с новым сервером не работает (rpc node checksum failed) - player.gd: input_frozen и place() для режимов карт; project.godot: действие ready (R) - Портал RACE в хабе, пресет экспорта Race (include_filter для ресурсов, грузимых строкой) - tools/test: двухклиентный тест гонки на локальном сервере, бот, облёт, проверка пакета - README: правила гонки, события, проверки, заметки Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
225 lines
9.1 KiB
GDScript
225 lines
9.1 KiB
GDScript
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>: после ворот n умереть (проверка возрождения лицом по трассе)
|
||
# --fall-at-gate=<n>: после ворот 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
|