extends Node3D
## Streaming terrain test with full procedural player.
## Validates LOD transitions, chunk generation, collision, and seam hiding.
@onready var terrain: Node3D = $DCTerrain
@onready var stats_label: Label = $CanvasLayer/StatsLabel
@onready var player: CharacterBody3D = $Player
var _terrain_ready: bool = false
var _wait_frames: int = 0
var _paused: bool = false
var _wireframe: bool = false
var _edit_radius: float = 3.0
const EDITS_SAVE_PATH := "user://terrain_edits.json"
func _ready() -> void:
# Keep this script running while paused (for HUD + screenshot + unpause)
process_mode = Node.PROCESS_MODE_ALWAYS
# Terrain noise values are configured in the editor inspector (dc_terrain_streaming.tscn).
# No hardcoded overrides — editor values carry through to runtime.
terrain.collision_distance = 200.0
terrain.rebuild()
# Load persisted edits from previous session
_load_edits()
# Player needs multiplayer authority for local input
player.set_multiplayer_authority(1)
player.safe_margin = 0.08
# Freeze player until terrain has collision ready
player.global_position = Vector3(0, 10, 0)
player.set_physics_process(false)
player.set_process(false)
# Capture mouse for camera control
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _process(_delta: float) -> void:
# Wait for terrain chunks AND collision to be ready before enabling player
if not _terrain_ready:
var active: int = terrain.get_active_chunk_count()
var collision_q: int = terrain.get_collision_queue_size()
if active >= 10 and collision_q == 0:
_wait_frames += 1
# Extra frames for physics to fully register the shapes
if _wait_frames >= 3:
_terrain_ready = true
player.set_physics_process(true)
player.set_process(true)
print("Terrain ready — enabling player (active: %d, collision_q: %d)" % [active, collision_q])
if stats_label and terrain:
var chunks: int = terrain.get_chunk_count()
var active: int = terrain.get_active_chunk_count()
var pending: int = terrain.get_pending_count()
var edits: int = terrain.get_edit_count()
var fps: int = Engine.get_frames_per_second()
var pos: Vector3 = player.global_position if player else Vector3.ZERO
var cam: Camera3D = get_viewport().get_camera_3d()
var cam_info: String = "cam: %s" % (str(cam.global_position) if cam else "NONE")
var pause_str: String = " [PAUSED]" if _paused else ""
var wire_str: String = " [WIRE]" if _wireframe else ""
var lod_str: String = " [LOD]" if terrain.debug_lod_colors else ""
stats_label.text = "FPS: %d%s%s%s\nChunks: %d (active: %d, pending: %d)\nEdits: %d\nPos: (%.0f, %.0f, %.0f)\n%s\n[P] pause [F12] screenshot [R] rebuild [L] LOD [F1] wire [F2] fandisk" % [
fps, pause_str, wire_str, lod_str, chunks, active, pending, edits, pos.x, pos.y, pos.z, cam_info]
func _unhandled_input(event: InputEvent) -> void:
# Escape to release/capture mouse
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# P to pause/unpause
if event is InputEventKey and event.pressed and event.keycode == KEY_P:
_paused = !_paused
get_tree().paused = _paused
print("Paused: %s" % _paused)
# F12 to screenshot with timestamp
if event is InputEventKey and event.pressed and event.keycode == KEY_F12:
var img: Image = get_viewport().get_texture().get_image()
var ts: String = Time.get_datetime_string_from_system().replace(":", "-")
var path: String = "/home/fleeky/Desktop/hyper-tps/terrain_%s.png" % ts
img.save_png(path)
print("Screenshot saved: %s" % path)
# R to rebuild terrain
if event is InputEventKey and event.pressed and event.keycode == KEY_R:
terrain.rebuild()
print("Terrain rebuild triggered")
# C to clear edits
if event is InputEventKey and event.pressed and event.keycode == KEY_C:
terrain.clear_edits()
print("Terrain edits cleared")
# L to toggle LOD debug colors
if event is InputEventKey and event.pressed and event.keycode == KEY_L:
terrain.debug_lod_colors = !terrain.debug_lod_colors
print("LOD colors: %s" % terrain.debug_lod_colors)
# E to toggle edit debug wireframes
if event is InputEventKey and event.pressed and event.keycode == KEY_E:
terrain.debug_edits = !terrain.debug_edits
print("Edit debug: %s" % terrain.debug_edits)
# V to toggle verbose logging
if event is InputEventKey and event.pressed and event.keycode == KEY_V:
terrain.debug_logging = !terrain.debug_logging
print("Verbose logging: %s" % terrain.debug_logging)
# F2 to stamp fandisk at crosshair (loads GLB via Godot, extracts mesh arrays)
if event is InputEventKey and event.pressed and event.keycode == KEY_F2:
var camera: Camera3D = get_viewport().get_camera_3d()
if camera:
var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
var from: Vector3 = camera.project_ray_origin(viewport_center)
var dir: Vector3 = camera.project_ray_normal(viewport_center)
var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 100.0)
if player:
query.exclude = [player.get_rid()]
var result: Dictionary = space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos: Vector3 = result["position"]
# Load mesh via Godot (avoids OBJ loader crash)
var scene: PackedScene = load("res://fandisk.glb")
if not scene:
print("Failed to load fandisk.glb")
return
var instance: Node3D = scene.instantiate()
var mesh_inst: MeshInstance3D = null
for child in instance.get_children():
if child is MeshInstance3D:
mesh_inst = child
break
if not mesh_inst or not mesh_inst.mesh:
print("No MeshInstance3D found in fandisk.glb")
instance.queue_free()
return
var mesh: Mesh = mesh_inst.mesh
var arrays: Array = mesh.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var idxs: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
# Lift the stamp so most of the mesh is above ground
hit_pos.y += _edit_radius * 0.8
print("Fandisk: %d verts, %d indices at %s scale=%.1f" % [verts.size(), idxs.size(), hit_pos, _edit_radius])
terrain.stamp_mesh_arrays(verts, idxs, hit_pos, _edit_radius, 3.0)
instance.queue_free()
# F3 to debug SDF at crosshair
if event is InputEventKey and event.pressed and event.keycode == KEY_F3:
var camera: Camera3D = get_viewport().get_camera_3d()
if camera:
var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
var from: Vector3 = camera.project_ray_origin(viewport_center)
var dir: Vector3 = camera.project_ray_normal(viewport_center)
var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 100.0)
if player:
query.exclude = [player.get_rid()]
var result: Dictionary = space_state.intersect_ray(query)
if result.size() > 0:
terrain.debug_sdf_at(result["position"])
# F1 to toggle wireframe
if event is InputEventKey and event.pressed and event.keycode == KEY_F1:
_wireframe = !_wireframe
if _wireframe:
get_viewport().debug_draw = Viewport.DEBUG_DRAW_WIREFRAME
else:
get_viewport().debug_draw = Viewport.DEBUG_DRAW_DISABLED
print("Wireframe: %s" % _wireframe)
# Right click = dig box, Shift+Right = add box
# Scroll = adjust edit size
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
# Shift+Scroll = adjust edit size (bare scroll = camera zoom in player script)
if event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP:
_edit_radius = minf(_edit_radius + 0.5, 20.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
elif event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_edit_radius = maxf(_edit_radius - 0.5, 1.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
elif event.button_index == MOUSE_BUTTON_RIGHT:
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera:
return
# Ray from view center (crosshair), not mouse position
var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
var from: Vector3 = camera.project_ray_origin(viewport_center)
var dir: Vector3 = camera.project_ray_normal(viewport_center)
var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 100.0)
if player:
query.exclude = [player.get_rid()]
var result: Dictionary = space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos: Vector3 = result["position"]
var half_ext: Vector3 = Vector3(_edit_radius, _edit_radius, _edit_radius)
var adding: bool = event.shift_pressed
# Place box centered at hit point — CSG handles the intersection
var strength: float = 3.0 if adding else -3.0
terrain.edit_terrain_box(hit_pos, half_ext, strength)
_save_edits()
print("Box edit at %s (size=%.1f, strength=%.1f)" % [hit_pos, _edit_radius, strength])
# Bump player up if adding material near them
if adding and player:
var dist: float = player.global_position.distance_to(hit_pos)
if dist < _edit_radius * 1.5:
player.global_position.y += _edit_radius + 1.0
func _save_edits() -> void:
if not terrain.has_method("get_edits_as_array"):
return
var edits: Array = terrain.get_edits_as_array()
var json_str: String = JSON.stringify(edits)
var f := FileAccess.open(EDITS_SAVE_PATH, FileAccess.WRITE)
if f:
f.store_string(json_str)
f.close()
func _load_edits() -> void:
if not terrain.has_method("load_edits_from_array"):
return
if not FileAccess.file_exists(EDITS_SAVE_PATH):
return
var f := FileAccess.open(EDITS_SAVE_PATH, FileAccess.READ)
if not f:
return
var json_str: String = f.get_as_text()
f.close()
var parsed: Variant = JSON.parse_string(json_str)
if parsed is Array:
terrain.load_edits_from_array(parsed)
print("[TerrainStreaming] Loaded %d edits from %s" % [parsed.size(), EDITS_SAVE_PATH])