196 lines
6.8 KiB
GDScript
196 lines
6.8 KiB
GDScript
extends SceneTree
|
|
|
|
const JajceWatercourse := preload("res://world/jajce/JajceWatercourse.gd")
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var camera := Camera3D.new()
|
|
camera.current = true
|
|
root.add_child(camera)
|
|
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
|
root.add_child(world)
|
|
await process_frame
|
|
for _frame in 4:
|
|
await physics_frame
|
|
|
|
var terrain := world.get_node("TerrainRoot/Terrain3D") as Terrain3D
|
|
_check(terrain != null, "Jajce watercourse validation requires Terrain3D")
|
|
if terrain == null:
|
|
_finish()
|
|
return
|
|
|
|
var shelf_z := -36.0
|
|
var shelf_center_x := JajceWatercourse.upper_stream_center_x(shelf_z)
|
|
var shelf_half_width := JajceWatercourse.upper_stream_half_width(shelf_z)
|
|
var shelf_stream_height := _terrain_height(terrain, shelf_center_x, shelf_z)
|
|
var shelf_bank_height := _terrain_height(
|
|
terrain, shelf_center_x + shelf_half_width + 4.0, shelf_z
|
|
)
|
|
var plunge_height := _terrain_height(
|
|
terrain, JajceWatercourse.PLUNGE_POOL_CENTER.x, JajceWatercourse.PLUNGE_POOL_CENTER.y
|
|
)
|
|
_check(shelf_stream_height >= 10.8, "Upper stream should remain on the raised waterfall shelf")
|
|
_check(
|
|
shelf_bank_height - shelf_stream_height >= 0.45,
|
|
"Upper stream should be visibly carved below its shelf bank"
|
|
)
|
|
_check(
|
|
shelf_stream_height - plunge_height >= 10.0,
|
|
"Waterfall shelf should create a readable drop into the plunge basin"
|
|
)
|
|
|
|
var downstream_samples := PackedFloat32Array([-10.0, 12.0, 34.0])
|
|
for world_z in downstream_samples:
|
|
var center_x := JajceWatercourse.downstream_center_x(world_z)
|
|
var half_width := JajceWatercourse.downstream_half_width(world_z)
|
|
var channel_height := _terrain_height(terrain, center_x, world_z)
|
|
var left_bank_height := _terrain_height(terrain, center_x - half_width - 5.0, world_z)
|
|
var right_bank_height := _terrain_height(terrain, center_x + half_width + 5.0, world_z)
|
|
_check(
|
|
channel_height <= JajceWatercourse.downstream_bed_height(world_z) + 0.08,
|
|
"Downstream bed should follow its authored profile at z=%.1f" % world_z
|
|
)
|
|
_check(
|
|
maxf(left_bank_height, right_bank_height) - channel_height >= 0.45,
|
|
"Downstream channel should read below a natural bank at z=%.1f" % world_z
|
|
)
|
|
|
|
var river_surface := world.get_node("WaterRoot/RiverSurface") as Node3D
|
|
_check_ribbon_tracks_terrain(
|
|
terrain, river_surface.get_node("RiverRibbon") as MeshInstance3D, "Downstream water", false
|
|
)
|
|
_check_ribbon_tracks_terrain(
|
|
terrain,
|
|
river_surface.get_node("UpperRiverRibbon") as MeshInstance3D,
|
|
"Upper-stream water",
|
|
true
|
|
)
|
|
|
|
print(
|
|
(
|
|
"[TEST] Jajce watercourse samples | shelf=%.2f bank=%.2f pool=%.2f"
|
|
% [shelf_stream_height, shelf_bank_height, plunge_height]
|
|
)
|
|
)
|
|
_finish()
|
|
|
|
|
|
func _terrain_height(terrain: Terrain3D, world_x: float, world_z: float) -> float:
|
|
return terrain.data.get_height(Vector3(world_x, 0.0, world_z))
|
|
|
|
|
|
func _check_ribbon_tracks_terrain(
|
|
terrain: Terrain3D, ribbon: MeshInstance3D, label: String, is_upper: bool
|
|
) -> void:
|
|
_check(ribbon != null and ribbon.mesh is ArrayMesh, "%s ribbon should be generated" % label)
|
|
if ribbon == null or not ribbon.mesh is ArrayMesh:
|
|
return
|
|
var arrays := ribbon.mesh.surface_get_arrays(0)
|
|
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
|
|
_check(not vertices.is_empty(), "%s ribbon should contain vertices" % label)
|
|
if vertices.is_empty() or uvs.size() != vertices.size():
|
|
failures.append("%s ribbon requires a complete coordinate profile" % label)
|
|
return
|
|
var start_z := (
|
|
JajceWatercourse.UPPER_STREAM_START_Z if is_upper else JajceWatercourse.DOWNSTREAM_START_Z
|
|
)
|
|
var end_z := (
|
|
JajceWatercourse.UPPER_STREAM_END_Z if is_upper else JajceWatercourse.DOWNSTREAM_END_Z
|
|
)
|
|
_check(
|
|
(
|
|
absf(ribbon.to_global(vertices[0]).z - start_z) < 0.03
|
|
and absf(ribbon.to_global(vertices[-1]).z - end_z) < 0.03
|
|
),
|
|
"%s ribbon should span its entire authored stream length" % label
|
|
)
|
|
var pool := ribbon.get_parent().get_node("PlungePool") as MeshInstance3D
|
|
var pool_mesh := pool.mesh as PlaneMesh
|
|
var pool_radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
|
|
var min_depth := INF
|
|
var max_depth := -INF
|
|
for vertex_index in vertices.size():
|
|
var world_vertex := ribbon.to_global(vertices[vertex_index])
|
|
var world_z := lerpf(start_z, end_z, uvs[vertex_index].y)
|
|
var center_x := (
|
|
JajceWatercourse.upper_stream_center_x(world_z)
|
|
if is_upper
|
|
else JajceWatercourse.downstream_center_x(world_z)
|
|
)
|
|
var half_width := (
|
|
JajceWatercourse.upper_stream_half_width(world_z)
|
|
if is_upper
|
|
else JajceWatercourse.downstream_half_width(world_z)
|
|
)
|
|
if not is_upper:
|
|
var pool_progress := (world_z - pool.global_position.z) / pool_radii.y
|
|
if absf(pool_progress) < 1.0:
|
|
half_width = maxf(
|
|
half_width, pool_radii.x * sqrt(1.0 - pool_progress * pool_progress)
|
|
)
|
|
var center_height := _terrain_height(terrain, center_x, world_z)
|
|
var terrain_height := terrain.data.get_height(world_vertex)
|
|
if (
|
|
not world_vertex.is_finite()
|
|
or not is_finite(terrain_height)
|
|
or not is_finite(center_height)
|
|
):
|
|
failures.append("%s ribbon sampled outside Terrain3D" % label)
|
|
return
|
|
if absf(world_vertex.y - center_height - JajceWatercourse.WATER_SURFACE_OFFSET) > 0.03:
|
|
failures.append("%s cross-section should follow its actual carved centerline" % label)
|
|
return
|
|
if (
|
|
absf(world_vertex.z - world_z) > 0.03
|
|
or absf(world_vertex.x - center_x) > half_width + 0.03
|
|
):
|
|
failures.append(
|
|
"%s ribbon should stay inside the authored stream/pool footprint" % label
|
|
)
|
|
return
|
|
var depth := world_vertex.y - terrain_height
|
|
min_depth = minf(min_depth, depth)
|
|
max_depth = maxf(max_depth, depth)
|
|
if depth < -0.03 or depth > JajceWatercourse.WATER_SURFACE_OFFSET + 0.03:
|
|
failures.append("%s surface should remain shallow and above the channel bed" % label)
|
|
return
|
|
var across := uvs[vertex_index].x
|
|
if is_zero_approx(across) or is_equal_approx(across, 1.0):
|
|
var edge_distance := absf(world_vertex.x - center_x)
|
|
if absf(edge_distance - half_width) > 0.03 and absf(depth) > 0.03:
|
|
failures.append(
|
|
"%s edges must reach the authored extent or the actual shoreline" % label
|
|
)
|
|
return
|
|
if (
|
|
(is_zero_approx(across) and world_vertex.x >= center_x)
|
|
or (is_equal_approx(across, 1.0) and world_vertex.x <= center_x)
|
|
):
|
|
failures.append(
|
|
"%s water must retain its authored centerline inside both banks" % label
|
|
)
|
|
return
|
|
print("[TEST] %s depth bounds | min=%.3f max=%.3f" % [label, min_depth, max_depth])
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] Jajce watercourse terrain passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|