scopedvr-map-editor/tests/test_place_once.gd

478 lines
18 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): Einmal-Platzieren — Karte t_5e3e97c9 (U5).
## Ausführen: godot4 --headless --path . --script res://tests/test_place_once.gd
## Exit 0 = alles grün.
##
## Verhalten (Spec KARTEN_UI_UNITY_vc27, Karte U5):
## 1. EIN Klick setzt EIN Objekt → Platzier-Werkzeug AUS, Ghost/Panel-
## Auswahl zurück, das NEUE Objekt ist ausgewählt (Index korrekt,
## Baum + Inspektor aktualisiert), Statuszeile „Objekt gesetzt…“.
## 2. Shift beim Klick = Serienmodus: Werkzeug bleibt AN, weitere Klicks
## setzen mehrfach; die Statuszeile zeigt „Serienmodus (Shift)“.
## 3. ESC / Rechtsklick bricht ab OHNE zu setzen.
## 4. EIN Undo entfernt genau das eine gesetzte Objekt.
## 5. Auswahl bleibt stabil: kein zweites Auswahl-Ereignis für dasselbe
## Objekt (Baum-Markierung nur 1×).
##
## Instanziert die ECHTE Main-UI (Muster tests/test_rmb_vorrang.gd) und
## schickt echte Maus-/Tastatur-Events durch den SubViewport; die direkten
## interaction-Pfade laufen gegen ein Probe-Main wie tests/test_shortcuts.gd.
var failures := 0
var checks := 0
var main: Control
var subvp: SubViewport
var selected_count := 0
# Zähler für die Auswahl-Stabilität (Baum-Markierungen).
var tree_marks := 0
func _initialize() -> void:
_run_all.call_deferred()
func _run_all() -> void:
_clear_library_cfg_section()
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
var container := _find_viewport_container(main)
_check(container != null, "SubViewportContainer gefunden")
if container == null:
_finish()
return
subvp = container.get_child(0) as SubViewport
_check(subvp != null, "SubViewport gefunden")
await _test_ein_klick_setzt_einmal()
await _test_serienmodus_shift()
await _test_esc_bricht_ab()
await _test_rechtsklick_laesst_nicht_fallen()
_test_undo_genau_eins()
_test_probe_auswahl_stabil()
_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
## Event-Position: Mitte des 3D-Viewports (SubViewport-Koordinaten) — trifft
## garantiert die Karte (K1d-Startansicht: 80 m über der Kartenmitte).
func _vp_center() -> Vector2:
return subvp.size * 0.5
func _mouse(button: MouseButton, pressed: bool, pos: Vector2, shift := false) -> void:
var mb := InputEventMouseButton.new()
mb.button_index = button
mb.pressed = pressed
mb.position = pos
mb.global_position = pos
mb.shift_pressed = shift
subvp.push_input(mb)
func _motion(pos: Vector2, shift := false) -> void:
var mm := InputEventMouseMotion.new()
mm.position = pos
mm.global_position = pos
mm.relative = Vector2.ZERO
mm.shift_pressed = shift
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 _objects() -> Array:
return main.doc.get("objects", []) as Array
func _first_oid() -> String:
var lib: Dictionary = main.manifest.get("objects", {})
for key in lib:
return str(key)
return ""
# ------------------------------------------------------------ 1 Ein Klick ----
func _test_ein_klick_setzt_einmal() -> void:
print("Testgruppe: Ein Klick setzt EIN Objekt und beendet das Werkzeug")
var oid := _first_oid()
_check(oid != "", "Manifest hat ein Objekt zum Platzieren")
if oid == "":
return
var interaction: Node = main.interaction
var assets: Node = main.assets_mod
selected_count = 0
assets.object_selected.connect(func(_s: String) -> void: selected_count += 1)
main.call("_set_tool_mode", "objects")
_check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size())
# Doppelklick auf die Kachel = Werkzeug-Start (U4-Vertrag).
assets._activate_tile(oid)
_check(bool(interaction.get("placing")), "Werkzeug läuft (Objekt in der Hand)")
_check(str(interaction.get("place_oid")) == oid, "place_oid stimmt (%s)" % str(interaction.get("place_oid")))
var placed_index := _objects().size()
_check(placed_index == 0, "nichts gesetzt vor dem Klick (Index des Neuen = %d)" % placed_index)
# EIN echter Linksklick in die Kartenmitte (ohne Shift).
var pos := _vp_center()
_motion(pos)
_mouse(MOUSE_BUTTON_LEFT, true, pos)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
_check(_objects().size() == 1, "EIN Klick = EIN Objekt im doc (ist %d)" % _objects().size())
_check(not bool(interaction.get("placing")), "Werkzeug nach dem Setzen AUS")
_check(str(interaction.get("place_oid")) == "", "Ghost-Auftrag geleert (place_oid leer)")
_check(int(interaction.get("selected_index")) == placed_index,
"das neue Objekt ist ausgewählt (Index %d, ist %d)" % [placed_index, int(interaction.get("selected_index"))])
var indices_arr: Variant = interaction.get("selected_indices")
var indices: Array = indices_arr if indices_arr is Array else []
_check(indices == ([placed_index] as Array), "Auswahl-Liste = [neues Objekt] (ist %s)" % str(indices))
_check(str(assets.get("selected_oid")) == "", "Panel-Auswahl zurückgesetzt (Kachel leuchtet nicht mehr)")
_check(str(main.status_label.text).contains("ausgewählt"), "Statuszeile meldet Klartext („…ausgewählt“): %s" % str(main.status_label.text))
# Inspektor zeigt das neue Objekt.
var inspector: Node = main.get("inspector")
_check(inspector != null, "Inspektor vorhanden")
if inspector != null:
var head_txt: String = str(inspector.get("_head").text)
_check(not str(head_txt).is_empty() and head_txt != "Objekt-Inspektor",
"Inspektor zeigt das neue Objekt (%s)" % head_txt)
# Zweiter Klick NACH dem Ende: wählt nichts / setzt nichts (Werkzeug ist aus).
var before := _objects().size()
_mouse(MOUSE_BUTTON_LEFT, true, pos)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
_check(_objects().size() == before, "Klick danach setzt NICHTS (Werkzeug beendet)")
_check(selected_count == 1, "Auswahl bleibt stabil: object_selected genau 1× (ist %d)" % selected_count)
await _cleanup_objects()
# ----------------------------------------------------------- 2 Serienmodus ----
func _test_serienmodus_shift() -> void:
print("Testgruppe: Shift = Serienmodus — Werkzeug bleibt AN, setzt mehrfach")
var oid := _first_oid()
var interaction: Node = main.interaction
var assets: Node = main.assets_mod
main.call("_set_tool_mode", "objects")
_check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size())
assets._activate_tile(oid)
_check(bool(interaction.get("placing")), "Werkzeug läuft")
var pos0 := _vp_center()
# Klickpunkte leicht streuen: der vp-Mittelpunkt liegt exakt auf der
# Symmetrie-Mitte (Standard Punktspiegelung) — dort wird absichtlich NICHT
# gedoppelt, der nächste Klick liegt daneben und zeigt den Zwilling.
# Für U5 unerheblich (beide Varianten setzen), aber die Zählung bleibt
# eindeutig, wenn jeder Klick off-center landet.
var pos := Vector2(pos0.x + 17.0, pos0.y + 11.0)
# Klick MIT Shift: Serienmodus — Werkzeug bleibt an, setzt trotzdem.
_motion(pos, true)
_mouse(MOUSE_BUTTON_LEFT, true, pos, true)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos, true)
await process_frame
_check(_objects().size() == 1, "1. Klick (Shift) setzt genau eins (ist %d)" % _objects().size())
_check(bool(interaction.get("placing")), "SERIENMODUS: Werkzeug bleibt AN")
_check(str(main.status_label.text).contains("Serienmodus"), "Statuszeile nennt den Serienmodus: %s" % str(main.status_label.text))
_check(int(interaction.get("selected_index")) == 0, "Serienmodus: das neue Objekt ist ausgewählt (Index 0)")
# Weiterer Klick MIT Shift: setzt das zweite Objekt, Werkzeug bleibt an.
pos = Vector2(pos0.x - 23.0, pos0.y + 7.0)
_motion(pos, true)
_mouse(MOUSE_BUTTON_LEFT, true, pos, true)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos, true)
await process_frame
_check(_objects().size() >= 2 and _objects().size() % 2 == 0, "1./2. Klick (Shift) setzen (mit Zwillingen: gerade Zahl — ist %d)" % _objects().size())
_check(bool(interaction.get("placing")), "Serienmodus läuft weiter (Werkzeug noch an)")
# Klick OHNE Shift in der Serie: beendet das Werkzeug (normales Ende).
pos = Vector2(pos0.x + 9.0, pos0.y - 13.0)
_motion(pos)
_mouse(MOUSE_BUTTON_LEFT, true, pos)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
_check(_objects().size() == 3, "3. Klick (ohne Shift) setzt das dritte (ist %d)" % _objects().size())
_check(not bool(interaction.get("placing")), "ohne Shift: Werkzeug beendet sich")
await _cleanup_objects()
# ------------------------------------------------------------- 3 ESC bricht ----
func _test_esc_bricht_ab() -> void:
print("Testgruppe: ESC bricht ab — nichts wird gesetzt")
var oid := _first_oid()
var interaction: Node = main.interaction
var assets: Node = main.assets_mod
main.call("_set_tool_mode", "objects")
_check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size())
assets._activate_tile(oid)
_check(bool(interaction.get("placing")), "Werkzeug läuft")
var pos := _vp_center()
_motion(pos)
main._unhandled_key_input(_press_key(KEY_ESCAPE))
await process_frame
_check(not bool(interaction.get("placing")), "ESC beendet das Werkzeug")
_check(_objects().size() == 0, "ESC hat NICHTS gesetzt (ist %d)" % _objects().size())
_check(str(interaction.get("place_oid")) == "", "Ghost-Auftrag weg")
_check(str(main.status_label.text).contains("abgebrochen") or str(main.status_label.text).contains("Bereit"),
"Statuszeile meldet den Abbruch (%s)" % str(main.status_label.text))
await _cleanup_objects()
# ------------------------------------------------------- 4 Rechtsklick = Flug ----
func _test_rechtsklick_laesst_nicht_fallen() -> void:
print("Testgruppe: Rechtsklick beim Platzieren = Flug (Objekt bleibt, RMB setzt NICHT)")
var oid := _first_oid()
var interaction: Node = main.interaction
var assets: Node = main.assets_mod
var camera: Camera3D = main.get("camera")
main.call("_set_tool_mode", "objects")
assets._activate_tile(oid)
_check(bool(interaction.get("placing")), "Werkzeug läuft")
var pos := _vp_center()
_motion(pos)
# Rechte Maustaste hat absoluten Vorrang (t_2470de89): Flug, kein Setzen,
# kein Abbruch.
_mouse(MOUSE_BUTTON_RIGHT, true, pos)
await process_frame
_check(bool(camera.call("fly_active")), "RMB → Flug an (Vorrang)")
_check(bool(interaction.get("placing")), "Objekt bleibt in der Hand (kein Abbruch, kein Setzen)")
_mouse(MOUSE_BUTTON_RIGHT, false, pos)
await process_frame
_check(not bool(camera.call("fly_active")), "RMB los → Flug aus")
_check(bool(interaction.get("placing")), "Werkzeug läuft nach dem Flug weiter")
# ESC wirft ab (bekannter Vertrag).
main._unhandled_key_input(_press_key(KEY_ESCAPE))
await process_frame
_check(not bool(interaction.get("placing")), "ESC wirft danach ab")
_check(_objects().size() == 0, "kein Objekt gesetzt (ist %d)" % _objects().size())
await _cleanup_objects()
# ------------------------------------------------------------- 5 EIN Undo ----
func _test_undo_genau_eins() -> void:
print("Testgruppe: Ein Undo nimmt genau das eine gesetzte Objekt zurück")
var oid := _first_oid()
var interaction: Node = main.interaction
var assets: Node = main.assets_mod
main.call("_set_tool_mode", "objects")
await _cleanup_objects()
_check(int(_objects().size()) == 0, "Startzustand: leere Map (%d Objekte)" % _objects().size())
assets._activate_tile(oid)
var pos := _vp_center()
_motion(pos)
_mouse(MOUSE_BUTTON_LEFT, true, pos)
await process_frame
_mouse(MOUSE_BUTTON_LEFT, false, pos)
await process_frame
_check(_objects().size() == 1, "Objekt gesetzt (ist %d)" % _objects().size())
_check(main.history.can_undo(), "History hat den Platzier-Schritt")
_check(str(main.history.undo_desc()).contains("platzieren"), "Undo-Beschreibung nennt das Platzieren (%s)" % str(main.history.undo_desc()))
main._do_undo()
await process_frame
_check(_objects().size() == 0, "EIN Undo: das eine Objekt ist weg (ist %d)" % _objects().size())
_check(not main.history.can_undo(), "History ist leer (kein weiterer Schritt)")
_check(int(interaction.get("selected_index")) == -1, "Auswahl nach dem Undo geleert (Index -1)")
await _cleanup_objects()
# --------------------------------------------------- 6 Auswahl-Stabilität ----
## Direkter Pfad (Probe-Main wie tests/test_shortcuts.gd): nach _place_object
## wird die Baum-Markierung für das neue Objekt GENAU EINMAL gesetzt — kein
## doppeltes Auswahl-Ereignis (1× in der Platzier-Meldung, 1× im Werkzeug-Ende).
func _test_probe_auswahl_stabil() -> void:
print("Testgruppe: Auswahl stabil — Baum-Markierung genau 1× je Platzierung")
var doc := SvmapDocument.create_new("U5Test", 1024.0, "mittag")
var fake := ProbeMain.new()
fake.doc = doc
fake.manifest = {"objects": {"crate_wood": {"poly": 12, "collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
fake.sym_mode = SymmetryCore.Mode.OFF # Auswahl-Stabilität, nicht M3
fake.history.begin(doc)
var vp := SubViewport.new()
vp.size = Vector2i(1280, 720)
root.add_child(vp)
var cam := Camera3D.new()
vp.add_child(cam)
cam.current = true
cam.look_at_from_position(Vector3(-95.0, 115.0, 135.0), Vector3.ZERO, Vector3.UP)
var builder := ProbeBuilder.new()
var interaction := Node3D.new()
interaction.set_script(load("res://src/editor/interaction.gd"))
vp.add_child(interaction)
interaction.setup(fake, cam, builder)
# Werkzeug an (wie start_placement, ohne GLB-Abhängigkeit).
interaction.set("placing", true)
interaction.set("place_oid", "crate_wood")
var h := SvmapDocument.terrain_height_at(doc, 100.0, 40.0)
interaction.call("_place_object", cam.unproject_position(Vector3(100.0, h, 40.0)))
var objs: Array = doc.get("objects", [])
_check(objs.size() == 1, "Probe: 1 Objekt gesetzt (ist %d)" % objs.size())
_check(not bool(interaction.get("placing")), "Probe: Werkzeug nach dem Setzen AUS")
_check(int(interaction.get("selected_index")) == 0, "Probe: neues Objekt ausgewählt (Index 0)")
_check(fake.tree_marks == 1, "Probe: Baum-Markierung GENAU 1× (ist %d)" % fake.tree_marks)
_check(fake.statuses.size() > 0 and str(fake.statuses[fake.statuses.size() - 1]).contains("ausgewählt"),
"Probe: letzte Meldung sagt „ausgewählt“ (%s)" % str(fake.statuses.back() if not fake.statuses.is_empty() else ""))
# Serienmodus-Gegenprobe: mit Shift bleibt das Werkzeug an.
var inter2 := Node3D.new()
inter2.set_script(load("res://src/editor/interaction.gd"))
vp.add_child(inter2)
var doc2 := SvmapDocument.create_new("U5Test2", 1024.0, "mittag")
var fake2 := ProbeMain.new()
fake2.doc = doc2
fake2.manifest = {"objects": {"crate_wood": {"poly": 12, "collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
fake2.sym_mode = SymmetryCore.Mode.OFF # Serienmodus-Zählung ohne Zwillinge
fake2.history.begin(doc2)
inter2.setup(fake2, cam, builder)
inter2.set("placing", true)
inter2.set("place_oid", "crate_wood")
inter2.call("_place_object", cam.unproject_position(Vector3(100.0, h, 40.0)), true)
_check(bool(inter2.get("placing")), "Probe Serienmodus: Werkzeug bleibt AN (Shift)")
_check((doc2["objects"] as Array).size() == 1, "Probe Serienmodus: Objekt gesetzt")
_check(str(fake2.statuses.back() if not fake2.statuses.is_empty() else "").contains("Serienmodus"),
"Probe: Statuszeile nennt „Serienmodus (Shift)“")
# Zweiter serieller Klick: zweites Objekt, immer noch AN.
inter2.set("placing", true)
inter2.set("place_oid", "crate_wood")
inter2.call("_place_object", cam.unproject_position(Vector3(-100.0, h, -40.0)), true)
_check((doc2["objects"] as Array).size() == 2, "Probe Serienmodus: 2 Objekte gesetzt")
_check(bool(inter2.get("placing")), "Probe Serienmodus: immer noch AN")
vp.queue_free()
# -------------------------------------------------------------- Helfer ----
## Test-Map leeren (Undo-History bleibt bewusst stehen — die nächste Gruppe
## baut ihren eigenen Zustand auf). Alle Objekte hart entfernen.
func _cleanup_objects() -> void:
# Symmetrie aus (Standard ist Punkt) — die Serien-/Einmal-Zählung soll
# nicht von automatischen Zwillingen abhängen (M3, separate Karte).
main.sym_mode = SymmetryCore.Mode.OFF
var objects: Array = main.doc.get("objects", [])
objects.clear()
main.view_builder.call("rebuild", main.doc, main.manifest)
var interaction: Node = main.interaction
interaction.call("clear_selection", true)
main.history.clear()
await process_frame
func _clear_library_cfg_section() -> void:
# Nur UNSEREN Abschnitt leeren — [test]/[update]/[fenster] bleiben.
var cfg := ConfigFile.new()
cfg.load("user://scopedvr_editor.cfg")
if cfg.has_section("library"):
cfg.erase_section("library")
cfg.save("user://scopedvr_editor.cfg")
## Probe-main-Doppel (wie tests/test_shortcuts.gd ProbeMain): zählt
## Baum-Markierungen mit, damit die Auswahl-Stabilität beweisbar ist.
class ProbeMain:
var doc: Dictionary = {}
var manifest: Dictionary = {}
var grid_snap := false
var dirty := false
var sym_mode: int = SymmetryCore.Mode.OFF # U5-Probe: Symmetrie bewusst aus
var history: ActionHistory = ActionHistory.new()
var statuses: Array = []
var tree_marks := 0
func object_label(_oid: String) -> String:
return "Kiste"
func _fmt_int(v: int) -> String:
return str(v)
func _set_status(text: String, _err := false) -> void:
statuses.append(text)
func _highlight_library(_oid: String) -> void:
pass
func _selection_status(_idx: int) -> void:
pass
func _sync_selection_panel() -> void:
pass
func _mark_tree_selection(_idx: int) -> void:
tree_marks += 1
func _mark_tree_selection_multi(_indices: Array) -> void:
pass
func _after_structure_change() -> void:
pass
func _refresh_history_buttons() -> void:
pass
func _refresh_stats() -> void:
pass
func _refresh_file_label() -> void:
pass
class ProbeBuilder extends Node3D:
var object_markers: Array = []
var last_selection: Array = []
func set_selected_multi(indices: Array) -> void:
last_selection = indices.duplicate()