Sly 25.10.: „weich“ soll ein Textur-Verlauf am Pinselrand sein, nicht fleckig; gerade Strichkanten wirkten weiter hart (1-m-Übergang). - Format (optional, alte Karten gültig): materials_b_r8_b64 (zweites Material je Punkt) + materials_w_r8_b64 (Anteil 0–255); ohne Mischung fallen die Felder weg. V15 prüft Länge/IDs/Paarigkeit. - TerrainField.apply_material_brush_mix: innen voll, außen Smoothstep-Anteil; nur verstärken, nie abschwächen; Gras drübermalen mischt zurück. - Shader: Top-2 aus 4 Nachbarpunkten × (a, b) mit Anteilen → nur noch 2 Albedo-Fetches; Warp bleibt. - Pinsel/Live-Vorschau, Spiegeln, Symmetrie-Analyse/-Anwenden, Resize, Anheben, Undo (terrain-Snapshot) tragen die Misch-Schicht mit. - Streu-Rand (vorheriger Commit) entfernt. - Tests: test_mal_fein 30/30 (Verlauf fällt weich, nie abschwächen, zurückmischen, Kodierung), Editor-Suite unverändert grün bis auf die bekannten Umgebungs-Tests; Kette Editor → Server → Client per run_map_download_e2e 15/15 mit gemischter Karte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
198 lines
8 KiB
GDScript
198 lines
8 KiB
GDScript
extends SceneTree
|
||
## Editor-Tests (headless): feines Material-Raster (Karte 24.10., Sly: „feiner malen“).
|
||
## godot --headless --path . -s res://tests/test_mal_fein.gd
|
||
##
|
||
## F1 Ziel-Raster: 1024 m/129 → 1025 (1 m), 256 m/129 → 257 (1 m), 2048 m → 1025.
|
||
## F2 Neue Map startet mit feinem Raster und ist sofort gültig (V1–V16).
|
||
## F3 Alte Karte (Raster = Höhenraster) wird beim Anheben optisch identisch
|
||
## übernommen und ist danach gültig.
|
||
## F4 Pinsel 2 m auf 1024-m-Map ändert nur ~π·2² Punkte (nicht eine 8-m-Zelle).
|
||
## F5 V15 lehnt kaputte material_resolution / falsche Länge ab.
|
||
## F6 Speichern → Laden → identische Schicht (Roundtrip mit Prüfsumme).
|
||
## F7 Resize (1024 → 2048 m) hält die gemalte Stelle an derselben Weltposition.
|
||
|
||
var failures := 0
|
||
var checks := 0
|
||
|
||
|
||
func _initialize() -> void:
|
||
_f1()
|
||
_f2_f4_f6_f7()
|
||
_f3()
|
||
_f5()
|
||
_f8_weich()
|
||
print("test_mal_fein: %d/%d ok" % [checks - failures, checks])
|
||
quit(1 if failures > 0 else 0)
|
||
|
||
|
||
func _check(cond: bool, label: String) -> void:
|
||
checks += 1
|
||
print("%s: %s" % ["ok " if cond else "FAIL", label])
|
||
if not cond:
|
||
failures += 1
|
||
|
||
|
||
func _f1() -> void:
|
||
_check(SvmapIO.material_resolution_for(129, 1024.0, 1024.0) == 1025, "F1 1024 m/129 → 1025")
|
||
_check(SvmapIO.material_resolution_for(129, 256.0, 256.0) == 257, "F1 256 m/129 → 257")
|
||
_check(SvmapIO.material_resolution_for(129, 2048.0, 2048.0) == 1025, "F1 2048 m → gedeckelt 1025")
|
||
_check(SvmapIO.material_resolution_for(129, 64.0, 64.0) == 129, "F1 kleine Map bleibt beim Höhenraster")
|
||
|
||
|
||
func _f2_f4_f6_f7() -> void:
|
||
var doc := SvmapDocument.create_new("Fein", 1024.0, "mittag")
|
||
var t: Dictionary = doc["terrain"]
|
||
_check(SvmapIO.material_resolution(t) == 1025, "F2 neue Map: material_resolution 1025")
|
||
_check(SvmapIO._validate_core(doc).is_empty(), "F2 neue Map gültig (%s)" % str(SvmapIO._validate_core(doc)))
|
||
var mats := SvmapIO.decode_materials(t)
|
||
_check(mats.size() == 1025 * 1025, "F2 Schicht hat 1025² Punkte")
|
||
# F4: Pinsel 2 m Sand mitten auf die Map
|
||
var changed := TerrainField.apply_material_brush(mats, 1025, 1024.0, 1024.0, 10.3, -7.6, 2.0, 1)
|
||
var n := 0
|
||
for v in mats:
|
||
if v == 1:
|
||
n += 1
|
||
_check(changed and n >= 8 and n <= 20, "F4 2-m-Pinsel ändert %d Punkte (≈ π·2² = 12,6; alt: 1 Zelle à 8 m)" % n)
|
||
t["materials_r8_b64"] = SvmapIO.encode_materials(mats)
|
||
# F6: Roundtrip
|
||
var path := "user://test_mal_fein.svmap"
|
||
var sv := SvmapIO.save_to_file(path, doc)
|
||
_check(bool(sv["ok"]), "F6 speichern ok (%s)" % str(sv.get("errors", [])))
|
||
var ld := SvmapIO.load_from_file(path)
|
||
_check(bool(ld["ok"]), "F6 laden ok (%s)" % str(ld.get("errors", [])))
|
||
if bool(ld["ok"]):
|
||
var m2 := SvmapIO.decode_materials(ld["data"]["terrain"])
|
||
_check(m2 == mats, "F6 Schicht bitgleich nach Roundtrip")
|
||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||
# F7: Resize auf 2048 m — Sand bleibt bei (10.3, -7.6)
|
||
var big := SvmapDocument.resize_terrain(doc, 2048.0, 2048.0)
|
||
var bt: Dictionary = big["terrain"]
|
||
var bm := SvmapIO.material_resolution(bt)
|
||
var bmats := SvmapIO.decode_materials(bt)
|
||
var gx := int(roundf((10.3 / 2048.0 + 0.5) * float(bm - 1)))
|
||
var gz := int(roundf((-7.6 / 2048.0 + 0.5) * float(bm - 1)))
|
||
_check(bmats.size() == bm * bm and int(bmats[gz * bm + gx]) == 1,
|
||
"F7 Resize 2048 m (Raster %d): Sand an derselben Weltstelle" % bm)
|
||
_check(SvmapIO._validate_core(big).is_empty(),
|
||
"F7 Resize-Ergebnis gültig")
|
||
|
||
|
||
func _f3() -> void:
|
||
# Alte Karte simulieren: Material-Raster = Höhenraster (129), ohne Feld.
|
||
var doc := SvmapDocument.create_new("Alt", 1024.0, "mittag")
|
||
var t: Dictionary = doc["terrain"]
|
||
var old := PackedByteArray()
|
||
old.resize(129 * 129)
|
||
for i in old.size():
|
||
old[i] = (i * 7) % 20
|
||
t.erase("material_resolution")
|
||
t["materials_r8_b64"] = SvmapIO.encode_materials(old)
|
||
_check(SvmapIO._validate_core(doc).is_empty(), "F3 alte Karte (ohne Feld) gültig")
|
||
_check(SvmapIO.upgrade_material_layer(t), "F3 Anheben meldet Änderung")
|
||
_check(SvmapIO.material_resolution(t) == 1025, "F3 danach Raster 1025")
|
||
var m := SvmapIO.decode_materials(t)
|
||
var same := true
|
||
for z in 129:
|
||
for x in 129:
|
||
if SvmapIO.material_at_height_point(m, 1025, 129, x, z) != int(old[z * 129 + x]):
|
||
same = false
|
||
_check(same, "F3 jeder Höhenpunkt behält sein Material (optisch identisch)")
|
||
_check(SvmapIO._validate_core(doc).is_empty(), "F3 angehobene Karte gültig")
|
||
_check(not SvmapIO.upgrade_material_layer(t), "F3 zweites Anheben ändert nichts")
|
||
|
||
|
||
func _f5() -> void:
|
||
var doc := SvmapDocument.create_new("Kaputt", 1024.0, "mittag")
|
||
var t: Dictionary = doc["terrain"]
|
||
t["material_resolution"] = 1000 # (1000−1) kein Vielfaches von 128
|
||
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 material_resolution nicht auf dem Raster → V15")
|
||
t["material_resolution"] = 2049
|
||
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 material_resolution > 1025 → V15")
|
||
t["material_resolution"] = 1025
|
||
var short := PackedByteArray()
|
||
short.resize(129 * 129)
|
||
t["materials_r8_b64"] = SvmapIO.encode_materials(short)
|
||
_check(_has_v15(SvmapIO._validate_core(doc)), "F5 Schichtlänge passt nicht → V15")
|
||
|
||
|
||
func _has_v15(errs: PackedStringArray) -> bool:
|
||
for e in errs:
|
||
if str(e).begins_with("V15"):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _only_checksum(errs: PackedStringArray) -> bool:
|
||
for e in errs:
|
||
if not str(e).begins_with("V13"):
|
||
return false
|
||
return true
|
||
|
||
|
||
## F8 (25.10.): Kante hart ↔ weich = ECHTER Verlauf (Misch-Schicht b + Anteil w).
|
||
func _f8_weich() -> void:
|
||
var r := 1025
|
||
var a := PackedByteArray()
|
||
a.resize(r * r)
|
||
var b := PackedByteArray()
|
||
b.resize(r * r)
|
||
var w := PackedByteArray()
|
||
w.resize(r * r)
|
||
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 0.0, 0.0, 10.0, 1, 0.8)
|
||
var c := (r - 1) / 2
|
||
var mitte := c * r + c
|
||
_check(a[mitte] == 1 and w[mitte] == 0, "F8 Pinselmitte voll Sand")
|
||
var anteil := func(i: int) -> float:
|
||
return (float(255 - w[i]) / 255.0 if a[i] == 1 else 0.0) + (float(w[i]) / 255.0 if b[i] == 1 else 0.0)
|
||
var s4: float = anteil.call(c * r + c + 4)
|
||
var s7: float = anteil.call(c * r + c + 7)
|
||
var s9: float = anteil.call(c * r + c + 9)
|
||
_check(s4 > s7 and s7 > s9 and s9 > 0.0 and s4 < 1.0,
|
||
"F8 Sand-Anteil fällt weich nach außen (4 m %.2f, 7 m %.2f, 9 m %.2f)" % [s4, s7, s9])
|
||
_check(anteil.call(c * r + c + 11) == 0.0, "F8 außerhalb des Radius unberührt")
|
||
# Nachmalen schwächer am Rand darf nichts abschwächen
|
||
var vorher := w.duplicate()
|
||
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 3.0, 0.0, 10.0, 1, 0.8)
|
||
_check(anteil.call(c * r + c + 7) >= s7, "F8 Nachmalen verstärkt, schwächt nie ab")
|
||
# Rückmalen mit Gras (Strg+Klick) am Rand mischt zurück
|
||
var vor4: float = anteil.call(c * r + c + 4)
|
||
TerrainField.apply_material_brush_mix(a, b, w, r, 1024.0, 1024.0, 10.0, 0.0, 10.0, 0, 0.8)
|
||
var nach4: float = anteil.call(c * r + c + 4)
|
||
_check(nach4 < vor4 and nach4 > 0.0, "F8 Gras drübermalen mischt Sand zurück (%.2f → %.2f)" % [vor4, nach4])
|
||
# Kodierung: ohne Mischung keine Felder, mit Mischung gültig
|
||
var doc := SvmapDocument.create_new("Mix", 1024.0, "mittag")
|
||
var t: Dictionary = doc["terrain"]
|
||
t["materials_r8_b64"] = SvmapIO.encode_materials(a)
|
||
SvmapIO.encode_mix(t, b, w)
|
||
_check(t.has("materials_w_r8_b64") and SvmapIO._validate_core(doc).is_empty(),
|
||
"F8 Misch-Schicht gespeichert und gültig")
|
||
var leer := PackedByteArray()
|
||
leer.resize(r * r)
|
||
SvmapIO.encode_mix(t, leer, leer)
|
||
_check(not t.has("materials_w_r8_b64"), "F8 ohne Mischung keine Zusatzfelder")
|
||
var eins := PackedByteArray()
|
||
eins.resize(r * r)
|
||
TerrainField.apply_material_brush(eins, r, 1024.0, 1024.0, 0.3, 0.2, 1.0, 1)
|
||
_check(_count(eins) >= 1 and _count(eins) <= 5, "F8 1-m-Pinsel trifft %d Punkte" % _count(eins))
|
||
|
||
|
||
func _count(m: PackedByteArray) -> int:
|
||
var n := 0
|
||
for v in m:
|
||
if v != 0:
|
||
n += 1
|
||
return n
|
||
|
||
|
||
func _ring(m: PackedByteArray, r: int, a: float, b: float) -> float:
|
||
var c := (r - 1) / 2
|
||
var tot := 0
|
||
var hit := 0
|
||
for z in range(c - 12, c + 13):
|
||
for x in range(c - 12, c + 13):
|
||
var d := Vector2(x - c, z - c).length()
|
||
if d >= a and d < b:
|
||
tot += 1
|
||
if m[z * r + x] != 0:
|
||
hit += 1
|
||
return float(hit) / float(maxi(tot, 1))
|