feat(environment): add painterly skies and living meadows
This commit is contained in:
@@ -8,6 +8,8 @@ func _initialize() -> void:
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var viewport := root as Viewport
|
||||
var original_msaa := viewport.msaa_3d
|
||||
var camera := Camera3D.new()
|
||||
camera.current = true
|
||||
root.add_child(camera)
|
||||
@@ -16,13 +18,14 @@ func _run() -> void:
|
||||
await process_frame
|
||||
await physics_frame
|
||||
|
||||
var viewport := root as Viewport
|
||||
var environment: Environment = (
|
||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
var light := world.get_node("DirectionalLight3D") as DirectionalLight3D
|
||||
var grass := world.get_node("TerrainRoot/Terrain3D/CozyGrassField") as Node3D
|
||||
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController") as Node
|
||||
var meadow := world.get_node("PainterlyMeadow") as PainterlyMeadow
|
||||
var balanced_meadow := meadow.get_presentation_stats()
|
||||
var world_grass_material := grass.get("mesh_material_override") as ShaderMaterial
|
||||
var world_process_material := grass.get("process_material") as ShaderMaterial
|
||||
var world_sky: Sky = environment.sky
|
||||
@@ -41,6 +44,10 @@ func _run() -> void:
|
||||
+ " (found %.2f)" % balanced_render_scale
|
||||
)
|
||||
)
|
||||
_check(
|
||||
viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X),
|
||||
"Balanced should cap authored multisample antialiasing at 2x"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
light.directional_shadow_max_distance <= JajceWorld.BALANCED_SHADOW_DISTANCE
|
||||
@@ -54,6 +61,14 @@ func _run() -> void:
|
||||
world_grass_material == grass_controller.get("grass_material"),
|
||||
"Grass rendering and interaction should share this world's isolated material"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
int(balanced_meadow["multimesh_batch_count"]) == 4
|
||||
and int(balanced_meadow["flower_clump_count"]) in range(1, 1801)
|
||||
and _meadow_has_finite_placements(meadow)
|
||||
),
|
||||
"Meadow batches should retain neutral instance colors, finite placement and their clump budget"
|
||||
)
|
||||
|
||||
_check(
|
||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
||||
@@ -63,6 +78,21 @@ func _run() -> void:
|
||||
var high_particle_count := int(grass.get("particle_count"))
|
||||
var high_render_scale := viewport.scaling_3d_scale
|
||||
var high_shadow_distance := light.directional_shadow_max_distance
|
||||
_check(
|
||||
viewport.msaa_3d == original_msaa,
|
||||
"High should restore the authored multisample antialiasing setting"
|
||||
)
|
||||
var high_meadow := meadow.get_presentation_stats()
|
||||
_check(
|
||||
(
|
||||
int(high_meadow["visible_flower_clump_count"]) == int(high_meadow["flower_clump_count"])
|
||||
and (
|
||||
int(balanced_meadow["visible_flower_clump_count"])
|
||||
< int(high_meadow["visible_flower_clump_count"])
|
||||
)
|
||||
),
|
||||
"High should restore every flower clump while Balanced reduces meadow density"
|
||||
)
|
||||
_check(
|
||||
high_particle_count in range(9000, 15001),
|
||||
"High should restore the authored grass population"
|
||||
@@ -94,6 +124,9 @@ func _run() -> void:
|
||||
viewport.scaling_3d_scale <= JajceWorld.LOW_RENDER_SCALE + 0.001,
|
||||
"Low should reduce 3D render resolution"
|
||||
)
|
||||
_check(
|
||||
viewport.msaa_3d == Viewport.MSAA_DISABLED, "Low should disable multisample antialiasing"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
environment.fog_enabled
|
||||
@@ -120,6 +153,18 @@ func _run() -> void:
|
||||
)
|
||||
_check(not grass_controller.is_processing(), "Low should stop grass interaction scans")
|
||||
_check(_not_any_grass_particle_emitting(grass), "Low should stop every grass particle emitter")
|
||||
var low_meadow := meadow.get_presentation_stats()
|
||||
_check(
|
||||
(
|
||||
int(low_meadow["visible_flower_clump_count"]) > 0
|
||||
and (
|
||||
int(low_meadow["visible_flower_clump_count"])
|
||||
< int(balanced_meadow["visible_flower_clump_count"])
|
||||
)
|
||||
and low_meadow["flower_clump_count"] == high_meadow["flower_clump_count"]
|
||||
),
|
||||
"Low should retain a reduced flower population without regenerating placements"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
world_grass_material != null
|
||||
@@ -136,6 +181,7 @@ func _run() -> void:
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, high_render_scale)
|
||||
and viewport.msaa_3d == original_msaa
|
||||
and environment.glow_enabled
|
||||
and (environment.volumetric_fog_enabled == world.call("_supports_volumetric_fog"))
|
||||
and environment.adjustment_enabled
|
||||
@@ -153,12 +199,19 @@ func _run() -> void:
|
||||
),
|
||||
"Returning to High should restore grass rendering and processing"
|
||||
)
|
||||
_check(
|
||||
meadow.get_presentation_stats() == high_meadow,
|
||||
"Returning to High should restore the same complete flower batches"
|
||||
)
|
||||
|
||||
var world_base_scale := float(world.get("_authored_render_scale"))
|
||||
world.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, world_base_scale),
|
||||
"An exiting world should restore the viewport scale it originally owned"
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, world_base_scale)
|
||||
and viewport.msaa_3d == original_msaa
|
||||
),
|
||||
"An exiting world should restore the viewport scale and antialiasing it originally owned"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
@@ -183,6 +236,7 @@ func _run() -> void:
|
||||
_check(
|
||||
(
|
||||
fresh_world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED
|
||||
and viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X)
|
||||
and fresh_environment.fog_enabled
|
||||
and not fresh_environment.volumetric_fog_enabled
|
||||
and fresh_grass.visible
|
||||
@@ -205,8 +259,11 @@ func _run() -> void:
|
||||
var original_scale := float(fresh_world.get("_authored_render_scale"))
|
||||
fresh_world.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
||||
"A sequential world should release its viewport-scale ownership"
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale)
|
||||
and viewport.msaa_3d == original_msaa
|
||||
),
|
||||
"A sequential world should release its viewport scale and antialiasing ownership"
|
||||
)
|
||||
|
||||
var older_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
||||
@@ -216,19 +273,29 @@ func _run() -> void:
|
||||
root.add_child(newer_owner)
|
||||
await process_frame
|
||||
_check(
|
||||
is_equal_approx(float(newer_owner.get("_authored_render_scale")), original_scale),
|
||||
"Concurrent owners should inherit the viewport base, not another owner's reduced scale"
|
||||
(
|
||||
is_equal_approx(float(newer_owner.get("_authored_render_scale")), original_scale)
|
||||
and int(newer_owner.get("_authored_msaa")) == original_msaa
|
||||
),
|
||||
"Concurrent owners should inherit the viewport's base scale and antialiasing"
|
||||
)
|
||||
older_owner.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, JajceWorld.BALANCED_RENDER_SCALE),
|
||||
"A stale owner exiting should not overwrite the active owner's scale"
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, JajceWorld.BALANCED_RENDER_SCALE)
|
||||
and viewport.msaa_3d == mini(original_msaa, Viewport.MSAA_2X)
|
||||
),
|
||||
"A stale owner exiting should preserve the active owner's scale and antialiasing"
|
||||
)
|
||||
newer_owner.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
||||
"The final viewport owner should restore the original scale on exit"
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale)
|
||||
and viewport.msaa_3d == original_msaa
|
||||
),
|
||||
"The final viewport owner should restore the original scale and antialiasing on exit"
|
||||
)
|
||||
await _check_msaa_previous_owner_and_external_change(viewport)
|
||||
|
||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||
var time_dial := main_scene.get_node("UI/TimeDial") as Control
|
||||
@@ -264,6 +331,32 @@ func _all_grass_particles_emitting(grass: Node3D) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func _meadow_has_finite_placements(meadow: PainterlyMeadow) -> bool:
|
||||
var terrain := meadow.get_node(meadow.terrain_path) as Terrain3D
|
||||
# The dummy headless renderer returns black for every instance-color readback.
|
||||
var can_read_instance_colors := DisplayServer.get_name() != "headless"
|
||||
for child in meadow.get_children():
|
||||
var batch := child as MultiMeshInstance3D
|
||||
if batch == null or batch.multimesh == null:
|
||||
return false
|
||||
if not batch.multimesh.use_colors or not batch.multimesh.use_custom_data:
|
||||
return false
|
||||
for index in batch.multimesh.instance_count:
|
||||
if (
|
||||
can_read_instance_colors
|
||||
and not batch.multimesh.get_instance_color(index).is_equal_approx(Color.WHITE)
|
||||
):
|
||||
return false
|
||||
var placement := batch.multimesh.get_instance_transform(index)
|
||||
if not placement.is_finite():
|
||||
return false
|
||||
var point := batch.to_global(placement.origin)
|
||||
var height := terrain.data.get_height(point)
|
||||
if is_nan(height) or absf(point.y - height) > 0.025:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _create_presentation_only_world(quality: int) -> JajceWorld:
|
||||
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
# Stable simulation-facing IDs deliberately allow only one loaded authority
|
||||
@@ -273,6 +366,33 @@ func _create_presentation_only_world(quality: int) -> JajceWorld:
|
||||
return world
|
||||
|
||||
|
||||
func _check_msaa_previous_owner_and_external_change(viewport: Viewport) -> void:
|
||||
var original_msaa := viewport.msaa_3d
|
||||
viewport.msaa_3d = Viewport.MSAA_8X
|
||||
var previous_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
||||
root.add_child(previous_owner)
|
||||
await process_frame
|
||||
var active_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.HIGH)
|
||||
root.add_child(active_owner)
|
||||
await process_frame
|
||||
_check(
|
||||
viewport.msaa_3d == Viewport.MSAA_8X,
|
||||
"A High owner should inherit authored antialiasing even when the previous owner uses Low"
|
||||
)
|
||||
active_owner.free()
|
||||
_check(
|
||||
viewport.msaa_3d == Viewport.MSAA_DISABLED,
|
||||
"An exiting active owner should restore the surviving previous owner's antialiasing"
|
||||
)
|
||||
viewport.msaa_3d = Viewport.MSAA_2X
|
||||
previous_owner.free()
|
||||
_check(
|
||||
viewport.msaa_3d == Viewport.MSAA_2X,
|
||||
"Releasing the final owner should preserve an external antialiasing change"
|
||||
)
|
||||
viewport.msaa_3d = original_msaa
|
||||
|
||||
|
||||
func _not_any_grass_particle_emitting(grass: Node3D) -> bool:
|
||||
var particles: Array = grass.get("particle_nodes")
|
||||
for value in particles:
|
||||
|
||||
@@ -20,6 +20,7 @@ func _run() -> void:
|
||||
|
||||
var manager = main_scene.get_node("SimulationManager")
|
||||
manager.set_process(false)
|
||||
_check_village_tree_bindings(main_scene, manager)
|
||||
var cluster := main_scene.get_node(CLUSTER_PATH) as RiverbankResourceCluster
|
||||
var berry := cluster.get_node("ResourceAnchors/BerryBush_River_02") as ResourceNode
|
||||
var tree := cluster.get_node("ResourceAnchors/Tree_River_Resource_01") as ResourceNode
|
||||
@@ -120,6 +121,34 @@ func _run() -> void:
|
||||
_finish()
|
||||
|
||||
|
||||
func _check_village_tree_bindings(main_scene: Node, manager: Node) -> void:
|
||||
var snapshot: String = manager.serialize_state()
|
||||
for tree_name in ["Tree_01", "Tree_02", "Tree_North_Outskirts_01", "Tree_South_Edge_01"]:
|
||||
var tree := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/" + tree_name)
|
||||
as ResourceNode
|
||||
)
|
||||
var visual := tree.get_node("Visual/TreePresentation") as HarvestableTreePresentation
|
||||
_check(
|
||||
visual.resource_node == tree,
|
||||
"Village tree art should bind the existing finite resource"
|
||||
)
|
||||
while tree.state.can_extract():
|
||||
manager.harvest_resource_node(tree)
|
||||
_check(visual.is_stump_visible(), "Harvesting the real village tree should leave a stump")
|
||||
_check(manager.restore_state_from_json(snapshot), "Village tree harvest review should restore")
|
||||
for tree_name in ["Tree_01", "Tree_02", "Tree_North_Outskirts_01", "Tree_South_Edge_01"]:
|
||||
var visual := (
|
||||
main_scene.get_node(
|
||||
"JajceWorld/WorldObjects/ResourceNodes/" + tree_name + "/Visual/TreePresentation"
|
||||
)
|
||||
as HarvestableTreePresentation
|
||||
)
|
||||
_check(
|
||||
visual.get_visible_canopy_count() == 4, "Restore rebuilds each full village tree crown"
|
||||
)
|
||||
|
||||
|
||||
func _check_initial_presentation(
|
||||
berry_visual: StylizedBerryPatch, tree_visual: HarvestableTreePresentation
|
||||
) -> void:
|
||||
|
||||
@@ -63,10 +63,13 @@ func _run() -> void:
|
||||
|
||||
var river_surface := world.get_node("WaterRoot/RiverSurface") as Node3D
|
||||
_check_ribbon_tracks_terrain(
|
||||
terrain, river_surface.get_node("RiverRibbon") as MeshInstance3D, "Downstream water"
|
||||
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"
|
||||
terrain,
|
||||
river_surface.get_node("UpperRiverRibbon") as MeshInstance3D,
|
||||
"Upper-stream water",
|
||||
true
|
||||
)
|
||||
|
||||
print(
|
||||
@@ -83,23 +86,98 @@ func _terrain_height(terrain: Terrain3D, world_x: float, world_z: float) -> floa
|
||||
|
||||
|
||||
func _check_ribbon_tracks_terrain(
|
||||
terrain: Terrain3D, ribbon: MeshInstance3D, label: String
|
||||
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)
|
||||
for vertex_index in range(0, vertices.size(), maxi(1, vertices.size() / 12)):
|
||||
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 is_nan(terrain_height):
|
||||
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 - terrain_height - JajceWatercourse.WATER_SURFACE_OFFSET) > 0.03:
|
||||
failures.append("%s ribbon should hug the carved terrain" % label)
|
||||
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:
|
||||
|
||||
@@ -198,7 +198,7 @@ func _run() -> void:
|
||||
if not tree.has_node("Canopy"):
|
||||
continue
|
||||
var canopy := tree.get_node("Canopy") as MeshInstance3D
|
||||
var material := canopy.mesh.material as ShaderMaterial
|
||||
var material := canopy.get_active_material(0) as ShaderMaterial
|
||||
tree_wind_is_bounded = (
|
||||
tree_wind_is_bounded
|
||||
and material != null
|
||||
@@ -249,6 +249,8 @@ func _run() -> void:
|
||||
upper_river_ribbon.mesh is ArrayMesh,
|
||||
"Waterfall shelf should expose a terrain-following upper stream"
|
||||
)
|
||||
_check_water_cross_sections(river_ribbon)
|
||||
_check_water_cross_sections(upper_river_ribbon)
|
||||
_check(
|
||||
world.has_node("WaterRoot/RiverSurface/PlungePool"),
|
||||
"River should widen into a readable plunge pool beneath the waterfall"
|
||||
@@ -270,6 +272,7 @@ func _run() -> void:
|
||||
),
|
||||
"Plunge-pool water should follow the carved basin height"
|
||||
)
|
||||
_check_pool_outlet(river_ribbon, plunge_pool, terrain)
|
||||
var environment: Environment = (
|
||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
@@ -628,6 +631,44 @@ func _run() -> void:
|
||||
quit(1)
|
||||
|
||||
|
||||
func _check_water_cross_sections(ribbon: MeshInstance3D) -> void:
|
||||
var arrays := ribbon.mesh.surface_get_arrays(0)
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
var uvs: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
|
||||
var row_height := 0.0
|
||||
for index in vertices.size():
|
||||
if is_zero_approx(uvs[index].x):
|
||||
row_height = vertices[index].y
|
||||
_check(
|
||||
is_equal_approx(vertices[index].y, row_height),
|
||||
"Water cross-sections should remain level instead of climbing either terrain bank"
|
||||
)
|
||||
|
||||
|
||||
func _check_pool_outlet(ribbon: MeshInstance3D, pool: MeshInstance3D, terrain: Terrain3D) -> void:
|
||||
var vertices: PackedVector3Array = ribbon.mesh.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
|
||||
var start := ribbon.to_global(vertices[0])
|
||||
var pool_mesh := pool.mesh as PlaneMesh
|
||||
var radii := pool_mesh.size * Vector2(pool.scale.x, pool.scale.z) * 0.5
|
||||
var progress := (start.z - pool.global_position.z) / radii.y
|
||||
var expected_edge := pool.global_position.x - radii.x * sqrt(1.0 - progress * progress)
|
||||
var meets_ellipse := absf(start.x - expected_edge) < 0.03
|
||||
var meets_shore := (
|
||||
start.x >= expected_edge
|
||||
and start.x < pool.global_position.x
|
||||
and absf(start.y - terrain.data.get_height(start)) < 0.03
|
||||
)
|
||||
_check(
|
||||
(meets_ellipse or meets_shore) and absf(start.y - pool.global_position.y) < 0.03,
|
||||
"River outlet should meet the pool ellipse or its terrain shoreline at the same surface height"
|
||||
)
|
||||
var material := pool_mesh.surface_get_material(0) as ShaderMaterial
|
||||
_check(
|
||||
is_equal_approx(float(material.get_shader_parameter("pool_outlet_z")), start.z),
|
||||
"Pool transparency should end where the river takes over to avoid coplanar overlap"
|
||||
)
|
||||
|
||||
|
||||
func _wait_for_navigation_map(navigation_map: RID) -> void:
|
||||
for attempt in 30:
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
extends GutTest
|
||||
|
||||
const CYCLE := preload("res://world/jajce/day_night_cycle.gd")
|
||||
|
||||
|
||||
func test_sun_is_highest_at_noon_and_lights_the_ground_throughout_day() -> void:
|
||||
var cycle := CYCLE.new()
|
||||
var light := DirectionalLight3D.new()
|
||||
cycle.directional_light = light
|
||||
cycle._apply_light_rotation(0.5)
|
||||
var noon_elevation := -light.rotation_degrees.x
|
||||
assert_gt(noon_elevation, 45.0, "Noon should have an elevated sun, not grazing light")
|
||||
for time in [0.15, 0.25, 0.4, 0.6, 0.75, 0.85]:
|
||||
cycle._apply_light_rotation(time)
|
||||
assert_lt(light.rotation_degrees.x, 0.0, "The daylight source points down toward terrain")
|
||||
assert_lt(-light.rotation_degrees.x, noon_elevation, "The solar arc peaks at noon")
|
||||
cycle.free()
|
||||
light.free()
|
||||
|
||||
|
||||
func test_warm_transitions_overlap_dawn_and_dusk_without_persisting_at_noon() -> void:
|
||||
var cycle := CYCLE.new()
|
||||
for time in [0.22, 0.78]:
|
||||
assert_gt(cycle._day_factor(time), 0.0)
|
||||
assert_lt(cycle._day_factor(time), 1.0)
|
||||
assert_gt(cycle._sunrise_sunset_weight(time), 0.8)
|
||||
assert_eq(cycle._sunrise_sunset_weight(0.5), 0.0)
|
||||
assert_eq(cycle._sunrise_sunset_weight(0.0), 0.0)
|
||||
cycle.free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnjghjrvx52cr
|
||||
Reference in New Issue
Block a user