Expand generated terrain with biome-driven props

This commit is contained in:
Alexander Sellite 2026-08-20 00:28:55 -04:00
parent e8888ed05a
commit 1e24fb0bfc
103 changed files with 5427 additions and 351 deletions

View file

@ -0,0 +1,20 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=4 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_sand_tree"]
script = ExtResource("2_rule")
procedural_group = &"sand_tree"
placement_chance = 4200
adjacency_bonus = 1200
maximum_density = 5000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_palm")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_coast"
label = "coast"
required_chunk_tags = PackedStringArray("sand")
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_sand_tree")])

View file

@ -0,0 +1,31 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 9000
adjacency_bonus = 1000
maximum_density = 10000
minimum_placements = 2
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 3500
adjacency_bonus = 1200
maximum_density = 5000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_forest"
label = "forest"
required_chunk_tags = PackedStringArray("grass")
forbidden_chunk_tags = PackedStringArray("spawn")
region_weight = 1.4
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,31 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 9000
adjacency_bonus = 1000
maximum_density = 10000
minimum_placements = 2
allowed_prop_ids = PackedStringArray("prop_pine")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 2500
adjacency_bonus = 1000
maximum_density = 4000
minimum_placements = 1
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_pine_forest"
label = "pine forest"
required_chunk_tags = PackedStringArray("grass")
forbidden_chunk_tags = PackedStringArray("spawn")
region_weight = 1.2
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,28 @@
[gd_resource type="Resource" script_class="TerrainBiomeDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="1_definition"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_prop_rule.gd" id="2_rule"]
[sub_resource type="Resource" id="Rule_grass_tree"]
script = ExtResource("2_rule")
procedural_group = &"grass_tree"
placement_chance = 1600
adjacency_bonus = 900
maximum_density = 2200
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
[sub_resource type="Resource" id="Rule_grass_detail"]
script = ExtResource("2_rule")
procedural_group = &"grass_detail"
placement_chance = 700
adjacency_bonus = 400
maximum_density = 1200
allowed_prop_ids = PackedStringArray("prop_log", "prop_mushroom")
[resource]
script = ExtResource("1_definition")
stable_id = &"biome_plains"
label = "plains"
required_chunk_tags = PackedStringArray("grass")
region_weight = 1.0
prop_rules = Array[ExtResource("2_rule")]([SubResource("Rule_grass_tree"), SubResource("Rule_grass_detail")])

View file

