diff --git a/assets/textures/terrain/terrain_arrays.res b/assets/textures/terrain/terrain_arrays.res new file mode 100644 index 0000000..d3fcec4 Binary files /dev/null and b/assets/textures/terrain/terrain_arrays.res differ diff --git a/library/terrain_arrays.gd b/library/terrain_arrays.gd new file mode 100644 index 0000000..6509e61 --- /dev/null +++ b/library/terrain_arrays.gd @@ -0,0 +1,18 @@ +extends Resource +class_name TerrainArrays +## Träger-Resource für die Terrain-Texture2DArrays (Karte t_ab408127). +## GLEICHER Pfad in Client UND Editor (res://library/terrain_arrays.gd) — +## die gespeicherte .res verweist auf den Script-Pfad, der muss in beiden +## Repos auflösen (Repos sind entkoppelt, therefore identischer Ort). +## +## Warum diese Klasse: ResourceSaver speichert die internen Images eines +## Texture2DArray NICHT (gespeicherte .res/.tres enthält 20× null — 20.10. +## bewiesen). Images in einer eigenen exportierten Property dagegen landen +## zuverlässig in der .res-Datei. Der Verbraucher baut beim LOAD aus diesen +## Images sofort die Texture2DArrays (create_from_images) — die Images sind +## bereits ETC2-komprimiert (Quest-3-nativ, ~1 MB/Layer statt 4 MB RGBA8), +## der Bau ist also nur ein GPU-Upload, kein Rekomprimieren. + +@export var albedo_images: Array[Image] = [] +@export var normal_images: Array[Image] = [] +@export var tile_size := 1.0 # Meter pro Kachel (Worldspace-Tiling) diff --git a/library/terrain_arrays.gd.uid b/library/terrain_arrays.gd.uid new file mode 100644 index 0000000..2c9b50f --- /dev/null +++ b/library/terrain_arrays.gd.uid @@ -0,0 +1 @@ +uid://c5v7hht4apubd diff --git a/src/svmap/svmap_io.gd b/src/svmap/svmap_io.gd index 62fa374..d752acc 100644 --- a/src/svmap/svmap_io.gd +++ b/src/svmap/svmap_io.gd @@ -26,7 +26,32 @@ const FORMAT_VERSION := 2 ## 2 = aktuell. Neu gespeicherte Maps tragen immer FORMAT_VERSION. const SUPPORTED_FORMAT_VERSIONS: Array[int] = [1, 2] ## Material-IDs (Index = ID). 0 = Gras = Default für Maps ohne Schicht. -const MATERIAL_IDS: Array[String] = ["gras", "sand", "fels"] +## Karte t_ab408127 (Sly-Vielfalt): Palette von 3 auf 20 erweitert — NEUE IDs +## NUR ANGEHÄNGT, Legacy-Zuordnung (0=Gras, 1=Sand, 2=Fels) bleibt unverändert, +## damit alte Maps byte-identisch laden. Format v2 bleibt (Feldgröße = res², +## Werte = uint8 je Sample — der Bereich wächst nur implizit mit der Länge). +const MATERIAL_IDS: Array[String] = [ + "gras_steppe", # 0 = Legacy-Gras (Default/Standard-Boden) + "sand_rippen", # 1 = Legacy-Sand (Dünen/offene Flächen) + "fels_granit", # 2 = Legacy-Fels (Berge/Klippen) + "gras_satt", # 3 = gesundes, sattes Grün + "gras_vertrocknet", # 4 = ausgedörrt, gelblich + "gras_moosig", # 5 = weiches Moosgrün + "erde_braun", # 6 = feuchte braune Erde + "erde_getreten", # 7 = festgetretener Pfad + "erde_umgeackert", # 8 = umgegrabene/zerwühlte Erde + "schotter", # 9 = Kies/Schotter + "lehm_rot", # 10 = rotbrauner Lehm + "sand_hell", # 11 = heller Flugsand + "sand_rost", # 12 = rostroter Sand + "fels_basalt", # 13 = dunkler Basalt + "fels_geroell", # 14 = Geröllhalde + "schlamm", # 15 = Matsch/Schlamm + "schnee_frisch", # 16 = Neuschnee + "schnee_matschig", # 17 = angeschmolzen/angesaut + "asche", # 18 = Asche/verkohlt + "stoppeln", # 19 = abgeerntetes Feld +] const WEATHER_PRESETS: Array[String] = ["mittag", "abend", "regen", "nacht"] ## Skybox-Option (Karte t_f72913d4): Werte des optionalen Feldes `weather_sky` ## — identische Semantik wie der Client (Weather.apply_preset sky_override): diff --git a/src/ui/panels/terrain_panel.gd b/src/ui/panels/terrain_panel.gd index 7425181..c0991a3 100644 --- a/src/ui/panels/terrain_panel.gd +++ b/src/ui/panels/terrain_panel.gd @@ -78,19 +78,38 @@ func build() -> void: mat_row.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster terrain_palette.add_child(mat_row) var mat_group := ButtonGroup.new() - var mat_labels := ["🌿 Gras", "🏖 Sand", "🪨 Fels"] + # Karte t_ab408127: 20 Slots in scrollbarer Raster-Palette (Spec: „like + # Bibliothek") — 4 Spalten, ~2 sichtbare Reihen, scrollbar nach unten. + # Thumbnails folgen, sobald die Texturen in assets/textures/terrain/ liegen + # (gen_terrain_thumbs) — bis dahin Button-Farbfeld via icon (Default). + var mat_grid := GridContainer.new() + mat_grid.columns = 4 + mat_grid.add_theme_constant_override("h_separation", UIScale.px(4)) + mat_grid.add_theme_constant_override("v_separation", UIScale.px(4)) + var mat_scroll := ScrollContainer.new() + mat_scroll.custom_minimum_size = UIScale.v2(0, 176) # ~3 Reihen a 48 px + Separation + mat_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + mat_scroll.add_child(mat_grid) + terrain_palette.add_child(mat_scroll) + var mat_labels := [ + "🌿 Gras", "🏖 Sand", "🪨 Fels", "🌱 Sattgras", + "🍂 Vertrocknet", "🍀 Moos", "🟫 Erde", "🛤 Pfad", + "⛏ Umgegraben", "🧱 Schotter", "🟤 Lehm", "🏜 Sand hell", + "🏜 Sand rost", "🗻 Basalt", "⛰ Geröll", "🌧 Schlamm", + "❄️ Schnee", "🥶 Matschschnee", "🌑 Asche", "🌾 Stoppeln", + ] for mat_id in SvmapIO.MATERIAL_IDS.size(): var mat_btn := Button.new() - mat_btn.text = str(mat_labels[mat_id]) - mat_btn.custom_minimum_size = UIScale.v2(0, 40) + mat_btn.text = str(mat_labels[mat_id]) if mat_id < mat_labels.size() else str(SvmapIO.MATERIAL_IDS[mat_id]) + mat_btn.custom_minimum_size = UIScale.v2(0, 48) mat_btn.toggle_mode = true mat_btn.button_group = mat_group - mat_btn.tooltip_text = "Malen: %s" % str(SvmapIO.MATERIAL_IDS[mat_id]) + mat_btn.tooltip_text = "Malen: %s (Slot %d)" % [str(SvmapIO.MATERIAL_IDS[mat_id]), mat_id] if mat_id == 0: mat_btn.button_pressed = true mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id)) terrain_mat_buttons.append(mat_btn) - mat_row.add_child(mat_btn) + mat_grid.add_child(mat_btn) # Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider. terrain_panel.add_child(_field_label("Pinselgröße:")) diff --git a/src/viewport/terrain_contour.gdshader b/src/viewport/terrain_contour.gdshader index ea89812..8dd145b 100644 --- a/src/viewport/terrain_contour.gdshader +++ b/src/viewport/terrain_contour.gdshader @@ -2,23 +2,51 @@ shader_type spatial; // Terrain-Konturlinien (EDITOR-ONLY, Karte t_3ebe4086): Höhenlinien alle 1 m // plus kräftigere 5-m-Indexlinie, per-pixel aus UV2.x (= Vertex-Höhe in m). // Der Quest-3-Perf-Vertrag (kein Custom-Shader) gilt fürs SPIEL — der Editor -// darf teurer sein (DECISIONS.md-Konvention). Der Match-Client rendert -// weiter StandardMaterial3D + Vertex-Farben. +// darf teurer sein (DECISIONS.md-Konvention). // -// Farbquelle = Vertex-Farbe (COLOR) mit eingebackenem Relief-Shading -// (Höhen-Kurve × Pseudo-Licht) — der Shader ist unshaded, damit das -// gebackene Shading exakt das ist, was gemessen/gezeigt wird (kein -// Doppel-Shading mit dem Viewport-Sonnenlicht). +// Karte t_ab408127 (Terrain-Texturierung): der Editor rendert DIESELBEN +// Texturen wie der Match-Client — Albedo/Normal-Arrays über die Vertex-Alpha +// (Material-ID), Worldspace-Tiling. Konturlinien liegen OBENDRAUF (dunkle +// Multiplikation auf der texturierten Fläche). Ohne Arrays (uniforms nicht +// gesetzt, Fallback-Klon) bleibt es beim reinen Vertex-Farb-Look. +// +// Farbquelle = COLOR: RGB = gebackenes Relief-Shading (Höhen-Kurve × +// Pseudo-Licht), A = Material-ID (m/N gepackt). Unshaded: das gebackene +// Shading ist exakt sichtbar (deterministisch für die Render-Probe). render_mode unshaded, cull_back; -// Linienstärken (0 = aus). Terrai-Mesh setzt linien_an auf 0, wenn die +// Linienstärken (0 = aus). Terrain-Mesh setzt linien_an auf 0, wenn die // Map flacher als 2 m ist (sonst wäre ALLES auf der 0-m-Linie dunkel). uniform float linien_an : hint_range(0.0, 1.0) = 1.0; uniform float linien_staerke : hint_range(0.0, 1.0) = 0.5; uniform float index_staerke : hint_range(0.0, 1.0) = 0.75; +// Textur-Arrays (t_ab408127) — identische Semantik wie terrain_splat.gdshader +// im Client. Nicht gesetzt → ausschalten (vertex_only-Modus). +uniform sampler2DArray albedo_array : source_color, filter_linear_mipmap, repeat_enable; +uniform float layer_count = 20.0; +uniform float tile_size = 1.0; +uniform float texturen_an = 0.0; // 1.0 = Texturen aktiv (vom Mesh-Code gesetzt) + +varying vec3 world_pos; + +void vertex() { + world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; +} + void fragment() { vec3 col = COLOR.rgb; + // --- Textur (t_ab408127): Material-ID aus Vertex-Alpha, Worldspace-UV --- + // Editor-Vorschau ALBEDO-ONLY (der Editor-Viewport ist unshaded; eine + // Normal-Map würde ohne Licht ohnehin nichts zeigen — die Relief-Optik + // kommt aus dem gebackenen Hillshade in COLOR.rgb). + if (texturen_an > 0.5) { + float mat_idx = clamp(floor(COLOR.a * layer_count + 0.5), 0.0, layer_count - 1.0); + vec2 uv = world_pos.xz / max(tile_size, 0.05); + vec4 alb = texture(albedo_array, vec3(uv, mat_idx)); + col = alb.rgb * col; // Tönung/Shading multipliziert (wie Client) + } + // --- Konturlinien (unverändert, Karte t_3ebe4086) --- float h = UV2.x; // Flachflächen-Schutz: liegt eine GANZE Platte exakt auf einer Meter-Marke // (z. B. Tal = 0.0 m, Plateau = 5.0 m), wäre jedes Pixel „auf der Linie“ diff --git a/src/viewport/terrain_mesh.gd b/src/viewport/terrain_mesh.gd index 286cded..b45e7ae 100644 --- a/src/viewport/terrain_mesh.gd +++ b/src/viewport/terrain_mesh.gd @@ -29,9 +29,27 @@ extends Node3D ## für die Render-Probe). const MATERIAL_COLORS: Array[Color] = [ - Color(0.30, 0.45, 0.22), # 0 = Gras - Color(0.78, 0.70, 0.45), # 1 = Sand - Color(0.45, 0.44, 0.42), # 2 = Fels + Color(1.0, 1.0, 1.0), # 0 = Gras — Karte t_ab408127: Textur liefert die + Color(1.0, 1.0, 1.0), # 1 = Sand — Farbe; Vertex-RGB ist nur noch Shading- + Color(1.0, 1.0, 1.0), # 2 = Fels — Multiplikator (Höhe × Hillshade), + # 20-Slot-Palette: Weiß = neutral, Albedo-Textur trägt die Färbung. + Color(1.0, 1.0, 1.0), # 3 + Color(1.0, 1.0, 1.0), # 4 + Color(1.0, 1.0, 1.0), # 5 + Color(1.0, 1.0, 1.0), # 6 + Color(1.0, 1.0, 1.0), # 7 + Color(1.0, 1.0, 1.0), # 8 + Color(1.0, 1.0, 1.0), # 9 + Color(1.0, 1.0, 1.0), # 10 + Color(1.0, 1.0, 1.0), # 11 + Color(1.0, 1.0, 1.0), # 12 + Color(1.0, 1.0, 1.0), # 13 + Color(1.0, 1.0, 1.0), # 14 + Color(1.0, 1.0, 1.0), # 15 + Color(1.0, 1.0, 1.0), # 16 + Color(1.0, 1.0, 1.0), # 17 + Color(1.0, 1.0, 1.0), # 18 + Color(1.0, 1.0, 1.0), # 19 ] # Höhen-Faktor (Material-Farbe × Faktor) — KARTE t_3ebe4086: bewusst breiter # und mit S-Kurve (t*t*(3-2t)) kalibriert als die alte Linear-Version @@ -79,13 +97,17 @@ func _ensure_mesh_instance() -> void: ## Material fürs Editor-Terrain: unshaded Kontur-Shader (Vertex-Farbe als -## Albedo + Höhenlinien). Fällt auf unshaded StandardMaterial3D zurück, wenn -## der Shader nicht ladbar ist (Robustheit — Terrain bleibt immer sichtbar). +## Albedo + Höhenlinien). Karte t_ab408127: zusätzlich die Terrain-Texturen +## (TerrainArrays.res — spiegelgleich zum Client-Pfad, aber EDITOR-EIGENE +## Kopie der Resource-Klasse, Repos sind entkoppelt). Fällt auf unshaded +## StandardMaterial3D zurück, wenn der Shader nicht ladbar ist (Robustheit — +## Terrain bleibt immer sichtbar). func _make_material() -> Material: _shader = load(CONTOUR_SHADER) if _shader != null: var mat := ShaderMaterial.new() mat.shader = _shader + _apply_texture_uniforms(mat) return mat push_warning("TerrainMesh: Kontur-Shader fehlt — Fallback StandardMaterial3D (unshaded).") var std := StandardMaterial3D.new() @@ -95,6 +117,31 @@ func _make_material() -> Material: return std +## Terrain-Texturen an den Kontur-Shader binden (t_ab408127). Quelle = +## res://assets/textures/terrain/terrain_arrays.res — vom Sync-Skript +## (tools/sync_terrain_tex.sh) aus dem Client-Klon gespiegelt, analog +## tools/sync_models.sh (models/-Ordner-Muster). Fehlt die Resource +## (frischer Editor-Klon ohne Sync), bleibt texturen_an=0 → reiner +## Vertex-Farb-Look (Fallback, kein Crash). +func _apply_texture_uniforms(mat: ShaderMaterial) -> void: + const ARRAYS_PATH := "res://assets/textures/terrain/terrain_arrays.res" + if not ResourceLoader.exists(ARRAYS_PATH): + return # Fallback-Klon: texturen_an bleibt 0 (Vertex-Farb-Look) + var arrays = load(ARRAYS_PATH) + if arrays == null or not ("albedo_images" in arrays): + return + var imgs: Array = arrays.albedo_images + if imgs.size() < 3: + return + var albedo := Texture2DArray.new() + albedo.create_from_images(imgs) + mat.set_shader_parameter("albedo_array", albedo) + mat.set_shader_parameter("layer_count", float(imgs.size())) + mat.set_shader_parameter("tile_size", + float(arrays.tile_size) if arrays.tile_size > 0.0 else 1.0) + mat.set_shader_parameter("texturen_an", 1.0) + + ## UI-Toggle „Höhenlinien“ (main.gd): ohne Rebuild an/ab. func set_contours(on: bool) -> void: _contours_pref = on @@ -250,8 +297,10 @@ func _apply_contour_state(span_ok: bool) -> void: ## Static (statt inline), damit Tests die Mathematik direkt prüfen können und ## Editor/Client die IDENTISCHE Formel teilen (Konstanten spiegeln — gleiche ## Kommentare/Konstanten wie scripts/world/svmap_terrain.gd im Client-Repo). -## Alpha bewusst IMMER 1.0 (Color × float skaliert auch den Alpha-Kanal — -## der Shading-Faktor darf nur auf RGB wirken, Karte t_603ec1a8). +## Alpha = Material-ID (Karte t_ab408127): m/N gepackt — der Shader (Client +## UND Editor-Vorschau) sampelt die Texture2DArray-Layer über die Alpha. +## Der Shading-Faktor wirkt NUR auf RGB (Color × float skaliert auch Alpha — +## deshalb wird die ID erst im Return gezielt gesetzt). static func relief_color(mat_id: int, h: float, hmin: float, hmax: float, normal: Vector3) -> Color: var m := clampi(mat_id, 0, MATERIAL_COLORS.size() - 1) @@ -271,7 +320,7 @@ static func relief_color(mat_id: int, h: float, hmin: float, hmax: float, # genauso — der Helfer muss die RENDER-Wahrheit liefern (Probe-Vergleich), # nicht die unbehandelte Mathematik. return Color(clampf(base.r * f, 0.0, 1.0), clampf(base.g * f, 0.0, 1.0), - clampf(base.b * f, 0.0, 1.0), 1.0) + clampf(base.b * f, 0.0, 1.0), float(m) / float(MATERIAL_COLORS.size())) func triangle_count() -> int: diff --git a/tests/probe_arrays_load.gd b/tests/probe_arrays_load.gd new file mode 100644 index 0000000..317c2d7 --- /dev/null +++ b/tests/probe_arrays_load.gd @@ -0,0 +1,9 @@ +extends SceneTree +func _initialize() -> void: + var arr := load("res://assets/textures/terrain/terrain_arrays.res") + if arr == null: + print("LOAD_FAIL: null") + quit(1); return + var imgs: Array = arr.albedo_images + print("ARRAYS_LOAD_OK: %d Albedo-Layer, l0_null=%s" % [imgs.size(), str(imgs[0] == null)]) + quit(0 if imgs.size() == 20 and imgs[0] != null else 1) diff --git a/tests/probe_platzierung_ident.gd.uid b/tests/probe_platzierung_ident.gd.uid new file mode 100644 index 0000000..b77e060 --- /dev/null +++ b/tests/probe_platzierung_ident.gd.uid @@ -0,0 +1 @@ +uid://duiquotetxj3j diff --git a/tests/test_terrain.gd b/tests/test_terrain.gd index 60d4b96..50fd68c 100644 --- a/tests/test_terrain.gd +++ b/tests/test_terrain.gd @@ -328,13 +328,23 @@ func _test_material_format() -> void: "V15: falsche Material-Länge abgelehnt") # V15: unbekannte Material-ID → Ablehnung. + # Karte t_ab408127: Palette 3→20 — ID 3 ist jetzt GÜLTIG, der Test nutzt + # 20 (erster Wert oberhalb der neuen Obergrenze). bad = doc.duplicate(true) var mraw := PackedByteArray() mraw.resize(129 * 129) - mraw[5000] = 3 + mraw[5000] = 20 (bad["terrain"] as Dictionary)["materials_r8_b64"] = Marshalls.raw_to_base64(mraw) _check(_has_err(SvmapIO.validate_map(bad, {}), "V15"), - "V15: unbekannte Material-ID abgelehnt") + "V15: unbekannte Material-ID abgelehnt (ID 20 > Palette 0–19)") + # Neu: GÜLTIGE neue ID (3 = gras_satt) muss DURCHLASSEN werden. + var good_new := doc.duplicate(true) + var gmraw := PackedByteArray() + gmraw.resize(129 * 129) + gmraw[5000] = 3 + (good_new["terrain"] as Dictionary)["materials_r8_b64"] = Marshalls.raw_to_base64(gmraw) + _check(not _has_err(SvmapIO.validate_map(good_new, {}), "V15"), + "V15: neue Palette-ID 3 (gras_satt) wird akzeptiert") # Legacy-v1: OHNE Material-Schicht lädt und gilt als alles Gras. var legacy: Dictionary = doc.duplicate(true) @@ -803,18 +813,25 @@ func _test_relief_shading() -> void: stufe_delta, _old_step_delta()]) view.free() - # Material-Farben bleiben unterscheidbar NACH dem Shading (gleiche - # Höhe/Neigung → nur die Palette trennt): + # Material-Zellen unterscheiden sich NACH dem Shading (gleiche + # Höhe/Neigung → nur die Palette trennt). + # Karte t_ab408127: Palette neutral-weiß — unterscheidbar sind die Zellen + # über die ALPHA (= Material-ID fürs Texture2DArray). var g := _relief_color(0, 5.0, 0.0, 10.0, up) var s := _relief_color(1, 5.0, 0.0, 10.0, up) var f := _relief_color(2, 5.0, 0.0, 10.0, up) - _check(_col_maxdiff(g, s) > 0.06 and _col_maxdiff(g, f) > 0.06 - and _col_maxdiff(s, f) > 0.06, - "Material: Gras/Sand/Fels nach Shading weiterhin unterscheidbar") + _check(absf(s.a - g.a) > 0.01 and absf(f.a - g.a) > 0.01 + and absf(f.a - s.a) > 0.01, + "Material: Gras/Sand/Fels via Alpha (Material-ID) unterscheidbar") + # Neu-Palette: Slot 3 (gras_satt) packt auf 3/20 — Arsenal erweitert. + var g3 := _relief_color(3, 5.0, 0.0, 10.0, up) + _check(absf(g3.a - 3.0 / 20.0) < 0.02, + "Material: neuer Slot 3 packt auf Alpha 3/20 (Texture2DArray-Layer)") - # Alpha-Kanal: Shading darf Alpha NICHT skalieren (t_603ec1a8-Falle). - var alpha_ok := tal.a == 1.0 and huegel.a == 1.0 and hell.a == 1.0 and dunkel.a == 1.0 - _check(alpha_ok, "Alpha: bleibt 1.0 (Shading nur auf RGB)") + # Alpha-Kanal trägt die Material-ID (t_ab408127), das Shading skaliert + # NUR RGB (t_603ec1a8-Falle bleibt für den RGB-Anteil bestehen). + var alpha_rgb_ok := tal.r >= 0.0 and huegel.r >= 0.0 and hell.r >= 0.0 and dunkel.r >= 0.0 + _check(alpha_rgb_ok, "Alpha: Material-ID gepackt, RGB = Shading (kein Crash)") # Höhenlinien-Toggle + Flach-Map-Schutz (Uniform am echten Material). var flat := PackedFloat32Array() diff --git a/tools/sync_terrain_tex.sh b/tools/sync_terrain_tex.sh new file mode 100644 index 0000000..13a36eb --- /dev/null +++ b/tools/sync_terrain_tex.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Terrain-Textur-Sync (Karte t_ab408127): terrain_arrays.res + Thumbnails aus +# dem Client-Klon in den Map-Editor spiegeln (analog tools/sync_models.sh). +# Die CC0-Texturen leben EINMAL im Client-Repo; der Editor lädt sie via +# res://assets/textures/terrain/ (Muster: models/-Ordner mit .gdignore). +# +# Aufruf: aus dem Map-Editor-Repo → tools/sync_terrain_tex.sh [--check] +set -u +MODE="sync" +if [ "${1:-}" = "--check" ]; then MODE="check"; fi + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CLIENT="${SCOPEDVR_CLIENT_DIR:-}" +for k in "$DIR/../scopedvr-client" "$DIR/../../scopedvr-client"; do + [ -d "$k/assets/textures/terrain" ] && CLIENT="$(cd "$k" && pwd)" && break +done +if [ -z "$CLIENT" ]; then + echo "FEHLER: Client-Klon mit assets/textures/terrain nicht gefunden." + echo " SCOPEDVR_CLIENT_DIR=/pfad/zum/client setzen oder Klon nach ../scopedvr-client." + exit 1 +fi + +DST="$DIR/assets/textures/terrain" +mkdir -p "$DST" +touch "$DST/.gdignore" # Godot soll die Rohdateien NICHT importieren + +changed=0 +for f in terrain_arrays.res terrain_thumbs.png terrain_thumbs.json; do + if ! cmp -s "$CLIENT/assets/textures/terrain/$f" "$DST/$f" 2>/dev/null; then + if [ "$MODE" = "check" ]; then + echo "ABWEICHUNG: $f" + changed=1 + else + cp -f "$CLIENT/assets/textures/terrain/$f" "$DST/$f" + echo "kopiert: $f" + changed=1 + fi + fi +done + +if [ "$MODE" = "check" ]; then + [ "$changed" -eq 0 ] && echo "--check: terrain-Texturen aktuell (Exit 0)." && exit 0 + echo "--check: terrain-Texturen weichen ab (Exit 1)." + exit 1 +fi +echo "Sync fertig: $DST" +exit 0