OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / rtlight_dump.gd

104 lines · gd ref:
extends SceneTree

## Headless diagnostic: parse start.rtlights through RtlightManager and dump the DERIVED
## per-light values (DarkPlaces canonical — spec/quake-darkplaces-transfer.md §3), so we can
## verify the parse/intensity/range/shadow-flag math OFFLINE before any deploy. Run:
##   "$HYPER_TPS_GODOT" --headless --script res://scenes/test/rtlight_dump.gd
##
## Expected (hand-computed from start.rtlights lines vs r_shadow.c):
##   line 1  "!992 1948 -14 160 1.0 0.5 0.2 ... 1 1 1 2"  -> shadow OFF, energy 1+1+1=3.0, range 160*0.028=4.48
##   line 2  "-150 2550 148 256 1.0 0.7 0.4 ... 0.6 1 1 2" -> shadow ON,  energy 0.6+1+1=2.6, range 256*0.028=7.17
## i.e. energy must VARY per line (was a flat 3.0) and range must be radius*scale (no ×1.25).

func _init() -> void:
	var map_path := "/storage/projects/gamedev/quake/quakemash/START.MAP"
	var rtlights_path := "/storage/projects/gamedev/quake/quake-rtlights/id1/maps/start.rtlights"
	var scale := 0.028
	# Import START.MAP first to recover the SAME recenter+offset the real map uses.
	var importer := QuakeMapImporter.new()
	var stub := StubTerrain.new()
	importer.import_file(stub, map_path, scale, Vector3(0, 20, 0), 1.0, null, true, {})
	var map_center: Vector3 = importer.out_map_center
	var offset: Vector3 = importer.out_aligned_offset

	var mgr := RtlightManager.new()
	var n := mgr.setup(rtlights_path, map_center, scale, offset)

	# Gather every spawned OmniLight3D + whether it is shadow-eligible (in _casters).
	var caster_lamps := {}
	for c in mgr._casters:
		caster_lamps[c.light.get_instance_id()] = true
	var energies: Array[float] = []
	var ranges: Array[float] = []
	var lamps: Array = []
	for child in mgr.get_children():
		if child is OmniLight3D:
			lamps.append(child)
			energies.append(child.light_energy)
			ranges.append(child.omni_range)

	print("=== rtlights dump: %d spawned, %d shadow-eligible ===" % [n, mgr._casters.size()])
	# Distribution: proves energy is no longer flat and ranges vary.
	print("energy  min=%.3f max=%.3f mean=%.3f  (flat 3.0 would mean the per-line scales are ignored)" % [
		_min(energies), _max(energies), _mean(energies)])
	print("range   min=%.3f max=%.3f mean=%.3f  (m; = radius*%.3f, no ×1.25 fudge)" % [
		_min(ranges), _max(ranges), _mean(ranges), scale])
	print("distinct energies:", _distinct(energies))

	# Spot-check the two known lines by their world position.
	_spot("line1 !992,1948,-14 (expect shadow OFF, E=3.0, R=4.48)",
		importer, Vector3(992, 1948, -14), map_center, scale, offset, lamps, caster_lamps)
	_spot("line2 -150,2550,148 (expect shadow ON, E=2.6, R=7.17)",
		importer, Vector3(-150, 2550, 148), map_center, scale, offset, lamps, caster_lamps)

	print("--- first 8 lamps ---")
	for i in mini(8, lamps.size()):
		var l: OmniLight3D = lamps[i]
		print("  [%d] E=%.3f R=%.3f col=(%.2f,%.2f,%.2f) caster=%s" % [
			i, l.light_energy, l.omni_range, l.light_color.r, l.light_color.g, l.light_color.b,
			str(caster_lamps.has(l.get_instance_id()))])
	quit()

func _spot(label: String, importer: QuakeMapImporter, quake_xyz: Vector3, mc: Vector3,
		scale: float, off: Vector3, lamps: Array, casters: Dictionary) -> void:
	var want := ((QuakeMapImporter._q2g(quake_xyz.x, quake_xyz.y, quake_xyz.z) - mc) * scale) + off
	var best: OmniLight3D = null
	var bd := INF
	for l in lamps:
		var d: float = (l as OmniLight3D).position.distance_to(want)   # mgr at origin → position == world
		if d < bd:
			bd = d; best = l
	if best == null:
		print("  %s -> NO LAMP" % label); return
	print("  %s -> E=%.3f R=%.3f col=(%.2f,%.2f,%.2f) caster=%s  (match dist=%.3fm)" % [
		label, best.light_energy, best.omni_range, best.light_color.r, best.light_color.g,
		best.light_color.b, str(casters.has(best.get_instance_id())), bd])

class StubTerrain extends Node:
	func edit_terrain_box(_c, _h, _s, _i, _m) -> void: pass
	func edit_terrain_convex(_planes, _c, _r, _s, _i, _m, _fp) -> void: pass

func _min(a: Array[float]) -> float:
	var m := INF
	for v in a: m = minf(m, v)
	return m if a.size() > 0 else 0.0

func _max(a: Array[float]) -> float:
	var m := -INF
	for v in a: m = maxf(m, v)
	return m if a.size() > 0 else 0.0

func _mean(a: Array[float]) -> float:
	if a.is_empty(): return 0.0
	var s := 0.0
	for v in a: s += v
	return s / a.size()

func _distinct(a: Array[float]) -> Array:
	var seen := {}
	for v in a:
		seen[snappedf(v, 0.01)] = true
	var keys := seen.keys()
	keys.sort()
	return keys