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://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-Werkzeug — RMB fliegt, wechselt NICHT das Werkzeug und ## behält die Bauteil-Auswahl. func _test_build_rmb_flies() -> void: print("Testgruppe: Bauen-Werkzeug + RMB = Flug (Werkzeug/Auswahl bleiben)") main.call("_set_tool_mode", "bauen") _check(str(main.get("tool_mode")) == "bauen", "Bauen-Werkzeug aktiv") var build: Node = main.get("build_tool") var pos := _vp_center() _motion(pos) # Ein Bauteil setzen, dann per Umschalt+Klick auswählen — Klickpunkt ist # die UNPROJIZIERTE Bauteil-Mitte (der gerastete Fußpunkt liegt sonst exakt # auf der Kante des Center-Strahls). _motion(pos) _mouse(MOUSE_BUTTON_LEFT, true, pos) _mouse(MOUSE_BUTTON_LEFT, false, pos) await process_frame var doc: Dictionary = main.doc var gebaeude: Array = doc.get("gebaeude", []) _check(gebaeude.size() > 0, "Bauteil gesetzt") if gebaeude.is_empty(): _finish() return var ppos: Array = (gebaeude[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) var piece_center := Vector3(float(ppos[0]), float(ppos[1]) + 0.3, float(ppos[2])) # Regression (t_555e33da): Selektion muss im DEFAULT-Framing funktionieren — # kein Heranzoomen mehr nötig (früher: ray_pick-Cap 500 m < Kamera-Distanz). var on_piece := camera.unproject_position(piece_center) _motion(on_piece) _mouse(MOUSE_BUTTON_LEFT, true, on_piece, false, true) _mouse(MOUSE_BUTTON_LEFT, false, on_piece, false, true) await process_frame var sel := int(build.get("selected_index")) _check(sel >= 0, "Bauteil per Umschalt+Klick ausgewählt") _mouse(MOUSE_BUTTON_RIGHT, true, on_piece) await process_frame _check(camera.call("fly_active"), "RMB → Flug aktiv (im Bauen-Werkzeug)") _check(str(main.get("tool_mode")) == "bauen", "RMB wechselt NICHT mehr das Werkzeug") _check(int(build.get("selected_index")) == sel, "RMB wirft die Bauteil-Auswahl NICHT weg") _mouse(MOUSE_BUTTON_RIGHT, false, pos) await process_frame _check(not bool(camera.call("fly_active")), "RMB losgelassen → Flug aus") main.call("_set_tool_mode", "objects") func _build_piece_count() -> int: var doc: Dictionary = main.doc return int((doc.get("gebaeude", []) as Array).size())