Quality Gate / Quality Gate (pull_request) Successful in 6m55s
New visual test site under game/world/sites: flat sandy floor at -40 m, calm surface at 0 m, fixed daytime sun ahead of and above the submarine. Light shafts come from a shadow-only surface occluder whose alpha-scissor ripples open only inside a pool a few tens of metres ahead; a subtractive FogVolume cancels the global volumetric fog above the water, and an unshaded ceiling mesh shows the lit underside of the surface inside that pool. A composed world (sunlit_shallows_world.tscn) mounts the submarine, cockpit and camera rig in it, with the projectors kept out of the fog. Cockpit: the porthole glass moves to its own render layer with a rougher material and the porthole fill light excludes that layer, so interior lamps no longer mirror as hard specular points. Quality gate: new integration suite for the site (42 checks), three glass assertions in the foundation suite, and the Input-access architecture rule now matches both path separators so the gate runs on Windows. project.godot raises the volumetric fog froxel grid to 128 so the shafts stay readable. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
249 lines
8.1 KiB
GDScript
249 lines
8.1 KiB
GDScript
extends SceneTree
|
|
|
|
## Checks the sunlit shallows visual site keeps its three promises:
|
|
## a flat floor, a calm shadow-only water surface, and a daytime sun whose
|
|
## hour never changes.
|
|
|
|
const SCENE_PATH := "res://game/world/sites/sunlit_shallows_environment.tscn"
|
|
const WORLD_SCENE_PATH := (
|
|
"res://game/composition/vertical_slice/sunlit_shallows_world.tscn"
|
|
)
|
|
|
|
var _failures: Array[String] = []
|
|
var _checks := 0
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
var scene := load(SCENE_PATH) as PackedScene
|
|
_expect(scene != null, "scene loads")
|
|
if scene == null:
|
|
_finish()
|
|
return
|
|
|
|
var site := scene.instantiate() as SunlitShallowsEnvironment
|
|
_expect(site != null, "scene root is SunlitShallowsEnvironment")
|
|
if site == null:
|
|
_finish()
|
|
return
|
|
root.add_child(site)
|
|
await process_frame
|
|
|
|
_test_fixed_daytime_sun(site)
|
|
_test_flat_floor(site)
|
|
_test_calm_surface_occluder(site)
|
|
_test_scattering_environment(site)
|
|
_test_air_fog_cut(site)
|
|
_test_surface_ceiling(site)
|
|
|
|
site.queue_free()
|
|
await process_frame
|
|
_test_composed_world()
|
|
_finish()
|
|
|
|
|
|
func _test_composed_world() -> void:
|
|
var world_scene := load(WORLD_SCENE_PATH) as PackedScene
|
|
_expect(world_scene != null, "composed world scene loads")
|
|
if world_scene == null:
|
|
return
|
|
var world := world_scene.instantiate() as Node3D
|
|
_expect(
|
|
world.get_node_or_null("Environment") is SunlitShallowsEnvironment,
|
|
"composed world uses the sunlit shallows environment"
|
|
)
|
|
var submarine := world.get_node_or_null("SubmarineActor") as Node3D
|
|
_expect(submarine != null, "composed world has a submarine")
|
|
if submarine != null:
|
|
_expect(
|
|
submarine.position.y < SunlitShallowsEnvironment.SURFACE_LEVEL_M
|
|
and submarine.position.y > SunlitShallowsEnvironment.FLOOR_DEPTH_M,
|
|
"submarine starts between surface and floor"
|
|
)
|
|
_expect(
|
|
submarine.get_node_or_null("CockpitInterior") != null
|
|
and submarine.get_node_or_null("CockpitCameraRig") != null,
|
|
"submarine carries cockpit and camera rig"
|
|
)
|
|
for projector_name: String in ["LeftProjector", "RightProjector"]:
|
|
var projector := submarine.get_node_or_null(projector_name) as SpotLight3D
|
|
_expect(projector != null, "%s exists" % projector_name)
|
|
if projector != null:
|
|
_expect(
|
|
projector.light_volumetric_fog_energy < 1.0,
|
|
"%s does not wash out the sun shafts in the fog" % projector_name
|
|
)
|
|
world.free()
|
|
|
|
|
|
func _test_fixed_daytime_sun(site: SunlitShallowsEnvironment) -> void:
|
|
var sun := site.get_node_or_null("Sun") as DirectionalLight3D
|
|
_expect(sun != null, "sun is a DirectionalLight3D")
|
|
if sun == null:
|
|
return
|
|
_expect(
|
|
sun.rotation_degrees.is_equal_approx(
|
|
SunlitShallowsEnvironment.SUN_ROTATION_DEGREES
|
|
),
|
|
"sun hour is the fixed constant"
|
|
)
|
|
var sun_direction := -sun.global_transform.basis.z
|
|
_expect(sun_direction.y < -0.6, "sun shines downward from above the porthole (daytime)")
|
|
_expect(
|
|
sun_direction.z > 0.15,
|
|
"sun shines toward the observer (rays come from ahead, world -Z)"
|
|
)
|
|
_expect(sun.shadow_enabled, "sun casts shadows (needed for light shafts)")
|
|
_expect(
|
|
sun.light_volumetric_fog_energy >= 1.0,
|
|
"sun injects energy into the volumetric fog"
|
|
)
|
|
|
|
|
|
func _test_flat_floor(site: SunlitShallowsEnvironment) -> void:
|
|
var floor_mesh := site.get_node_or_null("FlatFloor/Mesh") as MeshInstance3D
|
|
_expect(floor_mesh != null, "floor mesh exists")
|
|
if floor_mesh == null:
|
|
return
|
|
var vertices := floor_mesh.mesh.get_faces()
|
|
var flat := true
|
|
for vertex: Vector3 in vertices:
|
|
if not is_zero_approx(vertex.y):
|
|
flat = false
|
|
break
|
|
_expect(flat, "floor mesh is flat")
|
|
_expect(
|
|
is_equal_approx(
|
|
floor_mesh.global_position.y,
|
|
SunlitShallowsEnvironment.FLOOR_DEPTH_M
|
|
),
|
|
"floor sits at the site depth"
|
|
)
|
|
_expect(
|
|
floor_mesh.material_override is ShaderMaterial,
|
|
"floor uses the caustic shader material"
|
|
)
|
|
var collision := site.get_node_or_null("FlatFloor/Collision") as CollisionShape3D
|
|
_expect(collision != null and collision.shape is BoxShape3D, "floor has a box collider")
|
|
|
|
|
|
func _test_calm_surface_occluder(site: SunlitShallowsEnvironment) -> void:
|
|
var occluder := site.get_node_or_null("CalmSurfaceOccluder") as MeshInstance3D
|
|
_expect(occluder != null, "surface occluder exists")
|
|
if occluder == null:
|
|
return
|
|
_expect(
|
|
occluder.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY,
|
|
"surface occluder is shadow-only"
|
|
)
|
|
_expect(
|
|
is_equal_approx(
|
|
occluder.global_position.y,
|
|
SunlitShallowsEnvironment.SURFACE_LEVEL_M
|
|
),
|
|
"surface occluder sits at water level"
|
|
)
|
|
var material := occluder.material_override as ShaderMaterial
|
|
_expect(material != null, "surface occluder uses a shader material")
|
|
if material == null:
|
|
return
|
|
var drift: Variant = material.get_shader_parameter("drift_speed")
|
|
_expect(
|
|
drift is float and drift > 0.0 and drift < 0.05,
|
|
"surface drift is slow (calm water)"
|
|
)
|
|
_expect(
|
|
material.get_shader_parameter("ripple_noise") is NoiseTexture2D,
|
|
"surface ripple noise is assigned"
|
|
)
|
|
var pool_center: Variant = material.get_shader_parameter("pool_center_xz")
|
|
_expect(
|
|
pool_center is Vector2 and pool_center.y < -20.0 and pool_center.y > -120.0,
|
|
"light pool sits a few tens of metres ahead (world -Z)"
|
|
)
|
|
var pool_radius: Variant = material.get_shader_parameter("pool_radius")
|
|
_expect(
|
|
pool_radius is float and pool_radius > 5.0 and pool_radius < 60.0,
|
|
"light pool radius is local, not the whole surface"
|
|
)
|
|
|
|
|
|
func _test_scattering_environment(site: SunlitShallowsEnvironment) -> void:
|
|
var world_environment := site.get_node_or_null("WorldEnvironment") as WorldEnvironment
|
|
_expect(world_environment != null, "world environment exists")
|
|
if world_environment == null:
|
|
return
|
|
var environment := world_environment.environment
|
|
_expect(environment.volumetric_fog_enabled, "volumetric fog is enabled")
|
|
_expect(environment.volumetric_fog_anisotropy > 0.5, "fog scatters forward")
|
|
_expect(environment.fog_enabled, "depth fog is enabled")
|
|
|
|
|
|
func _test_surface_ceiling(site: SunlitShallowsEnvironment) -> void:
|
|
var ceiling := site.get_node_or_null("SurfaceCeiling") as MeshInstance3D
|
|
_expect(ceiling != null, "visible surface ceiling exists")
|
|
if ceiling == null:
|
|
return
|
|
_expect(
|
|
is_equal_approx(ceiling.global_position.y, SunlitShallowsEnvironment.SURFACE_LEVEL_M),
|
|
"surface ceiling sits at water level"
|
|
)
|
|
_expect(
|
|
ceiling.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
|
"surface ceiling leaves shadowing to the occluder"
|
|
)
|
|
var ceiling_material := ceiling.material_override as ShaderMaterial
|
|
var occluder := site.get_node_or_null("CalmSurfaceOccluder") as MeshInstance3D
|
|
var occluder_material: ShaderMaterial = null if occluder == null else occluder.material_override
|
|
_expect(
|
|
ceiling_material != null
|
|
and occluder_material != null
|
|
and ceiling_material.get_shader_parameter("pool_center_xz")
|
|
== occluder_material.get_shader_parameter("pool_center_xz")
|
|
and ceiling_material.get_shader_parameter("pool_radius")
|
|
== occluder_material.get_shader_parameter("pool_radius"),
|
|
"surface ceiling lights the same pool the occluder opens"
|
|
)
|
|
|
|
|
|
func _test_air_fog_cut(site: SunlitShallowsEnvironment) -> void:
|
|
var air := site.get_node_or_null("AirFogCut") as FogVolume
|
|
_expect(air != null, "air fog cut exists")
|
|
if air == null:
|
|
return
|
|
var bottom := air.global_position.y - air.size.y * 0.5
|
|
_expect(
|
|
is_equal_approx(bottom, SunlitShallowsEnvironment.SURFACE_LEVEL_M),
|
|
"air fog cut starts at the water surface"
|
|
)
|
|
var material := air.material as FogMaterial
|
|
_expect(material != null and material.density < 0.0, "air fog cut subtracts fog")
|
|
var world_environment := site.get_node("WorldEnvironment") as WorldEnvironment
|
|
_expect(
|
|
material != null and is_equal_approx(
|
|
-material.density,
|
|
world_environment.environment.volumetric_fog_density
|
|
),
|
|
"air fog cut cancels the global volumetric density exactly"
|
|
)
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures.append(label)
|
|
|
|
|
|
func _finish() -> void:
|
|
if _failures.is_empty():
|
|
print("Sunlit shallows environment: %d passed, 0 failed." % _checks)
|
|
quit(0)
|
|
else:
|
|
for failure: String in _failures:
|
|
push_error("sunlit shallows environment: %s" % failure)
|
|
print("Sunlit shallows environment: %d failed." % _failures.size())
|
|
quit(1)
|