scopedvr-map-editor/tests/test_playtest_comfort.gd
2026-11-01 19:34:05 +01:00

338 lines
13 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")