1844 lines
53 KiB
GDScript
1844 lines
53 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 DecorShopInteractionType = preload(
|
|
"res://world/decor_shop_interaction.gd"
|
|
)
|
|
const RVUpgradeInteractionType = preload(
|
|
"res://world/rv_upgrade_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 HOME_GROUND_HEIGHT_TOLERANCE := 0.5
|
|
const HOME_ELEVATION_HEIGHT_TOLERANCE := 0.6
|
|
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 _decor_shop: DecorShopInteractionType = (
|
|
$Interactables/DecorShopWorld/InteractionArea
|
|
)
|
|
@onready var _rv_upgrade_shop: RVUpgradeInteractionType = (
|
|
$Interactables/RVUpgradeShopWorld/InteractionArea
|
|
)
|
|
@onready var _shop_root: Node3D = %FishingShopWorld
|
|
@onready var _storage_root: Node3D = %PlayerStorageBox
|
|
@onready var _decor_shop_root: Node3D = %DecorShopWorld
|
|
@onready var _rv_upgrade_shop_root: Node3D = %RVUpgradeShopWorld
|
|
@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] = {}
|
|
var _home_accessible_elevations_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_decor_shop() -> DecorShopInteractionType:
|
|
return _decor_shop
|
|
|
|
|
|
func get_rv_upgrade_shop() -> RVUpgradeInteractionType:
|
|
return _rv_upgrade_shop
|
|
|
|
|
|
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,
|
|
maximum_global_y: float = 100.0,
|
|
) -> 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,
|
|
maximum_global_y,
|
|
)
|
|
return triangles
|
|
|
|
|
|
func is_home_exterior_position_accessible(position: Vector3) -> bool:
|
|
var local_position := _generator.to_local(position)
|
|
if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE:
|
|
return true
|
|
var coordinate := _generator.coordinate_for_local_position(local_position)
|
|
for value: Variant in _home_accessible_elevations_by_coordinate.get(
|
|
coordinate,
|
|
[],
|
|
):
|
|
if absf(local_position.y - float(value)) <= (
|
|
HOME_ELEVATION_HEIGHT_TOLERANCE
|
|
):
|
|
return true
|
|
return false
|
|
|
|
|
|
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()
|
|
_cache_home_accessible_elevations(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 _cache_home_accessible_elevations(
|
|
records: Array[Dictionary],
|
|
) -> void:
|
|
_home_accessible_elevations_by_coordinate.clear()
|
|
var records_by_coordinate: Dictionary[Vector2i, Dictionary] = {}
|
|
var elevation_tiers: Dictionary[int, bool] = {}
|
|
for record: Dictionary in records:
|
|
var coordinate: Vector2i = record.get(
|
|
"coordinate",
|
|
Vector2i(-1, -1),
|
|
)
|
|
records_by_coordinate[coordinate] = record
|
|
for tier: int in _elevation_tiers_for_tags(
|
|
record.get("tags", PackedStringArray())
|
|
):
|
|
elevation_tiers[tier] = true
|
|
var visited: Dictionary[String, bool] = {}
|
|
for tier: int in elevation_tiers:
|
|
var elevation_tag := "elevation_%d" % tier
|
|
for coordinate: Vector2i in records_by_coordinate:
|
|
var visit_key := _home_elevation_visit_key(coordinate, tier)
|
|
if visited.has(visit_key):
|
|
continue
|
|
var record: Dictionary = records_by_coordinate[coordinate]
|
|
var tags: PackedStringArray = record.get(
|
|
"tags",
|
|
PackedStringArray(),
|
|
)
|
|
if elevation_tag not in tags:
|
|
continue
|
|
var component: Array[Vector2i] = []
|
|
var pending: Array[Vector2i] = [coordinate]
|
|
var has_ramp := false
|
|
while not pending.is_empty():
|
|
var current: Vector2i = pending.pop_back()
|
|
var current_key := _home_elevation_visit_key(current, tier)
|
|
if visited.has(current_key):
|
|
continue
|
|
var current_record: Dictionary = records_by_coordinate.get(
|
|
current,
|
|
{},
|
|
)
|
|
var current_tags: PackedStringArray = current_record.get(
|
|
"tags",
|
|
PackedStringArray(),
|
|
)
|
|
if elevation_tag not in current_tags:
|
|
continue
|
|
visited[current_key] = true
|
|
component.append(current)
|
|
has_ramp = has_ramp or "ramp" in current_tags
|
|
for offset: Vector2i in [
|
|
Vector2i.LEFT,
|
|
Vector2i.RIGHT,
|
|
Vector2i.UP,
|
|
Vector2i.DOWN,
|
|
]:
|
|
pending.append(current + offset)
|
|
if not has_ramp:
|
|
continue
|
|
var surface_height := (
|
|
float(tier - 1) * _generator.elevated_cliff_level_height
|
|
)
|
|
for component_coordinate: Vector2i in component:
|
|
var heights: Array = (
|
|
_home_accessible_elevations_by_coordinate.get(
|
|
component_coordinate,
|
|
[],
|
|
)
|
|
)
|
|
heights.append(surface_height)
|
|
_home_accessible_elevations_by_coordinate[
|
|
component_coordinate
|
|
] = heights
|
|
|
|
|
|
func _elevation_tiers_for_tags(tags: PackedStringArray) -> Array[int]:
|
|
var tiers: Array[int] = []
|
|
for tag_value: String in tags:
|
|
if not tag_value.begins_with("elevation_"):
|
|
continue
|
|
var suffix := tag_value.trim_prefix("elevation_")
|
|
if suffix.is_valid_int():
|
|
tiers.append(suffix.to_int())
|
|
return tiers
|
|
|
|
|
|
func _home_elevation_visit_key(coordinate: Vector2i, tier: int) -> String:
|
|
return "%d:%d:%d" % [tier, coordinate.x, coordinate.y]
|
|
|
|
|
|
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
|
|
_decor_shop_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
|
_decor_shop_root.rotation.y = PI * 0.5
|
|
_rv_upgrade_shop_root.position = center + Vector3(-2.35, 0.0, -1.35)
|
|
_rv_upgrade_shop_root.rotation.y = PI
|
|
|
|
|
|
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,
|
|
100.0,
|
|
)
|
|
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,
|
|
maximum_global_y: 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,
|
|
maximum_global_y,
|
|
)
|
|
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,
|
|
maximum_global_y,
|
|
)
|
|
|
|
|
|
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,
|
|
maximum_global_y: 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 (
|
|
maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y
|
|
or minf(a.y, minf(b.y, c.y)) >= maximum_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()
|