extends SceneTree ## Editor-Test (headless): Karte t_aaff5dcc — T3 Komfort im Testlauf. ## Ausführen: godot4 --headless --path . --import && godot4 --headless --path . --script res://tests/test_playtest_comfort.gd ## Exit 0 = alles grün. ## ## Deckt ab (alles im Lauf-Modus, keine neuen Abhängigkeiten): ## 1. Fliegen/God-Mode (Taste G → toggle_fly): Gravitation AUS, Bewegung in ## Blickrichtung (inkl. Nickwinkel), Noclip durch Wände, Space = steigen, ## Ausschalten → Gravitation wirkt wieder. ## 2. Sprung zu Spawnpunkten (Taste 1–4 → jump_to_spawn): landet EXAKT auf ## den Spawnpunkten der Karte (x/z aus dem Dokument, Höhe vom Terrain, ## Toleranz ≤ 1 mm); fehlende Zone → abgelehnt, Position unverändert. ## 3. HUD-Toggle (Taste H): Sichtbarkeit wechselt; HUD zeigt Blickrichtung ## und Höhe. ## 4. Maus-Empfindlichkeit: gespeichert + geladen (Clamp 0,1–5,0), live ## angewandt (apply_sensitivity) und beim Start aus den Einstellungen ## gelesen; Panel über Registry angemeldet (main.gd unangetastet). ## 5. Rückkehr in den Editor weiterhin vollkommen sauber (Dokument + ## Undo unverändert, Panels sichtbar, Achsen/Hilfe/HUD abgeräumt). var failures := 0 var checks := 0 var main: Control 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 # Echte Map (Neu-Dialog-Konvention) + zwei weitere Spawn-Zonen für 3/4. var doc: Dictionary = SvmapDocument.create_new("Komfort-Karte", 512.0, "klar") var zones: Dictionary = doc.get("spawn_zones", {}) zones["2"] = {"label": "Zone 3", "points": [{"id": "C1", "pos": [7.5, 0.0, -12.5]}]} zones["3"] = {"label": "Zone 4", "points": [{"id": "D1", "pos": [-13.5, 0.0, 21.5]}]} 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 # Einstellungen auf bekannten Startwert (Standard). PlaytestSettings.reset_cache() PlaytestSettings.save_sens(1.0) # Einstellungen-Panel ist über die Registry im Baum (Haupttest der Anbindung). var registry_script: GDScript = load("res://core/ui/panels/registry.gd") var entry: Dictionary = registry_script.entry("einstellungen") _check(not entry.is_empty(), "Registry: Eintrag 'einstellungen' vorhanden") var panel: Node = main.find_child("einstellungen", true, false) _check(panel != null, "Einstellungen-Panel im Baum (Registry-Weg, main.gd unangetastet)") if panel != null: _check(panel.get("sens_box") != null, "Einstellungen: Empfindlichkeits-Feld vorhanden") var box: SpinBox = panel.get("sens_box") _check(is_equal_approx(box.min_value, 0.1) and is_equal_approx(box.max_value, 5.0), "Einstellungen: Bereich 0,1–5,0 im Feld") _check(is_equal_approx(box.value, 1.0), "Einstellungen: Standard 1,0 vorgeblendet") main.call("_on_test_run") await process_frame await process_frame await _test_fly_godmode() await _test_spawn_jumps() await _test_hud_toggle() await _test_sensitivity() await _test_exit_restores_editor() # Aufräumen: Standard-Empfindlichkeit für den echten Editor hinterlassen. PlaytestSettings.save_sens(1.0) _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 _pt() -> Node: return main.get("playtest") func _body() -> Node3D: var pt := _pt() if pt == null: return null return pt.get("body") func _key_event(code: Key) -> InputEventKey: var evt := InputEventKey.new() evt.keycode = code evt.physical_keycode = code evt.pressed = true return evt ## --- Test 1: Fliegen / God-Mode --------------------------------------------- func _test_fly_godmode() -> void: print("Testgruppe: Fliegen/God-Mode (G) — Gravitation aus, Blickrichtung, Noclip") var pt := _pt() if pt == null: _check(false, "PlaytestMode aktiv (Vorbedingung)") return var body: Node3D = _body() _check(not bool(body.get("fly_mode")), "Standard: Fliegen AUS") # Taste G über den echten Dispatch-Weg (mode._unhandled_input). pt.call("_unhandled_input", _key_event(KEY_G)) _check(bool(body.get("fly_mode")), "Taste G schaltet Fliegen AN (Dispatch)") # Gravitation AUS: in der Luft schwebt der Spieler ohne Input (Physik- # Frames, wie im echten Testlauf). body.set("position", Vector3(body.position.x, 10.0, body.position.z)) var y0: float = body.position.y for i in 30: await physics_frame _check(absf(float(body.position.y) - y0) < 0.001, "Keine Gravitation: Höhe hält (10 m, %.3f nach 0,5 s)" % float(body.position.y)) # Noclip: Fliegen geht durch alles (direkte Position, kein Kollider-Block). var start := Vector3(body.position) body.set("yaw_deg", 0.0) # Blick nach -Z body.set("auto_move_dir", Vector3(0, 0, -1)) for i in 60: await physics_frame body.set("auto_move_dir", Vector3.ZERO) var dist: float = start.distance_to(body.position) _check(dist > 3.5, "Fliegen noclip: 6 m Weg ohne Block (%.2f m)" % dist) # Bewegung in Blickrichtung: Blick 30° nach unten → Wegstrecke teilt sich # in -z und -y (sin30 = 0.5, cos30 ≈ 0.866; 6 m/s → 6 m in 1 s). var blick: Vector3 = body.call("view_direction") if body.has_method("view_direction") \ else Vector3.ZERO body.set("yaw_deg", 0.0) body.set("pitch_deg", -30.0) blick = body.call("view_direction") _check(absf(blick.z + 0.866) < 0.01 and absf(blick.y + 0.5) < 0.01, "Blickrichtung-Vektor 30° unter Horizont (%.3f, %.3f)" % [blick.z, blick.y]) var start2 := Vector3(body.position) body.set("auto_move_dir", blick) for i in 60: await physics_frame body.set("auto_move_dir", Vector3.ZERO) var moved: Vector3 = body.position - start2 _check(absf(moved.z + 5.19) < 0.1 and absf(moved.y + 3.0) < 0.1, "Flug entlang der Blickrichtung (Δz=%.2f, Δy=%.2f, Soll -5.19/-3.0)" % [moved.z, moved.y]) # Space = steigen. var y_up: float = body.position.y body.set("auto_input", {"fwd": false, "back": false, "left": false, "right": false, "run": false, "jump": true}) for i in 15: await physics_frame body.set("auto_input", {}) _check(float(body.position.y) > y_up + 1.0, "Space steigt im Flug (+%.1f m)" % float(body.position.y - y_up)) # Ausschalten: Gravitation wirkt wieder (Fällen zum Terrain-Trimesh). pt.call("_unhandled_input", _key_event(KEY_G)) _check(not bool(body.get("fly_mode")), "Zweites G schaltet Fliegen AUS") var y_fall: float = body.position.y var landed := false for i in 240: await physics_frame if body.is_on_floor(): landed = true break _check(landed and float(body.position.y) < y_fall, "Fliegen aus: Spieler fällt und landet (y=%.2f)" % float(body.position.y)) var terrain_y: float = body.call("terrain_height", body.position.x, body.position.z) _check(absf(float(body.fuss_hoehe()) - terrain_y) <= 0.05, "Landung auf Terrainhöhe (%.3f ≈ %.3f)" % [float(body.fuss_hoehe()), terrain_y]) ## --- Test 2: Sprung zu den Spawnpunkten -------------------------------------- func _test_spawn_jumps() -> void: print("Testgruppe: Sprung zu Spawnpunkten (1–4) — exakt, Toleranz ≤ 1 mm") var pt := _pt() if pt == null: _check(false, "PlaytestMode aktiv (Vorbedingung)") return var body: Node3D = _body() var doc: Dictionary = main.get("doc") var zones: Dictionary = doc.get("spawn_zones", {}) var idx := 1 for zone_key in ["0", "1", "2", "3"]: var zone: Dictionary = zones.get(zone_key, {}) var pts: Array = zone.get("points", []) if pts.is_empty() or not (pts[0] is Dictionary): _check(false, "Testmap braucht Zone %s" % zone_key) idx += 1 continue var pos: Array = (pts[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) var expected := Vector3(float(pos[0]), float(body.call("terrain_height", float(pos[0]), float(pos[2]))) + PlaytestBody.HALF_H + 0.02, float(pos[2])) # Vorher woanders hin (sichert, dass wirklich gesprungen wird). body.set("position", Vector3(99.0, 5.0, 99.0)) var ok := bool(pt.call("jump_to_spawn", idx)) _check(ok, "Sprung %d (Zone %s '%s') gelungen" % [idx, zone_key, str(zone.get("label", ""))]) var delta: float = body.position.distance_to(expected) _check(delta <= 0.001, "Landung exakt auf Spawnpunkt %d (%.4f mm Abweichung: Soll %s, Ist %s)" % [ idx, delta * 1000.0, expected, body.position]) idx += 1 # Fehlende Zone (Taste 5 wäre Zone 4): abgelehnt, Position UNVERÄNDERT. body.set("position", Vector3(3.0, 1.0, 4.0)) var rejected := not bool(pt.call("jump_to_spawn", 5)) _check(rejected, "Fehlende Zone: Sprung abgelehnt") _check(body.position == Vector3(3.0, 1.0, 4.0), "Fehlende Zone: Position unverändert") ## --- Test 3: HUD-Toggle + Blickrichtung/Höhe -------------------------------- func _test_hud_toggle() -> void: print("Testgruppe: HUD-Toggle (H) + Blickrichtung/Höhe im HUD") var pt := _pt() if pt == null: _check(false, "PlaytestMode aktiv (Vorbedingung)") return var hud: CanvasLayer = pt.get("hud") _check(hud != null and hud.visible, "HUD standardmäßig sichtbar") pt.call("_unhandled_input", _key_event(KEY_H)) _check(not hud.visible, "Taste H: HUD AUS") pt.call("_unhandled_input", _key_event(KEY_H)) _check(hud.visible, "Taste H: HUD wieder AN") var hud_dir: Label = pt.get("hud_dir") _check(hud_dir != null, "HUD-Blickrichtung-Label vorhanden") await process_frame _check(str(hud_dir.text).begins_with("Blick:"), "HUD zeigt Blickrichtung ('%s')" % hud_dir.text) _check("Höhe" in str(hud_dir.text), "HUD zeigt Höhe ('%s')" % hud_dir.text) ## --- Test 4: Maus-Empfindlichkeit ------------------------------------------- func _test_sensitivity() -> void: print("Testgruppe: Maus-Empfindlichkeit — speichern/laden, Clamp, live") PlaytestSettings.reset_cache() var saved: float = PlaytestSettings.save_sens(2.5) _check(is_equal_approx(saved, 2.5), "save_sens(2.5) gespeichert") PlaytestSettings.reset_cache() _check(is_equal_approx(PlaytestSettings.load_sens(), 2.5), "load_sens liest 2,5 zurück (Neustart-Simulation via reset_cache)") _check(is_equal_approx(PlaytestSettings.save_sens(9.0), 5.0), "Clamp oben: 9,0 → 5,0") _check(is_equal_approx(PlaytestSettings.save_sens(0.01), 0.1), "Clamp unten: 0,01 → 0,1") # Live-Anwendung: Body-Basis 0,25 Grad/Pixel × Multiplikator. var body: Node3D = _body() var pt := _pt() pt.call("apply_sensitivity", 2.0) _check(is_equal_approx(float(body.get("mouse_sens")), 0.5), "apply_sensitivity(2.0): mouse_sens = 0.25 × 2 = 0.5 (Ist %.3f)" % float(body.get("mouse_sens"))) # Beim (Neu-)Start aus den Einstellungen gelesen: Store = 0.1 → Basis 0.025. PlaytestSettings.save_sens(0.1) pt.call("handle_escape") await process_frame _check(main.get("playtest") == null, "Zwischen-Exit sauber (Vorbedingung Neustart)") main.call("_on_test_run") await process_frame await process_frame var pt2 := _pt() _check(pt2 != null, "PlaytestMode neu gestartet") var body2: Node3D = pt2.get("body") _check(is_equal_approx(float(body2.get("mouse_sens")), 0.025), "Start liest Empfindlichkeit (0,1 → Basis 0.025, Ist %.4f)" % float(body2.get("mouse_sens"))) _check(bool(body2.get("fly_mode")) == false, "Neustart: Fliegen wieder AUS (Body-Default)") # Panel-Änderung wirkt SOFORT auf den laufenden Testlauf (Panel→apply_sensitivity). var panel: Node = main.find_child("einstellungen", true, false) if panel != null: var box: SpinBox = panel.get("sens_box") box.value = 3.0 _check(is_equal_approx(float(body2.get("mouse_sens")), 0.75), "Panel 3,0 wirkt sofort (0.25 × 3 = 0.75, Ist %.3f)" % float(body2.get("mouse_sens"))) else: _check(false, "Einstellungen-Panel für Live-Test vorhanden") ## --- Test 5: Rückkehr in den Editor sauber ---------------------------------- 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 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") # Komfort-Zustände an: Achsen + Hilfe — auch die müssen exit() abräumen. pt.call("toggle_axis") pt.call("toggle_help") _check(pt.get("_axis_node") != null, "Achsen-Linien an (Vorbedingung Aufräumen)") _check(pt.get("help_layer") != null, "Hilfe-Overlay an (Vorbedingung Aufräumen)") pt.call("handle_escape") await process_frame 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(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(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Maus wieder frei")