533 lines
20 KiB
GDScript
533 lines
20 KiB
GDScript
extends SceneTree
|
||
## Editor-Test (headless): Karte t_1cdc1a72 + Physik-Neubau t_1da96275 —
|
||
## Lauf-Modus IM Map-Builder selbst mit ECHTER Physik (CharacterBody3D +
|
||
## Trimesh-Kollider via KolliderBau, identisch zum Quest-Client).
|
||
## Ausführen: godot4 --headless --path . --import && godot4 --headless --path . --script res://tests/test_playtest.gd
|
||
## Exit 0 = alles grün.
|
||
##
|
||
## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und deckt ab:
|
||
## 1. Start/Zurück-Zustand: Modus-Flags, Panels aus/an, Maus-Capture-Wunsch,
|
||
## Spieler = CharacterBody3D mit Kapsel 0.35 × 1.8 (TestmapPlayer-Params),
|
||
## Kollider-Baum existiert (Terrain + Objekte), Kamera übernimmt.
|
||
## 2. Bodenhöhe == Terrainhöhe an der Spielerposition (Physik-Landung auf
|
||
## dem Trimesh == SvmapDocument-Decoder am selben Dokument).
|
||
## 3. Echte Wand blockt den Spieler (StaticBody in der Physik-Welt).
|
||
## 4. Step-Up: Kiste 0.3 m wird erklommen, Hindernis 0.85 m blockt
|
||
## (Stufen-Semantik 1:1 wie TestmapPlayer, Karte t_f9adf97c).
|
||
## 5. Gehen 6 m/s / Rennen 14 m/s / Springen (Client-Geschwindigkeiten).
|
||
## 6. Rückkehr stellt den Editor-Zustand VOLLSTÄNDIG wieder her.
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
var main: Control
|
||
var camera: Camera3D
|
||
var subvp: SubViewport
|
||
|
||
const HALF_H := 0.9
|
||
|
||
|
||
func _initialize() -> void:
|
||
_run_all.call_deferred()
|
||
|
||
|
||
func _run_all() -> void:
|
||
var scene: PackedScene = load("res://core/scenes/main.tscn")
|
||
_check(scene != null, "scenes/main.tscn laedt")
|
||
if scene == null:
|
||
_finish()
|
||
return
|
||
main = scene.instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
await process_frame
|
||
|
||
camera = main.get("camera")
|
||
_check(camera != null, "Editor-Kamera gefunden")
|
||
var container := _find_viewport_container(main)
|
||
if container != null:
|
||
subvp = container.get_child(0) as SubViewport
|
||
_check(subvp != null, "SubViewport gefunden")
|
||
|
||
# Echte Map anlegen (Neu-Dialog-Konvention wie _on_new_map_confirmed).
|
||
var doc: Dictionary = SvmapDocument.create_new("Testlauf-Karte", 100.0, "klar")
|
||
main.set("doc", doc)
|
||
main.set("current_path", "")
|
||
main.set("dirty", false)
|
||
main.get("history").begin(doc)
|
||
main.call("_clear_edit_state")
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
|
||
_test_splitter_lagen_merken()
|
||
_test_start_state()
|
||
_test_splitter_lagen_verbiegen()
|
||
await _test_terrain_height_matches_document()
|
||
await _test_wall_blocks_player()
|
||
await _test_step_up_semantics()
|
||
await _test_run_and_jump()
|
||
await _test_yaw_relatives_laufen()
|
||
await _test_bauteile_treppe_tuer()
|
||
_test_exit_restores_editor()
|
||
await _test_splitter_lagen_pruefen()
|
||
|
||
_finish()
|
||
|
||
|
||
func _finish() -> void:
|
||
if failures == 0:
|
||
print("ALL OK: %d checks" % checks)
|
||
quit(0)
|
||
else:
|
||
print("FAILED: %d/%d checks" % [failures, checks])
|
||
quit(1)
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
if cond:
|
||
print(" ok: %s" % label)
|
||
else:
|
||
failures += 1
|
||
print(" FAIL: %s" % label)
|
||
|
||
|
||
func _find_viewport_container(node: Node) -> SubViewportContainer:
|
||
if node is SubViewportContainer:
|
||
return node as SubViewportContainer
|
||
for child in node.get_children():
|
||
var found := _find_viewport_container(child)
|
||
if found != null:
|
||
return found
|
||
return null
|
||
|
||
|
||
## PlaytestMode-Instanz der aktuellen Karte (main.playtest).
|
||
func _pt() -> Node:
|
||
return main.get("playtest")
|
||
|
||
|
||
func _body() -> Node:
|
||
var pt := _pt()
|
||
if pt == null:
|
||
return null
|
||
return pt.get("body")
|
||
|
||
|
||
## Statische Box in die Playtest-Physik-Welt hängen (wie im Spiel).
|
||
func _box(welt: Node3D, center: Vector3, size: Vector3) -> StaticBody3D:
|
||
var body := StaticBody3D.new()
|
||
var cs := CollisionShape3D.new()
|
||
var shape := BoxShape3D.new()
|
||
shape.size = size
|
||
cs.shape = shape
|
||
body.add_child(cs)
|
||
body.position = center
|
||
welt.add_child(body)
|
||
return body
|
||
|
||
|
||
## --- Test 1: Start/Zurück-Zustand -------------------------------------------
|
||
func _test_start_state() -> void:
|
||
print("Testgruppe: Start-Zustand (Modus-Flags, Panels, Maus, Kamera, Kollider)")
|
||
main.call("_on_test_run")
|
||
await process_frame
|
||
|
||
var pt := _pt()
|
||
_check(pt != null, "PlaytestMode angelegt (main.playtest)")
|
||
if pt == null:
|
||
return
|
||
_check(bool(pt.get("active")), "Modus-Flag active == true")
|
||
_check(pt.get("body") != null, "Spieler-Body existiert")
|
||
_check(pt.wants_capture(), "Maus-Capture-Wunsch gesetzt")
|
||
|
||
# Panels AUS: Top-Bar, linke/rechte Spalte, Statusleiste.
|
||
for pair in [["ui_top_bar", "Top-Bar"], ["ui_left_panel", "linke Spalte"],
|
||
["ui_right_panel", "rechte Spalte"], ["ui_status_bar", "Statusleiste"]]:
|
||
var panel: Control = main.get(pair[0])
|
||
_check(panel != null and not panel.visible, "%s ausgeblendet" % pair[1])
|
||
|
||
# Spieler lebt in der 3D-Welt (world3d) und ist an den PlaytestMode gehängt.
|
||
var body: Node = pt.get("body")
|
||
_check(body is CharacterBody3D, "Body ist ein CharacterBody3D (echte Physik)")
|
||
_check(body.get_parent() == pt, "Body ist Kind des PlaytestMode")
|
||
_check(pt.get_parent() == main.get("world3d"), "PlaytestMode lebt im world3d")
|
||
|
||
# Kollider-Baum: Terrain + (leere) Objektliste — mindestens Terrain drin.
|
||
var kb: Node3D = pt.get("_kollider_root")
|
||
_check(kb != null and kb.get_child_count() >= 1,
|
||
"Kollider-Baum existiert (Terrain-Kollider drin, %d Kinder)" %
|
||
(kb.get_child_count() if kb != null else 0))
|
||
var tbody := kb.get_node_or_null("TerrainKollider") if kb != null else null
|
||
_check(tbody != null, "Terrain-Kollider (Trimesh wie im Client) vorhanden")
|
||
|
||
# Kapsel-Parameter 1:1 wie TestmapPlayer.
|
||
var capsule_ok := false
|
||
for child in body.get_children():
|
||
var cs := child as CollisionShape3D
|
||
if cs != null and cs.shape is CapsuleShape3D:
|
||
var cap := cs.shape as CapsuleShape3D
|
||
capsule_ok = is_equal_approx(cap.radius, 0.35) and is_equal_approx(cap.height, 1.8)
|
||
_check(capsule_ok, "Kapsel 0.35 × 1.8 m (identisch zum Testmap-Spieler)")
|
||
|
||
# Spiel-Kamera ist current, Editor-Kamera abgegeben.
|
||
var body_cam: Camera3D = body.get("look_cam")
|
||
_check(body_cam != null and body_cam.current, "Spiel-Kamera ist current")
|
||
_check(not camera.current, "Editor-Kamera abgegeben")
|
||
|
||
# Start = erster Spawnpunkt (Team Blau) — sonst Kartenmitte.
|
||
var doc: Dictionary = main.get("doc")
|
||
var zones: Dictionary = doc.get("spawn_zones", {})
|
||
var team0: Dictionary = zones.get("0", {})
|
||
var pts: Array = team0.get("points", [])
|
||
if pts.is_empty():
|
||
_check(false, "Testmap braucht Spawn-Punkte (Team 0)")
|
||
return
|
||
var first: Dictionary = pts[0]
|
||
var pos: Array = first.get("pos", [0.0, 0.0, 0.0])
|
||
var expected := Vector3(float(pos[0]), 0.0, float(pos[2]))
|
||
var body_pos: Vector3 = body.position
|
||
_check(is_equal_approx(body_pos.x, expected.x) and is_equal_approx(body_pos.z, expected.z),
|
||
"Start == erster Spawnpunkt (%.1f, %.1f)" % [expected.x, expected.z])
|
||
|
||
# Kein History-Eintrag durch den Start (Dokument/Undo-Stapel unangetastet).
|
||
var hist: Variant = main.get("history")
|
||
_check(not hist.can_undo(), "Kein Undo-Eintrag durch den Testlauf-Start")
|
||
|
||
|
||
## --- Test 2: Bodenhöhe == Terrainhöhe ---------------------------------------
|
||
func _test_terrain_height_matches_document() -> void:
|
||
print("Testgruppe: Bodenhöhe == Terrainhöhe (Physik-Landung auf dem Trimesh)")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung Test 1)")
|
||
return
|
||
var body: Node = pt.get("body")
|
||
var doc: Dictionary = main.get("doc")
|
||
# Gravitation lässt den Spieler auf den Terrain-Trimesh fallen.
|
||
for i in 45:
|
||
await physics_frame
|
||
var feet_y: float = body.fuss_hoehe()
|
||
var terrain_y: float = SvmapDocument.terrain_height_at(doc, body.position.x, body.position.z)
|
||
var body_terrain: float = body.call("terrain_height", body.position.x, body.position.z)
|
||
_check(absf(feet_y - terrain_y) <= 0.05,
|
||
"Fusspunkt == Terrainhöhe am Spielerort (%.3f ≈ %.3f)" % [feet_y, terrain_y])
|
||
_check(is_equal_approx(body_terrain, terrain_y),
|
||
"Body-Decoder == SvmapDocument-Decoder (%.3f == %.3f)" % [body_terrain, terrain_y])
|
||
|
||
|
||
## --- Test 3: Echte Wand blockiert den Spieler -------------------------------
|
||
func _test_wall_blocks_player() -> void:
|
||
print("Testgruppe: Echte Wand blockt den Spieler (StaticBody in der Physik-Welt)")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||
return
|
||
var body: Node = pt.get("body")
|
||
var welt: Node3D = main.get("world3d")
|
||
var start_x: float = body.position.x
|
||
var start_z: float = body.position.z
|
||
# Wand VOR dem Spieler: Fläche bei start_x + 2.4 (Lauf +x).
|
||
var wall := _box(welt, Vector3(start_x + 2.7, 0.0, start_z), Vector3(0.6, 10.0, 4.0))
|
||
body.set("yaw_deg", -90.0) # Blick nach +x (Konsistenz mit dem Lauf)
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
for i in 90:
|
||
await physics_frame
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
for i in 10:
|
||
await physics_frame
|
||
# Abstand: führende Kante des Spielers (x + radius) zur Wand-Fläche.
|
||
var dist_to_wall: float = (start_x + 2.4) - (float(body.position.x) + 0.35)
|
||
_check(dist_to_wall >= -0.05,
|
||
"Spieler hält vor der Wand (Abstand %.2f m >= 0)" % dist_to_wall)
|
||
_check(absf(float(body.position.z) - start_z) < 0.01, "Kein Drift in Z")
|
||
_check(absf(float(body.position.x) - start_x) > 0.5, "Gegangen, nicht sofort blockiert")
|
||
wall.queue_free()
|
||
for i in 5:
|
||
await physics_frame
|
||
|
||
|
||
## --- Test 4: Step-Up-Semantik (1:1 TestmapPlayer) ---------------------------
|
||
func _test_step_up_semantics() -> void:
|
||
print("Testgruppe: Step-Up — Kiste 0.3 m erklommen, Hindernis 0.85 m blockt")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||
return
|
||
var body: Node = pt.get("body")
|
||
var welt: Node3D = main.get("world3d")
|
||
var sx: float = body.position.x
|
||
var sz: float = body.position.z
|
||
|
||
# a) Kiste 0.3 m (unter Step-Up-Grenze 0.45) → raufsteigen (Lauf +x).
|
||
var kiste := _box(welt, Vector3(sx + 3.0, 0.15, sz), Vector3(1.0, 0.3, 2.0))
|
||
body.set("yaw_deg", -90.0)
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
var feet_max := -INF
|
||
for i in 180:
|
||
await physics_frame
|
||
feet_max = maxf(feet_max, body.fuss_hoehe())
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
for i in 10:
|
||
await physics_frame
|
||
_check(feet_max >= 0.25, "Kiste 0.3 m erklommen (max feet %.2f)" % feet_max)
|
||
kiste.queue_free()
|
||
for i in 5:
|
||
await physics_frame
|
||
|
||
# b) Fass 0.85 m (über der Grenze) → blockt, kein Cheat-Klettern.
|
||
var barrel_001 := _box(welt, Vector3(sx + 3.0, 0.425, sz), Vector3(0.617, 0.85, 0.617))
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
var feet_max2 := -INF
|
||
for i in 180:
|
||
await physics_frame
|
||
feet_max2 = maxf(feet_max2, body.fuss_hoehe())
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
for i in 10:
|
||
await physics_frame
|
||
_check(feet_max2 < 0.55, "Hindernis 0.85 m blockt (max feet %.2f < 0.55)" % feet_max2)
|
||
barrel_001.queue_free()
|
||
for i in 5:
|
||
await physics_frame
|
||
|
||
|
||
## --- Test 5: Gehen/Rennen/Springen (Client-Geschwindigkeiten) ---------------
|
||
func _test_run_and_jump() -> void:
|
||
print("Testgruppe: Gehen 6 m/s, Rennen 14 m/s, Springen (Client-Parameter)")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||
return
|
||
var body: Node = pt.get("body")
|
||
|
||
# Gehen: 1 s bei 6 m/s → ~6 m Wegstrecke.
|
||
var start := Vector3(body.position)
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
for i in 60:
|
||
await physics_frame
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
var walk_dist: float = (Vector3(body.position) - start).length()
|
||
_check(walk_dist > 5.0 and walk_dist < 7.0,
|
||
"Gehen ≈ 6 m/s (gemessen %.2f m in 1 s)" % walk_dist)
|
||
|
||
# Rennen: gleiche Zeit, deutlich mehr Strecke (14 m/s).
|
||
var start2 := Vector3(body.position)
|
||
body.set("auto_run", true)
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
for i in 60:
|
||
await physics_frame
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
body.set("auto_run", false)
|
||
var run_dist: float = (Vector3(body.position) - start2).length()
|
||
_check(run_dist > 12.0 and run_dist < 16.0,
|
||
"Rennen ≈ 14 m/s (gemessen %.2f m in 1 s)" % run_dist)
|
||
|
||
# Springen: y-Impuls, Spieler verlässt den Boden und landet wieder.
|
||
var feet0: float = body.fuss_hoehe()
|
||
var airborne := false
|
||
body.set("auto_jump", true)
|
||
for i in 90:
|
||
await physics_frame
|
||
if body.fuss_hoehe() > feet0 + 0.2:
|
||
airborne = true
|
||
if i > 5 and airborne and body.is_on_floor():
|
||
break
|
||
body.set("auto_jump", false)
|
||
_check(airborne, "Sprung verlässt den Boden")
|
||
_check(absf(body.fuss_hoehe() - feet0) < 0.05, "Landung wieder auf Terrainhöhe")
|
||
|
||
|
||
## --- Test 5b: Laufrichtung folgt der Blickrichtung (Sly-Befund 20.10.) ------
|
||
## Bug: WASD benutzte transform.basis des Körpers, aber die Kamera-Drehung
|
||
## steckt im look_cam-Kindknoten → WASD lief stur in eine Himmelsrichtung.
|
||
## Fix: Richtung aus yaw_deg bauen (wie view_direction(), wie Quest-Kopf-Yaw).
|
||
func _test_yaw_relatives_laufen() -> void:
|
||
print("Testgruppe: Laufrichtung folgt Blickrichtung (yaw)")
|
||
var body := _body()
|
||
if body == null:
|
||
_check(false, "Playtest-Body aktiv (Vorbedingung)")
|
||
return
|
||
var start := Vector3(body.position)
|
||
# Blick nach -Z (yaw 0): W muss nach -Z laufen.
|
||
body.set("yaw_deg", 0.0)
|
||
body.set("auto_input", {"fwd": true})
|
||
for i in 20:
|
||
await physics_frame
|
||
body.set("auto_input", {})
|
||
var d0 := Vector3(body.position) - start
|
||
_check(d0.z < -1.0 and absf(d0.x) < 0.6,
|
||
"yaw 0° + W → Bewegung nach -Z (d=%.1f, %.1f)" % [d0.x, d0.z])
|
||
# Blick um 90° gedreht (nach -X): W muss nach -X laufen, NICHT weiter -Z.
|
||
body.set("position", start)
|
||
body.set("velocity", Vector3.ZERO)
|
||
body.set("yaw_deg", 90.0)
|
||
await physics_frame
|
||
body.set("auto_input", {"fwd": true})
|
||
for i in 20:
|
||
await physics_frame
|
||
body.set("auto_input", {})
|
||
var d1 := Vector3(body.position) - start
|
||
_check(d1.x < -1.0 and absf(d1.z) < 0.6,
|
||
"yaw 90° + W → Bewegung folgt Blick nach -X (d=%.1f, %.1f)" % [d1.x, d1.z])
|
||
|
||
## --- Test 6b: Splitter-Lagen überleben den Playtest (Sly-Befund 20.10.) -----
|
||
## Beim Verstecken der Panels klappen die Trenner zusammen; der Exit muss die
|
||
## gemerkten Lagen zurücksetzen. Der Test verbiegt die Lagen bewusst hart
|
||
## (wie der Clamp es tut) und verlangt nach dem Exit die Original-Lagen.
|
||
var _split_vor_start: Dictionary = {}
|
||
|
||
func _split_by_name(sname: String) -> SplitContainer:
|
||
var found := root.find_children(sname, "SplitContainer", true, false)
|
||
return found[0] if not found.is_empty() else null
|
||
|
||
func _test_splitter_lagen_merken() -> void:
|
||
# Bezugspunkt = VOR dem Start (nach dem Start klappen die Trenner bereits).
|
||
for sname in ["MainSplit", "BottomSplit"]:
|
||
var sp := _split_by_name(sname)
|
||
if sp != null:
|
||
_split_vor_start[sname] = Array(sp.split_offsets)
|
||
|
||
func _test_splitter_lagen_verbiegen() -> void:
|
||
var ms := _split_by_name("MainSplit")
|
||
if ms != null:
|
||
ms.split_offsets = PackedInt32Array([9999, 9999])
|
||
var bs := _split_by_name("BottomSplit")
|
||
if bs != null:
|
||
bs.split_offsets = PackedInt32Array([-9999])
|
||
|
||
func _test_splitter_lagen_pruefen() -> void:
|
||
for i in 3:
|
||
await process_frame
|
||
var ok := true
|
||
for sname in _split_vor_start.keys():
|
||
var sp := _split_by_name(str(sname))
|
||
if sp == null or Array(sp.split_offsets) != _split_vor_start[sname]:
|
||
ok = false
|
||
print(" DIFF ", sname, ": ", str(sp.split_offsets if sp != null else null),
|
||
" statt ", str(_split_vor_start[sname]))
|
||
_check(ok, "Splitter-Lagen nach Playtest unverändert (Sly-Befund)")
|
||
|
||
## --- Test 6: Rückkehr stellt den Editor-Zustand wieder her ------------------
|
||
func _test_exit_restores_editor() -> void:
|
||
print("Testgruppe: Rückkehr in den Editor (Dokument + Undo unverändert)")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||
return
|
||
var doc_before: Dictionary = main.get("doc")
|
||
var doc_before_str := str(doc_before)
|
||
var hist: RefCounted = main.get("history")
|
||
var undo_before: bool = hist.call("can_undo")
|
||
var redo_before: bool = hist.call("can_redo")
|
||
var tool_before: String = main.get("tool_mode")
|
||
var cam_target: Variant = camera.get("target")
|
||
var cam_yaw: float = camera.get("yaw_deg")
|
||
|
||
pt.call("handle_escape")
|
||
await process_frame
|
||
|
||
_check(main.get("playtest") == null, "PlaytestMode abgeräumt")
|
||
for pair in [["ui_top_bar", "Top-Bar"], ["ui_left_panel", "linke Spalte"],
|
||
["ui_right_panel", "rechte Spalte"], ["ui_status_bar", "Statusleiste"]]:
|
||
var panel: Control = main.get(pair[0])
|
||
_check(panel != null and panel.visible, "%s wieder sichtbar" % pair[1])
|
||
_check(camera.current, "Editor-Kamera zurück im Bild")
|
||
_check(is_equal_approx(camera.get("yaw_deg"), cam_yaw), "Kamera-Yaw unverändert")
|
||
_check(camera.get("target") == cam_target, "Kamera-Ziel unverändert")
|
||
_check(str(main.get("doc")) == doc_before_str, "Dokument byte-identisch")
|
||
_check(hist.can_undo() == undo_before and hist.can_redo() == redo_before,
|
||
"Undo/Redo-Stapel unverändert")
|
||
_check(str(main.get("tool_mode")) == tool_before, "Werkzeug-Modus unverändert")
|
||
_check(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Maus wieder frei")
|
||
|
||
|
||
## --- Test 7: Bauteile (Bauen-Modus) — Treppe begehbar, Tür durchlässig ------
|
||
## Karte t_1da96275: auch die Modular-Builder-Pieces bekommen echte Trimesh-
|
||
## Kollider (statt AABB-Kisten) — Treppen hochlaufen, durch Türen gehen.
|
||
func _test_bauteile_treppe_tuer() -> void:
|
||
print("Testgruppe: Bauteile — Treppe hoch, durch die Tür (Trimesh-Kollider)")
|
||
var pt := _pt()
|
||
if pt == null:
|
||
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||
return
|
||
# Erst raus aus dem laufenden Testlauf, dann Map mit Bauteilen neu starten.
|
||
pt.call("handle_escape")
|
||
await process_frame
|
||
var doc: Dictionary = SvmapDocument.create_new("Bauteil-Karte", 100.0, "klar")
|
||
var t0: float = SvmapDocument.terrain_height_at(doc, 0.0, 0.0)
|
||
# Treppe (4 m lang, 3 m hoch, Lauf +x) und Tür-Wand als NORMALE Objekte (bau_*).
|
||
var treppe := {"id": "bau_treppe", "pos": [6.0, t0, 0.0], "scale": 1.0, "rot_y_deg": 0.0}
|
||
var tuer := {"id": "bau_wand_tuer", "pos": [1.0, t0, 14.0], "scale": 1.0, "rot_y_deg": 0.0}
|
||
doc["objects"] = [treppe, tuer]
|
||
main.set("doc", doc)
|
||
main.set("current_path", "")
|
||
main.set("dirty", false)
|
||
main.get("history").begin(doc)
|
||
main.call("_clear_edit_state")
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
main.call("_on_test_run")
|
||
await process_frame
|
||
var pt2 := _pt()
|
||
_check(pt2 != null and bool(pt2.get("active")), "Testlauf mit Bauteil-Map läuft")
|
||
if pt2 == null:
|
||
return
|
||
var body: Node = pt2.get("body")
|
||
var kb: Node3D = pt2.get("_kollider_root")
|
||
var bauteile := 0
|
||
for child in kb.get_children():
|
||
if str(child.name).contains("_bau_"):
|
||
bauteile += 1
|
||
_check(bauteile == 2, "2 Bauteil-Kollider gebaut (Treppe + Tür, ist %d)" % bauteile)
|
||
|
||
# a) Treppe (4 m lang, 3 m hoch, 15 Stufen à 0.2 m): hochlaufen.
|
||
# Erst VOR die Treppe stellen (Spawn liegt weit weg — Karte Mitte).
|
||
body.set("position", Vector3(0.0, t0 + PlaytestBody.HALF_H + 0.05, 0.0))
|
||
body.set("velocity", Vector3.ZERO)
|
||
for i in 20:
|
||
await physics_frame
|
||
var feet0: float = body.fuss_hoehe()
|
||
_check(absf(feet0 - t0) <= 0.05, "Start auf Terrainhöhe (%.3f ≈ %.3f)" % [feet0, t0])
|
||
body.set("auto_move_dir", Vector3(1, 0, 0))
|
||
var feet_max := -INF
|
||
for i in 240:
|
||
await physics_frame
|
||
feet_max = maxf(feet_max, body.fuss_hoehe())
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
for i in 10:
|
||
await physics_frame
|
||
_check(feet_max >= t0 + 2.5,
|
||
"Treppe (3.0 m, Stufen à 0.2) erklommen (max feet %.2f, Start %.2f)" % [feet_max, t0])
|
||
|
||
# b) Tür (Wand 2 m entlang x, Öffnung 1,2 × 2,2 m): durchgehen (Lauf +z).
|
||
var t0_2: float = body.call("terrain_height", 1.0, 16.0)
|
||
body.set("position", Vector3(1.0, t0_2 + PlaytestBody.HALF_H + 0.05, 16.0))
|
||
body.set("velocity", Vector3.ZERO)
|
||
body.set("auto_move_dir", Vector3(0, 0, -1))
|
||
for i in 150:
|
||
await physics_frame
|
||
body.set("auto_move_dir", Vector3.ZERO)
|
||
for i in 10:
|
||
await physics_frame
|
||
var durch: bool = body.position.z < 13.4
|
||
_check(durch, "Tür-Öffnung durchlässig (Ende z=%.2f < 13.4; Wandfläche bei 14)" %
|
||
body.position.z)
|
||
if not durch:
|
||
_check(false, "Tür-Block-Debug: pos=%s feet=%.2f" % [str(body.position), body.fuss_hoehe()])
|
||
|
||
# c) Aufräumen: zurück, dann ALTE Map für den Exit-Test wiederherstellen.
|
||
pt2.call("handle_escape")
|
||
await process_frame
|
||
var doc0: Dictionary = SvmapDocument.create_new("Testlauf-Karte", 100.0, "klar")
|
||
main.set("doc", doc0)
|
||
main.set("current_path", "")
|
||
main.set("dirty", false)
|
||
main.get("history").begin(doc0)
|
||
main.call("_clear_edit_state")
|
||
main.call("_refresh_all")
|
||
await process_frame
|
||
# Playtest auf der wiederhergestellten Karte neu starten — der Exit-Test
|
||
# braucht einen aktiven Lauf-Modus.
|
||
main.call("_on_test_run")
|
||
await process_frame
|
||
# Nachtrag (Bugfix): das obige Dateiende endete ohne Neustart — der Exit-Test
|
||
# braucht einen aktiven Lauf-Modus auf der wiederhergestellten Karte.
|