extends SceneTree
## Gate: saved terrain knobs reach the GENERATOR on generator maps (level-state
## audit 2026-10-02, item #5).
##
## terrain_panel binds `_target` to the terrain's generator when it has one
## (map_dc_erosion's knobs live on DCTerrainGeneratorProcNodes) and saves through
## it -- but `_apply_external`, the path every RESTORE takes (_load_persisted,
## a peer's change via state_changed, the map-change reset to _defaults), wrote
## `terrain.set(prop)`. DCTerrain has no such property, so a saved value moved the
## slider and never reached the world.
##
## Run: HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_terrain_panel_restore.gd
## Loaded at RUN time, not preload: terrain_panel.gd names the GameState autoload, and a
## preload compiles it with this script, before autoloads exist.
const PANEL_PATH := "res://scenes/game/terrain_panel.gd"
const StubGen := preload("res://scenes/test/_stub_terrain_generator.gd")
var ok := true
var _ran := false
var _checks := 0
func _p(c: bool, m: String) -> void:
_checks += 1
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
if _checks < 6:
print("FAIL: only %d checks ran -- setup died before the assertions" % _checks)
ok = false
print("GATE terrain_panel_restore: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1)
return true
func _run() -> void:
var gs: Node = root.get_node_or_null(^"/root/GameState")
if gs == null:
_p(false, "GameState autoload present")
return
gs.install_routes()
var terrain := Node3D.new() # stands in for DCTerrain: no knob properties
var gen = StubGen.new()
# A plain Node3D cannot hold `generator`; give it one via a tiny script.
var holder := GDScript.new()
holder.source_code = "extends Node3D\nvar generator: Resource = null\nvar use_noise: bool = true # DCTerrain has it; the generator knobs it does NOT have\n"
holder.reload()
terrain.set_script(holder)
terrain.set("generator", gen)
root.add_child(terrain)
var panel_script: GDScript = load(PANEL_PATH)
_p(panel_script != null, "terrain_panel.gd compiles")
if panel_script == null:
return
var panel: Node = panel_script.new()
root.add_child(panel)
panel.setup(terrain)
_p(panel.get("_target") == gen, "the panel binds to the generator")
_p(is_equal_approx(float(panel.get("_defaults").get("mountain_gain", -1.0)), 0.5), "default captured from the generator (0.5)")
# A restored / remote value.
panel._apply_external("mountain_gain", 0.7)
_p(is_equal_approx(gen.mountain_gain, 0.7), "a restored value reaches the generator (got %s)" % str(gen.mountain_gain))
panel._apply_external("mountain_octaves", 5.0)
_p(gen.mountain_octaves == 5 and typeof(gen.mountain_octaves) == TYPE_INT, "an int knob is restored as an int (got %s)" % str(gen.mountain_octaves))
# The same through GameState, as a peer's change or a room load arrives.
gs.state_changed.emit("world:terrain:noise:mountain_gain", 0.3, 1)
_p(is_equal_approx(gen.mountain_gain, 0.3), "a peer's change via state_changed reaches the generator (got %s)" % str(gen.mountain_gain))
# The map-change reset restores the scene's defaults onto the generator.
gen.mountain_gain = 0.9 # as a room's saved value left it
panel._on_map_changing("other_map")
_p(is_equal_approx(gen.mountain_gain, 0.5), "a map change resets the generator to its default (got %s)" % str(gen.mountain_gain))
panel.queue_free()
terrain.queue_free()