class_name TerrainChunkDefinition extends Resource @export var stable_id: StringName @export var label := "" @export var packed_scene: PackedScene @export var primary_mesh_name: StringName @export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees") var allowed_rotation_mask := 15 @export_range(0.01, 100.0, 0.01) var selection_weight := 1.0 ## Negative values allow unlimited placements; zero disables random placement. @export_range(-1, 1024, 1) var maximum_placements := -1 @export var tags := PackedStringArray() ## Empty permits any non-water neighbor. Otherwise at least one listed tag ## must be present on the neighboring chunk. Water connectors are governed by ## their inlet/outlet topology instead. @export var allowed_neighbor_tags := PackedStringArray() ## These tags always reject a non-water neighbor, even when it also carries an ## allowed tag. @export var forbidden_neighbor_tags := PackedStringArray() ## Optional canonical per-edge overrides for allowed_neighbor_tags. Empty means ## the edge inherits the chunk-wide list. These describe the visible surface at ## a seam, such as a grass-backed coastline transition that must not meet sand. @export_group("Directional Neighbor Rules") @export var north_allowed_neighbor_tags := PackedStringArray() @export var east_allowed_neighbor_tags := PackedStringArray() @export var south_allowed_neighbor_tags := PackedStringArray() @export var west_allowed_neighbor_tags := PackedStringArray() @export_group("") ## When non-empty, at least this many in-grid cardinal neighbors must carry one ## of the listed tags. This supports statements such as "three sides around the ## spawn must be safe grass" without hard-coding a particular chunk ID. @export var required_neighbor_tags := PackedStringArray() @export_range(0, 4, 1) var minimum_required_neighbors := 0 ## Matching already-placed neighbors increase this definition's selection ## weight. This shapes regions without turning a visual preference into a hard ## generation constraint. @export var preferred_neighbor_tags := PackedStringArray() ## Coastal transition pieces should normally migrate toward the generated ## region's perimeter while remaining legal in the interior. @export var prefers_map_boundary := false ## Edges that descend from land into the surrounding ocean. Every rotated edge ## in this mask must face outside the generated grid; it may never meet another ## terrain chunk. @export_flags("North", "East", "South", "West") var ocean_facing_edges := 0 @export_flags("North", "East", "South", "West") var water_inlet_edges := 0 @export_flags("North", "East", "South", "West") var water_outlet_edges := 0 ## Optional generated water footprint in the chunk's unrotated local X/Z plane. ## Zero means this chunk contributes no standalone water surface. @export var water_surface_size := Vector2.ZERO @export var water_surface_offset := Vector2.ZERO func allows_quarter_turn(quarter_turns: int) -> bool: var normalized_turns := posmod(quarter_turns, 4) return (allowed_rotation_mask & (1 << normalized_turns)) != 0 func allows_non_water_neighbor(neighbor: TerrainChunkDefinition) -> bool: return _allows_neighbor_with_tags(neighbor, allowed_neighbor_tags) func allows_non_water_neighbor_on_edge( neighbor: TerrainChunkDefinition, edge: TerrainChunkTopology.Edge, ) -> bool: return _allows_neighbor_with_tags( neighbor, allowed_neighbor_tags_for_edge(edge), ) func allowed_neighbor_tags_for_edge( edge: TerrainChunkTopology.Edge, ) -> PackedStringArray: var edge_tags := directional_allowed_neighbor_tags(edge) return allowed_neighbor_tags if edge_tags.is_empty() else edge_tags func directional_allowed_neighbor_tags( edge: TerrainChunkTopology.Edge, ) -> PackedStringArray: match edge: TerrainChunkTopology.Edge.NORTH: return north_allowed_neighbor_tags TerrainChunkTopology.Edge.EAST: return east_allowed_neighbor_tags TerrainChunkTopology.Edge.SOUTH: return south_allowed_neighbor_tags TerrainChunkTopology.Edge.WEST: return west_allowed_neighbor_tags return PackedStringArray() func _allows_neighbor_with_tags( neighbor: TerrainChunkDefinition, allowed_tags: PackedStringArray, ) -> bool: if neighbor == null: return false for forbidden_tag: String in forbidden_neighbor_tags: if forbidden_tag in neighbor.tags: return false if allowed_tags.is_empty(): return true return has_any_tag(neighbor.tags, allowed_tags) func prefers_neighbor(neighbor: TerrainChunkDefinition) -> bool: return ( neighbor != null and has_any_tag(neighbor.tags, preferred_neighbor_tags) ) static func has_any_tag( available_tags: PackedStringArray, requested_tags: PackedStringArray, ) -> bool: for requested_tag: String in requested_tags: if requested_tag in available_tags: return true return false