README: заметки для будущего меня; тестовые сцены в tools/test

- Договорённости с владельцем, замеры мувсета (стенами высоту не набрать), исправление
  подтягивания, конвейер Parkour и его грабли, ловушка с ресурсами, загружаемыми по строке
- tools/test: measure_movement, mantle_test, parkour_gaps, parkour_tour, parkour_check.txt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxZo3ekR5m1af437Qzqwc1
This commit is contained in:
2026-10-28 15:05:29 +05:00
co-authored by Claude Sonnet 5.5
parent af462364c0
commit 556d3af615
14 changed files with 329 additions and 2 deletions
+51
View File
@@ -0,0 +1,51 @@
extends Node
# Подтягивание на уступы: тонкая полка и сплошной блок, коллизия «коробка» и «меш», ходьба и спринт, высоты 1,2–2,7 м.
# Запуск: godot --headless --path . res://tools/test/mantle_test.tscn -- --client (ожидание: везде «ДА»)
var root3d: Node3D
var p
# kind: "thin" (полка 0.6 м, под ней пусто) или "block" (сплошной блок от пола); shape: "box" или "mesh"
func build(kind: String, shape: String, h: float) -> void:
if root3d: root3d.queue_free()
await get_tree().physics_frame
root3d = Node3D.new(); add_child(root3d)
var sb := StaticBody3D.new(); root3d.add_child(sb)
var fl := CollisionShape3D.new(); var fb := BoxShape3D.new(); fb.size = Vector3(60, 1, 60); fl.shape = fb; fl.position = Vector3(0, -0.5, 0); sb.add_child(fl)
var size := Vector3(10, 0.6, 12) if kind == "thin" else Vector3(10, h, 12)
var cy := h - 0.3 if kind == "thin" else h * 0.5
var cs := CollisionShape3D.new()
if shape == "box":
var b := BoxShape3D.new(); b.size = size; cs.shape = b
else:
var m := BoxMesh.new(); m.size = size; cs.shape = m.create_trimesh_shape()
cs.position = Vector3(0, cy, -8 - size.z * 0.5) # передняя грань на z=-8
sb.add_child(cs)
func trial(kind: String, shape: String, h: float, sprint: bool) -> String:
await build(kind, shape, h)
p = (load("res://player/player.tscn") as PackedScene).instantiate()
root3d.add_child(p)
p.global_position = Vector3(0, 1.17, 0)
await get_tree().physics_frame
Input.action_press("move_forward")
if sprint: Input.action_press("sprint")
var f := 0; var jumped := false; var ok := false
while f < 420:
await get_tree().physics_frame; f += 1
var dz: float = p.global_position.z + 8.0 # до грани
if not jumped and dz < (-1.0 if not sprint else 0.0) + 2.4 and p.is_on_floor() and f > 30:
jumped = true; Input.action_press("jump")
if jumped and p.is_on_floor() and p.global_position.z < -8.2 and absf(p.global_position.y - (h + 1.16)) < 0.35:
ok = true; break
for a in ["move_forward","sprint","jump"]: Input.action_release(a)
var res := "ДА " if ok else "нет"
p.queue_free()
return res
func _ready() -> void:
print("высота уступа: 1.2 1.6 2.0 2.4 2.7")
for kind in ["thin", "block"]:
for shape in ["box", "mesh"]:
for sprint in [false, true]:
var row := "%-5s %-4s %-6s " % [kind, shape, "спринт" if sprint else "ходьба"]
for h in [1.2, 1.6, 2.0, 2.4, 2.7]:
row += await trial(kind, shape, h, sprint) + " "
print(row)
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://c1vd8asub3ox6
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/mantle_test.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+98
View File
@@ -0,0 +1,98 @@
extends Node
# Замеры мувсета ботом: дальность прыжка / двойного / рывка на спринте, бег по стене, прыжок от стены.
# Запуск: godot --headless --path . res://tools/test/measure_movement.tscn -- --client (~1 мин, печатает таблицу)
# Замеры мувсета: дальность прыжков, рывок, бег по стене, прыжок от стены
var root3d: Node3D
var p
func world(with_floor: bool, wall: bool) -> void:
if root3d: root3d.queue_free()
await get_tree().physics_frame
root3d = Node3D.new(); add_child(root3d)
var sb := StaticBody3D.new(); root3d.add_child(sb)
if with_floor:
var cs := CollisionShape3D.new(); var b := BoxShape3D.new(); b.size = Vector3(400, 1, 800); cs.shape = b; cs.position = Vector3(0, -0.5, -300); sb.add_child(cs)
if wall:
var cs2 := CollisionShape3D.new(); var b2 := BoxShape3D.new(); b2.size = Vector3(1, 400, 800); cs2.shape = b2; cs2.position = Vector3(-4.5, 0, -300); sb.add_child(cs2)
var cs3 := CollisionShape3D.new(); var b3 := BoxShape3D.new(); b3.size = Vector3(1, 400, 800); cs3.shape = b3; cs3.position = Vector3(4.5, 0, -300); sb.add_child(cs3)
func spawn(pos: Vector3, vel := Vector3.ZERO) -> void:
p = (load("res://player/player.tscn") as PackedScene).instantiate()
root3d.add_child(p)
p.global_position = pos
p.velocity = vel
func press(a: String) -> void: Input.action_press(a)
func release(a: String) -> void: Input.action_release(a)
func tap_dash() -> void:
var e := InputEventAction.new(); e.action = "dash"; e.pressed = true
Input.parse_input_event(e)
func release_all() -> void:
for a in ["move_forward","move_left","move_right","sprint","jump","crouch","dash"]: Input.action_release(a)
func frames(n: int) -> void:
for i in n: await get_tree().physics_frame
# прыжок с разбега по ровному полу: variant: single / dj / dash / djdash / dashdj
func jump_exp(variant: String) -> void:
await world(true, false)
spawn(Vector3(0, 1.17, 0))
await frames(5)
press("move_forward"); press("sprint")
await frames(60)
var z0: float = p.global_position.z
press("jump")
var maxy: float = 0.0; var f := 0; var did2 := false; var did3 := false; var lz := 0.0
while f < 400:
await get_tree().physics_frame; f += 1
maxy = maxf(maxy, p.global_position.y - 1.17)
if not did2 and p.velocity.y <= 0.0 and not p.is_on_floor():
did2 = true
if variant in ["dj", "djdash"]:
release("jump"); await get_tree().physics_frame; press("jump")
elif variant in ["dash", "dashdj"]:
tap_dash()
if did2 and not did3 and f > 40 and variant in ["djdash", "dashdj"]:
did3 = true
if variant == "djdash": tap_dash()
else: release("jump"); await get_tree().physics_frame; press("jump")
if f > 10 and p.is_on_floor():
lz = p.global_position.z; break
release_all()
print("прыжок %-7s: дальность %.1f м, высота %.2f м, время %.2f с" % [variant, z0 - lz, maxy, f / 60.0])
p.queue_free()
# бег по стене: летим вдоль левой стены со скоростью v, стена слева
func wall_exp(v: float, jump_after: float, then: String) -> void:
await world(false, true)
spawn(Vector3(-3.4, 100, 0), Vector3(0, 0, -v))
press("move_forward")
var z0: float = p.global_position.z
var f := 0; var run_f := 0; var started := false; var y_start := 0.0; var z_start := 0.0; var jumped := false; var x_max := -99.0; var z_j := 0.0; var y_j := 0.0
var landed_x := 0.0; var landed_z := 0.0; var t_j := 0
while f < 500:
await get_tree().physics_frame; f += 1
if p.is_wallrunning:
if not started: started = true; y_start = p.global_position.y; z_start = p.global_position.z
run_f += 1
if started and not jumped and (run_f / 60.0) >= jump_after:
jumped = true; z_j = p.global_position.z; y_j = p.global_position.y; t_j = f
release("move_forward"); press("move_forward")
press("jump"); await get_tree().physics_frame; release("jump")
if then == "dash": await frames(15); tap_dash()
if then == "dj": await frames(25); press("jump"); await get_tree().physics_frame; release("jump")
if jumped:
x_max = maxf(x_max, p.global_position.x)
if p.global_position.y < y_j - 3.0:
landed_x = p.global_position.x; landed_z = p.global_position.z; break
if not jumped and started and not p.is_wallrunning and f > 30:
break
release_all()
if jump_after < 0: pass
print("стена v=%.0f: бежали %.2f с, %.1f м вдоль, провал по y %.2f м; прыжок от стены (после %.1fс, затем %s): отлёт от стены до x=%.1f (стена x=-4), вперёд %.1f м, за %.2f с" % [v, run_f / 60.0, (z_start - (z_j if jumped else p.global_position.z)), y_start - (y_j if jumped else p.global_position.y), jump_after, then, x_max, z_j - landed_z, (f - t_j) / 60.0])
p.queue_free()
func _ready() -> void:
for v in ["single", "dj", "dash", "djdash", "dashdj"]:
await jump_exp(v)
await wall_exp(13.0, 99.0, "none")
await wall_exp(13.0, 0.5, "none")
await wall_exp(13.0, 0.5, "dash")
await wall_exp(13.0, 0.5, "dj")
get_tree().quit()
+1
View File
@@ -0,0 +1 @@
uid://ddgc0duo4j3mq
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/measure_movement.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+22
View File
@@ -0,0 +1,22 @@
# Проверка карты Parkour «по существу» после прохода через портал с настоящими пакетами.
# Вставь в tools/test/bot.gd ВРЕМЕННОЙ копии проекта (make_test_project.sh) перед строкой print("[%s] t=%4.1f сцена=...")
# и запусти: godot --headless --main-pack <site>/index.pck -- --client --name=T --pack-url=http://127.0.0.1:<порт>/ --tp=2:4.5,1,-8
# Ожидание: скрипт=true, шейдерных_мешей>=1, небо_панорама=true, музыка_играет=true, лазеров_и_ворот=10.
# Если в пакете не хватает файла, вместо этого будут false/0 (а имя сцены всё равно «Parkour»).
var sc := get_tree().current_scene
if sc != null and sc.name == "Parkour" and int(t) % 3 == 0:
var shaded := 0
for mi in sc.find_children("*", "MeshInstance3D", true, false):
if mi.get_surface_override_material_count() > 0 and mi.get_surface_override_material(0) is ShaderMaterial:
shaded += 1
var music := false
var areas := 0
for n in sc.get_children():
if n is AudioStreamPlayer and n.stream is AudioStreamOggVorbis and n.playing:
music = true
if n is Area3D and n.get_child_count() == 2:
areas += 1
var env := sc.find_children("*", "WorldEnvironment", false, false)
var sky_ok: bool = env.size() > 0 and env[0].environment.sky != null and env[0].environment.sky.sky_material is PanoramaSkyMaterial and env[0].environment.sky.sky_material.panorama != null
print(" ПРОВЕРКА Parkour: скрипт=%s шейдерных_мешей=%d небо_панорама=%s музыка_играет=%s лазеров_и_ворот=%d" % [sc.get_script() != null, shaded, sky_ok, music, areas])
+68
View File
@@ -0,0 +1,68 @@
extends Node
# Проходимость пропасти контейнеров на настоящей карте Parkour: прыжок у края, двойной прыжок на спуске, подруливание к цели.
# Запуск: godot --headless --path . res://tools/test/parkour_gaps.tscn -- --client (ожидание: все зазоры «ДА»)
# Стартовые точки заданы в коде — после правки layout.py обнови их
var level
var p
var f := 0
# (имя, x, z_старта(центр), z_края, ожидаемая посадка z-диапазон, высота пола)
var cases := [
["k0->k1 (18 м)", 4.0, -465.0, -475.0, 45.0],
["k2->k3 (22 м)", 2.0, -544.0, -549.0, 48.0] ,
]
func _ready() -> void:
level = (load("res://maps/parkour/parkour.tscn") as PackedScene).instantiate()
add_child(level)
p = level.get_node("Player")
p.set_physics_process(false)
await get_tree().physics_frame
for variant in ["dj", "dash+dj"]:
# имя, x старта, пол старта, z старта, z края, пол цели, z цели ближний, дальний, x цели
for c in [
["exit->k1 18", 4.0, 44.0, -446.0, -457.7, 45.0, -476.0, -488.0, 5.0],
["k1->k2 16", 0.0, 45.0, -479.0, -487.7, 46.5, -504.0, -516.0, -4.0],
["k2->k3 22", -2.5, 46.5, -507.0, -515.7, 48.0, -538.0, -550.0, -2.5],
["k3->k4 24", 7.0, 48.0, -541.0, -549.7, 49.0, -574.0, -586.0, 7.5],
["k4->k5 20", 7.0, 49.0, -577.0, -585.7, 50.5, -606.0, -618.0, 2.0],
["k5->k6 22", 2.0, 50.5, -609.0, -617.7, 52.0, -640.0, -656.0, 0.0]]:
await trial(c, variant)
get_tree().quit()
func key(a: String, down: bool) -> void:
if down: Input.action_press(a)
else: Input.action_release(a)
func trial(c: Array, variant: String) -> void:
p.set_physics_process(true)
p.global_position = Vector3(c[1], c[2] + 1.18, c[3])
p.velocity = Vector3.ZERO
p.is_dead = false
await get_tree().physics_frame
key("move_forward", true); key("sprint", true)
var fr := 0; var jumped := false; var did := 0; var res := "промах"
while fr < 360:
await get_tree().physics_frame; fr += 1
if not jumped and p.is_on_floor() and p.global_position.z < c[4] + 1.0 and fr > 20:
jumped = true; key("jump", true); did = 0
if jumped and not p.is_on_floor(): # подруливание к центру цели
var dx: float = c[8] - p.global_position.x
key("move_left", dx < -0.6); key("move_right", dx > 0.6)
if jumped and did == 0 and not p.is_on_floor() and p.velocity.y < 0.0:
did = fr
if variant == "dj" or variant == "dj+dash": key("jump", false); await get_tree().physics_frame; key("jump", true)
if variant == "dash+dj":
var e := InputEventAction.new(); e.action = "dash"; e.pressed = true; Input.parse_input_event(e)
if jumped and did > 0 and variant == "dj+dash" and fr == did + 8:
var e2 := InputEventAction.new(); e2.action = "dash"; e2.pressed = true; Input.parse_input_event(e2)
if jumped and did > 0 and variant == "dash+dj" and fr == did + 14:
key("jump", false); await get_tree().physics_frame; key("jump", true)
if jumped and fr > did + 25 and did > 0 and p.is_on_floor():
var z: float = p.global_position.z
if z < c[6] + 0.5 and z > c[7] - 0.5 and absf(p.global_position.y - (c[5] + 1.16)) < 0.3:
res = "ДА (z=%.1f)" % z
else:
res = "упал/на старте (z=%.1f y=%.1f)" % [z, p.global_position.y]
break
if p.global_position.y < c[2] - 25.0:
res = "упал вниз"; break
for a in ["move_forward","sprint","jump","move_left","move_right"]: key(a, false)
print("%-8s %-10s %s" % [variant, c[0], res])
p.set_physics_process(false)
+1
View File
@@ -0,0 +1 @@
uid://cfy83ai6f6hw5
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/parkour_gaps.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")
+26
View File
@@ -0,0 +1,26 @@
extends Node
# Облёт карты Parkour для скриншотов: телепорт игрока по точкам, кадры пишет Godot (--write-movie).
# Запуск (окно мелькнёт):
# godot --path . --resolution 1152x648 --fixed-fps 10 --write-movie /tmp/shots/p.png --quit-after 42 res://tools/test/parkour_tour.tscn -- --client
# Кадры: p00000005.png и т. д.; точки — массив spots (позиция в координатах Godot, рыскание, наклон головы)
var level
var p
var frame := 0
# позиция центра капсулы (Godot), рыскание в градусах, наклон головы в градусах
var spots := [
[Vector3(0, 1.25, 6), 0, 6], [Vector3(0, 11.0, -196), 0, 3], [Vector3(-3.4, 10.0, -222), 0, 0], [Vector3(2, 14.0, -310), 0, 20],
[Vector3(0, 46.0, -352), 0, 3], [Vector3(0, 46.0, -400), 0, 0], [Vector3(0, 49.0, -560), 0, 6], [Vector3(0, 76.0, -672), 0, 14],
]
func _ready() -> void:
level = (load("res://maps/parkour/parkour.tscn") as PackedScene).instantiate()
add_child(level)
p = level.get_node("Player")
p.set_physics_process(false)
func _process(_d: float) -> void:
frame += 1
var i := int(frame / 5)
if i < spots.size() and frame % 5 == 1:
var s = spots[i]
p.global_position = s[0]
p.rotation.y = deg_to_rad(s[1])
p.head.rotation.x = deg_to_rad(s[2])
+1
View File
@@ -0,0 +1 @@
uid://drhimdbliiox8
+6
View File
@@ -0,0 +1,6 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://tools/test/parkour_tour.gd" id="1"]
[node name="T" type="Node"]
script = ExtResource("1")