extends SceneTree ## Identitäts-Vertrag (Karte t_1da96275 — „Editor-Playtest-Physik = Quest 3 ## identisch“): dieselbe Probe-Map läuft hier im EDITOR-Kontext (KolliderBau ## + PlaytestBody im SubViewport — exakt der Playtest-Aufbau des Editors) ## und in tests/test_identitaet_client.gd im QUEST-CLIENT-Kontext ## (SvmapWorld + TestmapPlayer). Der Runner (tests/run_identitaet.sh) ## verlangt IDENTISCHE Entscheidungen. ## ## Jede Probe druckt eine Zeile `IDENT =` — Werte sind ## grobe Entscheidungen (KLIMM/BLOCK/DRIN/DRAUSSEN) plus auf cm gerundete ## Höhen, damit beide Welten vergleichbar sind (gleiche Engine, gleiche ## Shapes → deterministisch gleich). ## ## Run: godot4 --headless --path . -s res://tests/test_playtest_identitaet.gd ## Map: $SVR_IDENTITAET_MAP (Default /tmp/svr_identitaet_probe.svmap, ## erzeugt von tests/gen_identitaet_map.gd). const HALF_H := 0.9 const TUER_AZIMUT_DEG := 202.5 # Befund Karte t_f9adf97c (building_001, rot 0) const GEB_TUER := Vector2(-16.0, -16.0) # building_001-Fußpunkt (x, z) const GEB_HALF := Vector2(8.94, 3.54) # AABB-Halbmaße (x, z) rot 0 var fails: Array[String] = [] func _initialize() -> void: _run.call_deferred() func _check(cond: bool, what: String) -> void: if cond: print("IDENT ok: ", what) else: fails.append(what) print("IDENT FAIL: ", what) func _run() -> void: var map_path := OS.get_environment("SVR_IDENTITAET_MAP") if map_path.is_empty(): map_path = "/tmp/svr_identitaet_probe.svmap" if not FileAccess.file_exists(map_path): printerr("IDENT FAIL: Probe-Map fehlt (%s) — erst gen_identitaet_map.gd laufen lassen." % map_path) quit(1) return var doc_v: Variant = JSON.parse_string(FileAccess.get_file_as_string(map_path)) if not (doc_v is Dictionary): printerr("IDENT FAIL: Probe-Map nicht lesbar.") quit(1) return var doc: Dictionary = doc_v # --- Editor-Playtest-Aufbau: SubViewport (own world) + KolliderBau + Body var vp := SubViewport.new() vp.own_world_3d = true root.add_child(vp) var welt := Node3D.new() vp.add_child(welt) welt.add_child(KolliderBau.baue_welt_kollider(doc)) var body: CharacterBody3D = load("res://core/editor/playtest/playtest_body.gd").new() welt.add_child(body) body.call("set_terrain", doc) await physics_frame await physics_frame # 1) Stufe 0.3 m (Terrain) — erklommen. var r := await _lauf(body, Vector3(-8.0, 0.0, 20.0), Vector3(1, 0, 0), 300) var st30 := "BLOCK" if r.max_feet >= 0.25 and r.end.x > 1.0: st30 = "KLIMM" _ident("stufe_terrain_030", "%s %.2f" % [st30, r.max_feet]) _check(st30 == "KLIMM", "Stufe 0.3 m erklommen (max feet %.2f, x %.2f)" % [r.max_feet, r.end.x]) # 2) Hohes Hindernis 0.85 m (barrel_001, Box-Kollider) — blockt: über der # Step-Up-Grenze 0.45 m gibt es KEIN Klettern (Karte t_f9adf97c). r = await _lauf(body, Vector3(0.5, 0.0, 20.0), Vector3(1, 0, 0), 240) var st80 := "KLIMM" if r.end.x < 1.55 and r.max_feet < 0.55: st80 = "BLOCK" _ident("wand_barrel", "%s %.2f x=%.2f" % [st80, r.max_feet, r.end.x]) _check(st80 == "BLOCK", "Hindernis 0.85 m blockt (max feet %.2f, x %.2f)" % [r.max_feet, r.end.x]) # 3) Kisten-Stufe 0.334 m (box_006) — erklommen (Step-Up über Objekt). r = await _lauf(body, Vector3(-24.0, 0.0, 8.0), Vector3(1, 0, 0), 260) var box := "BLOCK" if r.max_feet >= 0.28: box = "KLIMM" _ident("kisten_stufe", "%s %.2f" % [box, r.max_feet]) _check(box == "KLIMM", "Kiste 0.334 m erklommen (max feet %.2f)" % r.max_feet) # 4) Wand (barracks_001-Trimesh) — blockt. r = await _lauf(body, Vector3(20.0, 0.0, 31.5), Vector3(0, 0, -1), 300) var wand := "DURCH" if r.end.z > 29.3: wand = "BLOCK" _ident("wand_barracks", "%s z=%.2f" % [wand, r.end.z]) _check(wand == "BLOCK", "Kaserne blockt (Ende z=%.2f, Fläche bei 29.73)" % r.end.z) # 5) Tür (building_001) — durchlässig, Spieler landet IM Gebäude. var outward := Vector3(cos(deg_to_rad(TUER_AZIMUT_DEG)), 0.0, sin(deg_to_rad(TUER_AZIMUT_DEG))) var start := Vector3(GEB_TUER.x + outward.x * 12.0, 0.0, GEB_TUER.y + outward.z * 12.0) r = await _lauf(body, start, -outward, 600) var drin := absf(r.end.x - GEB_TUER.x) < GEB_HALF.x - 0.3 \ and absf(r.end.z - GEB_TUER.y) < GEB_HALF.y - 0.3 _ident("tuer_building", "%s %.2f %.2f" % ["DRIN" if drin else "DRAUSSEN", r.end.x, r.end.z]) _check(drin, "Tür building_001 durchlässig (Ende %.2f, %.2f — Gebäude x∈[%.1f,%.1f] z∈[%.1f,%.1f])" % [r.end.x, r.end.z, GEB_TUER.x - GEB_HALF.x, GEB_TUER.x + GEB_HALF.x, GEB_TUER.y - GEB_HALF.y, GEB_TUER.y + GEB_HALF.y]) # 6) Terrain-Höhe unter den Füßen (Strahl abwärts, exakt). _hoehe(body, Vector3(-10.0, 10.0, 0.0), "terrain_hoehe_a", 0.0) _hoehe(body, Vector3(4.0, 10.0, 20.0), "terrain_hoehe_b", 0.3) _hoehe(body, Vector3(10.0, 12.0, -28.0), "terrain_hoehe_c", 1.1) if fails.is_empty(): print("IDENT EDITOR: ALLE OK") quit(0) else: print("IDENT EDITOR: FAILED %d" % fails.size()) quit(1) func _ident(name: String, wert: String) -> void: print("IDENT %s=%s" % [name, wert]) ## Autopilot-Lauf — 1:1 zur Client-Seite: Start y=3.0 (Fall-Settle auf den ## Trimesh), dann Richtung fahren, max Füß-Höhe + Endposition messen. func _lauf(body: CharacterBody3D, start_xz: Vector3, dir: Vector3, frames: int) -> Dictionary: body.auto_move_dir = Vector3.ZERO body.velocity = Vector3.ZERO body.global_position = Vector3(start_xz.x, 3.0, start_xz.z) for i in 45: await physics_frame body.auto_move_dir = dir var max_feet := -INF for i in frames: await physics_frame max_feet = maxf(max_feet, body.fuss_hoehe()) if body.fuss_hoehe() < -5.0: break # unter die Welt — nichts mehr zu holen body.auto_move_dir = Vector3.ZERO for i in 10: await physics_frame return {"end": Vector3(body.global_position), "max_feet": max_feet} ## Boden-Strahl an einem Punkt → IDENT-Zeile + Soll-Prüfung (±1 cm). func _hoehe(body: CharacterBody3D, p: Vector3, name: String, soll: float) -> void: var raum := body.get_world_3d().direct_space_state var hit := raum.intersect_ray(PhysicsRayQueryParameters3D.create( p, p + Vector3(0, -30, 0))) var y := INF if not hit.is_empty(): y = (hit["position"] as Vector3).y if y == INF: _ident(name, "KEIN_TREFFER") _check(false, name + ": kein Boden-Treffer") return _ident(name, "%.2f" % y) _check(absf(y - soll) <= 0.01, "%s: %.3f ≈ Soll %.2f" % [name, y, soll])