extends SceneTree ## Editor-Test (headless): Karte t_1cdc1a72 — Lauf-Modus IM Map-Builder selbst ## (erste Person, WASD + Maus, OHNE Client-Ordner/VR/Server). ## Ausführen: 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 lebt im world3d, Kamera übernimmt. ## 2. Bodenhöhe == Terrainhöhe an der Spielerposition (Body-Decoder == ## SvmapDocument-Decoder am selben Dokument). ## 3. Spieler bleibt vor einem Objekt-AABB stehen (Block-Kollision). ## 4. Rennen/Springen wirkt korrekt (step-Input-Dictionary). ## 5. Rückkehr stellt den Editor-Zustand VOLLSTÄNDIG wieder her ## (Dokument byte-identisch, Undo-Stapel unverändert, Panels sichtbar). var failures := 0 var checks := 0 var main: Control var camera: Camera3D var subvp: SubViewport func _initialize() -> void: _run_all.call_deferred() func _run_all() -> void: var scene: PackedScene = load("res://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_start_state() _test_terrain_height_matches_document() _test_obstacle_blocks_player() _test_run_and_jump() _test_exit_restores_editor() _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") ## --- Test 1: Start/Zurück-Zustand ------------------------------------------- func _test_start_state() -> void: print("Testgruppe: Start-Zustand (Modus-Flags, Panels, Maus, Kamera)") 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: Node3D = pt.get("body") _check(body != null and body.get_parent() == pt, "Body ist Kind des PlaytestMode") _check(pt.get_parent() == main.get("world3d"), "PlaytestMode lebt im world3d") # 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: Node = 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 an der Spielerposition") var pt := _pt() if pt == null: _check(false, "PlaytestMode noch aktiv (Vorbedingung Test 1)") return var body: Node3D = pt.get("body") var doc: Dictionary = main.get("doc") # Schritt ausführen (Gravitation lässt den Spieler auf den Boden fallen). body.call("step", 1.0 / 30.0, {"fwd": false, "back": false, "left": false, "right": false, "run": false, "jump": false}) var feet_y: float = body.position.y 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(is_equal_approx(feet_y, terrain_y), "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: Objekt-AABB blockiert den Spieler ------------------------------ func _test_obstacle_blocks_player() -> void: print("Testgruppe: Spieler bleibt vor einem Objekt-AABB stehen") var pt := _pt() if pt == null: _check(false, "PlaytestMode noch aktiv (Vorbedingung)") return var body: Node3D = pt.get("body") # Hindernis VOR dem Spieler (Blick -Z bei yaw 0): Wand spannt # z ∈ [start_z - 3.0, start_z - 2.4] — der Spieler kommt aus +Z und # trifft zuerst die Face bei end.z (start_z - 2.4). var start_x: float = body.position.x var start_z: float = body.position.z var wall := AABB(Vector3(body.position.x - 2.0, -5.0, start_z - 3.0), Vector3(4.0, 10.0, 0.6)) body.get("obstacles").append(wall) body.set("yaw_deg", 0.0) # Blick nach -Z var ok := true for i in 60: # 2 Sekunden laufen body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, "right": false, "run": false, "jump": false}) # Abstand: führende Kante des Spielers (z - radius) zur dem Spieler # zugewandten Wand-Face (end.z — er kommt aus +Z). >= 0 = davor geblieben. var dist_to_wall: float = ( float(body.position.z) - float(body.get("radius")) - float(wall.end.z)) _check(dist_to_wall >= -0.05, "Spieler hält vor der Wand (Abstand %.2f m >= 0)" % dist_to_wall) if dist_to_wall < -0.05: ok = false # Kein Drift in X (kein Links/Rechts-Input → Position unverändert). _check(absf(float(body.position.x) - start_x) < 0.01, "Kein Drift in X") # Hindernis wieder entfernen (sauberer Zustand für die Folgetests). body.get("obstacles").erase(wall) ## --- Test 4: Rennen/Springen ------------------------------------------------ func _test_run_and_jump() -> void: print("Testgruppe: Rennen/Springen wirkt korrekt") var pt := _pt() if pt == null: _check(false, "PlaytestMode noch aktiv (Vorbedingung)") return var body: Node3D = pt.get("body") # Gehen: 1 s bei 4 m/s → ~4 m Wegstrecke. var start := Vector3(body.position) for i in 30: body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, "right": false, "run": false, "jump": false}) var walk_dist: float = (Vector3(body.position) - start).length() _check(walk_dist > 3.0 and walk_dist < 5.0, "Gehen ≈ 4 m/s (gemessen %.2f m in 1 s)" % walk_dist) # Rennen: gleiche Zeit, Verdopplung → deutlich mehr Strecke. var start2 := Vector3(body.position) for i in 30: body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, "right": false, "run": true, "jump": false}) var run_dist: float = (Vector3(body.position) - start2).length() _check(run_dist > walk_dist * 1.5, "Rennen deutlich schneller (%.2f m > %.2f m)" % [run_dist, walk_dist]) # Springen: y-Impuls, Spieler verlässt den Boden und landet wieder. var y0: float = body.position.y var airborne := false for i in 60: var state: Dictionary = body.call("step", 1.0 / 30.0, {"fwd": false, "back": false, "left": false, "right": false, "run": false, "jump": i < 2}) if not bool(state["on_floor"]): airborne = true if i > 5 and bool(state["on_floor"]): break _check(airborne, "Sprung verlässt den Boden") _check(is_equal_approx(float(body.position.y), y0), "Landung wieder auf Terrainhöhe") ## --- Test 5: 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(not pt.wants_capture() if pt != null else true, "Maus-Capture-Wunsch zurückgenommen") _check(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Maus wieder frei")