338 lines
13 KiB
GDScript
338 lines
13 KiB
GDScript
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")
|