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://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://src/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 const NO_INPUT := {"fwd": false, "back": false, "left": false, "right": false, "run": false, "jump": false} ## --- 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. body.set("position", Vector3(body.position.x, 10.0, body.position.z)) var y0: float = body.position.y for i in 30: body.call("step", 1.0 / 30.0, NO_INPUT) _check(absf(float(body.position.y) - y0) < 0.001, "Keine Gravitation: Höhe hält (10 m, %.3f nach 1 s)" % float(body.position.y)) # Noclip: Wand quer vor dem Spieler, Fliegen geht DURCH (kein Block). var start := Vector3(body.position) body.set("yaw_deg", 0.0) # Blick nach -Z body.get("obstacles").append(AABB(Vector3(start.x - 2.0, -5.0, start.z - 3.0), Vector3(4.0, 10.0, 0.6))) for i in 60: body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, "right": false, "run": false, "jump": false}) var dist: float = start.distance_to(body.position) _check(dist > 3.5, "Fliegen geht durch die Wand (%.2f m Weg, Wand bei 2.4–3.0)" % dist) body.get("obstacles").clear() # Bewegung in Blickrichtung: Blick 30° nach unten → Wegstrecke teilt sich # in -z und -y (sin30 = 0.5, cos30 ≈ 0.866; 4 m in 1 s). var start2 := Vector3(body.position) body.set("yaw_deg", 0.0) body.set("pitch_deg", -30.0) for i in 30: body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, "right": false, "run": false, "jump": false}) var moved: Vector3 = body.position - start2 _check(absf(moved.z + 3.46) < 0.05 and absf(moved.y + 2.0) < 0.05, "Blickrichtung inkl. Nickwinkel (Δz=%.2f, Δy=%.2f, Soll -3.46/-2.0)" % [moved.z, moved.y]) # Space = steigen. var y_up: float = body.position.y for i in 15: body.call("step", 1.0 / 30.0, {"fwd": false, "back": false, "left": false, "right": false, "run": false, "jump": true}) _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). 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 120: var state: Dictionary = body.call("step", 1.0 / 30.0, NO_INPUT) if bool(state["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(is_equal_approx(float(body.position.y), terrain_y), "Landung auf Terrainhöhe (%.3f == %.3f)" % [float(body.position.y), 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]), body.call("terrain_height", float(pos[0]), float(pos[2])), 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")