extends SceneTree ## Editor-Test (headless): R3 Resize-Dialog — Karte t_c863b93b. ## Ausführen: godot --headless --path . --script res://tests/test_resize_dialog.gd ## Exit 0 = alles grün. ## ## Deckt ab: ## 1. Dialog-Aufbau (Stil wie new_map_dialog): Presets, Clamp 256–4096, ## Info-Zeile mit aktueller Größe/Auflösung. ## 2. Preview-Texte (R2 resize_preview): Verlust-Liste beim Verkleinern, ## „nichts verloren“-Fall, Spawn-Speicher-Warnung (< 4 Punkte/Team, V11), ## Warnband beim Vergrößern über ~1024 m (Dichte-Leiter-Deckel). ## 3. Verdrahtung in main.gd: Menü-ID 6 öffnet Dialog, Bestätigen = resize_apply ## als EINE Undo-Einheit, Undo-Roundtrip speichert die alte Map ## BYTE-IDENTISCH, Redo stellt den Resize wieder her. var failures := 0 var checks := 0 var main: Control func _initialize() -> void: _run_all.call_deferred() func _run_all() -> void: _test_dialog_basics() _test_preview_texts() await _test_main_wiring() _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) # --------------------------------------------------------------- Dialog ----- func _make_dialog() -> ConfirmationDialog: var dlg: ConfirmationDialog = preload("res://src/ui/resize_map_dialog.gd").new() return dlg func _test_dialog_basics() -> void: print("Testgruppe: Dialog-Aufbau + Clamp") var dlg := _make_dialog() var doc := SvmapDocument.create_new("Test", 1024.0, "mittag") dlg.setup(doc) _check(dlg.info_label.text.contains("1024"), "Info-Zeile zeigt aktuelle Groesse (1024 m)") _check(dlg.info_label.text.contains("129"), "Info-Zeile zeigt Aufloesung (129 Punkte)") _check(is_equal_approx(dlg.get_new_size_x(), 1024.0), "Freifeld startet bei aktueller Groesse") # Preset-Button 256: beide Achsen wandern mit (Button sitzt im Preset-HBox, # unter dem ScrollContainer — FIX 20.10.b hat den dazwischengeschoben). var preset_btn: Button = null for h in dlg.find_children("*", "HBoxContainer", true, false): for b in (h as HBoxContainer).get_children(): if b is Button and b.text == "256 m": preset_btn = b _check(preset_btn != null, "Preset-Button 256 m vorhanden") if preset_btn != null: preset_btn.emit_signal("pressed") _check(is_equal_approx(dlg.get_new_size_x(), 256.0) and is_equal_approx(dlg.get_new_size_z(), 256.0), "Preset 256 setzt beide Achsen") # Clamp: SpinBox deckelt auf 256–4096. dlg.size_x_edit.value = 99999.0 _check(is_equal_approx(dlg.get_new_size_x(), 4096.0), "Clamp oben: 4096 m") dlg.size_x_edit.value = 1.0 _check(is_equal_approx(dlg.get_new_size_x(), 256.0), "Clamp unten: 256 m") dlg.free() # -------------------------------------------------------------- Preview ----- ## Map mit Objekt + Bauteil + Spawn-Punkt draußen (X = ±5000 m) bauen. func _doc_with_outsiders(size: float) -> Dictionary: var doc := SvmapDocument.create_new("Preview", size, "mittag") doc["objects"] = [ {"id": "drinnen", "pos": [10.0, 0.0, 10.0], "rot": 0.0, "scale": 1.0}, {"id": "draussen", "pos": [5000.0, 0.0, 0.0], "rot": 0.0, "scale": 1.0}, ] doc["gebaeude"] = [ {"pos": [-30.0, 0.0, -30.0], "rot": 0.0, "punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]}, {"pos": [-5000.0, 0.0, 0.0], "rot": 0.0, "punkte": [[-2.0, 0.0, -2.0], [2.0, 0.0, 2.0]]}, ] var zone0: Dictionary = doc["spawn_zones"]["0"] var pts: Array = zone0["points"] for i in pts.size(): pts[i]["pos"][0] = 5000.0 + float(i) # Team Blau komplett draußen zone0["points"] = pts return doc func _test_preview_texts() -> void: print("Testgruppe: Preview-Texte (Verlust-Liste, Spawn-Warnung, Warnband)") var dlg := _make_dialog() # 1) Verkleinern mit Verlust: Liste + Entfernen-Hinweis + Spawn-Warnung. var doc := _doc_with_outsiders(1024.0) dlg.setup(doc) dlg.size_x_edit.value = 256.0 dlg.size_z_edit.value = 256.0 dlg._update_preview() # auerhalb des Baums feuert value_changed nicht (in-tree: live) var text: String = dlg.preview_label.text _check(text.contains("draußen") and text.contains("1 Objekt"), "Verlust-Liste nennt 1 Objekt draußen") _check(text.contains("1 Bauteil"), "Verlust-Liste nennt 1 Bauteil draußen") # 4 verschobene (Team Blau) + 4 Default-Punkte (Team Rot bei ±0,8 × 512 m) = 8 draußen. _check(text.contains("8 Spawn-Punkt"), "Verlust-Liste nennt 8 Spawn-Punkte draußen") _check(text.contains("ENTFERNT"), "Hinweis: draußen wird entfernt (Checkbox an)") _check(text.contains("Team") and text.contains("4 pro Team"), "Spawn-Speicher-Warnung: Team unter 4 Punkte") _check(dlg.get_drop_outside(), "Checkbox Entfernen ist Default-an") # 2) Entfernen aus: draußen bleibt, Map ist dann nicht speicherbar (V9). dlg.drop_check.button_pressed = false dlg._update_preview() text = dlg.preview_label.text _check(text.contains("bleiben") and text.contains("Rand"), "Ohne Entfernen: draußen bleibt in der Map") _check(text.contains("NICHT speicherbar"), "Ohne Entfernen: Warnung vor V9 (Speichern blockiert)") _check(not text.contains("ENTFERNT"), "Kein Entfernen-Hinweis bei Checkbox aus") dlg.drop_check.button_pressed = true # 3) Verkleinern ohne Verlust: alles innen → Klartext „nichts verloren“. var doc_leer := SvmapDocument.create_new("Leer", 512.0, "mittag") var z0: Dictionary = doc_leer["spawn_zones"]["0"] var z1: Dictionary = doc_leer["spawn_zones"]["1"] for zone in [z0, z1]: var pts2: Array = zone["points"] for i in pts2.size(): pts2[i]["pos"][0] = 5.0 * float(i) # alle nah am Zentrum pts2[i]["pos"][2] = 0.0 # Z ebenfalls rein (Default-Z wäre ±153 m) zone["points"] = pts2 dlg.setup(doc_leer) dlg.size_x_edit.value = 256.0 dlg.size_z_edit.value = 256.0 dlg._update_preview() text = dlg.preview_label.text _check(text.contains("nichts verloren"), "Verkleinern ohne Verlust: Klartext-Meldung") # 4) Vergrößern > 1024 m: Warnband (Dichte grobt, Quest-Perf). dlg.setup(SvmapDocument.create_new("Grow", 512.0, "mittag")) dlg.size_x_edit.value = 2048.0 dlg.size_z_edit.value = 2048.0 dlg._update_preview() text = dlg.preview_label.text _check(text.contains("grober") and text.contains("Quest"), "Warnband bei Grow > 1024 m (Dichte + Quest-Perf)") _check(text.contains("flaches Gras") or text.contains("Randzone"), "Grow-Hinweis: neue Randzone wird Gras") dlg.free() # -------------------------------------------------------- main.gd-Drähte ---- func _test_main_wiring() -> void: print("Testgruppe: main.gd-Verdrahtung + Undo-Roundtrip") var scene: PackedScene = load("res://scenes/main.tscn") _check(scene != null, "scenes/main.tscn laedt") if scene == null: return main = scene.instantiate() root.add_child(main) await process_frame await process_frame _check(main.resize_dialog != null, "Resize-Dialog in der Main-UI gebaut") main._start_with_new_map() var doc: Dictionary = main.doc _check(is_equal_approx(float(doc["terrain"]["size_x_m"]), 1024.0), "Start-Map 1024 m") # Team Blau komplett draußen schieben (Verkleinern 1024 → 256). var zone0: Dictionary = doc["spawn_zones"]["0"] var pts: Array = zone0["points"] for i in pts.size(): pts[i]["pos"][0] = 500.0 + float(i) # drin (±512), aber draußen bei 256 zone0["points"] = pts # Referenz-Sicherheit: history hält doc als Objekt (keine Kopie). _check(is_same(main.history.doc, main.doc), "History haelt doc per Referenz") # Vorher-Zustand speichern → später Byte-Identität prüfen. var path_a := "/tmp/r3_vorher.svmap" var path_b := "/tmp/r3_nach_undo.svmap" var save_a := SvmapIO.save_to_file(path_a, doc) _check(bool(save_a["ok"]), "Vorher-Zustand speicherbar") # Dialog öffnen (Menü-Pfad): Info-Zeile + Preview gefüllt. main._open_resize_dialog() _check(main.resize_dialog.visible, "Menue-Eintrag oeffnet den Dialog") _check(main.resize_dialog.info_label.text.contains("1024"), "Dialog zeigt aktuelle Groesse") main.resize_dialog.size_x_edit.value = 256.0 main.resize_dialog.size_z_edit.value = 256.0 _check(main.resize_dialog.preview_label.text.contains("draußen"), "Live-Preview listet Verlust beim Verkleinern") # Bestätigen: resize_apply + EINE Undo-Einheit. main._on_resize_confirmed() var t: Dictionary = main.doc["terrain"] _check(is_equal_approx(float(t["size_x_m"]), 256.0), "Terrain auf 256 m verkleinert") _check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(), "Draußen liegende Spawn-Punkte entfernt (Drop an)") _check(bool(main.dirty), "Dirty-Flag gesetzt") _check(main.history.can_undo() and main.history.undo_desc().contains("256"), "EINE Undo-Einheit: Karte auf 256 m") _check(is_same(main.history.doc, main.doc), "doc-Identitaet bleibt beim Resize erhalten") # Strg+Z: kompletter Vorher-Zustand, Map speichert BYTE-IDENTISCH. main._do_undo() var t2: Dictionary = main.doc["terrain"] _check(is_equal_approx(float(t2["size_x_m"]), 1024.0), "Undo: Terrain wieder 1024 m") _check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == 4, "Undo: entfernte Spawn-Punkte komplett zurueck") var save_b := SvmapIO.save_to_file(path_b, main.doc) _check(bool(save_b["ok"]), "Nach-Undo-Zustand speicherbar") var bytes_a := FileAccess.get_file_as_bytes(path_a) var bytes_b := FileAccess.get_file_as_bytes(path_b) _check(bytes_a == bytes_b, "Undo-Roundtrip: alte Map BYTE-IDENTISCH") # Strg+Y: Resize wieder hergestellt. main._do_redo() _check(is_equal_approx(float(main.doc["terrain"]["size_x_m"]), 256.0), "Redo: Resize wiederhergestellt") _check((main.doc["spawn_zones"]["0"]["points"] as Array).is_empty(), "Redo: Spawn-Punkte wieder entfernt") # Drop aus: draußen bleibt in der Map (Dialog-Checkbox aus) — dokumentiertes # Verhalten: Map bleibt, ist ABER nicht speicherbar (V9: Spawn außerhalb der # Match-Grenzen); der Dialog warnt davor. main.resize_dialog.drop_check.button_pressed = false main.resize_dialog.size_x_edit.value = 256.0 main.resize_dialog.size_z_edit.value = 256.0 # Erst Undo (zurück auf 1024 mit Punkten), dann Resize ohne Drop. main._do_undo() var before_count := (main.doc["spawn_zones"]["0"]["points"] as Array).size() main._on_resize_confirmed() _check((main.doc["spawn_zones"]["0"]["points"] as Array).size() == before_count, "Drop aus: draußen liegende Spawn-Punkte bleiben in der Map") _check(not bool(SvmapIO.save_to_file("/tmp/r3_drop_aus.svmap", main.doc)["ok"]), "Drop aus + Punkte draußen: Speichern blockiert (V9), wie im Dialog gewarnt") # REGRESSION 20.10. (Sly-Befund „Kartengröße ändern tut nichts“): der # ECHTE UI-Pfad ist das id_pressed-SIGNAL des Datei-Popups. Vorher hing # main.call.bind("_on_menu_action") daran — die Menü-ID landete als # ARGUMENT VOR dem Methodennamen (call(6, "_on_menu_action")) → # „Cannot convert argument 1 from int to StringName“, das GESAMTE # Datei-Menü war tot. Diese Suite rief _open_resize_dialog() direkt auf # und sah den Bruch nicht — jetzt wird der echte Klick nachgestellt. var file_menu_btn: MenuButton = null for n in main.find_children("*", "MenuButton", true, false): if n is MenuButton and n.text == "Datei": file_menu_btn = n _check(file_menu_btn != null, "Datei-Menü (MenuButton) gefunden") if file_menu_btn != null: main.resize_dialog.hide() (file_menu_btn.get_popup() as PopupMenu).id_pressed.emit(6) _check(main.resize_dialog.visible, "Echter Menü-Klick (id_pressed=6) öffnet den Resize-Dialog") # REGRESSION 20.10.b (Sly: „Fenster viel zu lang, nichts bestätigbar“): # autowrap-Labels ohne Mindestbreite melden VOR dem ersten Layout eine # explosionsartige Mindesthöhe (jedes Wort eine Zeile bei ~1 px Breite) → # popup_centered() übernimmt diesen Wert als Fensterhöhe (Probe: 5930 px, # OK/Abbrechen unterhalb des Bildschirms). Fix: Mindestbreite ~ Dialog- # breite an den Labels. Der Check MISST VOR dem Popup — genau da saugte # sich das Fenster voll. Absolutwert (kein Prozentbezug, headless-Wurzel- # fenster ist nur 64 px hoch). var dlg_min_h: float = main.resize_dialog.get_contents_minimum_size().y _check(dlg_min_h > 200.0 and dlg_min_h < 1500.0, "Resize-Dialog Mindesthöhe normal vor Popup (%.0f px)" % dlg_min_h) var nm_min_h: float = main.new_dialog.get_contents_minimum_size().y _check(nm_min_h > 150.0 and nm_min_h < 1500.0, "Neue-Map-Dialog Mindesthöhe normal (%.0f px)" % nm_min_h)