2026-07-27 03:30:03 -04:00
|
|
|
@tool
|
2026-07-27 00:13:07 -04:00
|
|
|
class_name StarterIslandRegion
|
2026-07-27 03:30:03 -04:00
|
|
|
extends WorldRegion
|
2026-07-27 00:13:07 -04:00
|
|
|
|
|
|
|
|
const FishingShopInteractionType = preload(
|
|
|
|
|
"res://world/fishing_shop_interaction.gd"
|
|
|
|
|
)
|
2026-08-09 12:44:45 -04:00
|
|
|
const FOLIAGE_WIND_SHADER: Shader = preload(
|
|
|
|
|
"res://world/materials/foliage_wind.gdshader"
|
|
|
|
|
)
|
|
|
|
|
const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [
|
|
|
|
|
&"leaf",
|
|
|
|
|
&"leaf_light",
|
|
|
|
|
&"leaf_dark",
|
|
|
|
|
]
|
2026-07-27 00:13:07 -04:00
|
|
|
|
2026-07-27 03:30:03 -04:00
|
|
|
@export_group("Owned Nodes")
|
|
|
|
|
@export_node_path("MeshInstance3D")
|
|
|
|
|
var visual_mesh_path: NodePath = (
|
|
|
|
|
^"Terrain/Visual/starter_island"
|
2026-07-27 00:13:07 -04:00
|
|
|
)
|
2026-08-06 16:13:14 -04:00
|
|
|
@export_node_path("Node3D")
|
|
|
|
|
var terrain_visual_root_path: NodePath = ^"Terrain/Visual"
|
2026-07-27 03:30:03 -04:00
|
|
|
@export_node_path("CollisionShape3D")
|
|
|
|
|
var terrain_collision_shape_path: NodePath = (
|
|
|
|
|
^"Terrain/Collision/Shape"
|
2026-07-27 00:13:07 -04:00
|
|
|
)
|
2026-07-27 03:30:03 -04:00
|
|
|
@export_node_path("Marker3D")
|
|
|
|
|
var player_spawn_path: NodePath = ^"PlayerSpawn"
|
|
|
|
|
@export_node_path("Area3D")
|
|
|
|
|
var fishing_shop_path: NodePath = (
|
|
|
|
|
^"Interactables/FishingShopWorld/InteractionArea"
|
2026-07-27 00:13:07 -04:00
|
|
|
)
|
2026-08-08 00:34:06 -04:00
|
|
|
@export_group("Terrain Collision")
|
|
|
|
|
@export var rebuild_terrain_collision_on_ready: bool = false
|
2026-08-09 12:44:45 -04:00
|
|
|
@export_group("Foliage Wind")
|
2026-08-09 22:27:33 -04:00
|
|
|
@export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36
|
2026-08-09 12:44:45 -04:00
|
|
|
@export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9
|
2026-07-27 00:13:07 -04:00
|
|
|
|
|
|
|
|
func _ready() -> void:
|
2026-08-08 00:34:06 -04:00
|
|
|
if rebuild_terrain_collision_on_ready or not has_terrain_collision():
|
|
|
|
|
_build_terrain_collision()
|
2026-08-09 12:44:45 -04:00
|
|
|
_apply_foliage_wind()
|
2026-07-27 03:30:03 -04:00
|
|
|
if Engine.is_editor_hint():
|
|
|
|
|
update_configuration_warnings()
|
2026-07-27 00:13:07 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_player_spawn_transform() -> Transform3D:
|
|
|
|
|
var spawn: Marker3D = get_node_or_null(player_spawn_path) as Marker3D
|
|
|
|
|
return spawn.global_transform if spawn != null else global_transform
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_fishing_shop() -> FishingShopInteractionType:
|
|
|
|
|
return get_node_or_null(fishing_shop_path) as FishingShopInteractionType
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 15:44:56 -04:00
|
|
|
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
|
|
|
|
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
|
|
|
|
|
|
|
|
|
|
2026-07-27 00:13:07 -04:00
|
|
|
func has_terrain_collision() -> bool:
|
|
|
|
|
var collision_shape: CollisionShape3D = (
|
|
|
|
|
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D
|
|
|
|
|
)
|
|
|
|
|
return collision_shape != null and collision_shape.shape != null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _build_terrain_collision() -> void:
|
2026-08-06 16:13:14 -04:00
|
|
|
var terrain_root: Node = get_node_or_null(terrain_visual_root_path)
|
|
|
|
|
if terrain_root == null:
|
|
|
|
|
terrain_root = get_node_or_null(visual_mesh_path)
|
2026-07-27 00:13:07 -04:00
|
|
|
var collision_shape: CollisionShape3D = (
|
|
|
|
|
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D
|
|
|
|
|
)
|
2026-08-06 16:13:14 -04:00
|
|
|
if terrain_root == null:
|
|
|
|
|
push_error("Starter island visual terrain root is unavailable.")
|
2026-07-27 00:13:07 -04:00
|
|
|
return
|
|
|
|
|
if collision_shape == null:
|
|
|
|
|
push_error("Starter island collision owner is unavailable.")
|
|
|
|
|
return
|
2026-08-06 16:13:14 -04:00
|
|
|
var mesh_instances := _collect_terrain_mesh_instances(terrain_root)
|
|
|
|
|
if mesh_instances.is_empty():
|
|
|
|
|
var visual_mesh: MeshInstance3D = terrain_root as MeshInstance3D
|
|
|
|
|
if visual_mesh == null or visual_mesh.mesh == null:
|
|
|
|
|
push_error("Starter island visual mesh is unavailable.")
|
|
|
|
|
return
|
|
|
|
|
var fallback_shape: ConcavePolygonShape3D = (
|
|
|
|
|
visual_mesh.mesh.create_trimesh_shape()
|
|
|
|
|
)
|
|
|
|
|
if fallback_shape == null or fallback_shape.get_faces().is_empty():
|
|
|
|
|
push_error("Starter island terrain collision could not be created.")
|
|
|
|
|
return
|
|
|
|
|
collision_shape.shape = fallback_shape
|
|
|
|
|
return
|
|
|
|
|
var terrain_shape := ConcavePolygonShape3D.new()
|
|
|
|
|
var faces := PackedVector3Array()
|
|
|
|
|
for mesh_instance: MeshInstance3D in mesh_instances:
|
|
|
|
|
var mesh: Mesh = mesh_instance.mesh
|
|
|
|
|
if mesh == null:
|
|
|
|
|
continue
|
|
|
|
|
var mesh_shape := mesh.create_trimesh_shape()
|
|
|
|
|
if mesh_shape == null:
|
|
|
|
|
continue
|
|
|
|
|
for face_vertex: Vector3 in mesh_shape.get_faces():
|
|
|
|
|
faces.append(to_local(mesh_instance.to_global(face_vertex)))
|
|
|
|
|
if faces.is_empty():
|
2026-07-27 00:13:07 -04:00
|
|
|
push_error("Starter island terrain collision could not be created.")
|
|
|
|
|
return
|
2026-08-06 16:13:14 -04:00
|
|
|
terrain_shape.set_faces(faces)
|
2026-07-27 00:13:07 -04:00
|
|
|
collision_shape.shape = terrain_shape
|
2026-07-27 03:30:03 -04:00
|
|
|
|
|
|
|
|
|
2026-08-06 16:13:14 -04:00
|
|
|
func _collect_terrain_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
|
|
|
|
var mesh_instances: Array[MeshInstance3D] = []
|
|
|
|
|
if root is MeshInstance3D:
|
|
|
|
|
mesh_instances.append(root as MeshInstance3D)
|
|
|
|
|
for child: Node in root.get_children():
|
|
|
|
|
mesh_instances.append_array(_collect_terrain_mesh_instances(child))
|
|
|
|
|
return mesh_instances
|
|
|
|
|
|
|
|
|
|
|
2026-08-09 12:44:45 -04:00
|
|
|
func _apply_foliage_wind() -> void:
|
|
|
|
|
var terrain_root: Node = get_node_or_null(terrain_visual_root_path)
|
|
|
|
|
if terrain_root == null:
|
|
|
|
|
return
|
|
|
|
|
for mesh_instance: MeshInstance3D in _collect_terrain_mesh_instances(
|
|
|
|
|
terrain_root
|
|
|
|
|
):
|
|
|
|
|
_apply_foliage_wind_to_mesh(mesh_instance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void:
|
|
|
|
|
if mesh_instance.mesh == null:
|
|
|
|
|
return
|
|
|
|
|
var bounds: AABB = mesh_instance.get_aabb()
|
2026-08-09 13:31:30 -04:00
|
|
|
var mesh_global_scale: Vector3 = mesh_instance.global_basis.get_scale().abs()
|
2026-08-09 12:44:45 -04:00
|
|
|
var horizontal_scale: float = maxf(
|
2026-08-09 13:31:30 -04:00
|
|
|
(mesh_global_scale.x + mesh_global_scale.z) * 0.5,
|
2026-08-09 12:44:45 -04:00
|
|
|
0.001,
|
|
|
|
|
)
|
|
|
|
|
var local_strength: float = foliage_wind_strength / horizontal_scale
|
|
|
|
|
var phase: float = fposmod(
|
|
|
|
|
mesh_instance.global_position.x * 0.73
|
|
|
|
|
+ mesh_instance.global_position.z * 0.41,
|
|
|
|
|
TAU,
|
|
|
|
|
)
|
|
|
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
|
|
|
var source_material: Material = mesh_instance.get_active_material(
|
|
|
|
|
surface_index
|
|
|
|
|
)
|
|
|
|
|
if not source_material is StandardMaterial3D:
|
|
|
|
|
continue
|
|
|
|
|
if not FOLIAGE_MATERIAL_NAMES.has(
|
|
|
|
|
StringName(source_material.resource_name)
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
var source_standard := source_material as StandardMaterial3D
|
|
|
|
|
var wind_material := ShaderMaterial.new()
|
|
|
|
|
wind_material.shader = FOLIAGE_WIND_SHADER
|
|
|
|
|
wind_material.set_shader_parameter(
|
|
|
|
|
"albedo_color",
|
|
|
|
|
source_standard.albedo_color,
|
|
|
|
|
)
|
|
|
|
|
wind_material.set_shader_parameter(
|
|
|
|
|
"material_roughness",
|
|
|
|
|
source_standard.roughness,
|
|
|
|
|
)
|
|
|
|
|
wind_material.set_shader_parameter(
|
|
|
|
|
"material_metallic",
|
|
|
|
|
source_standard.metallic,
|
|
|
|
|
)
|
|
|
|
|
wind_material.set_shader_parameter("local_min_y", bounds.position.y)
|
|
|
|
|
wind_material.set_shader_parameter(
|
|
|
|
|
"local_height",
|
|
|
|
|
maxf(bounds.size.y, 0.001),
|
|
|
|
|
)
|
|
|
|
|
wind_material.set_shader_parameter(
|
|
|
|
|
"local_wind_strength",
|
|
|
|
|
local_strength,
|
|
|
|
|
)
|
|
|
|
|
wind_material.set_shader_parameter("wind_speed", foliage_wind_speed)
|
|
|
|
|
wind_material.set_shader_parameter("wind_phase", phase)
|
|
|
|
|
mesh_instance.set_surface_override_material(
|
|
|
|
|
surface_index,
|
|
|
|
|
wind_material,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-27 03:30:03 -04:00
|
|
|
func _get_configuration_warnings() -> PackedStringArray:
|
|
|
|
|
var warnings := PackedStringArray()
|
|
|
|
|
var visual_mesh := get_node_or_null(visual_mesh_path) as MeshInstance3D
|
2026-08-06 16:13:14 -04:00
|
|
|
if visual_mesh == null:
|
|
|
|
|
warnings.append("Terrain/Visual terrain mesh node is unavailable.")
|
|
|
|
|
return warnings
|
2026-07-27 03:30:03 -04:00
|
|
|
if visual_mesh == null or visual_mesh.mesh == null:
|
|
|
|
|
warnings.append("Terrain/Visual must provide the island mesh.")
|
2026-08-07 11:59:03 -04:00
|
|
|
return warnings
|
|
|
|
|
|
2026-07-27 03:30:03 -04:00
|
|
|
if get_node_or_null(terrain_collision_shape_path) == null:
|
|
|
|
|
warnings.append("Terrain/Collision must provide a collision shape.")
|
2026-08-06 16:13:14 -04:00
|
|
|
if get_node_or_null(terrain_visual_root_path) == null:
|
|
|
|
|
warnings.append("Terrain/Visual root is missing for terrain collision generation.")
|
2026-07-27 03:30:03 -04:00
|
|
|
if get_node_or_null(player_spawn_path) == null:
|
|
|
|
|
warnings.append("PlayerSpawn marker is missing.")
|
|
|
|
|
if get_node_or_null(fishing_shop_path) == null:
|
|
|
|
|
warnings.append("Fishing Shop placement is missing.")
|
|
|
|
|
return warnings
|