scopedvr-map-editor/tests/test_playtest_identitaet.gd

164 lines
6.3 KiB
GDScript

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 <name>=<wert>` — 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://src/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])