extends SceneTree ## Editor-Test (headless): Wasser pro Map (Karte t_28fdef03). ## Ausführen: godot --headless --path . --script res://tests/test_wasser.gd ## Exit 0 = alles grün. ## ## W1 Format (SvmapIO V20): water_level optional; gültige Zahl innerhalb ## des Terrain-Höhenbereichs ±10 m → keine Fehler; String/INF/draußen → ## V20-Fehler; Legacy (ohne Feld) validiert unverändert. ## W2 Map-Panel „Wasserhöhe“: Regler existiert, doc-Roundtrip ## (slider.value = Wasserhöhe; „Kein Wasser“ = Feld ENTFERNEN; ## refresh_values füllt aus dem doc auf; Slider-Nutzung markiert dirty). ## W3 Editor-Vorschau (map_view_builder): doc mit water_level → ## Wasser/WasserFlaeche existiert, liegt exakt auf der Höhe, Material = ## dasselbe Shader-Verfahren wie der Client (SvmapWasser.make_material); ## ohne Feld → kein Wasser-Node (alte Maps unverändert); Matsch-Ring ## in den Terrain-Zellfarben (Ufer-Automatik, gleiche Formel wie Client). ## W4 Validator-Spiegel: V20-Band/-Grenzen identisch zum Match-Server. var failures := 0 var checks := 0 var main var map_panel func _initialize() -> void: _run_all.call_deferred() func _check(cond: bool, label: String) -> void: checks += 1 print("%s: %s" % ["ok " if cond else "FAIL", label]) if not cond: failures += 1 func _run_all() -> void: var scene: PackedScene = load("res://scenes/main.tscn") _check(scene != null, "Aufbau: scenes/main.tscn laedt") if scene == null: _finish() return main = scene.instantiate() root.add_child(main) await process_frame await process_frame map_panel = main.get("map_mod") _check(map_panel != null, "Aufbau: Map-Panel in main eingebaut") _check(main.get("terrain_mod") != null, "Aufbau: Terrain-Panel eingebaut") _check(main.view_builder != null, "Aufbau: view_builder vorhanden") if main == null or map_panel == null or main.view_builder == null: _finish() return await _w1_validierung() await _w2_panel() await _w3_vorschau() _w4_validator_spiegel() _finish() func _w1_validierung() -> void: print("== W1: svmap-Validierung water_level (V20) ==") var terrain: Dictionary = main.doc["terrain"] var hmin := float(terrain["min_height_m"]) var hmax := float(terrain["max_height_m"]) # Legacy ohne Feld: unverändert gültig. var alte: Dictionary = main.doc.duplicate(true) alte.erase("water_level") SvmapIO.sign_map(alte) var f0: PackedStringArray = SvmapIO.validate_map(alte) _check(f0.is_empty(), "W1 Karte ohne water_level validiert (%s)" % str(f0)) # Gültige Werte. for wert in [hmin, hmax, (hmin + hmax) * 0.5]: var d: Dictionary = main.doc.duplicate(true) (d["terrain"] as Dictionary)["water_level"] = wert SvmapIO.sign_map(d) var f: PackedStringArray = SvmapIO.validate_map(d) _check(f.is_empty(), "W1 water_level=%.1f (Höhenbereich) validiert (%s)" % [wert, str(f)]) # Ungültige Werte → V20 (_validate_core, ohne Prüfsumme). for wert in ["hoch", INF, hmax + 10.5, hmin - 10.5]: var d2: Dictionary = main.doc.duplicate(true) (d2["terrain"] as Dictionary)["water_level"] = wert var f2: PackedStringArray = SvmapIO._validate_core(d2) var hat_v20 := false for e in f2: if str(e).begins_with("V20"): hat_v20 = true _check(hat_v20, "W1 water_level=%s → V20-Fehler (%s)" % [str(wert), str(f2)]) func _w2_panel() -> void: print("== W2: Map-Panel Wasserhöhe ==") var slider: HSlider = map_panel.get("prop_water") _check(slider != null, "W2 prop_water (Regler) existiert") if slider == null: return _check(is_equal_approx(float(slider.get_meta("kein_wasser_value", 0.0)), -10.0), "W2 Slider trägt die Kein-Wasser-Kennung (−10)") # doc OHNE Feld → Regler auf "Kein Wasser". main.doc.erase("water_level") map_panel.call("refresh_values") _check(is_equal_approx(float(slider.value), -10.0), "W2 ohne Feld → Regler auf „Kein Wasser“") # doc MIT Feld → Regler zeigt die Höhe. main.doc["water_level"] = 3.5 map_panel.call("refresh_values") _check(is_equal_approx(float(slider.value), 3.5), "W2 doc=3.5 → Regler 3.5") # Regler bewegen → doc + dirty. Erst das dirty-Flag aus dem refresh löschen. main.set("dirty", false) slider.value_changed.emit(4.25) _check(bool(main.get("dirty")), "W2 Regler-Bewegung markiert dirty") _check(is_equal_approx(float(main.doc.get("water_level", -999.0)), 4.25), "W2 Regler schreibt water_level ins doc") # Regler zurück auf „Kein Wasser“ → Feld ENTFERNEN (Legacy-sauber). slider.set_value_no_signal(-10.0) slider.value_changed.emit(-10.0) _check(not main.doc.has("water_level"), "W2 „Kein Wasser“ entfernt das Feld") _check(bool(main.get("dirty")), "W2 Entfernen markiert ebenfalls dirty") # Für W3: Feld wieder setzen (per Panel-Handler — dieselbe Verdrahtung). slider.set_value_no_signal(2.0) slider.value_changed.emit(2.0) _check(is_equal_approx(float(main.doc.get("water_level", -999.0)), 2.0), "W2 Feld wieder gesetzt (2.0) für die Vorschau-Prüfung") func _w3_vorschau() -> void: print("== W3: Editor-Vorschau (map_view_builder) ==") var vb = main.view_builder # Terrain mit einer echten Senke bauen, damit Wasser sichtbar ist. var terrain: Dictionary = main.doc["terrain"] var res := int(terrain["resolution"]) var hmin := float(terrain["min_height_m"]) var heights: PackedFloat32Array = SvmapIO.decode_heights(terrain) for i in heights.size(): heights[i] = hmin # Senke in der Map-Mitte unter die Wasserhöhe (2.0) graben. var mitte := int(float(res) * 0.5) for z in range(maxi(mitte - 4, 0), mini(mitte + 5, res)): for x in range(maxi(mitte - 4, 0), mini(mitte + 5, res)): heights[z * res + x] = 2.0 - 1.5 terrain["heightmap_r16_b64"] = TerrainField.encode(heights, hmin, float(terrain["max_height_m"])) SvmapIO.sign_map(main.doc) await process_frame await process_frame # queue_free der alten Kinder aus den W2-Rebuilds vb.call("rebuild", main.doc, main.manifest) await process_frame await process_frame # queue_free der alten Kinder braucht 2 Frames var wasser: Node3D = _wasser_node(vb) _check(wasser != null, "W3 Wasser-Node in der Vorschau (Senke gefüllt)") if wasser == null: return var mi := wasser.get_node_or_null("WasserFlaeche") as MeshInstance3D _check(mi != null, "W3 WasserFlaeche existiert") if mi != null: _check(is_equal_approx(mi.position.y, 2.0), "W3 Fläche auf Wasserhöhe (y=2.0)") var mat := mi.material_override _check(mat is ShaderMaterial and (mat as ShaderMaterial).shader != null, "W3 Material = Wasser-Shader (dasselbe Verfahren wie der Client)") if mat is ShaderMaterial: var sp: Variant = (mat as ShaderMaterial).get_shader_parameter("ufer_tex") _check(sp != null, "W3 Ufer-Tiefen-Textur gesetzt") # Matsch-Ring: dieselbe Formel wie der Client (Spiegel-Modul) mischt in # die Terrain-Zellfarben ein — direkt gegen die Spiegel-Mathe geprüft. # Knapp unter der Wasserlinie = fast voller Matsch (ufer_faktor ≈ 1), # 0,25 m tiefe Zelle = im weichen Band-Auslauf, Tiefwasser = 0. var f_ring := SvmapWasser.ufer_faktor(2.0 - 0.05, 2.0) var f_mitte := SvmapWasser.ufer_faktor(2.0 - 0.25, 2.0) var f_tief := SvmapWasser.ufer_faktor(-4.0, 2.0) _check(f_ring > 0.9, "W3 Matsch-Ring an der Wasserlinie (ufer_faktor > 0.9, ist %.2f)" % f_ring) _check(f_mitte > 0.4 and f_mitte < f_ring, "W3 Matsch-Band fällt weich ab (0,25 m Tiefe = %.2f)" % f_mitte) _check(f_tief == 0.0, "W3 Tiefwasser ohne Matsch (ufer_faktor = 0)") var base := Color(0.9, 0.9, 0.9, 0.25) # Alpha = gepackte Material-ID var gemischt := SvmapWasser.matsch_anwenden(base, 1.0) _check(gemischt.a == base.a, "W3 Matsch fasst die Material-ID (Alpha) nicht an") # Feld entfernen → kein Wasser (alte Maps unverändert). main.doc.erase("water_level") vb.call("rebuild", main.doc, main.manifest) await process_frame await process_frame # queue_free der alten Kinder braucht 2 Frames _check(_wasser_node(vb) == null, "W3 ohne water_level → kein Wasser-Node (Legacy)") func _wasser_node(vb) -> Node3D: return vb.get_node_or_null("Wasser") as Node3D func _w4_validator_spiegel() -> void: print("== W4: Validator-Spiegel (V20-Band wie Match-Server) ==") # Die Konstanten des Spiegel-Moduls decken die Server-Bandbreite ab. _check(SvmapWasser.LEVEL_MIN == -5.0 and SvmapWasser.LEVEL_MAX == 500.0, "W4 Plausibilitäts-Band Spiegel = Server (−5..500)") # Editor-Validator testet Terrain-Höhenbereich ±10 m — die Server-Regel # hat der Server-Suite identisch gezeigt; hier der Shader-Spiegel-Vertrag: _check(ResourceLoader.exists("res://src/viewport/wasser.gdshader"), "W4 Spiegel-Shader im Editor-Pfad vorhanden") func _finish() -> void: print("--------------------------------------------------") print("CHECKS: %d FAILURES: %d" % [checks, failures]) quit(0 if failures == 0 else 1)