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()