OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_terrain_panel_restore.gd

88 lines · gd ref:
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()