extends SceneTree
## GATE — the Terrain material panel's model (TerrainMaterial) drives the textured
## Geiss terrain shader (terrain_layers.gdshader) and saves what it drives.
## T1 defaults() + apply(): five layers, every shader input set, no error
## T2 a control change (rotation, offset, parallax ×) lands in the shader at once
## AND is handed to `save` with the new value
## T3 a texture-set change REBUILDS the atlases (new Texture2DArray, new layer index);
## a non-set change does NOT (TexturePacks.build is a hitch)
## T4 merged(): a saved cfg overrides the defaults; an unknown set id is ignored
## T5 the panel builds (one OptionButton per layer)
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_terrain_material.gd
const SETS := ["polyhaven/clean_pebbles", "trimsheets/stone_base", "polyhaven/cliff_side",
"polyhaven/castle_wall_slates", "polyhaven/clean_pebbles"]
var ok := true
var checks := 0
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _init() -> void:
var mat := ShaderMaterial.new()
mat.shader = load("res://scenes/maps/undersea/terrain_layers.gdshader")
var tm := TerrainMaterial.new(mat, 64)
var saved: Array = []
tm.save = func(c: Dictionary) -> void: saved.append(c.duplicate(true))
# ---- T1 ----
var why := tm.apply(TerrainMaterial.defaults(SETS))
var idx: PackedInt32Array = mat.get_shader_parameter(&"layer_index")
_p(why == "" and idx.size() == 5, "T1 defaults apply: 5 layers", "%s %s" % [why, str(idx)])
_p(mat.get_shader_parameter(&"tex_atlas") is Texture2DArray and (mat.get_shader_parameter(&"uv_rot_deg") as PackedFloat32Array).size() == 5,
"T1 atlases and per-layer UV transforms are set")
# ---- T2 ----
var atlas_before: Variant = mat.get_shader_parameter(&"tex_atlas")
tm.set_value(tm.cfg["layers"][2], "rot", 45.0)
tm.set_value(tm.cfg["layers"][1], "off", [0.25, -0.5])
tm.set_value(tm.cfg, "pom_scale", 2.5)
tm.set_value(tm.cfg, "far_blend", 0.8)
tm.set_value(tm.cfg, "macro", 0.1)
var rot: PackedFloat32Array = mat.get_shader_parameter(&"uv_rot_deg")
var off: PackedVector2Array = mat.get_shader_parameter(&"uv_off")
_p(is_equal_approx(rot[2], 45.0) and off[1].is_equal_approx(Vector2(0.25, -0.5))
and is_equal_approx(float(mat.get_shader_parameter(&"pom_scale")), 2.5),
"T2 rotation / offset / parallax × reach the shader", "%s %s %s" % [rot[2], off[1], mat.get_shader_parameter(&"pom_scale")])
_p(is_equal_approx(float(mat.get_shader_parameter(&"far_blend")), 0.8) and is_equal_approx(float(mat.get_shader_parameter(&"macro_strength")), 0.1),
"T2 anti-tiling controls (far tiling mix, macro variation) reach the shader")
_p(saved.size() == 5 and is_equal_approx(float(saved[2]["layers"][2]["rot"]), 45.0) and is_equal_approx(float(saved[2]["pom_scale"]), 2.5),
"T2 every change is handed to save with the new values", saved.size())
# ---- T3 ----
_p(mat.get_shader_parameter(&"tex_atlas") == atlas_before, "T3 a non-set change does not rebuild the atlases")
var other := ""
for id in TexturePacks.ids():
if not SETS.has(id):
other = id
break
tm.set_value(tm.cfg["layers"][0], "set", other)
var idx2: PackedInt32Array = mat.get_shader_parameter(&"layer_index")
_p(other != "" and mat.get_shader_parameter(&"tex_atlas") != atlas_before and idx2.size() == 5,
"T3 a texture-set change rebuilds the atlases (%s)" % other, str(idx2))
# ---- T4 ----
var m := TerrainMaterial.merged(TerrainMaterial.defaults(SETS),
{"normal": 0.4, "layers": [{"set": "no/such_set", "rot": 30.0}, {"tile": 7.0}]})
_p(is_equal_approx(float(m["normal"]), 0.4) and String(m["layers"][0]["set"]) == SETS[0]
and is_equal_approx(float(m["layers"][0]["rot"]), 30.0) and is_equal_approx(float(m["layers"][1]["tile"]), 7.0),
"T4 a saved cfg overrides the defaults; an unknown set id is ignored")
# ---- T5 ----
var ui := tm.build_ui()
var opts := ui.find_children("*", "OptionButton", true, false).size()
_p(opts == 5, "T5 the panel builds with one texture picker per layer", opts)
ui.free()
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)