class_name SvmapWasser extends RefCounted ## Wasser pro Map (Karte t_28fdef03) — SPIEGELGLEICHES Modul in Client UND ## Map-Editor (Muster: library/terrain_arrays.gd, t_ab408127 — identischer ## Pfad in beiden Repos, identischer Code; Repos bleiben entkoppelt). ## ## Sly-Entscheidungen 20.10.: Wasserlevel-Ansatz (EINE Höhe pro Map — Senken ## unter der Höhe füllen sich automatisch), Wat-Trägheit (knietief, kein ## Schwimmen), Ufer-Automatik (schlammiges Overlay knapp unter Wasserhöhe). ## ## Verantwortung dieses Moduls (Dictionary rein, Node out — wie SvmapTerrain): ## - hat()/level(): optionales svmap-Feld `water_level` (float, y-Höhe in m). ## Abwesenheit oder Fehltyp = KEIN Wasser (Legacy-Maps unverändert). ## - baue(): EINE transparente Fläche auf Wasserhöhe über der ganzen Map ## (1 Draw-Call, keine Geometrie-Wogen — Wellen NUR im Fragment-Shader). ## - ufer_bild(): Tiefe-Feld (R8, depth = wasser − Terrainhöhe, geklemmt auf ## tiefe_max) als Textur — der Shader liest daraus Ufer-Schaum + Flach- ## Wasser-Transparenz, OHNE Screen-Space-Tricks. ## - ufer_faktor()/matsch_anwenden(): Ufer-Automatik — Zellen knapp unter ## Wasserhöhe bekommen den Matsch-Ton. BEWUSST nur eine abgeleitete Tönung ## (kein gespeichertes Feld): die Material-Schicht bleibt malbar, der ## Matsch liegt als Höhen-Regel drüber — Client UND Editor-Vorschau ## rechnen IDENTISCH (dieselbe Datei). ## - make_material(): dasselbe Material wie der Client (Identitäts-Vertrag ## der Vorschau) — Shader-Pfad je Repo über SHADER_KANDIDATEN. ## ## KEINE echte Spiegelung/Brechung/SSR (bewusste Absage, Performance): ## Spiegel-Impression = Skybox-Farbreflex (uniform) + Fresnel-Glanz. ## ## Wasserdistanz-Regel (Perf-Beweis-Plan): ferne Fragmente vereinfachen im ## Shader selbst (kein Wellen-/Schaum-Aufwand) — keine Per-Frame-Logik nötig. ## Shader-Pfade je Repo (erster vorhandener gewinnt — Spiegel-Dateien sind ## byteidentisch, siehe tools/sync_wasser.sh im Map-Editor). const SHADER_KANDIDATEN: Array[String] = [ "res://shaders/wasser.gdshader", # scopedvr-client "res://core/viewport/wasser.gdshader", # scopedvr-map-editor (Spiegel) ] ## Plausibilitätsbereich des svmap-Feldes `water_level` (Meter, y-Höhe). ## GLEICHE Werte wie V18 im Match-Server (svmap_loader) und im Editor ## (svmap_io) — nur Dokumentations-Konstante hier. const LEVEL_MIN := -5.0 const LEVEL_MAX := 500.0 ## Ufer-Bild: Tiefe in 0..tiefe_max m → 0..255 (R8). Mehr Tiefe braucht kein ## Detail — Schaum lebt in den ersten Dezimetern, Transparenz in den ersten Metern. const TIEFE_MAX := 4.0 ## Schaum-Band am Ufer (Meter Wassertiefe, in denen Schaum sichtbar ist). const SCHAUM_BAND := 0.35 ## Matsch-Ring (Ufer-Automatik): Zellen, deren Terrainhöhe weniger als ## MATSCH_HOEHE unter dem Wasserlevel liegt (bis MATSCH_UEBER leicht darüber, ## damit die Wasserlinie selbst matschig ist), werden zum Matsch-Ton überblendet. const MATSCH_HOEHE := 0.55 const MATSCH_UEBER := 0.06 const MATSCH_FARBE := Color(0.34, 0.26, 0.18) # schlammiges Braun-Betonung ## Wetter-Farbkopplung (Entscheidung der Atmosphären-Karte): Wasser-Tint je ## Preset. farbe_flach = Seicht-Wasser, farbe_tief = tiefe Mitte, reflex = ## Skybox-Farbreflex (Himmelsfarbe des Presets), glanz = Glint-Stärke. ## Schlüssel = Client-Wetter-Keys (WeatherPresets). Unbekannt → "mittag". const WETTER_WASSER := { "mittag": { "farbe_flach": Color(0.10, 0.30, 0.34), "farbe_tief": Color(0.02, 0.10, 0.15), "reflex": Color(0.72, 0.83, 0.92), "glanz": 1.0, }, "abend": { "farbe_flach": Color(0.18, 0.26, 0.28), "farbe_tief": Color(0.04, 0.07, 0.11), "reflex": Color(0.95, 0.62, 0.38), "glanz": 1.2, }, "regen": { "farbe_flach": Color(0.20, 0.24, 0.26), "farbe_tief": Color(0.06, 0.09, 0.11), "reflex": Color(0.55, 0.58, 0.62), "glanz": 0.35, }, "nacht": { "farbe_flach": Color(0.02, 0.05, 0.09), "farbe_tief": Color(0.0, 0.015, 0.03), "reflex": Color(0.16, 0.22, 0.38), "glanz": 0.9, }, } ## Optionales svmap-Feld `water_level` vorhanden und brauchbar? Abwesenheit ## ODER Fehltyp = kein Wasser (Legacy-Maps unverändert, Fehltyp toleriert). static func hat(data: Dictionary) -> bool: var v: Variant = data.get("water_level", null) return v is float or v is int ## Wasserhöhe aus der Map (INF = kein Wasser). static func level(data: Dictionary) -> float: var v: Variant = data.get("water_level", null) if v is float or v is int: return float(v) return INF ## Wasserfläche bauen. terrain = der geparste terrain-Block, wasser = Wasser- ## Höhe (Meter), wetter = Preset-Key ("mittag"/"abend"/"regen"/"nacht"). ## Rückgabe: Node3D mit EINER transparenten Fläche — null, wenn kein Wasser ## sinnvoll ist (kein Feld, kein Terrain, Wasserhöhe unter dem Tiefpunkt, ## ungültige Maße). Die Senken "füllen sich" von selbst: alles unter der ## Höhe liegt hinterher unter der Fläche. static func baue(terrain: Dictionary, wasser: float, wetter: String) -> Node3D: if not is_finite(wasser): return null var res := int(terrain.get("resolution", 0)) var size_x := float(terrain.get("size_x_m", 0.0)) var size_z := float(terrain.get("size_z_m", 0.0)) if res < 2 or size_x <= 0.0 or size_z <= 0.0: return null var heights := _decode_heights(terrain) if heights.size() < res * res: return null var hmin := heights[0] for h in heights: if h < hmin: hmin = h if wasser <= hmin: return null # Wasserhöhe unter dem Tiefpunkt — nichts gefüllt. var root := Node3D.new() root.name = "Wasser" var mi := MeshInstance3D.new() mi.name = "WasserFlaeche" var plane := PlaneMesh.new() plane.size = Vector2(size_x, size_z) mi.mesh = plane mi.position = Vector3(0.0, wasser, 0.0) mi.material_override = make_material(wetter, ufer_bild(heights, res, wasser)) root.add_child(mi) return root ## Ufer-Tiefe-Bild (R8): Pixel = Terrainhöhe der Zelle, R-Wert = Tiefe unter ## dem Wasserlevel (0 = Wasserlinie/Ufer, 255 = tiefe_max oder tiefer). ## Identische Index-Konvention wie das Höhenfeld: (0,0) = −X/−Z-Ecke, x → +X, ## z → +Z (svmap-Vertrag). Der PlaneMesh des Wassers überdeckt denselben ## Bereich (zentriert, UV 0..1), der Shader sampelt direkt mit UV. static func ufer_bild(heights: PackedFloat32Array, res: int, wasser: float) -> Image: var img := Image.create(res, res, false, Image.FORMAT_R8) for z in res: for x in res: var d := wasser - heights[z * res + x] # Tiefe (negativ = Land) var v := int(round(clampf(d / TIEFE_MAX, 0.0, 1.0) * 255.0)) img.set_pixel(x, z, Color8(v, 0, 0)) return img ## Ufer-Automatik (Matsch-Ring): Faktor 0 (Land/tief) … 1 (Uferstreifen). ## Band = MATSCH_HOEHE unter bis MATSCH_UEBER über dem Wasserlevel, weich ## (smoothstep) an beiden Kanten — kein Harter Pixel-Kantensprung. static func ufer_faktor(h: float, wasser: float) -> float: if not is_finite(wasser): return 0.0 var d := wasser - h # Tiefe der Zelle (negativ = über Wasser) var oben := clampf((d + MATSCH_UEBER) / MATSCH_UEBER, 0.0, 1.0) if oben <= 0.0: return 0.0 var unten := clampf(d / MATSCH_HOEHE, 0.0, 1.0) var f := minf(oben, 1.0 - unten * unten * (3.0 - 2.0 * unten)) return clampf(f, 0.0, 1.0) ## Matsch-Ton auf eine Terrain-Zellfarbe anwenden (Client UND Editor rufen ## dieselbe Formel — Identität der Vorschau). faktor = ufer_faktor(...). ## BEWUSST nur RGB: die Alpha der Terrain-Zellfarbe trägt die gepackte ## Material-ID (m/N, Karte t_ab408127) — die darf der Matsch nie anfassen, ## sonst landen die Zellen im Splat-Shader auf dem falschen Textur-Layer. static func matsch_anwenden(farbe: Color, faktor: float) -> Color: var gemischt := farbe.lerp(MATSCH_FARBE, faktor * 0.75) # 0.75: Ton bleibt sichtbar gemischt.a = farbe.a return gemischt ## Wasser-Material (dasselbe im Client UND in der Editor-Vorschau — ## Identitäts-Vertrag): Custom-Shader + Wetter-Tint + Ufer-Tiefen-Textur. ## Fehlt der Shader (Frisch-Klon ohne Spiegel), fällt das Material auf ein ## simples transparentes StandardMaterial zurück — Wasser bleibt sichtbar. static func make_material(wetter: String, ufer: Image = null) -> Material: var preset: Dictionary = WETTER_WASSER.get(wetter, WETTER_WASSER["mittag"]) var shader := _shader_laden() if shader != null: var mat := ShaderMaterial.new() mat.shader = shader mat.set_shader_parameter("farbe_flach", preset["farbe_flach"]) mat.set_shader_parameter("farbe_tief", preset["farbe_tief"]) mat.set_shader_parameter("reflex", preset["reflex"]) mat.set_shader_parameter("glanz", float(preset["glanz"])) if ufer != null: mat.set_shader_parameter("ufer_tex", ImageTexture.create_from_image(ufer)) return mat # Fallback ohne Shader (Robustheit — Wasser bleibt sichtbar, nur ohne Wellen). var std := StandardMaterial3D.new() std.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA std.albedo_color = Color(preset["farbe_flach"], 0.6) std.roughness = 0.15 std.metallic = 0.1 return std ## Shader aus den Repo-Pfaden laden (erster vorhandener gewinnt). static func _shader_laden() -> Shader: for pfad in SHADER_KANDIDATEN: if ResourceLoader.exists(pfad): var s := load(pfad) if s is Shader: return s return null ## Höhenfeld dekodieren (R16, identische Formel wie SvmapTerrain/SvmapIO/ ## match-server — hier NOCHMAL lokal, damit dieses Spiegel-Modul von keinem ## Repo-Skript abhängt; 15 Zeilen Duplikat gegen Repo-Kopplung getauscht). static func _decode_heights(terrain: Dictionary) -> PackedFloat32Array: var res := int(terrain.get("resolution", 0)) var out := PackedFloat32Array() if res < 2: return out out.resize(res * res) var raw: PackedByteArray = Marshalls.base64_to_raw(str(terrain.get("heightmap_r16_b64", ""))) if raw.size() < res * res * 2: return PackedFloat32Array() # kaputtes/fehlendes Höhenfeld → kein Wasser var hmin := float(terrain.get("min_height_m", 0.0)) var hmax := float(terrain.get("max_height_m", 1.0)) if hmax <= hmin: hmax = hmin + 1.0 var n := mini(raw.size() / 2, res * res) for i in n: var v: int = raw.decode_u16(i * 2) out[i] = hmin + (float(v) / 65535.0) * (hmax - hmin) return out