Quality Gate / Quality Gate (pull_request) Failing after 2m58s
SeabedFloraActor scatters seagrass tufts on flat sand in the lit band and sea fans on rock between 15 and 60 m, deterministically from the site id, one MultiMesh per tile and kind, with procedural alpha-cut meshes and textures (SeabedFloraAssets), vertex sway and wrapped lighting, no collision and no sonar presence. Placement reads the field arrays directly and runs on the worker thread pool. The seabed shader grows turf algae on sunlit rock, coralline crusts in patches and encrusting sponges in the shade, fading out between 40 and 75 m. The water-column optics move to a shared shader include. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
127 lines
3.9 KiB
GDScript
127 lines
3.9 KiB
GDScript
class_name DiveSite
|
|
extends Node3D
|
|
|
|
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
|
|
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
|
|
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
|
|
const BathymetricTerrainActor = preload(
|
|
"res://game/world/sites/bathymetric_terrain_actor.gd"
|
|
)
|
|
const SeabedFloraActor = preload("res://game/world/sites/seabed_flora_actor.gd")
|
|
|
|
const METAL_REFLECTIVITY := 1.0
|
|
const BEACON_COLOR := Color(0.2, 0.78, 0.9)
|
|
|
|
@onready var _terrain: BathymetricTerrainActor = %Terrain
|
|
@onready var _flora: SeabedFloraActor = %Flora
|
|
@onready var _placements_root: Node3D = %Placements
|
|
|
|
var _definition: DiveSiteDefinition
|
|
var _placeholder_count := 0
|
|
|
|
|
|
func configure(definition: DiveSiteDefinition) -> String:
|
|
if _terrain == null or _placements_root == null:
|
|
return "dive site is not in the scene tree"
|
|
if definition == null:
|
|
return "dive site definition is missing"
|
|
var field := definition.height_field()
|
|
if field == null:
|
|
return "dive site definition is invalid"
|
|
var terrain_error := _terrain.configure(field)
|
|
if not terrain_error.is_empty():
|
|
return terrain_error
|
|
if _flora != null:
|
|
# The flora is seeded by the site id, so a site always grows the same meadows.
|
|
var flora_error := _flora.configure(field, hash(String(definition.site_id)))
|
|
if not flora_error.is_empty():
|
|
return flora_error
|
|
_definition = definition
|
|
for child in _placements_root.get_children():
|
|
_placements_root.remove_child(child)
|
|
child.free()
|
|
_placeholder_count = 0
|
|
for placement: SitePlacement in definition.placements:
|
|
_place(placement)
|
|
return ""
|
|
|
|
|
|
func definition() -> DiveSiteDefinition:
|
|
return _definition
|
|
|
|
|
|
func terrain() -> BathymetricTerrainActor:
|
|
return _terrain
|
|
|
|
|
|
func flora() -> SeabedFloraActor:
|
|
return _flora
|
|
|
|
|
|
func spawn_transform() -> Transform3D:
|
|
return _definition.spawn_transform() if _definition != null else Transform3D.IDENTITY
|
|
|
|
|
|
func placeholder_count() -> int:
|
|
return _placeholder_count
|
|
|
|
|
|
func _place(placement: SitePlacement) -> void:
|
|
if placement.is_sonar_metal():
|
|
_placements_root.add_child(_metal_placeholder(placement))
|
|
_placeholder_count += 1
|
|
elif placement.kind == SitePlacement.Kind.VESSEL_ANCHOR:
|
|
_placements_root.add_child(_beacon(placement))
|
|
_placeholder_count += 1
|
|
|
|
|
|
func _metal_placeholder(placement: SitePlacement) -> StaticBody3D:
|
|
var body := StaticBody3D.new()
|
|
body.name = String(placement.placement_id)
|
|
body.position = placement.position_m
|
|
body.rotation.y = placement.yaw_radians
|
|
body.set_meta("sonar_material", SonarMaterial.Value.METAL)
|
|
body.set_meta("sonar_reflectivity", METAL_REFLECTIVITY)
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = placement.footprint_m
|
|
var mesh_instance := MeshInstance3D.new()
|
|
mesh_instance.name = "Mesh"
|
|
mesh_instance.mesh = mesh
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = Color(0.24, 0.18, 0.08)
|
|
material.metallic = 0.88
|
|
material.roughness = 0.27
|
|
mesh_instance.material_override = material
|
|
body.add_child(mesh_instance)
|
|
var shape := BoxShape3D.new()
|
|
shape.size = placement.footprint_m
|
|
var collision := CollisionShape3D.new()
|
|
collision.name = "Collision"
|
|
collision.shape = shape
|
|
body.add_child(collision)
|
|
return body
|
|
|
|
|
|
func _beacon(placement: SitePlacement) -> MeshInstance3D:
|
|
var mesh := SphereMesh.new()
|
|
mesh.radius = 0.55
|
|
mesh.height = 1.1
|
|
var beacon := MeshInstance3D.new()
|
|
beacon.name = String(placement.placement_id)
|
|
beacon.mesh = mesh
|
|
beacon.position = placement.position_m
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.albedo_color = BEACON_COLOR
|
|
material.emission_enabled = true
|
|
material.emission = BEACON_COLOR
|
|
material.emission_energy_multiplier = 5.0
|
|
beacon.material_override = material
|
|
var light := OmniLight3D.new()
|
|
light.name = "BeaconLight"
|
|
light.omni_range = 12.0
|
|
light.light_color = BEACON_COLOR
|
|
light.light_energy = 4.0
|
|
beacon.add_child(light)
|
|
return beacon
|