extends SceneTree ## Pinsel-Falloff, Mal-Slot-Logik, Einebnen, Rampe, Druck (Karte 25.10.). ## Ausführen: godot --headless --path . --script res://tests/test_pinsel_werkzeuge.gd var failures := 0 var checks := 0 const R := 129 const S := 1024.0 func _init() -> void: _falloff() _druck() _flatten() _ramp() _glaetten() _mal_slots() _undo() 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) func _flat(h := 0.0) -> PackedFloat32Array: var a := PackedFloat32Array() a.resize(R * R) a.fill(h) return a func _falloff() -> void: print("Testgruppe: Falloff lerp(t, t², f)") _check(is_equal_approx(TerrainField.falloff_weight(0.0, 10.0, 0.0), 1.0), "Mitte = 1 (hart)") _check(is_equal_approx(TerrainField.falloff_weight(0.0, 10.0, 1.0), 1.0), "Mitte = 1 (weich)") _check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 0.0), 0.5), "hart: halber Radius = 0.5") _check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 1.0), 0.25), "weich: halber Radius = 0.25") _check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 0.5), 0.375), "mittel: lerp = 0.375") _check(TerrainField.falloff_weight(10.0, 10.0, 0.5) == 0.0, "Rand = 0") var h := _flat() TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 40.0, TerrainField.Mode.RAISE, 1.0, 0.0) var h2 := _flat() TerrainField.apply_brush(h2, R, S, S, -1e5, 1e5, 0.0, 0.0, 40.0, TerrainField.Mode.RAISE, 1.0, 1.0) var i := 64 * R + 64 + 3 # ~24 m neben der Mitte _check(h[i] > h2[i] and h2[i] > 0.0, "Raise: hart hebt am Rand mehr als weich") func _druck() -> void: print("Testgruppe: Druck") _check(TerrainField.pressure_of(0.0, 0.0) == 1.0, "Maus = 1.0") _check(is_equal_approx(TerrainField.pressure_of(0.4, 0.9), 0.4), "Pen vor Trigger") _check(is_equal_approx(TerrainField.pressure_of(0.0, 0.6), 0.6), "Trigger") _check(TerrainField.pressure_of(0.001, 0.0) == 0.05 or TerrainField.pressure_of(0.001, 0.0) == 1.0, "Minimum begrenzt") var t := Node3D.new() t.set_script(load("res://src/editor/terrain_tool.gd")) t.set_pressure(0.5) var a: float = t.call("_step_amount", TerrainField.Mode.RAISE, 1.0) t.set_pressure(1.0) var b: float = t.call("_step_amount", TerrainField.Mode.RAISE, 1.0) _check(is_equal_approx(a * 2.0, b), "Stärke skaliert mit Druck") t.free() func _flatten() -> void: print("Testgruppe: Einebnen") var h := _flat(0.0) for i in h.size(): h[i] = float((i % R)) * 0.1 # Rampe in x var target := 6.4 for k in 40: TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 60.0, TerrainField.Mode.FLATTEN, 0.3, 0.3, target) _check(absf(h[64 * R + 64] - target) < 0.05, "Mitte erreicht Zielhöhe") _check(absf(h[64 * R + 4] - 0.4) < 0.0001, "weit außerhalb unverändert") _check(absf(h[64 * R + 60] - target) < absf(0.1 * 60 - target) , "Rand näher am Ziel als vorher") func _ramp() -> void: print("Testgruppe: Rampe") var h := _flat(0.0) var a := Vector2(-80.0, 0.0) var b := Vector2(80.0, 0.0) TerrainField.apply_ramp(h, R, S, S, a, b, 2.0, 12.0, 30.0, 0.5) var cx := func(wx: float) -> int: return int(round((wx / S + 0.5) * float(R - 1))) var iz := 64 var mid: float = h[iz * R + cx.call(0.0)] _check(absf(mid - 7.0) < 0.3, "Mitte ≈ lineare Höhe 7 (%.2f)" % mid) var q: float = h[iz * R + cx.call(40.0)] _check(absf(q - 9.5) < 0.4, "Viertelpunkt ≈ 9.5 (%.2f)" % q) _check(h[iz * R + cx.call(-200.0)] == 0.0 and h[iz * R + cx.call(200.0)] == 0.0, "außerhalb der Enden unverändert") var seitlich: float = h[(iz + 4) * R + cx.call(0.0)] # ≈ 32 m quer: außerhalb der halben Breite 15 _check(seitlich == 0.0, "außerhalb der Breite unverändert") var rand: float = h[(iz + 1) * R + cx.call(0.0)] # 8 m quer: Falloff _check(rand > 0.0 and rand < mid, "Rand mit Falloff schwächer als Achse") func _glaetten() -> void: print("Testgruppe: Glätten mit Falloff") var h := _flat(0.0) h[64 * R + 64] = 10.0 var changed := TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 30.0, TerrainField.Mode.SMOOTH, 1.0, 0.5) _check(changed and h[64 * R + 64] < 10.0 and h[64 * R + 65] > 0.0, "Spitze verteilt sich") var h2 := _flat(0.0) h2[64 * R + 64] = 10.0 TerrainField.apply_brush(h2, R, S, S, -1e5, 1e5, 0.0, 0.0, 30.0, TerrainField.Mode.SMOOTH, 1.0) _check(h2[64 * R + 64] < 10.0, "ohne Falloff (alt: Kosinus) funktioniert weiter") func _mal_slots() -> void: print("Testgruppe: Mal-Slots") var n := 33 var a := PackedByteArray(); a.resize(n * n); a.fill(0) var b := PackedByteArray(); b.resize(n * n); b.fill(0) var w := PackedByteArray(); w.resize(n * n); w.fill(0) var mid := 16 * n + 16 # Mitte: Slot a=1 (Anteil 255−200=55), Slot b=2 (Anteil 200 = stärker). a[mid] = 1; b[mid] = 2; w[mid] = 200 # Ziel 3 mit voller Stärke: ersetzt das SCHWÄCHERE (1) → übrig: 2 + 3. TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 3, 0.0, 0.5) var slots := [int(a[mid]), int(b[mid])] _check(slots.has(2) and slots.has(3) and not slots.has(1), "schwächeres Slot-Material ersetzt (%s)" % str(slots)) _check(int(w[mid]) > 100 and int(w[mid]) < 140 and int(b[mid]) == 3, "Anteil schiebt weich Richtung Ziel (w=%d)" % int(w[mid])) TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 3, 0.0, 1.0) _check(int(a[mid]) == 3 and int(w[mid]) == 0, "voll gemalt → reines Zielmaterial") # Ziel ist schon in Slot b: Anteil wächst, kein Wechsel. a[mid] = 1; b[mid] = 2; w[mid] = 100 TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 2, 0.0, 0.8) _check(int(a[mid]) == 1 and int(b[mid]) == 2 and int(w[mid]) > 100, "Ziel in Slot b: Anteil wächst") # Schwacher Strich schwächt nie ab. var before := int(w[mid]) TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 2, 0.0, 0.1) _check(int(w[mid]) == before, "schwächerer Strich ändert nichts") func _undo() -> void: print("Testgruppe: Einebnen/Rampe + Undo") var doc := SvmapDocument.create_new("Pinsel", 1024.0, "mittag") var hist := ActionHistory.new() hist.begin(doc) var terrain: Dictionary = doc["terrain"] var h := SvmapIO.decode_heights(terrain) var vor := str(terrain["heightmap_r16_b64"]) var before: Dictionary = hist.snapshot(["terrain"]) TerrainField.apply_ramp(h, 129, 1024.0, 1024.0, Vector2(-50, 0), Vector2(50, 0), 0.0, 20.0, 20.0, 0.3) terrain["max_height_m"] = 30.0 terrain["heightmap_r16_b64"] = TerrainField.encode(h, float(terrain["min_height_m"]), 30.0) hist.commit("Boden geformt (Rampe)", before, hist.snapshot(["terrain"])) _check(str(terrain["heightmap_r16_b64"]) != vor, "Rampe verändert Doc") hist.undo() _check(str(terrain["heightmap_r16_b64"]) == vor, "Undo stellt Höhenfeld wieder her")