@ -0,0 +1,12 @@
[gd_resource type="Resource" script_class="TerrainBiomeCatalog" load_steps=7 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_biome_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_plains.tres" id="2_plains"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_forest.tres" id="3_forest"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_pine_forest.tres" id="4_pine"]
[ext_resource type="Resource" path="res://world/generation/biomes/definitions/biome_coast.tres" id="5_coast"]
[ext_resource type="Script" path="res://world/generation/terrain_biome_definition.gd" id="6_definition_script"]
[resource]
script = ExtResource("1_catalog")
definitions = Array[ExtResource("6_definition_script")]([ExtResource("2_plains"), ExtResource("3_forest"), ExtResource("4_pine"), ExtResource("5_coast")])

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cg6jtwiq41mbr"
path="res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005.glb"
dest_files=["res://.godot/imported/chunk_0005.glb-fccbd2231305ec7b7f5a0903feee3737.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://diybhc4ciplal"
path.s3tc="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.s3tc.ctex", "res://.godot/imported/chunk_0005_cliff_wall.png-ce0994f179fdecaed785c59a34d7b735.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://i6vbptsv8276"
path.s3tc="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0005_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.s3tc.ctex", "res://.godot/imported/chunk_0005_grass_lite.png-ed24123dd98452d08e5de6e6b0eaad0d.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ddjqd4kl4tkhg"
path="res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006.glb"
dest_files=["res://.godot/imported/chunk_0006.glb-3c19b65d4a38bd1731728ce1ea2dda2b.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://beiaoue0vy8c7"
path.s3tc="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.s3tc.ctex", "res://.godot/imported/chunk_0006_cliff_wall.png-ac5bf5875f430089a760275aac53f7c5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djrg33nkvpnih"
path.s3tc="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.s3tc.ctex", "res://.godot/imported/chunk_0006_grass_lite.png-965daad85495f67f8a702417724c30a5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vxomtbcs2mc0"
path.s3tc="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "adfa66b8fe9da7f3450d3ddfd432b86a"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0006_sand.png"
dest_files=["res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.s3tc.ctex", "res://.godot/imported/chunk_0006_sand.png-b81070e87c8688cdfae71416692c7e7b.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cniughsb3gacd"
path="res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007.glb"
dest_files=["res://.godot/imported/chunk_0007.glb-ba9f86d0e228adfa3c9dcc6c2c6d2bea.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2xro1br3b8hr"
path.s3tc="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "fc97184f537cc23f83a731327e10c836"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007_cliff_wall.png"
dest_files=["res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.s3tc.ctex", "res://.godot/imported/chunk_0007_cliff_wall.png-b91883984675adf65a5448c549fa5414.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3skvv046r4cn"
path.s3tc="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "4dc1ac400d85d9b3e4f43a5af0a38ed4"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0007_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.s3tc.ctex", "res://.godot/imported/chunk_0007_grass_lite.png-2d878613a1a7bd71881adeafa310116a.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

View file

@ -11,4 +11,6 @@ packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 15
selection_weight = 4.0
tags = PackedStringArray("land", "grass", "flat")
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn_safe")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast", "fresh_water")
preferred_neighbor_tags = PackedStringArray("grass")

View file

@ -11,4 +11,8 @@ packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0001"
allowed_rotation_mask = 15
selection_weight = 1.5
tags = PackedStringArray("land", "sand", "flat")
tags = PackedStringArray("land", "walkable", "sand", "flat")
allowed_neighbor_tags = PackedStringArray("sand", "grass")
required_neighbor_tags = PackedStringArray("coast")
minimum_required_neighbors = 1
preferred_neighbor_tags = PackedStringArray("coast", "sand")

View file

@ -10,5 +10,10 @@ label = "beach"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0002"
selection_weight = 0.75
maximum_placements = 10
tags = PackedStringArray("land", "sand", "coast", "slope")
maximum_placements = 20
tags = PackedStringArray("land", "walkable", "sand", "coast", "slope")
allowed_neighbor_tags = PackedStringArray("sand", "grass")
required_neighbor_tags = PackedStringArray("sand")
minimum_required_neighbors = 1
preferred_neighbor_tags = PackedStringArray("sand", "grass")
ocean_facing_edges = 2

View file

@ -10,7 +10,9 @@ label = "stream"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0003"
selection_weight = 0.6
tags = PackedStringArray("land", "fresh_water", "river", "flowing")
tags = PackedStringArray("land", "walkable", "fresh_water", "river", "flowing")
allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass")
water_inlet_edges = 1
water_outlet_edges = 4
water_surface_size = Vector2(2.1, 10)

View file

@ -10,7 +10,9 @@ label = "pond"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0004"
selection_weight = 0.35
tags = PackedStringArray("land", "fresh_water", "pond")
tags = PackedStringArray("land", "walkable", "fresh_water", "pond")
allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass")
water_inlet_edges = 1
water_surface_size = Vector2(6.7, 7.75)
water_surface_offset = Vector2(-0.32, -1.03)

View file

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://cg6jtwiq41mbr" path="res://world/generation/chunks/assets/chunk_0005.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0005"
label = "grass ocean edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0005"
selection_weight = 1.0
maximum_placements = 20
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "grass_ocean_edge")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
north_allowed_neighbor_tags = PackedStringArray("coast")
south_allowed_neighbor_tags = PackedStringArray("coast")
west_allowed_neighbor_tags = PackedStringArray("grass")
preferred_neighbor_tags = PackedStringArray("grass", "coast")
prefers_map_boundary = true
ocean_facing_edges = 2

View file

@ -0,0 +1,23 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://ddjqd4kl4tkhg" path="res://world/generation/chunks/assets/chunk_0006.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0006"
label = "grass to beach ocean edge"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0006"
selection_weight = 0.35
maximum_placements = 8
tags = PackedStringArray("land", "walkable", "sand", "coast", "transition", "slope")
allowed_neighbor_tags = PackedStringArray("grass", "sand", "coast")
north_allowed_neighbor_tags = PackedStringArray("coast")
south_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
west_allowed_neighbor_tags = PackedStringArray("grass")
required_neighbor_tags = PackedStringArray("coast")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("coast")
prefers_map_boundary = true
ocean_facing_edges = 2

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://cniughsb3gacd" path="res://world/generation/chunks/assets/chunk_0007.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0007"
label = "grass ocean corner"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0007"
selection_weight = 0.2
maximum_placements = 4
tags = PackedStringArray("land", "walkable", "grass", "coast", "cliff", "corner")
allowed_neighbor_tags = PackedStringArray("grass", "coast")
north_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
west_allowed_neighbor_tags = PackedStringArray("grass_ocean_edge")
required_neighbor_tags = PackedStringArray("grass_ocean_edge")
minimum_required_neighbors = 2
preferred_neighbor_tags = PackedStringArray("coast")
prefers_map_boundary = true
ocean_facing_edges = 6

View file

@ -12,4 +12,8 @@ primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 1
selection_weight = 0.01
maximum_placements = 1
tags = PackedStringArray("land", "grass", "flat", "spawn")
tags = PackedStringArray("land", "walkable", "grass", "flat", "spawn")
allowed_neighbor_tags = PackedStringArray("grass", "coast")
required_neighbor_tags = PackedStringArray("spawn_safe")
minimum_required_neighbors = 3
preferred_neighbor_tags = PackedStringArray("spawn_safe")

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=9 format=3]
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=12 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0000.tres" id="2_grass"]
@ -8,8 +8,11 @@
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0004.tres" id="6_pond"]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="7_definition_script"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_spawn.tres" id="8_spawn"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0005.tres" id="9_grass_ocean_edge"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0006.tres" id="10_grass_beach_transition"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0007.tres" id="11_grass_ocean_corner"]
[resource]
script = ExtResource("1_catalog")
chunk_size = 10.0
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("8_spawn")])
definitions = Array[ExtResource("7_definition_script")]([ExtResource("2_grass"), ExtResource("3_sand"), ExtResource("4_beach"), ExtResource("5_stream"), ExtResource("6_pond"), ExtResource("9_grass_ocean_edge"), ExtResource("10_grass_beach_transition"), ExtResource("11_grass_ocean_corner"), ExtResource("8_spawn")])

View file

@ -9,9 +9,6 @@ const FishingShopInteractionType = preload(
const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd"
)
const PROP_SOURCE: PackedScene = preload(
"res://art/exported/environment/terrain/starter_island.glb"
)
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
const SALT_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water.tres"
@ -25,16 +22,21 @@ const POND_POOL: FishPool = preload(
const OCEAN_POOL: FishPool = preload(
"res://fish/pools/starter_ocean_pool.tres"
)
const TREE_SOURCE_NAME := "tree_2_001"
const PALM_SOURCE_NAME := "plam_tree"
const TREE_RUNTIME_NAME := "regular_tree"
const PALM_RUNTIME_NAME := "palm_tree"
const WATER_HEIGHT := -0.25
const PROP_EDGE_MARGIN := 2.2
const TREE_CHANCE := 0.58
const PALM_CHANCE := 0.42
const PROP_PLACEMENT_ATTEMPTS := 12
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
const PROP_CHANCE_SCALE := 10000
const PROP_SELECTION_WEIGHT_SCALE := 1000
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
@ -56,15 +58,17 @@ const PALM_CHANCE := 0.42
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
var _light_performance_profile := false
var _source_meshes: Dictionary[String, Mesh] = {}
var _source_bases: Dictionary[String, Basis] = {}
var _source_minimum_y: Dictionary[String, float] = {}
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] = {}
func _ready() -> void:
_generator.generation_completed.connect(_on_generation_completed)
_cache_prop_source(TREE_SOURCE_NAME)
_cache_prop_source(PALM_SOURCE_NAME)
_validate_prop_catalog()
_validate_biome_catalog()
_configure_static_water()
_build_shoreline_reference()
if not generate_world(initial_seed):
@ -107,6 +111,18 @@ 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()
@ -118,10 +134,9 @@ func get_spawn_surface_triangles(
minimum_up_dot: float = 0.6,
) -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
var root: Node3D = _generator.get_generated_chunks_root()
if root == null or material_names.is_empty():
if material_names.is_empty():
return triangles
for mesh_instance: MeshInstance3D in _collect_mesh_instances(root):
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
var mesh: Mesh = mesh_instance.mesh
if mesh == null:
continue
@ -143,15 +158,31 @@ func get_spawn_surface_triangles(
func _on_generation_completed(summary: Dictionary) -> void:
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_centers: Array[Vector3] = []
var sand_centers: Array[Vector3] = []
var tree_count := 0
var palm_count := 0
var terrain_triangles := _terrain_surface_triangles()
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(
@ -159,50 +190,124 @@ func _on_generation_completed(summary: Dictionary) -> void:
)
if "spawn" in tags:
spawn_position = position
if "spawn" not in tags and "grass" in tags:
grass_centers.append(position)
if _maybe_add_prop(
TREE_SOURCE_NAME,
TREE_RUNTIME_NAME,
position,
TREE_CHANCE,
random,
true,
):
tree_count += 1
elif "sand" in tags:
sand_centers.append(position)
if _maybe_add_prop(
PALM_SOURCE_NAME,
PALM_RUNTIME_NAME,
position,
PALM_CHANCE,
random,
false,
):
palm_count += 1
if tree_count == 0 and not grass_centers.is_empty():
_add_prop(
TREE_SOURCE_NAME,
TREE_RUNTIME_NAME,
grass_centers[random.randi_range(0, grass_centers.size() - 1)],
random,
true,
)
if palm_count == 0 and not sand_centers.is_empty():
_add_prop(
PALM_SOURCE_NAME,
PALM_RUNTIME_NAME,
sand_centers[random.randi_range(0, sand_centers.size() - 1)],
random,
false,
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 definitions := _spawn_distance_eligible_definitions(
_prop_definitions_for(rule, tags),
coordinate,
)
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,
random,
terrain_triangles,
):
group_counts[group_key] += 1
_record_prop_group_coordinate(
group_key,
coordinate,
)
_ensure_minimum_props(
eligible_records,
group_counts,
random,
terrain_triangles,
)
_place_spawn_amenities(spawn_position)
_configure_fresh_water(records)
_configure_diggable_area()
world_generated.emit(_current_seed, summary)
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
@ -312,60 +417,410 @@ func _light_water_material(color: Color) -> StandardMaterial3D:
func _maybe_add_prop(
source_name: String,
runtime_name: String,
definitions: Array[TerrainPropDefinition],
chunk_center: Vector3,
chance: float,
chunk_coordinate: Vector2i,
biome_id: StringName,
random: RandomNumberGenerator,
add_anchor: bool,
terrain_triangles: Array[PackedVector3Array],
) -> bool:
if random.randf() > chance or not _source_meshes.has(source_name):
return false
_add_prop(
source_name,
runtime_name,
var definition := _pick_prop_definition(
definitions,
chunk_center,
random,
add_anchor,
)
return true
if definition == null:
return false
return _add_prop(
definition,
chunk_center,
chunk_coordinate,
biome_id,
random,
terrain_triangles,
)
func _add_prop(
source_name: String,
runtime_name: String,
definition: TerrainPropDefinition,
chunk_center: Vector3,
chunk_coordinate: Vector2i,
biome_id: StringName,
random: RandomNumberGenerator,
add_anchor: bool,
) -> void:
if not _source_meshes.has(source_name):
return
terrain_triangles: Array[PackedVector3Array],
) -> bool:
if definition == null or definition.packed_scene == null:
return false
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 _has_prop_clearance(
placement,
definition.clearance_radius,
)
):
placement.y = surface_height
found_surface = true
break
if not found_surface:
return false
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" % [runtime_name, _decorations.get_child_count()]
var offset := Vector3(
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
0.0,
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
)
prop.position = chunk_center + offset
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.position = placement
prop.rotation.y = random.randf_range(-PI, PI)
_decorations.add_child(prop)
var visual := MeshInstance3D.new()
visual.name = "Visual"
visual.mesh = _source_meshes[source_name]
visual.basis = _source_bases[source_name]
visual.position.y = -_source_minimum_y.get(source_name, 0.0)
visual.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
prop.add_child(visual)
_add_prop_collision(prop, source_name == TREE_SOURCE_NAME)
if add_anchor:
visual_root.name = "Visual"
prop.add_child(visual_root)
_configure_prop_visuals(visual_root)
_add_prop_collision(prop, definition)
_placed_prop_positions.append(placement)
_placed_prop_clearance_radii.append(definition.clearance_radius)
_placed_prop_groups.append(definition.procedural_group)
if definition.gatherable_anchor_height > 0.0:
var anchor := Marker3D.new()
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
anchor.position = prop.position + Vector3(0.0, 2.15, 0.0)
anchor.position = prop.position + Vector3(
0.0,
definition.gatherable_anchor_height,
0.0,
)
_tree_anchors.add_child(anchor)
return true
func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
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.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,
terrain_triangles: Array[PackedVector3Array],
) -> 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 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,
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 _terrain_surface_triangles() -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
for mesh_instance: MeshInstance3D in _generator.get_primary_terrain_meshes():
if mesh_instance.mesh == null:
continue
for surface_index: int in mesh_instance.mesh.get_surface_count():
_append_surface_triangles(
triangles,
mesh_instance,
mesh_instance.mesh.surface_get_arrays(surface_index),
-INF,
0.35,
)
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 _add_prop_collision(
prop: Node3D,
definition: TerrainPropDefinition,
) -> void:
if not definition.has_collision():
return
var body := StaticBody3D.new()
body.name = "TrunkCollision"
body.collision_layer = 1
@ -373,44 +828,21 @@ func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
prop.add_child(body)
var collision := CollisionShape3D.new()
collision.name = "CollisionShape"
var shape := CylinderShape3D.new()
shape.radius = 0.5 if broad_tree else 0.35
shape.height = 4.2 if broad_tree else 4.8
collision.position.y = shape.height * 0.5
collision.shape = shape
body.add_child(collision)
func _cache_prop_source(source_name: String) -> void:
var source_root := PROP_SOURCE.instantiate()
var source := source_root.find_child(
source_name,
true,
false,
) as MeshInstance3D
if source != null and source.mesh != null:
_source_meshes[source_name] = source.mesh
_source_bases[source_name] = source.transform.basis
_source_minimum_y[source_name] = _minimum_transformed_mesh_y(
source.mesh,
source.transform.basis,
)
if definition.has_box_collision():
var box := BoxShape3D.new()
box.size = definition.collision_box_size
collision.shape = box
collision.position = definition.collision_offset
else:
push_warning("Generated terrain prop source '%s' is unavailable." % source_name)
source_root.free()
func _minimum_transformed_mesh_y(mesh: Mesh, basis: Basis) -> float:
var bounds := mesh.get_aabb()
var minimum_y := INF
for corner_index: int in 8:
var corner := bounds.position + Vector3(
bounds.size.x if (corner_index & 1) != 0 else 0.0,
bounds.size.y if (corner_index & 2) != 0 else 0.0,
bounds.size.z if (corner_index & 4) != 0 else 0.0,
var cylinder := CylinderShape3D.new()
cylinder.radius = definition.collision_radius
cylinder.height = definition.collision_height
collision.shape = cylinder
collision.position = (
definition.collision_offset
+ Vector3.UP * cylinder.height * 0.5
)
minimum_y = minf(minimum_y, (basis * corner).y)
return minimum_y if minimum_y < INF else 0.0
body.add_child(collision)
func _build_shoreline_reference() -> void:
@ -492,15 +924,6 @@ func _append_triangle(
result.append(PackedVector3Array([a, b, c]))
func _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]:
var result: Array[MeshInstance3D] = []
if root is MeshInstance3D:
result.append(root as MeshInstance3D)
for child: Node in root.get_children():
result.append_array(_collect_mesh_instances(child))
return result
func _clear_children(root: Node) -> void:
for child: Node in root.get_children():
root.remove_child(child)

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=12 format=3]
[gd_scene load_steps=14 format=3]
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
@ -11,9 +11,13 @@
[ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="9_anchors"]
[ext_resource type="Resource" path="res://fish/pools/starter_ocean_pool.tres" id="10_ocean_pool"]
[ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"]
[ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"]
[ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"]
[node name="GeneratedWorldRegion" type="Node3D"]
script = ExtResource("1_region")
prop_catalog = ExtResource("12_prop_catalog")
biome_catalog = ExtResource("13_biome_catalog")
region_id = &"generated_world"
fishable_water_root = NodePath("WaterBodies")
water_recovery_root = NodePath("WaterBodies")
@ -27,12 +31,12 @@ gatherable_anchor_root = NodePath("GatherableAnchors")
unique_name_in_owner = true
script = ExtResource("2_generator")
catalog = ExtResource("3_catalog")
grid_size = Vector2i(5, 5)
grid_size = Vector2i(7, 7)
generation_seed = 13001
generate_on_ready = false
build_collision = true
force_center_chunk_id = &"chunk_spawn"
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004", "chunk_0005", "chunk_0006", "chunk_0007")
required_chunk_weight_multiplier = 64.0
maximum_backtracks = 100000

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://d02hbwdie8qvm"
path="res://.godot/imported/prop_bridge.glb-f46c3958ff825fa424225306a13cf192.scn"
[deps]
source_file="res://world/generation/props/assets/prop_bridge.glb"
dest_files=["res://.godot/imported/prop_bridge.glb-f46c3958ff825fa424225306a13cf192.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://doq8pbpyvm6ue"
path="res://.godot/imported/prop_dock.glb-1bab9084db9428b6aab78b02d3957fdb.scn"
[deps]
source_file="res://world/generation/props/assets/prop_dock.glb"
dest_files=["res://.godot/imported/prop_dock.glb-1bab9084db9428b6aab78b02d3957fdb.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://tjvd2e6pu13v"
path="res://.godot/imported/prop_log.glb-2315521a55d44d3ed4d9bffda15d9534.scn"
[deps]
source_file="res://world/generation/props/assets/prop_log.glb"
dest_files=["res://.godot/imported/prop_log.glb-2315521a55d44d3ed4d9bffda15d9534.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bus7qaun1g63r"
path="res://.godot/imported/prop_log_post_1.glb-276ebe594961fac10b1b3228d38fdabf.scn"
[deps]
source_file="res://world/generation/props/assets/prop_log_post_1.glb"
dest_files=["res://.godot/imported/prop_log_post_1.glb-276ebe594961fac10b1b3228d38fdabf.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cb76u4p4xw0on"
path="res://.godot/imported/prop_log_post_2.glb-da62150ce947f75be274e2a5c99daa99.scn"
[deps]
source_file="res://world/generation/props/assets/prop_log_post_2.glb"
dest_files=["res://.godot/imported/prop_log_post_2.glb-da62150ce947f75be274e2a5c99daa99.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dpwkottav2wmg"
path="res://.godot/imported/prop_mushroom.glb-7427f0262b74e22f1f26f32e05833572.scn"
[deps]
source_file="res://world/generation/props/assets/prop_mushroom.glb"
dest_files=["res://.godot/imported/prop_mushroom.glb-7427f0262b74e22f1f26f32e05833572.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://fourevm3if2u"
path="res://.godot/imported/prop_palm.glb-bb3dfaeaead57bcc8695fc4152589ab2.scn"
[deps]
source_file="res://world/generation/props/assets/prop_palm.glb"
dest_files=["res://.godot/imported/prop_palm.glb-bb3dfaeaead57bcc8695fc4152589ab2.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c7hw7rdlhy8rw"
path="res://.godot/imported/prop_pine.glb-372fa54a69ad2e1228537194c2ebb4b8.scn"
[deps]
source_file="res://world/generation/props/assets/prop_pine.glb"
dest_files=["res://.godot/imported/prop_pine.glb-372fa54a69ad2e1228537194c2ebb4b8.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bk5g0cbea3sr7"
path="res://.godot/imported/prop_timber_1.glb-c24976466df75b6076557ecf1717e207.scn"
[deps]
source_file="res://world/generation/props/assets/prop_timber_1.glb"
dest_files=["res://.godot/imported/prop_timber_1.glb-c24976466df75b6076557ecf1717e207.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bvjtbge6isr5m"
path="res://.godot/imported/prop_timber_2.glb-9d476f292645d06049bc0a83c82d7389.scn"
[deps]
source_file="res://world/generation/props/assets/prop_timber_2.glb"
dest_files=["res://.godot/imported/prop_timber_2.glb-9d476f292645d06049bc0a83c82d7389.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dgp6c7syxu0b4"
path="res://.godot/imported/prop_tree_1.glb-e6234d9deb054d47d88f248b79266362.scn"
[deps]
source_file="res://world/generation/props/assets/prop_tree_1.glb"
dest_files=["res://.godot/imported/prop_tree_1.glb-e6234d9deb054d47d88f248b79266362.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dt26eb0s5o7h1"
path="res://.godot/imported/prop_tree_2.glb-e0700c61f161e28a6cdb5966d1110da2.scn"
[deps]
source_file="res://world/generation/props/assets/prop_tree_2.glb"
dest_files=["res://.godot/imported/prop_tree_2.glb-e0700c61f161e28a6cdb5966d1110da2.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cprt30ic278ea"
path="res://.godot/imported/prop_tree_3.glb-57719668b78f614e94bfdab89d68d54d.scn"
[deps]
source_file="res://world/generation/props/assets/prop_tree_3.glb"
dest_files=["res://.godot/imported/prop_tree_3.glb-57719668b78f614e94bfdab89d68d54d.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
mesh_library/use_node_names_as_mesh_names=false
array_mesh/deduplicate_surfaces=true
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
gltf/texture_map_mode=1

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_bridge.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_bridge"
label = "small bridge"
packed_scene = ExtResource("2_scene")
clearance_radius = 3.75

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_dock.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_dock"
label = "dock"
packed_scene = ExtResource("2_scene")
clearance_radius = 6.0

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_log.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_log"
label = "decorative log"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_detail"
required_chunk_tags = PackedStringArray("grass")
selection_weight = 1.0
minimum_spawn_chunk_distance = 2
clearance_radius = 2.8
collision_box_size = Vector3(1.1, 0.9, 5.0)
collision_offset = Vector3(0, 0.45, 0)

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_log_post_1.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_log_post_1"
label = "large decorative post"
packed_scene = ExtResource("2_scene")
clearance_radius = 0.5

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_log_post_2.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_log_post_2"
label = "small decorative post"
packed_scene = ExtResource("2_scene")
clearance_radius = 0.35

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_mushroom.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_mushroom"
label = "mushrooms"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_detail"
required_chunk_tags = PackedStringArray("grass")
selection_weight = 2.0
minimum_spawn_chunk_distance = 1
preferred_nearby_prop_groups = PackedStringArray("grass_tree")
preferred_nearby_radius = 12.0
nearby_preference_weight_multiplier = 3.0
clearance_radius = 0.8

View file

@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_palm.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_palm"
label = "palm"
packed_scene = ExtResource("2_scene")
procedural_group = &"sand_tree"
required_chunk_tags = PackedStringArray("sand")
minimum_spawn_chunk_distance = 2
clearance_radius = 2.0
collision_radius = 0.4
collision_height = 6.0

View file

@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_pine.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_pine"
label = "pine"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
selection_weight = 0.7
minimum_spawn_chunk_distance = 2
clearance_radius = 1.55
collision_radius = 0.5
collision_height = 4.0
gatherable_anchor_height = 2.15

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_timber_1.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_timber_1"
label = "large path timber"
packed_scene = ExtResource("2_scene")
clearance_radius = 2.5

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_timber_2.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_timber_2"
label = "small path timber"
packed_scene = ExtResource("2_scene")
clearance_radius = 2.5

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_tree_1"
label = "small tree"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
clearance_radius = 1.3
collision_radius = 0.4
collision_height = 3.2
gatherable_anchor_height = 2.15

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_tree_2"
label = "medium tree"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
clearance_radius = 1.5
collision_radius = 0.5
collision_height = 3.8
gatherable_anchor_height = 2.15

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"prop_tree_3"
label = "large tree"
packed_scene = ExtResource("2_scene")
procedural_group = &"grass_tree"
required_chunk_tags = PackedStringArray("grass")
minimum_spawn_chunk_distance = 2
clearance_radius = 1.8
collision_radius = 0.55
collision_height = 4.2
gatherable_anchor_height = 2.15

View file

@ -0,0 +1,21 @@
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=16 format=3]
[ext_resource type="Script" path="res://world/generation/terrain_prop_catalog.gd" id="1_catalog"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_bridge.tres" id="2_bridge"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_dock.tres" id="3_dock"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log.tres" id="4_log"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log_post_1.tres" id="5_post_1"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_log_post_2.tres" id="6_post_2"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_mushroom.tres" id="7_mushroom"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_palm.tres" id="8_palm"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_pine.tres" id="9_pine"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_timber_1.tres" id="10_timber_1"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_timber_2.tres" id="11_timber_2"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_1.tres" id="12_tree_1"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_2.tres" id="13_tree_2"]
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_3.tres" id="14_tree_3"]
[ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="15_definition_script"]
[resource]
script = ExtResource("1_catalog")
definitions = Array[ExtResource("15_definition_script")]([ExtResource("2_bridge"), ExtResource("3_dock"), ExtResource("4_log"), ExtResource("5_post_1"), ExtResource("6_post_2"), ExtResource("7_mushroom"), ExtResource("8_palm"), ExtResource("9_pine"), ExtResource("10_timber_1"), ExtResource("11_timber_2"), ExtResource("12_tree_1"), ExtResource("13_tree_2"), ExtResource("14_tree_3")])

View file

@ -0,0 +1,220 @@
class_name TerrainBiomeAssigner
extends RefCounted
const BIOME_SEED_SALT := 0xB10E5
const SCORE_JITTER := 0.2
static func assign(
catalog: TerrainBiomeCatalog,
records: Array[Dictionary],
generation_seed: int,
) -> Dictionary[Vector2i, StringName]:
var assignments: Dictionary[Vector2i, StringName] = {}
if catalog == null:
return assignments
var records_by_signature: Dictionary[String, Array] = {}
var definitions_by_signature: Dictionary[String, Array] = {}
for record: Dictionary in records:
var tags: PackedStringArray = record.get("tags", PackedStringArray())
var definitions := catalog.definitions_for_tags(tags)
if definitions.is_empty():
continue
var signature := _definition_signature(definitions)
var grouped_records: Array = records_by_signature.get(signature, [])
grouped_records.append(record)
records_by_signature[signature] = grouped_records
definitions_by_signature[signature] = definitions
var signatures := PackedStringArray()
for signature_value: Variant in records_by_signature:
signatures.append(str(signature_value))
signatures.sort()
for signature: String in signatures:
var grouped_records: Array[Dictionary] = []
grouped_records.assign(records_by_signature[signature])
grouped_records.sort_custom(_record_less_than)
var definitions: Array[TerrainBiomeDefinition] = []
definitions.assign(definitions_by_signature[signature])
if definitions.size() == 1:
for record: Dictionary in grouped_records:
assignments[record.get("coordinate", Vector2i.ZERO)] = (
definitions[0].stable_id
)
continue
_assign_clustered_group(
assignments,
grouped_records,
definitions,
generation_seed,
signature,
)
return assignments
static func counts(
assignments: Dictionary[Vector2i, StringName],
) -> Dictionary[StringName, int]:
var result: Dictionary[StringName, int] = {}
for biome_id: StringName in assignments.values():
result[biome_id] = result.get(biome_id, 0) + 1
return result
static func fingerprint(
assignments: Dictionary[Vector2i, StringName],
) -> String:
var coordinates: Array[Vector2i] = []
coordinates.assign(assignments.keys())
coordinates.sort_custom(_coordinate_less_than)
var tokens := PackedStringArray()
for coordinate: Vector2i in coordinates:
tokens.append(
"%d,%d:%s"
% [coordinate.x, coordinate.y, assignments[coordinate]]
)
return "\n".join(tokens).sha256_text()
static func _assign_clustered_group(
assignments: Dictionary[Vector2i, StringName],
records: Array[Dictionary],
definitions: Array[TerrainBiomeDefinition],
generation_seed: int,
signature: String,
) -> void:
var anchors := _build_anchors(
records,
definitions,
generation_seed,
signature,
)
for record: Dictionary in records:
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
var best_definition: TerrainBiomeDefinition
var best_score := INF
for definition: TerrainBiomeDefinition in definitions:
var anchor: Vector2i = anchors.get(
definition.stable_id,
coordinate,
)
var delta := coordinate - anchor
var score := (
float(delta.length_squared())
/ maxf(definition.region_weight, 0.1)
+ _score_jitter(
generation_seed,
coordinate,
definition.stable_id,
)
)
if score < best_score:
best_score = score
best_definition = definition
if best_definition != null:
assignments[coordinate] = best_definition.stable_id
static func _build_anchors(
records: Array[Dictionary],
definitions: Array[TerrainBiomeDefinition],
generation_seed: int,
signature: String,
) -> Dictionary[StringName, Vector2i]:
var anchors: Dictionary[StringName, Vector2i] = {}
var available_coordinates: Array[Vector2i] = []
for record: Dictionary in records:
available_coordinates.append(
record.get("coordinate", Vector2i.ZERO)
)
var random := RandomNumberGenerator.new()
random.seed = (
generation_seed
^ int(signature.hash())
^ BIOME_SEED_SALT
)
for definition: TerrainBiomeDefinition in definitions:
if available_coordinates.is_empty():
for record: Dictionary in records:
available_coordinates.append(
record.get("coordinate", Vector2i.ZERO)
)
var selected_index := 0
if anchors.is_empty():
selected_index = random.randi_range(
0,
available_coordinates.size() - 1,
)
else:
selected_index = _most_separated_coordinate_index(
available_coordinates,
anchors.values(),
random,
)
anchors[definition.stable_id] = available_coordinates[selected_index]
available_coordinates.remove_at(selected_index)
return anchors
static func _most_separated_coordinate_index(
coordinates: Array[Vector2i],
anchor_values: Array,
random: RandomNumberGenerator,
) -> int:
var best_distance := -1
var best_indices: Array[int] = []
for index: int in coordinates.size():
var coordinate := coordinates[index]
var nearest_distance := 1 << 30
for anchor_value: Variant in anchor_values:
var anchor: Vector2i = anchor_value
nearest_distance = mini(
nearest_distance,
(coordinate - anchor).length_squared(),
)
if nearest_distance > best_distance:
best_distance = nearest_distance
best_indices = [index]
elif nearest_distance == best_distance:
best_indices.append(index)
return best_indices[random.randi_range(0, best_indices.size() - 1)]
static func _score_jitter(
generation_seed: int,
coordinate: Vector2i,
biome_id: StringName,
) -> float:
var token := "%d:%d:%d:%s" % [
generation_seed,
coordinate.x,
coordinate.y,
biome_id,
]
return (
float(posmod(token.hash(), 1000))
/ 1000.0
* SCORE_JITTER
)
static func _definition_signature(
definitions: Array[TerrainBiomeDefinition],
) -> String:
var ids := PackedStringArray()
for definition: TerrainBiomeDefinition in definitions:
ids.append(String(definition.stable_id))
return "|".join(ids)
static func _record_less_than(first: Dictionary, second: Dictionary) -> bool:
return _coordinate_less_than(
first.get("coordinate", Vector2i.ZERO),
second.get("coordinate", Vector2i.ZERO),
)
static func _coordinate_less_than(first: Vector2i, second: Vector2i) -> bool:
if first.y == second.y:
return first.x < second.x
return first.y < second.y

View file

@ -0,0 +1 @@
uid://c3fsddgk0ksdq

View file

@ -0,0 +1,74 @@
class_name TerrainBiomeCatalog
extends Resource
@export var definitions: Array[TerrainBiomeDefinition] = []
func definition_for_id(stable_id: StringName) -> TerrainBiomeDefinition:
for definition: TerrainBiomeDefinition in definitions:
if definition != null and definition.stable_id == stable_id:
return definition
return null
func definitions_for_tags(
chunk_tags: PackedStringArray,
) -> Array[TerrainBiomeDefinition]:
var result: Array[TerrainBiomeDefinition] = []
for definition: TerrainBiomeDefinition in definitions:
if definition != null and definition.supports_chunk_tags(chunk_tags):
result.append(definition)
return result
func validation_errors() -> PackedStringArray:
var errors := PackedStringArray()
var seen_ids: Dictionary[StringName, bool] = {}
for index: int in definitions.size():
var definition := definitions[index]
if definition == null:
errors.append("Biome definition %d is empty." % index)
continue
if definition.stable_id == &"":
errors.append("Biome definition %d has no stable ID." % index)
elif seen_ids.has(definition.stable_id):
errors.append("Biome ID %s is duplicated." % definition.stable_id)
else:
seen_ids[definition.stable_id] = true
if definition.required_chunk_tags.is_empty():
errors.append(
"%s has no required chunk tags." % definition.stable_id
)
for required_tag: String in definition.required_chunk_tags:
if required_tag in definition.forbidden_chunk_tags:
errors.append(
"%s both requires and forbids chunk tag '%s'."
% [definition.stable_id, required_tag]
)
var seen_groups: Dictionary[StringName, bool] = {}
for rule_index: int in definition.prop_rules.size():
var rule := definition.prop_rules[rule_index]
if rule == null:
errors.append(
"%s prop rule %d is empty."
% [definition.stable_id, rule_index]
)
continue
if rule.procedural_group == &"":
errors.append(
"%s prop rule %d has no group."
% [definition.stable_id, rule_index]
)
elif seen_groups.has(rule.procedural_group):
errors.append(
"%s duplicates prop group %s."
% [definition.stable_id, rule.procedural_group]
)
else:
seen_groups[rule.procedural_group] = true
if rule.minimum_placements > 0 and rule.placement_chance <= 0:
errors.append(
"%s requires %s props but gives them no placement chance."
% [definition.stable_id, rule.procedural_group]
)
return errors

View file

@ -0,0 +1 @@
uid://jope30oqqt4

View file

@ -0,0 +1,27 @@
class_name TerrainBiomeDefinition
extends Resource
@export var stable_id: StringName
@export var label := ""
@export var required_chunk_tags := PackedStringArray()
@export var forbidden_chunk_tags := PackedStringArray()
## Larger values give this biome more territory around its seeded anchor.
@export_range(0.1, 10.0, 0.1) var region_weight := 1.0
@export var prop_rules: Array[TerrainBiomePropRule] = []
func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool:
for required_tag: String in required_chunk_tags:
if required_tag not in chunk_tags:
return false
for forbidden_tag: String in forbidden_chunk_tags:
if forbidden_tag in chunk_tags:
return false
return true
func prop_rule_for_group(group: StringName) -> TerrainBiomePropRule:
for rule: TerrainBiomePropRule in prop_rules:
if rule != null and rule.procedural_group == group:
return rule
return null

View file

@ -0,0 +1 @@
uid://g43mwisr1g3x

View file

@ -0,0 +1,21 @@
class_name TerrainBiomePropRule
extends Resource
@export var procedural_group: StringName
@export_range(0, 10000, 1) var placement_chance := 0
@export_range(0, 10000, 1) var adjacency_bonus := 0
@export_range(0, 10000, 1) var maximum_density := 10000
@export_range(0, 64, 1) var minimum_placements := 0
## Empty permits every compatible prop in the procedural group.
@export var allowed_prop_ids := PackedStringArray()
func allows_prop(definition: TerrainPropDefinition) -> bool:
return (
definition != null
and definition.procedural_group == procedural_group
and (
allowed_prop_ids.is_empty()
or String(definition.stable_id) in allowed_prop_ids
)
)

View file

@ -0,0 +1 @@
uid://bmsdftvupugx5

View file

@ -32,6 +32,18 @@ func validation_errors() -> PackedStringArray:
errors.append("%s has no primary mesh name." % definition.stable_id)
if definition.allowed_rotation_mask == 0:
errors.append("%s allows no rotations." % definition.stable_id)
var has_coast := "coast" in definition.tags
var has_ocean_edge := definition.ocean_facing_edges != 0
if has_coast and not has_ocean_edge:
errors.append(
"%s is coastal but has no ocean-facing edge."
% definition.stable_id
)
elif not has_coast and has_ocean_edge:
errors.append(
"%s has an ocean-facing edge without the coast tag."
% definition.stable_id
)
var has_fresh_water := "fresh_water" in definition.tags
var has_water_footprint := (
definition.water_surface_size.x > 0.0
@ -47,4 +59,37 @@ func validation_errors() -> PackedStringArray:
"%s has a water footprint without the freshwater tag."
% definition.stable_id
)
for allowed_tag: String in definition.allowed_neighbor_tags:
if allowed_tag in definition.forbidden_neighbor_tags:
errors.append(
(
"%s both allows and forbids neighbor tag '%s'."
% [definition.stable_id, allowed_tag]
)
)
for edge_value: int in TerrainChunkTopology.Edge.values():
var edge := edge_value as TerrainChunkTopology.Edge
for allowed_tag: String in (
definition.directional_allowed_neighbor_tags(edge)
):
if allowed_tag not in definition.forbidden_neighbor_tags:
continue
errors.append(
(
"%s both allows and forbids neighbor tag '%s' on its %s edge."
% [
definition.stable_id,
allowed_tag,
TerrainChunkTopology.edge_name(edge),
]
)
)
if (
definition.minimum_required_neighbors > 0
and definition.required_neighbor_tags.is_empty()
):
errors.append(
"%s requires neighbors but defines no required neighbor tags."
% definition.stable_id
)
return errors

View file

@ -11,6 +11,38 @@ var allowed_rotation_mask := 15
## 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.
@ -22,3 +54,70 @@ var allowed_rotation_mask := 15
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

File diff suppressed because it is too large Load diff

View file

@ -39,6 +39,19 @@ static func edge_normal(edge: Edge) -> Vector3:
return Vector3.ZERO
static func grid_offset(edge: Edge) -> Vector2i:
match edge:
Edge.NORTH:
return Vector2i.UP
Edge.EAST:
return Vector2i.RIGHT
Edge.SOUTH:
return Vector2i.DOWN
Edge.WEST:
return Vector2i.LEFT
return Vector2i.ZERO
static func rotated_edge(edge: Edge, quarter_turns: int) -> Edge:
var angle := float(posmod(quarter_turns, 4)) * PI * 0.5
var normal := edge_normal(edge).rotated(Vector3.UP, angle)

View file

@ -18,6 +18,23 @@ func stable_key() -> String:
return "%s@%d" % [definition.stable_id, posmod(quarter_turns, 4)]
func source_edge(edge: TerrainChunkTopology.Edge) -> TerrainChunkTopology.Edge:
return TerrainChunkTopology.rotated_edge(edge, -quarter_turns)
func allows_non_water_neighbor_on_edge(
neighbor: TerrainChunkVariant,
edge: TerrainChunkTopology.Edge,
) -> bool:
return (
neighbor != null
and definition.allows_non_water_neighbor_on_edge(
neighbor.definition,
source_edge(edge),
)
)
func topology_signature(quantization: float) -> String:
var tokens := PackedStringArray()
for edge_value: int in TerrainChunkTopology.Edge.values():
@ -26,6 +43,28 @@ func topology_signature(quantization: float) -> String:
return "|".join(tokens)
func constraint_signature(quantization: float) -> String:
return "%s|neighbors:%s|ocean:%d|in:%d|out:%d" % [
topology_signature(quantization),
_neighbor_rule_signature(),
rotated_edge_mask(definition.ocean_facing_edges),
rotated_edge_mask(definition.water_inlet_edges),
rotated_edge_mask(definition.water_outlet_edges),
]
func _neighbor_rule_signature() -> String:
var edge_tokens := PackedStringArray()
for edge_value: int in TerrainChunkTopology.Edge.values():
var edge := edge_value as TerrainChunkTopology.Edge
var tags: PackedStringArray = definition.allowed_neighbor_tags_for_edge(
source_edge(edge)
).duplicate()
tags.sort()
edge_tokens.append(",".join(tags))
return "/".join(edge_tokens)
func rotated_edge_mask(source_mask: int) -> int:
var result := 0
for edge_value: int in TerrainChunkTopology.Edge.values():

View file

@ -0,0 +1,85 @@
class_name TerrainPropCatalog
extends Resource
@export var definitions: Array[TerrainPropDefinition] = []
func definition_for_id(stable_id: StringName) -> TerrainPropDefinition:
for definition: TerrainPropDefinition in definitions:
if definition != null and definition.stable_id == stable_id:
return definition
return null
func procedural_definitions(
group: StringName,
chunk_tags: PackedStringArray,
) -> Array[TerrainPropDefinition]:
var result: Array[TerrainPropDefinition] = []
for definition: TerrainPropDefinition in definitions:
if (
definition != null
and definition.procedural_group == group
and definition.supports_chunk_tags(chunk_tags)
):
result.append(definition)
return result
func validation_errors() -> PackedStringArray:
var errors := PackedStringArray()
var seen_ids: Dictionary[StringName, bool] = {}
for index: int in definitions.size():
var definition := definitions[index]
if definition == null:
errors.append("Prop definition %d is empty." % index)
continue
if definition.stable_id == &"":
errors.append("Prop definition %d has no stable ID." % index)
elif seen_ids.has(definition.stable_id):
errors.append("Prop ID %s is duplicated." % definition.stable_id)
else:
seen_ids[definition.stable_id] = true
if definition.packed_scene == null:
errors.append("%s has no PackedScene." % definition.stable_id)
if (
definition.is_procedural()
and definition.required_chunk_tags.is_empty()
):
errors.append(
"%s is procedural but has no required chunk tags."
% definition.stable_id
)
if (
not definition.preferred_nearby_prop_groups.is_empty()
and definition.preferred_nearby_radius <= 0.0
):
errors.append(
"%s prefers nearby props but defines no search radius."
% definition.stable_id
)
var has_collision_radius := definition.collision_radius > 0.0
var has_collision_height := definition.collision_height > 0.0
if has_collision_radius != has_collision_height:
errors.append(
"%s must define both collision radius and height."
% definition.stable_id
)
if (
definition.has_cylinder_collision()
and definition.has_box_collision()
):
errors.append(
"%s defines more than one collision shape."
% definition.stable_id
)
if (
definition.collision_box_size.x < 0.0
or definition.collision_box_size.y < 0.0
or definition.collision_box_size.z < 0.0
):
errors.append(
"%s has a negative box collision dimension."
% definition.stable_id
)
return errors

View file

@ -0,0 +1 @@
uid://b7w7ouvnnf68r

View file

@ -0,0 +1,53 @@
class_name TerrainPropDefinition
extends Resource
@export var stable_id: StringName
@export var label := ""
@export var packed_scene: PackedScene
## Empty means the prop is available for deliberate placement only.
@export var procedural_group: StringName
@export var required_chunk_tags := PackedStringArray()
@export_range(0.01, 100.0, 0.01) var selection_weight := 1.0
## Manhattan distance in chunks from the forced center spawn. Two keeps the
## spawn chunk and its four immediate neighbors clear of this prop.
@export_range(0, 16, 1) var minimum_spawn_chunk_distance := 0
## These groups influence placement selection but are not a hard requirement.
## A nearby match multiplies this prop's weight; an absent match divides it by
## the same amount.
@export var preferred_nearby_prop_groups := PackedStringArray()
@export_range(0.0, 100.0, 0.5) var preferred_nearby_radius := 0.0
@export_range(1.0, 10.0, 0.1) var nearby_preference_weight_multiplier := 1.0
@export_range(0.0, 10.0, 0.05) var clearance_radius := 0.5
@export_range(0.0, 5.0, 0.05) var collision_radius := 0.0
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0
@export var collision_box_size := Vector3.ZERO
@export var collision_offset := Vector3.ZERO
## Values above zero add this prop to the tree-gathering anchor set.
@export_range(0.0, 20.0, 0.05) var gatherable_anchor_height := 0.0
func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool:
for required_tag: String in required_chunk_tags:
if required_tag not in chunk_tags:
return false
return true
func has_collision() -> bool:
return has_cylinder_collision() or has_box_collision()
func has_cylinder_collision() -> bool:
return collision_radius > 0.0 and collision_height > 0.0
func has_box_collision() -> bool:
return (
collision_box_size.x > 0.0
and collision_box_size.y > 0.0
and collision_box_size.z > 0.0
)
func is_procedural() -> bool:
return procedural_group != &""

View file

@ -0,0 +1 @@
uid://dc2883ey0dnys