Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
1681 lines
48 KiB
GDScript
1681 lines
48 KiB
GDScript
class_name GeneratedWorldRegion
|
|
extends WorldRegion
|
|
|
|
signal world_generated(seed: int, summary: Dictionary)
|
|
|
|
const FishingShopInteractionType = preload(
|
|
"res://world/fishing_shop_interaction.gd"
|
|
)
|
|
const PlayerStorageInteractionType = preload(
|
|
"res://world/player_storage_interaction.gd"
|
|
)
|
|
const PrecipitationOcclusionType = preload(
|
|
"res://world/environment/precipitation_occlusion.gd"
|
|
)
|
|
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
|
const SALT_WATER_MATERIAL: Material = preload(
|
|
"res://world/materials/stylized_water.tres"
|
|
)
|
|
const FRESH_WATER_MATERIAL: Material = preload(
|
|
"res://world/materials/stylized_water_fresh.tres"
|
|
)
|
|
const GENERATED_TERRAIN_DIRT_MATERIAL: Material = preload(
|
|
"res://world/materials/generated_terrain_dirt.tres"
|
|
)
|
|
const GENERATED_POND_POOL: FishPool = preload(
|
|
"res://fish/pools/generated_pond_pool.tres"
|
|
)
|
|
const GENERATED_LAKE_POOL: FishPool = preload(
|
|
"res://fish/pools/generated_lake_pool.tres"
|
|
)
|
|
const GENERATED_RIVER_POOL: FishPool = preload(
|
|
"res://fish/pools/generated_river_pool.tres"
|
|
)
|
|
const OCEAN_POOL: FishPool = preload(
|
|
"res://fish/pools/starter_ocean_pool.tres"
|
|
)
|
|
const WATER_HEIGHT := -0.25
|
|
const GENERATED_FRESH_WATER_BED_HEIGHT := -2.85
|
|
const PROP_EDGE_MARGIN := 2.2
|
|
const PROP_PLACEMENT_ATTEMPTS := 12
|
|
const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
|
|
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
|
const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35
|
|
const PROP_CHANCE_SCALE := 10000
|
|
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
|
const WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE := 0.05
|
|
const WATER_RECOVERY_MINIMUM_UP_DOT := 0.6
|
|
const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6
|
|
const WATER_RECOVERY_INSET_STEP := 0.25
|
|
const WATER_RECOVERY_MAXIMUM_INSET := 1.5
|
|
const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0]
|
|
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
|
|
Vector2(1.0, 0.0),
|
|
Vector2(0.70710678, 0.70710678),
|
|
Vector2(0.0, 1.0),
|
|
Vector2(-0.70710678, 0.70710678),
|
|
Vector2(-1.0, 0.0),
|
|
Vector2(-0.70710678, -0.70710678),
|
|
Vector2(0.0, -1.0),
|
|
Vector2(0.70710678, -0.70710678),
|
|
]
|
|
const GRASS_PROP_SURFACE_MATERIALS: Array[StringName] = [&"grass_lite"]
|
|
const SAND_PROP_SURFACE_MATERIALS: Array[StringName] = [&"sand"]
|
|
const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
|
&"grass_tree",
|
|
&"grass_detail",
|
|
&"sand_tree",
|
|
]
|
|
|
|
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
|
@export var prop_catalog: TerrainPropCatalog
|
|
@export var biome_catalog: TerrainBiomeCatalog
|
|
|
|
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
|
|
@onready var _player_spawn: Marker3D = %PlayerSpawn
|
|
@onready var _safe_spawn: SafeRespawnPoint = %SafeSpawn
|
|
@onready var _fishing_shop: FishingShopInteractionType = (
|
|
$Interactables/FishingShopWorld/InteractionArea
|
|
)
|
|
@onready var _player_storage: PlayerStorageInteractionType = (
|
|
$Interactables/PlayerStorageBox/InteractionArea
|
|
)
|
|
@onready var _shop_root: Node3D = %FishingShopWorld
|
|
@onready var _storage_root: Node3D = %PlayerStorageBox
|
|
@onready var _decorations: Node3D = %Decorations
|
|
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
|
|
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
|
|
@onready var _ocean: WaterBodyAuthoring = %OceanWater
|
|
@onready var _fresh_water_root: Node3D = %FreshWaterBodies
|
|
@onready var _shoreline_reference: MeshInstance3D = %ShorelineReference
|
|
|
|
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
|
var _light_performance_profile := false
|
|
var _placed_prop_positions: Array[Vector3] = []
|
|
var _placed_prop_clearance_radii: Array[float] = []
|
|
var _placed_prop_groups: Array[StringName] = []
|
|
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
|
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
|
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
_generator.generation_completed.connect(_on_generation_completed)
|
|
_validate_prop_catalog()
|
|
_validate_biome_catalog()
|
|
_configure_static_water()
|
|
_build_shoreline_reference()
|
|
|
|
|
|
func generate_world(seed: int) -> bool:
|
|
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
|
|
return false
|
|
if (
|
|
seed == _current_seed
|
|
and _generator.get_generated_chunks_root() != null
|
|
):
|
|
return true
|
|
_current_seed = seed
|
|
_generator.generation_seed = seed
|
|
return _generator.generate()
|
|
|
|
|
|
func get_generation_seed() -> int:
|
|
return _current_seed
|
|
|
|
|
|
func is_world_generated() -> bool:
|
|
return is_instance_valid(_generator.get_generated_chunks_root())
|
|
|
|
|
|
func get_playable_half_extents() -> Vector2:
|
|
var size := Vector2(
|
|
float(_generator.grid_size.x),
|
|
float(_generator.grid_size.y),
|
|
) * _generator.catalog.chunk_size
|
|
return size * 0.5
|
|
|
|
|
|
func get_player_spawn_transform() -> Transform3D:
|
|
return _player_spawn.global_transform
|
|
|
|
|
|
func get_fishing_shop() -> FishingShopInteractionType:
|
|
return _fishing_shop
|
|
|
|
|
|
func get_player_storage() -> PlayerStorageInteractionType:
|
|
return _player_storage
|
|
|
|
|
|
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
|
return _shoreline_reference
|
|
|
|
|
|
func get_prop_catalog() -> TerrainPropCatalog:
|
|
return prop_catalog
|
|
|
|
|
|
func get_biome_catalog() -> TerrainBiomeCatalog:
|
|
return biome_catalog
|
|
|
|
|
|
func get_biome_at(coordinate: Vector2i) -> StringName:
|
|
return _biome_assignments.get(coordinate, &"")
|
|
|
|
|
|
func set_light_performance_profile(enabled: bool) -> void:
|
|
_light_performance_profile = enabled
|
|
_apply_water_materials()
|
|
|
|
|
|
func get_spawn_surface_triangles(
|
|
material_names: Array[StringName],
|
|
minimum_global_y: float,
|
|
minimum_up_dot: float = 0.6,
|
|
) -> Array[PackedVector3Array]:
|
|
var triangles: Array[PackedVector3Array] = []
|
|
if material_names.is_empty():
|
|
return triangles
|
|
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
|
var mesh: Mesh = mesh_instance.mesh
|
|
if mesh == null:
|
|
continue
|
|
for surface_index: int in mesh.get_surface_count():
|
|
var material: Material = mesh_instance.get_active_material(surface_index)
|
|
if material == null or not material_names.has(
|
|
StringName(material.resource_name)
|
|
):
|
|
continue
|
|
_append_surface_triangles(
|
|
triangles,
|
|
mesh_instance,
|
|
mesh.surface_get_arrays(surface_index),
|
|
minimum_global_y,
|
|
minimum_up_dot,
|
|
)
|
|
return triangles
|
|
|
|
|
|
func get_water_recovery_position(
|
|
entry_position: Vector3,
|
|
fallback_position: Vector3,
|
|
) -> Vector3:
|
|
if (
|
|
not entry_position.is_finite()
|
|
or _water_recovery_triangles_by_coordinate.is_empty()
|
|
):
|
|
return super(entry_position, fallback_position)
|
|
var nearest_surface_distance := _nearest_recovery_surface_distance(
|
|
entry_position
|
|
)
|
|
if not is_finite(nearest_surface_distance):
|
|
return super(entry_position, fallback_position)
|
|
for expansion: float in WATER_RECOVERY_SEARCH_EXPANSIONS:
|
|
var maximum_distance := nearest_surface_distance + expansion
|
|
var result: Variant = _nearest_safe_recovery_surface_position(
|
|
entry_position,
|
|
maximum_distance,
|
|
)
|
|
if result is Vector3:
|
|
return result as Vector3
|
|
var map_wide_result: Variant = _nearest_safe_recovery_surface_position(
|
|
entry_position,
|
|
get_playable_half_extents().length() * 2.0,
|
|
)
|
|
if map_wide_result is Vector3:
|
|
return map_wide_result as Vector3
|
|
return super(entry_position, fallback_position)
|
|
|
|
|
|
func _on_generation_completed(summary: Dictionary) -> void:
|
|
_diggable_beach.invalidate_surface_cache()
|
|
var records: Array[Dictionary] = _generator.placement_records()
|
|
_assign_biomes(records, summary)
|
|
var spawn_position := Vector3.ZERO
|
|
_clear_children(_decorations)
|
|
_clear_children(_tree_anchors)
|
|
_placed_prop_positions.clear()
|
|
_placed_prop_clearance_radii.clear()
|
|
_placed_prop_groups.clear()
|
|
_placed_group_coordinates.clear()
|
|
var random := RandomNumberGenerator.new()
|
|
random.seed = _current_seed ^ 0x5EED71
|
|
var grass_surface_triangles_by_coordinate := (
|
|
_terrain_surface_triangles_by_coordinate(
|
|
GRASS_PROP_SURFACE_MATERIALS,
|
|
)
|
|
)
|
|
var sand_surface_triangles_by_coordinate := (
|
|
_terrain_surface_triangles_by_coordinate(
|
|
SAND_PROP_SURFACE_MATERIALS,
|
|
)
|
|
)
|
|
var biomes: Array[TerrainBiomeDefinition] = []
|
|
if biome_catalog != null:
|
|
biomes.assign(biome_catalog.definitions)
|
|
var eligible_records: Dictionary[StringName, Array] = {}
|
|
var group_counts: Dictionary[StringName, int] = {}
|
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
|
for biome: TerrainBiomeDefinition in biomes:
|
|
var rule := biome.prop_rule_for_group(group)
|
|
if rule == null:
|
|
continue
|
|
var group_key := _biome_group_key(biome.stable_id, group)
|
|
eligible_records[group_key] = []
|
|
group_counts[group_key] = 0
|
|
_placed_group_coordinates[group_key] = []
|
|
for record: Dictionary in records:
|
|
var position: Vector3 = record.get("position", Vector3.ZERO)
|
|
var tags: PackedStringArray = record.get(
|
|
"tags", PackedStringArray()
|
|
)
|
|
if "spawn" in tags:
|
|
spawn_position = position
|
|
continue
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
var biome_id: StringName = record.get("biome_id", &"")
|
|
if biome_catalog == null:
|
|
continue
|
|
var biome := biome_catalog.definition_for_id(biome_id)
|
|
if biome == null:
|
|
continue
|
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
|
var rule := biome.prop_rule_for_group(group)
|
|
if rule == null:
|
|
continue
|
|
var definitions := _spawn_distance_eligible_definitions(
|
|
_prop_definitions_for(rule, tags),
|
|
coordinate,
|
|
)
|
|
if definitions.is_empty():
|
|
continue
|
|
var group_key := _biome_group_key(biome_id, group)
|
|
var group_records: Array = eligible_records[group_key]
|
|
group_records.append(record)
|
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
|
for biome: TerrainBiomeDefinition in biomes:
|
|
var rule := biome.prop_rule_for_group(group)
|
|
if rule == null:
|
|
continue
|
|
var group_key := _biome_group_key(biome.stable_id, group)
|
|
var group_records: Array = eligible_records[group_key]
|
|
var maximum := _prop_group_maximum(rule, group_records.size())
|
|
for record: Dictionary in group_records:
|
|
if group_counts.get(group_key, 0) >= maximum:
|
|
break
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
var tags: PackedStringArray = record.get(
|
|
"tags",
|
|
PackedStringArray(),
|
|
)
|
|
var terrain_triangles := _surface_triangles_for_coordinate(
|
|
(
|
|
sand_surface_triangles_by_coordinate
|
|
if group == &"sand_tree"
|
|
else grass_surface_triangles_by_coordinate
|
|
),
|
|
coordinate,
|
|
)
|
|
var definitions := _spawn_distance_eligible_definitions(
|
|
_prop_definitions_for(rule, tags),
|
|
coordinate,
|
|
)
|
|
for _placement_attempt: int in (
|
|
rule.placement_attempts_per_chunk
|
|
):
|
|
if group_counts.get(group_key, 0) >= maximum:
|
|
break
|
|
if (
|
|
definitions.is_empty()
|
|
or not _prop_group_roll_succeeds(
|
|
rule,
|
|
group_key,
|
|
coordinate,
|
|
random,
|
|
)
|
|
):
|
|
continue
|
|
if _maybe_add_prop(
|
|
definitions,
|
|
record.get("position", Vector3.ZERO),
|
|
coordinate,
|
|
biome.stable_id,
|
|
int(record.get("ocean_facing_edges", 0)),
|
|
random,
|
|
terrain_triangles,
|
|
):
|
|
group_counts[group_key] += 1
|
|
_record_prop_group_coordinate(
|
|
group_key,
|
|
coordinate,
|
|
)
|
|
_ensure_minimum_props(
|
|
eligible_records,
|
|
group_counts,
|
|
random,
|
|
grass_surface_triangles_by_coordinate,
|
|
sand_surface_triangles_by_coordinate,
|
|
)
|
|
_cache_water_recovery_surfaces(
|
|
grass_surface_triangles_by_coordinate,
|
|
sand_surface_triangles_by_coordinate,
|
|
)
|
|
_place_spawn_amenities(spawn_position)
|
|
_configure_fresh_water(records)
|
|
_configure_diggable_area()
|
|
# Imported prop instances may finish applying their scene state while the
|
|
# generated world is assembled. Mark the final decoration tree once every
|
|
# prop and material variant is in place so rain canopy occlusion persists.
|
|
_mark_precipitation_occluders()
|
|
world_generated.emit(_current_seed, summary)
|
|
|
|
|
|
func _mark_precipitation_occluders() -> void:
|
|
for prop: Node in _decorations.get_children():
|
|
var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName
|
|
if prop_group not in [&"grass_tree", &"sand_tree"]:
|
|
continue
|
|
PrecipitationOcclusionType.mark_tree_meshes(prop)
|
|
|
|
|
|
func _assign_biomes(
|
|
records: Array[Dictionary],
|
|
summary: Dictionary,
|
|
) -> void:
|
|
_biome_assignments = TerrainBiomeAssigner.assign(
|
|
biome_catalog,
|
|
records,
|
|
_current_seed,
|
|
)
|
|
for index: int in records.size():
|
|
var record := records[index]
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
record["biome_id"] = _biome_assignments.get(coordinate, &"")
|
|
records[index] = record
|
|
var generated_chunks := _generator.get_generated_chunks_root()
|
|
if generated_chunks != null:
|
|
for child: Node in generated_chunks.get_children():
|
|
var coordinate: Vector2i = child.get_meta(
|
|
&"terrain_chunk_coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
child.set_meta(
|
|
&"terrain_biome_id",
|
|
_biome_assignments.get(coordinate, &""),
|
|
)
|
|
summary["biome_counts"] = TerrainBiomeAssigner.counts(
|
|
_biome_assignments
|
|
)
|
|
summary["biome_fingerprint"] = TerrainBiomeAssigner.fingerprint(
|
|
_biome_assignments
|
|
)
|
|
|
|
|
|
func _place_spawn_amenities(center: Vector3) -> void:
|
|
_player_spawn.position = center + Vector3(0.0, 0.18, 2.2)
|
|
_safe_spawn.position = _player_spawn.position
|
|
_shop_root.position = center + Vector3(2.35, 0.0, -1.35)
|
|
_shop_root.rotation.y = PI
|
|
_storage_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
|
_storage_root.rotation.y = PI * 0.5
|
|
|
|
|
|
func _configure_static_water() -> void:
|
|
_ocean.water_type = WaterType.Type.SALT_WATER
|
|
_ocean.fish_pool = OCEAN_POOL
|
|
_ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"]
|
|
_ocean.surface_size = Vector2(10000.0, 10000.0)
|
|
_ocean.position.y = WATER_HEIGHT
|
|
_apply_water_materials()
|
|
|
|
|
|
func _configure_fresh_water(records: Array[Dictionary]) -> void:
|
|
_clear_children(_fresh_water_root)
|
|
for record: Dictionary in records:
|
|
var tags: PackedStringArray = record.get(
|
|
"tags", PackedStringArray()
|
|
)
|
|
if "fresh_water" not in tags:
|
|
continue
|
|
var surface_size: Vector2 = record.get(
|
|
"water_surface_size", Vector2.ZERO
|
|
)
|
|
var surface_polygon: PackedVector2Array = record.get(
|
|
"water_surface_polygon",
|
|
PackedVector2Array(),
|
|
)
|
|
if (
|
|
(surface_size.x <= 0.0 or surface_size.y <= 0.0)
|
|
and surface_polygon.size() < 3
|
|
):
|
|
continue
|
|
var body := WATER_BODY_SCENE.instantiate() as WaterBodyAuthoring
|
|
if body == null:
|
|
continue
|
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
|
body.name = "FreshWater_%d_%d" % [coordinate.x, coordinate.y]
|
|
_fresh_water_root.add_child(body)
|
|
var turns := int(record.get("rotation_quarters", 0))
|
|
var angle := float(posmod(turns, 4)) * PI * 0.5
|
|
var offset: Vector2 = record.get(
|
|
"water_surface_offset", Vector2.ZERO
|
|
)
|
|
var rotated_offset := Vector3(offset.x, 0.0, offset.y).rotated(
|
|
Vector3.UP,
|
|
angle,
|
|
)
|
|
body.position = (
|
|
record.get("position", Vector3.ZERO)
|
|
+ rotated_offset
|
|
+ Vector3.UP * WATER_HEIGHT
|
|
)
|
|
body.rotation.y = angle
|
|
if surface_polygon.size() >= 3:
|
|
body.surface_polygon = surface_polygon
|
|
else:
|
|
body.surface_size = surface_size
|
|
_add_generated_fresh_water_bed(
|
|
body,
|
|
surface_polygon,
|
|
surface_size,
|
|
)
|
|
body.visual_surface_enabled = false
|
|
body.water_material = _fresh_water_material()
|
|
body.water_type = WaterType.Type.FRESH_WATER
|
|
body.recovery_entry_height_reference = (
|
|
PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN
|
|
)
|
|
body.recovery_entry_depth_threshold = 0.1
|
|
body.recovery_entry_confirmation_seconds = 0.2
|
|
body.fish_pool = _fresh_water_pool(tags)
|
|
body.location_tags = _fresh_water_location_tags(tags)
|
|
body.selection_priority = 10
|
|
|
|
|
|
func _add_generated_fresh_water_bed(
|
|
body: WaterBodyAuthoring,
|
|
surface_polygon: PackedVector2Array,
|
|
surface_size: Vector2,
|
|
) -> void:
|
|
var polygon := surface_polygon
|
|
if polygon.size() < 3:
|
|
var half_size := surface_size * 0.5
|
|
polygon = PackedVector2Array([
|
|
Vector2(-half_size.x, -half_size.y),
|
|
Vector2(half_size.x, -half_size.y),
|
|
Vector2(half_size.x, half_size.y),
|
|
Vector2(-half_size.x, half_size.y),
|
|
])
|
|
var triangle_indices := Geometry2D.triangulate_polygon(polygon)
|
|
if triangle_indices.size() < 3:
|
|
return
|
|
var vertices := PackedVector3Array()
|
|
var normals := PackedVector3Array()
|
|
for offset: int in range(0, triangle_indices.size(), 3):
|
|
var triangle := PackedVector3Array()
|
|
for point_offset: int in 3:
|
|
var point := polygon[triangle_indices[offset + point_offset]]
|
|
triangle.append(Vector3(point.x, 0.0, point.y))
|
|
if (
|
|
(triangle[1] - triangle[0]).cross(
|
|
triangle[2] - triangle[0]
|
|
).dot(Vector3.UP) < 0.0
|
|
):
|
|
var swap := triangle[1]
|
|
triangle[1] = triangle[2]
|
|
triangle[2] = swap
|
|
vertices.append_array(triangle)
|
|
normals.append_array(PackedVector3Array([
|
|
Vector3.UP,
|
|
Vector3.UP,
|
|
Vector3.UP,
|
|
]))
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
var bed := MeshInstance3D.new()
|
|
bed.name = "GeneratedBed"
|
|
bed.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
bed.position.y = GENERATED_FRESH_WATER_BED_HEIGHT - WATER_HEIGHT
|
|
bed.material_override = GENERATED_TERRAIN_DIRT_MATERIAL
|
|
bed.mesh = mesh
|
|
body.add_child(bed)
|
|
|
|
|
|
func _fresh_water_location_tags(tags: PackedStringArray) -> Array[StringName]:
|
|
var result: Array[StringName] = [&"generated_fresh_water"]
|
|
if "pond" in tags:
|
|
result.append(&"pond")
|
|
if "river" in tags:
|
|
result.append(&"river")
|
|
if "lake" in tags:
|
|
result.append(&"lake")
|
|
return result
|
|
|
|
|
|
func _fresh_water_pool(tags: PackedStringArray) -> FishPool:
|
|
if "river" in tags:
|
|
return GENERATED_RIVER_POOL
|
|
if "lake" in tags:
|
|
return GENERATED_LAKE_POOL
|
|
return GENERATED_POND_POOL
|
|
|
|
|
|
func _configure_diggable_area() -> void:
|
|
var half_extents := get_playable_half_extents()
|
|
_diggable_beach.generation_bounds = Rect2(
|
|
-half_extents,
|
|
half_extents * 2.0,
|
|
)
|
|
|
|
|
|
func _apply_water_materials() -> void:
|
|
if not is_node_ready():
|
|
return
|
|
if _light_performance_profile:
|
|
_ocean.water_material = _light_water_material(
|
|
Color(0.11, 0.345, 0.435),
|
|
)
|
|
else:
|
|
_ocean.water_material = SALT_WATER_MATERIAL
|
|
for child: Node in _fresh_water_root.get_children():
|
|
var body := child as WaterBodyAuthoring
|
|
if body != null:
|
|
body.water_material = _fresh_water_material()
|
|
|
|
|
|
func _fresh_water_material() -> Material:
|
|
if _light_performance_profile:
|
|
return _light_water_material(Color(0.18, 0.46, 0.50))
|
|
return FRESH_WATER_MATERIAL
|
|
|
|
|
|
func _light_water_material(color: Color) -> StandardMaterial3D:
|
|
var material := StandardMaterial3D.new()
|
|
material.albedo_color = color
|
|
material.roughness = 1.0
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
|
|
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
|
return material
|
|
|
|
|
|
func _maybe_add_prop(
|
|
definitions: Array[TerrainPropDefinition],
|
|
chunk_center: Vector3,
|
|
chunk_coordinate: Vector2i,
|
|
biome_id: StringName,
|
|
ocean_facing_edges: int,
|
|
random: RandomNumberGenerator,
|
|
terrain_triangles: Array[PackedVector3Array],
|
|
) -> bool:
|
|
var definition := _pick_prop_definition(
|
|
definitions,
|
|
chunk_center,
|
|
random,
|
|
)
|
|
if definition == null:
|
|
return false
|
|
return _add_prop(
|
|
definition,
|
|
chunk_center,
|
|
chunk_coordinate,
|
|
biome_id,
|
|
ocean_facing_edges,
|
|
random,
|
|
terrain_triangles,
|
|
)
|
|
|
|
|
|
func _add_prop(
|
|
definition: TerrainPropDefinition,
|
|
chunk_center: Vector3,
|
|
chunk_coordinate: Vector2i,
|
|
biome_id: StringName,
|
|
ocean_facing_edges: int,
|
|
random: RandomNumberGenerator,
|
|
terrain_triangles: Array[PackedVector3Array],
|
|
) -> bool:
|
|
if definition == null or definition.packed_scene == null:
|
|
return false
|
|
var cluster_size := random.randi_range(
|
|
definition.minimum_cluster_size,
|
|
definition.maximum_cluster_size,
|
|
)
|
|
var visual_scales := _prop_visual_scales(
|
|
definition,
|
|
cluster_size,
|
|
random,
|
|
)
|
|
var primary_scale := visual_scales[0]
|
|
var scaled_clearance := definition.clearance_radius * primary_scale
|
|
var support_radius := clampf(scaled_clearance * 0.35, 0.25, 0.65)
|
|
var placement := Vector3.ZERO
|
|
var found_surface := false
|
|
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
|
var offset := Vector3(
|
|
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
|
0.0,
|
|
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
|
)
|
|
placement = chunk_center + offset
|
|
var surface_height := _surface_height_at(
|
|
placement,
|
|
terrain_triangles,
|
|
)
|
|
if (
|
|
surface_height > -INF
|
|
and surface_height
|
|
> WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
|
|
and _surface_supports_prop_footprint(
|
|
placement,
|
|
surface_height,
|
|
support_radius,
|
|
terrain_triangles,
|
|
)
|
|
and _has_prop_clearance(
|
|
placement,
|
|
scaled_clearance,
|
|
)
|
|
):
|
|
placement.y = surface_height
|
|
found_surface = true
|
|
break
|
|
if not found_surface:
|
|
return false
|
|
var cluster_id := _decorations.get_child_count()
|
|
if not _instantiate_prop(
|
|
definition,
|
|
placement,
|
|
primary_scale,
|
|
_prop_yaw(definition, ocean_facing_edges, placement, random),
|
|
chunk_coordinate,
|
|
biome_id,
|
|
cluster_id,
|
|
0,
|
|
random,
|
|
):
|
|
return false
|
|
if cluster_size <= 1:
|
|
return true
|
|
var cluster_angle := random.randf_range(-PI, PI)
|
|
var companion_count := cluster_size - 1
|
|
var chunk_half_extent := _generator.catalog.chunk_size * 0.5 - 0.25
|
|
for member_index: int in range(1, cluster_size):
|
|
var member_scale := visual_scales[member_index]
|
|
var member_clearance := definition.clearance_radius * member_scale
|
|
var member_support_radius := clampf(
|
|
member_clearance * 0.35,
|
|
0.25,
|
|
0.65,
|
|
)
|
|
for _attempt: int in PROP_CLUSTER_PLACEMENT_ATTEMPTS:
|
|
var sector_angle := (
|
|
cluster_angle
|
|
+ TAU * float(member_index - 1) / float(companion_count)
|
|
+ random.randf_range(-0.45, 0.45)
|
|
)
|
|
var radius := random.randf_range(
|
|
definition.minimum_cluster_radius,
|
|
definition.maximum_cluster_radius,
|
|
)
|
|
var candidate := placement + Vector3(
|
|
cos(sector_angle) * radius,
|
|
0.0,
|
|
sin(sector_angle) * radius,
|
|
)
|
|
if (
|
|
absf(candidate.x - chunk_center.x) > chunk_half_extent
|
|
or absf(candidate.z - chunk_center.z) > chunk_half_extent
|
|
):
|
|
continue
|
|
var surface_height := _surface_height_at(
|
|
candidate,
|
|
terrain_triangles,
|
|
)
|
|
if (
|
|
surface_height <= WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
|
|
or not _surface_supports_prop_footprint(
|
|
candidate,
|
|
surface_height,
|
|
member_support_radius,
|
|
terrain_triangles,
|
|
)
|
|
or not _has_prop_clearance(candidate, member_clearance)
|
|
):
|
|
continue
|
|
candidate.y = surface_height
|
|
if _instantiate_prop(
|
|
definition,
|
|
candidate,
|
|
member_scale,
|
|
_prop_yaw(
|
|
definition,
|
|
ocean_facing_edges,
|
|
candidate,
|
|
random,
|
|
),
|
|
chunk_coordinate,
|
|
biome_id,
|
|
cluster_id,
|
|
member_index,
|
|
random,
|
|
):
|
|
break
|
|
return true
|
|
|
|
|
|
func _prop_visual_scales(
|
|
definition: TerrainPropDefinition,
|
|
count: int,
|
|
random: RandomNumberGenerator,
|
|
) -> Array[float]:
|
|
var result: Array[float] = []
|
|
if count <= 1:
|
|
result.append(
|
|
random.randf_range(
|
|
definition.minimum_visual_scale,
|
|
definition.maximum_visual_scale,
|
|
)
|
|
)
|
|
return result
|
|
for index: int in count:
|
|
var descending_band := count - index - 1
|
|
var band_minimum := lerpf(
|
|
definition.minimum_visual_scale,
|
|
definition.maximum_visual_scale,
|
|
float(descending_band) / float(count),
|
|
)
|
|
var band_maximum := lerpf(
|
|
definition.minimum_visual_scale,
|
|
definition.maximum_visual_scale,
|
|
float(descending_band + 1) / float(count),
|
|
)
|
|
result.append(random.randf_range(band_minimum, band_maximum))
|
|
return result
|
|
|
|
|
|
func _instantiate_prop(
|
|
definition: TerrainPropDefinition,
|
|
placement: Vector3,
|
|
visual_scale: float,
|
|
yaw: float,
|
|
chunk_coordinate: Vector2i,
|
|
biome_id: StringName,
|
|
cluster_id: int,
|
|
cluster_member_index: int,
|
|
random: RandomNumberGenerator,
|
|
) -> bool:
|
|
var packed_instance := definition.packed_scene.instantiate()
|
|
var visual_root := packed_instance as Node3D
|
|
if visual_root == null:
|
|
packed_instance.free()
|
|
return false
|
|
var prop := Node3D.new()
|
|
prop.name = "%s_%d" % [
|
|
String(definition.stable_id).trim_prefix("prop_"),
|
|
_decorations.get_child_count(),
|
|
]
|
|
prop.set_meta(&"terrain_prop_id", definition.stable_id)
|
|
prop.set_meta(&"terrain_prop_group", definition.procedural_group)
|
|
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
|
|
prop.set_meta(&"terrain_biome_id", biome_id)
|
|
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
|
|
prop.set_meta(&"terrain_prop_cluster_id", cluster_id)
|
|
prop.set_meta(&"terrain_prop_cluster_member_index", cluster_member_index)
|
|
prop.position = placement
|
|
prop.rotation.y = yaw
|
|
_decorations.add_child(prop)
|
|
visual_root.name = "Visual"
|
|
prop.add_child(visual_root)
|
|
visual_root.position = definition.visual_offset
|
|
visual_root.scale = Vector3.ONE * visual_scale
|
|
_configure_prop_visuals(visual_root)
|
|
_apply_prop_material_variant(visual_root, definition, random)
|
|
_add_prop_collision(prop, definition, visual_scale)
|
|
_placed_prop_positions.append(placement)
|
|
_placed_prop_clearance_radii.append(
|
|
definition.clearance_radius * visual_scale
|
|
)
|
|
_placed_prop_groups.append(definition.procedural_group)
|
|
if definition.has_gatherable_surface():
|
|
var authored_sockets := _authored_beetle_sockets(visual_root)
|
|
for socket: Node3D in authored_sockets:
|
|
_add_tree_gatherable_anchor(
|
|
prop,
|
|
socket.global_position,
|
|
definition,
|
|
)
|
|
return true
|
|
|
|
|
|
func _authored_beetle_sockets(root: Node) -> Array[Node3D]:
|
|
var sockets: Array[Node3D] = []
|
|
var node_3d := root as Node3D
|
|
if (
|
|
node_3d != null
|
|
and String(node_3d.name).to_lower().contains("beetle_socket")
|
|
):
|
|
sockets.append(node_3d)
|
|
for child: Node in root.get_children():
|
|
sockets.append_array(_authored_beetle_sockets(child))
|
|
return sockets
|
|
|
|
|
|
func _add_tree_gatherable_anchor(
|
|
prop: Node3D,
|
|
global_anchor_position: Vector3,
|
|
definition: TerrainPropDefinition,
|
|
) -> void:
|
|
var anchor := Marker3D.new()
|
|
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
|
|
anchor.position = _tree_anchors.to_local(global_anchor_position)
|
|
anchor.set_meta(&"terrain_prop_id", definition.stable_id)
|
|
anchor.set_meta(&"terrain_prop_name", prop.name)
|
|
anchor.set_meta(&"authored_beetle_socket", true)
|
|
_tree_anchors.add_child(anchor)
|
|
|
|
|
|
func _prop_yaw(
|
|
definition: TerrainPropDefinition,
|
|
ocean_facing_edges: int,
|
|
placement: Vector3,
|
|
random: RandomNumberGenerator,
|
|
) -> float:
|
|
if not definition.prefer_ocean_facing:
|
|
return random.randf_range(-PI, PI)
|
|
var ocean_direction := Vector2.ZERO
|
|
for edge_value: int in TerrainChunkTopology.Edge.values():
|
|
if (ocean_facing_edges & (1 << edge_value)) == 0:
|
|
continue
|
|
var edge_normal := TerrainChunkTopology.edge_normal(
|
|
edge_value as TerrainChunkTopology.Edge
|
|
)
|
|
ocean_direction += Vector2(edge_normal.x, edge_normal.z)
|
|
if ocean_direction.is_zero_approx():
|
|
ocean_direction = _nearest_ocean_direction(placement)
|
|
var local_direction := definition.local_overhang_direction.normalized()
|
|
var target_angle := atan2(ocean_direction.x, ocean_direction.y)
|
|
var local_angle := atan2(local_direction.x, local_direction.y)
|
|
var spread := deg_to_rad(definition.ocean_facing_spread_degrees)
|
|
return (
|
|
target_angle
|
|
- local_angle
|
|
+ random.randf_range(-spread, spread)
|
|
)
|
|
|
|
|
|
func _nearest_ocean_direction(position: Vector3) -> Vector2:
|
|
var half_extents := get_playable_half_extents()
|
|
var distance_x := half_extents.x - absf(position.x)
|
|
var distance_z := half_extents.y - absf(position.z)
|
|
var direction_x := Vector2.RIGHT if position.x >= 0.0 else Vector2.LEFT
|
|
var direction_z := Vector2.DOWN if position.z >= 0.0 else Vector2.UP
|
|
if absf(distance_x - distance_z) <= _generator.catalog.chunk_size * 0.35:
|
|
return (direction_x + direction_z).normalized()
|
|
return direction_x if distance_x < distance_z else direction_z
|
|
|
|
|
|
func _validate_prop_catalog() -> void:
|
|
if prop_catalog == null:
|
|
push_error("Generated world terrain prop catalog is unavailable.")
|
|
return
|
|
for error: String in prop_catalog.validation_errors():
|
|
push_error("Generated world terrain prop catalog: %s" % error)
|
|
|
|
|
|
func _validate_biome_catalog() -> void:
|
|
if biome_catalog == null:
|
|
push_error("Generated world terrain biome catalog is unavailable.")
|
|
return
|
|
for error: String in biome_catalog.validation_errors():
|
|
push_error("Generated world terrain biome catalog: %s" % error)
|
|
if prop_catalog == null:
|
|
return
|
|
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
|
|
if biome == null:
|
|
continue
|
|
for rule: TerrainBiomePropRule in biome.prop_rules:
|
|
if rule == null:
|
|
continue
|
|
if rule.procedural_group not in PROCEDURAL_PROP_GROUPS:
|
|
push_error(
|
|
"Biome %s references unsupported prop group %s."
|
|
% [biome.stable_id, rule.procedural_group]
|
|
)
|
|
for prop_id_value: String in rule.allowed_prop_ids:
|
|
var prop_id := StringName(prop_id_value)
|
|
var definition := prop_catalog.definition_for_id(prop_id)
|
|
if definition == null:
|
|
push_error(
|
|
"Biome %s references missing prop %s."
|
|
% [biome.stable_id, prop_id]
|
|
)
|
|
elif definition.procedural_group != rule.procedural_group:
|
|
push_error(
|
|
"Biome %s assigns prop %s to the wrong group."
|
|
% [biome.stable_id, prop_id]
|
|
)
|
|
|
|
|
|
func _prop_definitions_for(
|
|
rule: TerrainBiomePropRule,
|
|
tags: PackedStringArray,
|
|
) -> Array[TerrainPropDefinition]:
|
|
if prop_catalog == null or rule == null:
|
|
return []
|
|
var result: Array[TerrainPropDefinition] = []
|
|
for definition: TerrainPropDefinition in (
|
|
prop_catalog.procedural_definitions(rule.procedural_group, tags)
|
|
):
|
|
if rule.allows_prop(definition):
|
|
result.append(definition)
|
|
return result
|
|
|
|
|
|
func _prop_group_roll_succeeds(
|
|
rule: TerrainBiomePropRule,
|
|
group_key: StringName,
|
|
coordinate: Vector2i,
|
|
random: RandomNumberGenerator,
|
|
) -> bool:
|
|
var threshold := rule.placement_chance
|
|
if _group_has_adjacent_placement(group_key, coordinate):
|
|
threshold += rule.adjacency_bonus
|
|
threshold = mini(threshold, PROP_CHANCE_SCALE)
|
|
return random.randi_range(0, PROP_CHANCE_SCALE - 1) < threshold
|
|
|
|
|
|
func _pick_prop_definition(
|
|
definitions: Array[TerrainPropDefinition],
|
|
chunk_center: Vector3,
|
|
random: RandomNumberGenerator,
|
|
) -> TerrainPropDefinition:
|
|
var total_weight := 0
|
|
var weights: Array[int] = []
|
|
for definition: TerrainPropDefinition in definitions:
|
|
var weight := maxi(
|
|
1,
|
|
roundi(
|
|
definition.selection_weight
|
|
* float(PROP_SELECTION_WEIGHT_SCALE)
|
|
),
|
|
)
|
|
if not definition.preferred_nearby_prop_groups.is_empty():
|
|
var multiplier := definition.nearby_preference_weight_multiplier
|
|
if _has_preferred_prop_near(definition, chunk_center):
|
|
weight = maxi(roundi(float(weight) * multiplier), 1)
|
|
else:
|
|
weight = maxi(roundi(float(weight) / multiplier), 1)
|
|
weights.append(weight)
|
|
total_weight += weight
|
|
if total_weight <= 0:
|
|
return null
|
|
var roll := random.randi_range(1, total_weight)
|
|
for index: int in definitions.size():
|
|
roll -= weights[index]
|
|
if roll <= 0:
|
|
return definitions[index]
|
|
return definitions.back() if not definitions.is_empty() else null
|
|
|
|
|
|
func _prop_group_maximum(
|
|
rule: TerrainBiomePropRule,
|
|
eligible_count: int,
|
|
) -> int:
|
|
if eligible_count <= 0:
|
|
return 0
|
|
var maximum := ceili(
|
|
float(
|
|
eligible_count
|
|
* rule.placement_attempts_per_chunk
|
|
* rule.maximum_density
|
|
)
|
|
/ float(PROP_CHANCE_SCALE)
|
|
)
|
|
return maxi(maximum, rule.minimum_placements)
|
|
|
|
|
|
func _spawn_distance_eligible_definitions(
|
|
definitions: Array[TerrainPropDefinition],
|
|
coordinate: Vector2i,
|
|
) -> Array[TerrainPropDefinition]:
|
|
var result: Array[TerrainPropDefinition] = []
|
|
var center := Vector2i(
|
|
_generator.grid_size.x / 2,
|
|
_generator.grid_size.y / 2,
|
|
)
|
|
var distance := (
|
|
absi(coordinate.x - center.x)
|
|
+ absi(coordinate.y - center.y)
|
|
)
|
|
for definition: TerrainPropDefinition in definitions:
|
|
if distance >= definition.minimum_spawn_chunk_distance:
|
|
result.append(definition)
|
|
return result
|
|
|
|
|
|
func _group_has_adjacent_placement(
|
|
group: StringName,
|
|
coordinate: Vector2i,
|
|
) -> bool:
|
|
var coordinates: Array = _placed_group_coordinates.get(group, [])
|
|
for coordinate_value: Variant in coordinates:
|
|
var other: Vector2i = coordinate_value
|
|
if (
|
|
absi(coordinate.x - other.x)
|
|
+ absi(coordinate.y - other.y)
|
|
== 1
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _record_prop_group_coordinate(
|
|
group: StringName,
|
|
coordinate: Vector2i,
|
|
) -> void:
|
|
var coordinates: Array = _placed_group_coordinates.get(group, [])
|
|
coordinates.append(coordinate)
|
|
_placed_group_coordinates[group] = coordinates
|
|
|
|
|
|
func _has_preferred_prop_near(
|
|
definition: TerrainPropDefinition,
|
|
position: Vector3,
|
|
) -> bool:
|
|
for index: int in _placed_prop_positions.size():
|
|
if (
|
|
_placed_prop_groups[index]
|
|
not in definition.preferred_nearby_prop_groups
|
|
):
|
|
continue
|
|
var other := _placed_prop_positions[index]
|
|
if Vector2(position.x - other.x, position.z - other.z).length() <= (
|
|
definition.preferred_nearby_radius
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _ensure_minimum_props(
|
|
eligible_records: Dictionary[StringName, Array],
|
|
group_counts: Dictionary[StringName, int],
|
|
random: RandomNumberGenerator,
|
|
grass_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
|
|
sand_surface_triangles_by_coordinate: Dictionary[Vector2i, Array],
|
|
) -> void:
|
|
if biome_catalog == null:
|
|
return
|
|
for group: StringName in PROCEDURAL_PROP_GROUPS:
|
|
for biome: TerrainBiomeDefinition in biome_catalog.definitions:
|
|
var rule := biome.prop_rule_for_group(group)
|
|
if rule == null or rule.minimum_placements <= 0:
|
|
continue
|
|
var group_key := _biome_group_key(biome.stable_id, group)
|
|
var records: Array = eligible_records.get(group_key, [])
|
|
if records.is_empty():
|
|
continue
|
|
var maximum_attempts := maxi(
|
|
PROP_PLACEMENT_ATTEMPTS,
|
|
records.size() * rule.minimum_placements * 2,
|
|
)
|
|
for _attempt: int in maximum_attempts:
|
|
if (
|
|
group_counts.get(group_key, 0)
|
|
>= rule.minimum_placements
|
|
):
|
|
break
|
|
var record: Dictionary = records[
|
|
random.randi_range(0, records.size() - 1)
|
|
]
|
|
var tags: PackedStringArray = record.get(
|
|
"tags",
|
|
PackedStringArray(),
|
|
)
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate",
|
|
Vector2i.ZERO,
|
|
)
|
|
var terrain_triangles := _surface_triangles_for_coordinate(
|
|
(
|
|
sand_surface_triangles_by_coordinate
|
|
if group == &"sand_tree"
|
|
else grass_surface_triangles_by_coordinate
|
|
),
|
|
coordinate,
|
|
)
|
|
var definitions := _spawn_distance_eligible_definitions(
|
|
_prop_definitions_for(rule, tags),
|
|
coordinate,
|
|
)
|
|
if _maybe_add_prop(
|
|
definitions,
|
|
record.get("position", Vector3.ZERO),
|
|
coordinate,
|
|
biome.stable_id,
|
|
int(record.get("ocean_facing_edges", 0)),
|
|
random,
|
|
terrain_triangles,
|
|
):
|
|
group_counts[group_key] += 1
|
|
_record_prop_group_coordinate(
|
|
group_key,
|
|
coordinate,
|
|
)
|
|
|
|
|
|
func _biome_group_key(
|
|
biome_id: StringName,
|
|
group: StringName,
|
|
) -> StringName:
|
|
return StringName("%s:%s" % [biome_id, group])
|
|
|
|
|
|
func _has_prop_clearance(position: Vector3, radius: float) -> bool:
|
|
for index: int in _placed_prop_positions.size():
|
|
var other := _placed_prop_positions[index]
|
|
var distance := Vector2(
|
|
position.x - other.x,
|
|
position.z - other.z,
|
|
).length()
|
|
if distance < radius + _placed_prop_clearance_radii[index]:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _configure_prop_visuals(root_node: Node) -> void:
|
|
if root_node is GeometryInstance3D:
|
|
(root_node as GeometryInstance3D).cast_shadow = (
|
|
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
)
|
|
for child: Node in root_node.get_children():
|
|
_configure_prop_visuals(child)
|
|
|
|
|
|
func _apply_prop_material_variant(
|
|
root_node: Node,
|
|
definition: TerrainPropDefinition,
|
|
random: RandomNumberGenerator,
|
|
) -> void:
|
|
_apply_random_material_variant(
|
|
root_node,
|
|
definition.variant_material_slot_names,
|
|
definition.material_variants,
|
|
random,
|
|
)
|
|
_apply_random_material_variant(
|
|
root_node,
|
|
definition.secondary_variant_material_slot_names,
|
|
definition.secondary_material_variants,
|
|
random,
|
|
)
|
|
|
|
|
|
func _apply_random_material_variant(
|
|
root_node: Node,
|
|
target_material_names: PackedStringArray,
|
|
variants: Array[Material],
|
|
random: RandomNumberGenerator,
|
|
) -> void:
|
|
if variants.is_empty() or target_material_names.is_empty():
|
|
return
|
|
var variant := variants[random.randi_range(0, variants.size() - 1)]
|
|
_apply_material_variant_to_meshes(
|
|
root_node,
|
|
target_material_names,
|
|
variant,
|
|
)
|
|
|
|
|
|
func _apply_material_variant_to_meshes(
|
|
root_node: Node,
|
|
target_material_names: PackedStringArray,
|
|
variant: Material,
|
|
) -> void:
|
|
var mesh_instance := root_node as MeshInstance3D
|
|
if mesh_instance != null and mesh_instance.mesh != null:
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
var active_material := mesh_instance.get_active_material(surface_index)
|
|
if (
|
|
active_material != null
|
|
and active_material.resource_name in target_material_names
|
|
):
|
|
mesh_instance.set_surface_override_material(
|
|
surface_index,
|
|
variant,
|
|
)
|
|
for child: Node in root_node.get_children():
|
|
_apply_material_variant_to_meshes(
|
|
child,
|
|
target_material_names,
|
|
variant,
|
|
)
|
|
|
|
|
|
func _terrain_surface_triangles_by_coordinate(
|
|
material_names: Array[StringName],
|
|
) -> Dictionary[Vector2i, Array]:
|
|
var triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
|
|
if material_names.is_empty():
|
|
return triangles_by_coordinate
|
|
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
|
|
if mesh_instance.mesh == null:
|
|
continue
|
|
var coordinate := _terrain_coordinate_for(mesh_instance)
|
|
if coordinate.x < 0 or coordinate.y < 0:
|
|
continue
|
|
var triangles: Array[PackedVector3Array] = []
|
|
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
|
var material := mesh_instance.get_active_material(surface_index)
|
|
if (
|
|
material == null
|
|
or StringName(material.resource_name) not in material_names
|
|
):
|
|
continue
|
|
_append_surface_triangles(
|
|
triangles,
|
|
mesh_instance,
|
|
mesh_instance.mesh.surface_get_arrays(surface_index),
|
|
-INF,
|
|
0.35,
|
|
)
|
|
if triangles.is_empty():
|
|
continue
|
|
var coordinate_triangles: Array = triangles_by_coordinate.get(
|
|
coordinate,
|
|
[],
|
|
)
|
|
coordinate_triangles.append_array(triangles)
|
|
triangles_by_coordinate[coordinate] = coordinate_triangles
|
|
return triangles_by_coordinate
|
|
|
|
|
|
func _cache_water_recovery_surfaces(
|
|
grass_triangles: Dictionary[Vector2i, Array],
|
|
sand_triangles: Dictionary[Vector2i, Array],
|
|
) -> void:
|
|
_water_recovery_triangles_by_coordinate.clear()
|
|
_append_water_recovery_surfaces(grass_triangles)
|
|
_append_water_recovery_surfaces(sand_triangles)
|
|
|
|
|
|
func _append_water_recovery_surfaces(
|
|
source: Dictionary[Vector2i, Array],
|
|
) -> void:
|
|
for coordinate: Vector2i in source:
|
|
var eligible: Array = _water_recovery_triangles_by_coordinate.get(
|
|
coordinate,
|
|
[],
|
|
)
|
|
for triangle: PackedVector3Array in source[coordinate]:
|
|
if triangle.size() != 3:
|
|
continue
|
|
if minf(
|
|
triangle[0].y,
|
|
minf(triangle[1].y, triangle[2].y),
|
|
) <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE:
|
|
continue
|
|
var cross := (triangle[1] - triangle[0]).cross(
|
|
triangle[2] - triangle[0]
|
|
)
|
|
if (
|
|
cross.length_squared() <= 0.0000001
|
|
or absf(cross.normalized().dot(Vector3.UP))
|
|
< WATER_RECOVERY_MINIMUM_UP_DOT
|
|
):
|
|
continue
|
|
eligible.append(triangle)
|
|
if not eligible.is_empty():
|
|
_water_recovery_triangles_by_coordinate[coordinate] = eligible
|
|
|
|
|
|
func _nearest_recovery_surface_distance(entry_position: Vector3) -> float:
|
|
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
|
|
var nearest_distance_squared := INF
|
|
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
|
|
for triangle: PackedVector3Array in (
|
|
_water_recovery_triangles_by_coordinate[coordinate]
|
|
):
|
|
var closest := _closest_horizontal_point_on_triangle(
|
|
entry_horizontal,
|
|
triangle,
|
|
)
|
|
nearest_distance_squared = minf(
|
|
nearest_distance_squared,
|
|
entry_horizontal.distance_squared_to(closest),
|
|
)
|
|
return sqrt(nearest_distance_squared)
|
|
|
|
|
|
func _nearest_safe_recovery_surface_position(
|
|
entry_position: Vector3,
|
|
maximum_distance: float,
|
|
) -> Variant:
|
|
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
|
|
var maximum_distance_squared := maximum_distance * maximum_distance
|
|
var best_distance_squared := INF
|
|
var best_position: Variant = null
|
|
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
|
|
for triangle: PackedVector3Array in (
|
|
_water_recovery_triangles_by_coordinate[coordinate]
|
|
):
|
|
var closest := _closest_horizontal_point_on_triangle(
|
|
entry_horizontal,
|
|
triangle,
|
|
)
|
|
if (
|
|
entry_horizontal.distance_squared_to(closest)
|
|
> maximum_distance_squared
|
|
):
|
|
continue
|
|
var centroid := Vector2(
|
|
(triangle[0].x + triangle[1].x + triangle[2].x) / 3.0,
|
|
(triangle[0].z + triangle[1].z + triangle[2].z) / 3.0,
|
|
)
|
|
var inset_distance := minf(
|
|
closest.distance_to(centroid),
|
|
WATER_RECOVERY_MAXIMUM_INSET,
|
|
)
|
|
var inset_steps := ceili(
|
|
inset_distance / WATER_RECOVERY_INSET_STEP
|
|
)
|
|
for inset_index: int in inset_steps + 1:
|
|
var candidate_horizontal := closest.move_toward(
|
|
centroid,
|
|
minf(
|
|
float(inset_index) * WATER_RECOVERY_INSET_STEP,
|
|
inset_distance,
|
|
),
|
|
)
|
|
var distance_squared := entry_horizontal.distance_squared_to(
|
|
candidate_horizontal
|
|
)
|
|
if (
|
|
distance_squared > maximum_distance_squared
|
|
or distance_squared >= best_distance_squared
|
|
):
|
|
continue
|
|
var candidate := Vector3(
|
|
candidate_horizontal.x,
|
|
entry_position.y,
|
|
candidate_horizontal.y,
|
|
)
|
|
var safe_position: Variant = _safe_recovery_surface_position(
|
|
candidate
|
|
)
|
|
if safe_position is Vector3:
|
|
best_distance_squared = distance_squared
|
|
best_position = safe_position
|
|
return best_position
|
|
|
|
|
|
func _safe_recovery_surface_position(candidate: Vector3) -> Variant:
|
|
var nearby_triangles := _water_recovery_surface_triangles_near(candidate)
|
|
var surface_height := _surface_height_at(candidate, nearby_triangles)
|
|
if (
|
|
surface_height <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
|
|
or not _surface_supports_prop_footprint(
|
|
candidate,
|
|
surface_height,
|
|
WATER_RECOVERY_FOOTPRINT_RADIUS,
|
|
nearby_triangles,
|
|
)
|
|
or not _has_prop_clearance(
|
|
candidate,
|
|
WATER_RECOVERY_FOOTPRINT_RADIUS,
|
|
)
|
|
):
|
|
return null
|
|
candidate.y = surface_height
|
|
return candidate
|
|
|
|
|
|
func _water_recovery_surface_triangles_near(
|
|
position: Vector3,
|
|
) -> Array[PackedVector3Array]:
|
|
var result: Array[PackedVector3Array] = []
|
|
if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0:
|
|
return result
|
|
var local_position := _generator.to_local(position)
|
|
var half_grid := Vector2(
|
|
float(_generator.grid_size.x - 1) * 0.5,
|
|
float(_generator.grid_size.y - 1) * 0.5,
|
|
)
|
|
var coordinate := Vector2i(
|
|
roundi(local_position.x / _generator.catalog.chunk_size + half_grid.x),
|
|
roundi(local_position.z / _generator.catalog.chunk_size + half_grid.y),
|
|
)
|
|
for offset_x: int in range(-1, 2):
|
|
for offset_y: int in range(-1, 2):
|
|
var nearby_coordinate := coordinate + Vector2i(offset_x, offset_y)
|
|
result.append_array(
|
|
_water_recovery_triangles_by_coordinate.get(
|
|
nearby_coordinate,
|
|
[],
|
|
)
|
|
)
|
|
return result
|
|
|
|
|
|
func _closest_horizontal_point_on_triangle(
|
|
point: Vector2,
|
|
triangle: PackedVector3Array,
|
|
) -> Vector2:
|
|
var polygon := PackedVector2Array([
|
|
Vector2(triangle[0].x, triangle[0].z),
|
|
Vector2(triangle[1].x, triangle[1].z),
|
|
Vector2(triangle[2].x, triangle[2].z),
|
|
])
|
|
if Geometry2D.is_point_in_polygon(point, polygon):
|
|
return point
|
|
var closest := Geometry2D.get_closest_point_to_segment(
|
|
point,
|
|
polygon[0],
|
|
polygon[1],
|
|
)
|
|
var nearest_distance_squared := point.distance_squared_to(closest)
|
|
for edge_index: int in range(1, 3):
|
|
var edge_closest := Geometry2D.get_closest_point_to_segment(
|
|
point,
|
|
polygon[edge_index],
|
|
polygon[(edge_index + 1) % 3],
|
|
)
|
|
var distance_squared := point.distance_squared_to(edge_closest)
|
|
if distance_squared < nearest_distance_squared:
|
|
nearest_distance_squared = distance_squared
|
|
closest = edge_closest
|
|
return closest
|
|
|
|
|
|
func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i:
|
|
var current: Node = mesh_instance
|
|
while current != null and current != _generator:
|
|
if current.has_meta(&"terrain_chunk_coordinate"):
|
|
return current.get_meta(
|
|
&"terrain_chunk_coordinate",
|
|
Vector2i(-1, -1),
|
|
) as Vector2i
|
|
current = current.get_parent()
|
|
return Vector2i(-1, -1)
|
|
|
|
|
|
func _surface_triangles_for_coordinate(
|
|
triangles_by_coordinate: Dictionary[Vector2i, Array],
|
|
coordinate: Vector2i,
|
|
) -> Array[PackedVector3Array]:
|
|
var triangles: Array[PackedVector3Array] = []
|
|
triangles.assign(triangles_by_coordinate.get(coordinate, []))
|
|
return triangles
|
|
|
|
|
|
func _surface_height_at(
|
|
position: Vector3,
|
|
triangles: Array[PackedVector3Array],
|
|
) -> float:
|
|
var highest := -INF
|
|
var segment_start := position + Vector3.UP * 100.0
|
|
var segment_end := position + Vector3.DOWN * 100.0
|
|
for triangle: PackedVector3Array in triangles:
|
|
if triangle.size() != 3:
|
|
continue
|
|
var hit: Variant = Geometry3D.segment_intersects_triangle(
|
|
segment_start,
|
|
segment_end,
|
|
triangle[0],
|
|
triangle[1],
|
|
triangle[2],
|
|
)
|
|
if hit is Vector3:
|
|
highest = maxf(highest, (hit as Vector3).y)
|
|
return highest
|
|
|
|
|
|
func _surface_supports_prop_footprint(
|
|
position: Vector3,
|
|
surface_height: float,
|
|
radius: float,
|
|
triangles: Array[PackedVector3Array],
|
|
) -> bool:
|
|
for direction: Vector2 in PROP_SURFACE_SAMPLE_DIRECTIONS:
|
|
var sample_position := position + Vector3(
|
|
direction.x * radius,
|
|
0.0,
|
|
direction.y * radius,
|
|
)
|
|
var sample_height := _surface_height_at(sample_position, triangles)
|
|
if (
|
|
sample_height <= -INF
|
|
or absf(sample_height - surface_height)
|
|
> PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _add_prop_collision(
|
|
prop: Node3D,
|
|
definition: TerrainPropDefinition,
|
|
visual_scale: float,
|
|
) -> void:
|
|
if not definition.has_collision():
|
|
return
|
|
var body := StaticBody3D.new()
|
|
body.name = "TrunkCollision"
|
|
body.collision_layer = 1
|
|
body.collision_mask = 0
|
|
prop.add_child(body)
|
|
var collision := CollisionShape3D.new()
|
|
collision.name = "CollisionShape"
|
|
if definition.has_box_collision():
|
|
var box := BoxShape3D.new()
|
|
box.size = definition.collision_box_size * visual_scale
|
|
collision.shape = box
|
|
collision.position = definition.collision_offset * visual_scale
|
|
else:
|
|
var cylinder := CylinderShape3D.new()
|
|
cylinder.radius = definition.collision_radius * visual_scale
|
|
cylinder.height = definition.collision_height * visual_scale
|
|
collision.shape = cylinder
|
|
collision.position = (
|
|
definition.collision_offset * visual_scale
|
|
+ Vector3.UP * cylinder.height * 0.5
|
|
)
|
|
body.add_child(collision)
|
|
|
|
|
|
func _build_shoreline_reference() -> void:
|
|
var half := get_playable_half_extents()
|
|
var width := 0.05
|
|
var vertices := PackedVector3Array()
|
|
var indices := PackedInt32Array()
|
|
var corners := [
|
|
Vector3(-half.x, WATER_HEIGHT, -half.y),
|
|
Vector3(half.x, WATER_HEIGHT, -half.y),
|
|
Vector3(half.x, WATER_HEIGHT, half.y),
|
|
Vector3(-half.x, WATER_HEIGHT, half.y),
|
|
]
|
|
for index: int in 4:
|
|
var start: Vector3 = corners[index]
|
|
var finish: Vector3 = corners[(index + 1) % 4]
|
|
var direction := (finish - start).normalized()
|
|
var perpendicular := Vector3(-direction.z, 0.0, direction.x) * width
|
|
var base := vertices.size()
|
|
vertices.append_array(PackedVector3Array([
|
|
start - perpendicular,
|
|
start + perpendicular,
|
|
finish + perpendicular,
|
|
finish - perpendicular,
|
|
]))
|
|
indices.append_array(PackedInt32Array([
|
|
base, base + 1, base + 2,
|
|
base, base + 2, base + 3,
|
|
]))
|
|
var arrays := []
|
|
arrays.resize(Mesh.ARRAY_MAX)
|
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
|
arrays[Mesh.ARRAY_INDEX] = indices
|
|
var mesh := ArrayMesh.new()
|
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
|
_shoreline_reference.mesh = mesh
|
|
|
|
|
|
func _append_surface_triangles(
|
|
result: Array[PackedVector3Array],
|
|
mesh_instance: MeshInstance3D,
|
|
arrays: Array,
|
|
minimum_global_y: float,
|
|
minimum_up_dot: float,
|
|
) -> void:
|
|
if arrays.size() <= Mesh.ARRAY_INDEX:
|
|
return
|
|
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
|
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
|
if vertices.is_empty():
|
|
return
|
|
if indices.is_empty():
|
|
for index: int in range(0, vertices.size() - 2, 3):
|
|
_append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot)
|
|
return
|
|
for index: int in range(0, indices.size() - 2, 3):
|
|
_append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot)
|
|
|
|
|
|
func _append_triangle(
|
|
result: Array[PackedVector3Array],
|
|
mesh_instance: MeshInstance3D,
|
|
local_a: Vector3,
|
|
local_b: Vector3,
|
|
local_c: Vector3,
|
|
minimum_global_y: float,
|
|
minimum_up_dot: float,
|
|
) -> void:
|
|
var a := mesh_instance.to_global(local_a)
|
|
var b := mesh_instance.to_global(local_b)
|
|
var c := mesh_instance.to_global(local_c)
|
|
if minf(a.y, minf(b.y, c.y)) <= minimum_global_y:
|
|
return
|
|
var cross := (b - a).cross(c - a)
|
|
if cross.length_squared() <= 0.0000001:
|
|
return
|
|
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
|
|
return
|
|
result.append(PackedVector3Array([a, b, c]))
|
|
|
|
|
|
func _clear_children(root: Node) -> void:
|
|
for child: Node in root.get_children():
|
|
root.remove_child(child)
|
|
child.free()
|