288 lines
10 KiB
GDScript
288 lines
10 KiB
GDScript
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)
|