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

288 lines
10 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_2470de89 — RMB = Flug mit ABSOLUTEM Vorrang.
## Ausführen: godot4 --headless --path . --script res://tests/test_rmb_vorrang.gd
## Exit 0 = alles grün.
##
## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und schickt echte
## Maus-/Tastatur-Events durch den Viewport. Geprüft wird:
## 1. Terrain-Werkzeug aktiv + rechte Maustaste → Flug startet, KEIN
## Terrain-Strich (Höhenfeld byte-identisch, keine History-Zeile).
## 2. Strg+Linksklick im Terrain-Werkzeug SENKT (bei Pinsel-Modus Heben),
## linker Klick hebt — Invert-Konvention.
## 3. Objekt „in der Hand“ (Platzier-Modus) + rechte Maustaste → Flug,
## das Objekt BLEIBT in der Hand nach dem Flug; Esc wirft ab.
## 4. Bauen-Werkzeug: rechte Maustaste fliegt, wechselt NICHT mehr das
## Werkzeug und wirft die Bauteil-Auswahl nicht weg.
var failures := 0
var checks := 0
var main: Control
var camera: Camera3D
var container: SubViewportContainer
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
container = _find_viewport_container(main)
_check(container != null, "SubViewportContainer gefunden")
camera = main.get("camera")
_check(camera != null, "Kamera-Referenz gefunden")
if container == null or camera == null:
_finish()
return
subvp = container.get_child(0) as SubViewport
_check(subvp != null, "SubViewport gefunden")
await _test_terrain_rmb_flies()
await _test_terrain_ctrl_lmb()
await _test_placement_rmb_keeps_hand()
await _test_build_rmb_flies()
_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
## Headless-Besonderheit: das Fenster ist nur 64×64 groß — root.push_input
## leitet NICHT in den SubViewportContainer weiter (GUI-Delivery braucht das
## Event im Fenster-Rect). Deshalb pushen wir direkt in den SubViewport mit
## LOKALEN Koordinaten — die Werkzeug-/Kamera-Handler laufen exakt wie echt.
var subvp: SubViewport
## Event-Position: Mitte des 3D-Viewports (in SubViewport-Koordinaten).
func _vp_center() -> Vector2:
return subvp.size * 0.5
func _mouse(button: MouseButton, pressed: bool, pos: Vector2, ctrl := false, shift := false) -> void:
var mb := InputEventMouseButton.new()
mb.button_index = button
mb.pressed = pressed
mb.position = pos
mb.global_position = pos
mb.ctrl_pressed = ctrl
mb.shift_pressed = shift
subvp.push_input(mb)
func _motion(pos: Vector2) -> void:
var mm := InputEventMouseMotion.new()
mm.position = pos
mm.global_position = pos
mm.relative = Vector2.ZERO
subvp.push_input(mm)
func _press_key(keycode: Key) -> InputEventKey:
var ev := InputEventKey.new()
ev.keycode = keycode
ev.physical_keycode = keycode
ev.pressed = true
return ev
func _heights() -> PackedFloat32Array:
return SvmapIO.decode_heights((main.doc.get("terrain", {}) as Dictionary))
func _height_at(heights: PackedFloat32Array, x: float, z: float) -> float:
var terrain: Dictionary = main.doc.get("terrain", {})
return TerrainField.height_at(heights,
int(terrain.get("resolution", 0)),
float(terrain.get("size_x_m", 0.0)),
float(terrain.get("size_z_m", 0.0)), x, z)
## Abnahme 1: Terrain-Werkzeug aktiv + RMB → Flug an, KEIN Terrain-Change,
## RMB loslassen → Flug aus. Auch im Flug bewegt die Kamera mit W.
func _test_terrain_rmb_flies() -> void:
print("Testgruppe: Terrain-Werkzeug + RMB = Flug (kein Strich)")
main.call("_set_tool_mode", "terrain")
_check(str(main.get("tool_mode")) == "terrain", "Terrain-Werkzeug aktiv")
var before := _heights()
var pos := _vp_center()
_motion(pos)
_mouse(MOUSE_BUTTON_RIGHT, true, pos)
await process_frame
_check(camera.call("fly_active"), "RMB gedrückt → Flug aktiv (im Terrain-Werkzeug)")
_check(not bool(terrain_stroking()), "RMB startet KEINEN Terrain-Strich")
# Im Flug bewegt W die Kamera (echter Bewegungsnachweis).
var pos0 := camera.position
Input.action_press("editor_fly_forward")
await create_timer(0.25).timeout
await process_frame
Input.action_release("editor_fly_forward")
_check(camera.position.distance_to(pos0) > 0.5, "Im Flug bewegt die Kamera (W)")
_mouse(MOUSE_BUTTON_RIGHT, false, pos)
await process_frame
_check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus")
var after := _heights()
_check(before == after, "Höhenfeld byte-identisch (kein Terrain-Change)")
_check(not str(main.history.undo_desc()).contains("Boden geformt"),
"Keine History-Zeile 'Boden geformt' durch RMB")
main.call("_set_tool_mode", "objects")
func terrain_stroking() -> bool:
return bool(main.get("terrain_tool").get("_stroking"))
## Abnahme 2: Strg+LMB senkt (bei Pinsel-Modus Heben), LMB ohne Strg hebt.
func _test_terrain_ctrl_lmb() -> void:
print("Testgruppe: Terrain Strg+LMB senkt / LMB hebt")
main.call("_set_tool_mode", "terrain")
var tool: Node = main.get("terrain_tool")
tool.call("set_brush_mode", TerrainField.Mode.RAISE)
main.terrain_strength_slider.value = 20.0
var pos := _vp_center()
_motion(pos)
_check(bool(tool.get("_cursor_valid")), "Pinsel-Cursor trifft das Gelände")
var cursor: Vector3 = tool.get("_cursor")
var heights0 := _heights()
var h0 := _height_at(heights0, cursor.x, cursor.z)
# Strg+LMB-Strich → senken.
_mouse(MOUSE_BUTTON_LEFT, true, pos, true)
await process_frame
_check(bool(tool.get("_stroking")), "Strg+LMB startet einen Strich")
_check(int(tool.get("_stroke_mode")) == TerrainField.Mode.LOWER,
"Strg+LMB bei Modus Heben → Strich-Modus SENKEN")
await create_timer(0.3).timeout
_mouse(MOUSE_BUTTON_LEFT, false, pos, true)
await process_frame
var heights1 := _heights()
var h1 := _height_at(heights1, cursor.x, cursor.z)
_check(h1 < h0 - 0.5, "Strg+LMB hat DEN BODEN GESSENKT (%.2f → %.2f m)" % [h0, h1])
_check(str(main.history.undo_desc()).contains("Senken"),
"History: Strg+LMB-Strich läuft als 'Senken'")
# LMB ohne Strg → heben (unverändert). Nach dem Senk-Strich trifft der
# Cursorstrahl eine NEUE Geländestelle (Grube) — Cursor frisch holen und
# dort vor/nach dem Strich messen.
_motion(pos)
var tool_fresh: Node = main.get("terrain_tool")
var cursor2: Vector3 = tool_fresh.get("_cursor")
var h1b := _height_at(_heights(), cursor2.x, cursor2.z)
_mouse(MOUSE_BUTTON_LEFT, true, pos)
await process_frame
await create_timer(0.2).timeout
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
var h2 := _height_at(_heights(), cursor2.x, cursor2.z)
_check(h2 > h1b + 0.3, "LMB ohne Strg HEBT weiter (%.2f → %.2f m)" % [h1b, h2])
_check(str(main.history.undo_desc()).contains("Heben"),
"History: LMB-Strich läuft als 'Heben'")
main.call("_set_tool_mode", "objects")
## Abnahme 3: Objekt in der Hand + RMB → Flug, Objekt bleibt in der Hand,
## Esc wirft ab.
func _test_placement_rmb_keeps_hand() -> void:
print("Testgruppe: Platzieren + RMB = Flug (Objekt bleibt in der Hand)")
main.call("_set_tool_mode", "objects")
var oid := ""
for key in (main.manifest.get("objects", {}) as Dictionary):
oid = str(key)
break
_check(oid != "", "Manifest hat ein Objekt zum Platzieren")
if oid == "":
return
var interaction: Node = main.get("interaction")
interaction.call("start_placement", oid)
_check(bool(interaction.get("placing")), "Platzier-Modus an (Objekt in der Hand)")
var pos := _vp_center()
_motion(pos)
_mouse(MOUSE_BUTTON_RIGHT, true, pos)
await process_frame
_check(camera.call("fly_active"), "RMB → Flug aktiv, WIEDERHOLTES Abwurf-RMB ist weg")
_check(bool(interaction.get("placing")), "Objekt ist WÄHREND des Flugs noch in der Hand")
_mouse(MOUSE_BUTTON_RIGHT, false, pos)
await process_frame
_check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus")
_check(bool(interaction.get("placing")), "Objekt bleibt NACH dem Flug in der Hand (kein Abwurf)")
# Esc wirft ab (zwei Frames, sonst reagiert der Viewport-Frame nicht).
main._unhandled_key_input(_press_key(KEY_ESCAPE))
await process_frame
_check(not bool(interaction.get("placing")), "Esc wirft das Objekt ab (Platzieren aus)")
## Abnahme 4: Bauen — RMB fliegt, die Bauteil-Serie (Teil in der Hand) bleibt.
func _test_build_rmb_flies() -> void:
print("Testgruppe: Bauen + RMB = Flug (Teil bleibt in der Hand)")
main.call("_set_tool_mode", "objects")
var interaction: Node = main.get("interaction")
var pos := _vp_center()
_motion(pos)
interaction.call("start_placement", "bau_fundament")
_check(bool(interaction.get("placing")) and str(interaction.get("place_oid")) == "bau_fundament", "Bauteil in der Hand")
_motion(pos)
_mouse(MOUSE_BUTTON_LEFT, true, pos)
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
_check(_build_piece_count() > 0, "Bauteil per Klick gesetzt (normales Objekt)")
_check(bool(interaction.get("placing")), "Serie: Teil bleibt nach dem Setzen in der Hand")
var doc: Dictionary = main.doc
_check(GroupOps.group_id((doc["objects"] as Array).back()) != GroupOps.UNGROUPED, "Teil gehört zu einem Gebäude (Gruppe)")
_mouse(MOUSE_BUTTON_RIGHT, true, pos)
await process_frame
_check(camera.call("fly_active"), "RMB → Flug aktiv")
_check(bool(interaction.get("placing")), "RMB wirft das Teil NICHT ab")
_mouse(MOUSE_BUTTON_RIGHT, false, pos)
await process_frame
_check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus")
main._unhandled_key_input(_press_key(KEY_ESCAPE))
await process_frame
_check(not bool(interaction.get("placing")), "Esc beendet das Bauen")
func _build_piece_count() -> int:
var doc: Dictionary = main.doc
return BauOps.teile_zahl(doc)