Add deterministic generated worlds

This commit is contained in:
Alexander Sellite 2026-08-19 17:32:40 -04:00
parent 19f63bd656
commit e8888ed05a
87 changed files with 3743 additions and 139 deletions

Binary file not shown.

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://jv50rtyjn3j1"
path="res://.godot/imported/chunk_0000.glb-8221861eeebadbe0547c1162b0dc4018.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0000.glb"
dest_files=["res://.godot/imported/chunk_0000.glb-8221861eeebadbe0547c1162b0dc4018.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: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://doju2lbj2ghwa"
path.s3tc="res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.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_0000_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.s3tc.ctex", "res://.godot/imported/chunk_0000_grass_lite.png-12306f832f5f55ac4ec98276bdf209b9.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=false
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://uxb30e8bfw5v"
path="res://.godot/imported/chunk_0001.glb-a498b960fb79406faee2341d1edecb1a.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0001.glb"
dest_files=["res://.godot/imported/chunk_0001.glb-a498b960fb79406faee2341d1edecb1a.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: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3s1ebrqrsylj"
path.s3tc="res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.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_0001_sand.png"
dest_files=["res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.s3tc.ctex", "res://.godot/imported/chunk_0001_sand.png-67653175c1ccdaa529250778f806d3f8.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=false
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://dgd530dol7mfd"
path="res://.godot/imported/chunk_0002.glb-0a0d64db0049184f39957f77063387f4.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0002.glb"
dest_files=["res://.godot/imported/chunk_0002.glb-0a0d64db0049184f39957f77063387f4.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: 6.2 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://delam1fb8dqwx"
path.s3tc="res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.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_0002_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.s3tc.ctex", "res://.godot/imported/chunk_0002_grass_lite.png-c1692a06eb751734c5b996f92a9ca1af.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=false
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://bjsjdf3lknj1g"
path.s3tc="res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.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_0002_sand.png"
dest_files=["res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.s3tc.ctex", "res://.godot/imported/chunk_0002_sand.png-044d6a274c3b25b166e2b43f026482ed.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=false
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://2wl647bi18v5"
path="res://.godot/imported/chunk_0003.glb-913e99fdd0f30f24832fe5a7c1b7b865.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003.glb"
dest_files=["res://.godot/imported/chunk_0003.glb-913e99fdd0f30f24832fe5a7c1b7b865.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: 5.5 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://614ano6tj7ny"
path.s3tc="res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003_dirt.png"
dest_files=["res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.s3tc.ctex", "res://.godot/imported/chunk_0003_dirt.png-aec7f6dc456d2c2fe0bb22bdba6b692f.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=false
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: 3.6 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2votr034k2g3"
path.s3tc="res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "b5430dba0b52db87d30bae85ae640b09"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0003_dirt_wall.png"
dest_files=["res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.s3tc.ctex", "res://.godot/imported/chunk_0003_dirt_wall.png-19ae8be57bf9c33db0df849cb088b83c.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=false
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://cynuisx0jdsq5"
path.s3tc="res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.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_0003_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.s3tc.ctex", "res://.godot/imported/chunk_0003_grass_lite.png-4a0a167fc9e6752477003141049a1d21.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=false
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://codcyn5ta6aj"
path="res://.godot/imported/chunk_0004.glb-0ea01a6391edb5f687fee1991ba71993.scn"
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004.glb"
dest_files=["res://.godot/imported/chunk_0004.glb-0ea01a6391edb5f687fee1991ba71993.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: 5.5 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdpfqe4vn7235"
path.s3tc="res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "07d90b3c46a4c3d0c172bed143dc9436"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004_dirt.png"
dest_files=["res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.s3tc.ctex", "res://.godot/imported/chunk_0004_dirt.png-9c64a7ac5c9e75161623be94b3197f43.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=false
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: 3.6 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cm42r4jmqis7h"
path.s3tc="res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "b5430dba0b52db87d30bae85ae640b09"
}
[deps]
source_file="res://world/generation/chunks/assets/chunk_0004_dirt_wall.png"
dest_files=["res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.s3tc.ctex", "res://.godot/imported/chunk_0004_dirt_wall.png-8cbb5921c02a9dc6be9e3fff14c91409.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=false
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://kvyeovkwx72h"
path.s3tc="res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.s3tc.ctex"
path.etc2="res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.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_0004_grass_lite.png"
dest_files=["res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.s3tc.ctex", "res://.godot/imported/chunk_0004_grass_lite.png-152b0775592d1fbccc26c10057d15004.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=false
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

@ -0,0 +1,14 @@
[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" path="res://world/generation/chunks/assets/chunk_0000.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0000"
label = "grass"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 15
selection_weight = 4.0
tags = PackedStringArray("land", "grass", "flat")

View file

@ -0,0 +1,14 @@
[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" path="res://world/generation/chunks/assets/chunk_0001.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0001"
label = "sand"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0001"
allowed_rotation_mask = 15
selection_weight = 1.5
tags = PackedStringArray("land", "sand", "flat")

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://b2ha4ekenoiaj"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://dgd530dol7mfd" path="res://world/generation/chunks/assets/chunk_0002.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0002"
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")

View file

@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://dl7tsxml6g6na"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://2wl647bi18v5" path="res://world/generation/chunks/assets/chunk_0003.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0003"
label = "stream"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0003"
selection_weight = 0.6
tags = PackedStringArray("land", "fresh_water", "river", "flowing")
water_inlet_edges = 1
water_outlet_edges = 4
water_surface_size = Vector2(2.1, 10)
water_surface_offset = Vector2(-0.315, 0)

View file

@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="TerrainChunkDefinition" format=3 uid="uid://duotyi05ua5gp"]
[ext_resource type="Script" uid="uid://ck6m4auf5epde" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"]
[ext_resource type="PackedScene" uid="uid://codcyn5ta6aj" path="res://world/generation/chunks/assets/chunk_0004.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_0004"
label = "pond"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0004"
selection_weight = 0.35
tags = PackedStringArray("land", "fresh_water", "pond")
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,15 @@
[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" path="res://world/generation/chunks/assets/chunk_0000.glb" id="2_scene"]
[resource]
script = ExtResource("1_definition")
stable_id = &"chunk_spawn"
label = "spawn"
packed_scene = ExtResource("2_scene")
primary_mesh_name = &"chunk_0000"
allowed_rotation_mask = 1
selection_weight = 0.01
maximum_placements = 1
tags = PackedStringArray("land", "grass", "flat", "spawn")

View file

@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="TerrainChunkCatalog" load_steps=9 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"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0001.tres" id="3_sand"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0002.tres" id="4_beach"]
[ext_resource type="Resource" path="res://world/generation/chunks/definitions/chunk_0003.tres" id="5_stream"]
[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"]
[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")])

View file

@ -0,0 +1,507 @@
class_name GeneratedWorldRegion
extends WorldRegion
signal world_generated(seed: int, summary: Dictionary)
const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd"
)
const 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"
)
const FRESH_WATER_MATERIAL: Material = preload(
"res://world/materials/stylized_water_fresh.tres"
)
const POND_POOL: FishPool = preload(
"res://fish/pools/starter_pond_pool.tres"
)
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
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
@onready var _player_spawn: Marker3D = %PlayerSpawn
@onready var _safe_spawn: SafeRespawnPoint = %SafeSpawn
@onready var _fishing_shop: FishingShopInteractionType = (
$Interactables/FishingShopWorld/InteractionArea
)
@onready var _player_storage: PlayerStorageInteractionType = (
$Interactables/PlayerStorageBox/InteractionArea
)
@onready var _shop_root: Node3D = %FishingShopWorld
@onready var _storage_root: Node3D = %PlayerStorageBox
@onready var _decorations: Node3D = %Decorations
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
@onready var _ocean: WaterBodyAuthoring = %OceanWater
@onready var _fresh_water_root: Node3D = %FreshWaterBodies
@onready var _shoreline_reference: MeshInstance3D = %ShorelineReference
var _current_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
var _light_performance_profile := false
var _source_meshes: Dictionary[String, Mesh] = {}
var _source_bases: Dictionary[String, Basis] = {}
var _source_minimum_y: Dictionary[String, float] = {}
func _ready() -> void:
_generator.generation_completed.connect(_on_generation_completed)
_cache_prop_source(TREE_SOURCE_NAME)
_cache_prop_source(PALM_SOURCE_NAME)
_configure_static_water()
_build_shoreline_reference()
if not generate_world(initial_seed):
push_error("The initial generated world could not be built.")
func generate_world(seed: int) -> bool:
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
return false
_current_seed = seed
_generator.generation_seed = seed
return _generator.generate()
func get_generation_seed() -> int:
return _current_seed
func get_playable_half_extents() -> Vector2:
var size := Vector2(
float(_generator.grid_size.x),
float(_generator.grid_size.y),
) * _generator.catalog.chunk_size
return size * 0.5
func get_player_spawn_transform() -> Transform3D:
return _player_spawn.global_transform
func get_fishing_shop() -> FishingShopInteractionType:
return _fishing_shop
func get_player_storage() -> PlayerStorageInteractionType:
return _player_storage
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return _shoreline_reference
func set_light_performance_profile(enabled: bool) -> void:
_light_performance_profile = enabled
_apply_water_materials()
func get_spawn_surface_triangles(
material_names: Array[StringName],
minimum_global_y: float,
minimum_up_dot: float = 0.6,
) -> Array[PackedVector3Array]:
var triangles: Array[PackedVector3Array] = []
var root: Node3D = _generator.get_generated_chunks_root()
if root == null or material_names.is_empty():
return triangles
for mesh_instance: MeshInstance3D in _collect_mesh_instances(root):
var mesh: Mesh = mesh_instance.mesh
if mesh == null:
continue
for surface_index: int in mesh.get_surface_count():
var material: Material = mesh_instance.get_active_material(surface_index)
if material == null or not material_names.has(
StringName(material.resource_name)
):
continue
_append_surface_triangles(
triangles,
mesh_instance,
mesh.surface_get_arrays(surface_index),
minimum_global_y,
minimum_up_dot,
)
return triangles
func _on_generation_completed(summary: Dictionary) -> void:
var records: Array[Dictionary] = _generator.placement_records()
var spawn_position := Vector3.ZERO
_clear_children(_decorations)
_clear_children(_tree_anchors)
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
for record: Dictionary in records:
var position: Vector3 = record.get("position", Vector3.ZERO)
var tags: PackedStringArray = record.get(
"tags", PackedStringArray()
)
if "spawn" in tags:
spawn_position = position
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,
)
_place_spawn_amenities(spawn_position)
_configure_fresh_water(records)
_configure_diggable_area()
world_generated.emit(_current_seed, summary)
func _place_spawn_amenities(center: Vector3) -> void:
_player_spawn.position = center + Vector3(0.0, 0.18, 2.2)
_safe_spawn.position = _player_spawn.position
_shop_root.position = center + Vector3(2.35, 0.0, -1.35)
_shop_root.rotation.y = PI
_storage_root.position = center + Vector3(-2.4, 0.0, 1.2)
_storage_root.rotation.y = PI * 0.5
func _configure_static_water() -> void:
_ocean.water_type = WaterType.Type.SALT_WATER
_ocean.fish_pool = OCEAN_POOL
_ocean.location_tags = [&"coast", &"ocean", &"generated_ocean"]
_ocean.surface_size = Vector2(10000.0, 10000.0)
_ocean.position.y = WATER_HEIGHT - 0.025
_apply_water_materials()
func _configure_fresh_water(records: Array[Dictionary]) -> void:
_clear_children(_fresh_water_root)
for record: Dictionary in records:
var tags: PackedStringArray = record.get(
"tags", PackedStringArray()
)
if "fresh_water" not in tags:
continue
var surface_size: Vector2 = record.get(
"water_surface_size", Vector2.ZERO
)
if surface_size.x <= 0.0 or surface_size.y <= 0.0:
continue
var body := WATER_BODY_SCENE.instantiate() as WaterBodyAuthoring
if body == null:
continue
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
body.name = "FreshWater_%d_%d" % [coordinate.x, coordinate.y]
_fresh_water_root.add_child(body)
var turns := int(record.get("rotation_quarters", 0))
var angle := float(posmod(turns, 4)) * PI * 0.5
var offset: Vector2 = record.get(
"water_surface_offset", Vector2.ZERO
)
var rotated_offset := Vector3(offset.x, 0.0, offset.y).rotated(
Vector3.UP,
angle,
)
body.position = (
record.get("position", Vector3.ZERO)
+ rotated_offset
+ Vector3.UP * WATER_HEIGHT
)
body.rotation.y = angle
body.surface_size = surface_size
body.visual_surface_enabled = false
body.water_material = _fresh_water_material()
body.water_type = WaterType.Type.FRESH_WATER
body.fish_pool = POND_POOL
body.location_tags = _fresh_water_location_tags(tags)
body.selection_priority = 10
func _fresh_water_location_tags(tags: PackedStringArray) -> Array[StringName]:
var result: Array[StringName] = [&"generated_fresh_water"]
if "pond" in tags:
result.append(&"pond")
if "river" in tags:
result.append(&"river")
return result
func _configure_diggable_area() -> void:
var half_extents := get_playable_half_extents()
_diggable_beach.generation_bounds = Rect2(
-half_extents,
half_extents * 2.0,
)
func _apply_water_materials() -> void:
if not is_node_ready():
return
if _light_performance_profile:
_ocean.water_material = _light_water_material(
Color(0.11, 0.345, 0.435),
)
else:
_ocean.water_material = SALT_WATER_MATERIAL
for child: Node in _fresh_water_root.get_children():
var body := child as WaterBodyAuthoring
if body != null:
body.water_material = _fresh_water_material()
func _fresh_water_material() -> Material:
if _light_performance_profile:
return _light_water_material(Color(0.18, 0.46, 0.50))
return FRESH_WATER_MATERIAL
func _light_water_material(color: Color) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.albedo_color = color
material.roughness = 1.0
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
return material
func _maybe_add_prop(
source_name: String,
runtime_name: String,
chunk_center: Vector3,
chance: float,
random: RandomNumberGenerator,
add_anchor: bool,
) -> bool:
if random.randf() > chance or not _source_meshes.has(source_name):
return false
_add_prop(
source_name,
runtime_name,
chunk_center,
random,
add_anchor,
)
return true
func _add_prop(
source_name: String,
runtime_name: String,
chunk_center: Vector3,
random: RandomNumberGenerator,
add_anchor: bool,
) -> void:
if not _source_meshes.has(source_name):
return
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.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:
var anchor := Marker3D.new()
anchor.name = "TreeAnchor_%d" % _tree_anchors.get_child_count()
anchor.position = prop.position + Vector3(0.0, 2.15, 0.0)
_tree_anchors.add_child(anchor)
func _add_prop_collision(prop: Node3D, broad_tree: bool) -> void:
var body := StaticBody3D.new()
body.name = "TrunkCollision"
body.collision_layer = 1
body.collision_mask = 0
prop.add_child(body)
var collision := CollisionShape3D.new()
collision.name = "CollisionShape"
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,
)
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,
)
minimum_y = minf(minimum_y, (basis * corner).y)
return minimum_y if minimum_y < INF else 0.0
func _build_shoreline_reference() -> void:
var half := get_playable_half_extents()
var width := 0.05
var vertices := PackedVector3Array()
var indices := PackedInt32Array()
var corners := [
Vector3(-half.x, WATER_HEIGHT, -half.y),
Vector3(half.x, WATER_HEIGHT, -half.y),
Vector3(half.x, WATER_HEIGHT, half.y),
Vector3(-half.x, WATER_HEIGHT, half.y),
]
for index: int in 4:
var start: Vector3 = corners[index]
var finish: Vector3 = corners[(index + 1) % 4]
var direction := (finish - start).normalized()
var perpendicular := Vector3(-direction.z, 0.0, direction.x) * width
var base := vertices.size()
vertices.append_array(PackedVector3Array([
start - perpendicular,
start + perpendicular,
finish + perpendicular,
finish - perpendicular,
]))
indices.append_array(PackedInt32Array([
base, base + 1, base + 2,
base, base + 2, base + 3,
]))
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_INDEX] = indices
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
_shoreline_reference.mesh = mesh
func _append_surface_triangles(
result: Array[PackedVector3Array],
mesh_instance: MeshInstance3D,
arrays: Array,
minimum_global_y: float,
minimum_up_dot: float,
) -> void:
if arrays.size() <= Mesh.ARRAY_INDEX:
return
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
if vertices.is_empty():
return
if indices.is_empty():
for index: int in range(0, vertices.size() - 2, 3):
_append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot)
return
for index: int in range(0, indices.size() - 2, 3):
_append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot)
func _append_triangle(
result: Array[PackedVector3Array],
mesh_instance: MeshInstance3D,
local_a: Vector3,
local_b: Vector3,
local_c: Vector3,
minimum_global_y: float,
minimum_up_dot: float,
) -> void:
var a := mesh_instance.to_global(local_a)
var b := mesh_instance.to_global(local_b)
var c := mesh_instance.to_global(local_c)
if minf(a.y, minf(b.y, c.y)) <= minimum_global_y:
return
var cross := (b - a).cross(c - a)
if cross.length_squared() <= 0.0000001:
return
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
return
result.append(PackedVector3Array([a, b, c]))
func _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)
child.free()

View file

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

View file

@ -0,0 +1,94 @@
[gd_scene load_steps=12 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"]
[ext_resource type="Resource" path="res://world/generation/chunks/terrain_chunk_catalog.tres" id="3_catalog"]
[ext_resource type="PackedScene" path="res://world/water_body.tscn" id="4_water"]
[ext_resource type="PackedScene" path="res://world/interactables/fishing_shop_world.tscn" id="5_shop"]
[ext_resource type="PackedScene" path="res://world/interactables/player_storage_box.tscn" id="6_storage"]
[ext_resource type="Script" path="res://world/safe_respawn_point.gd" id="7_safe"]
[ext_resource type="Script" path="res://world/digging/diggable_area_3d.gd" id="8_diggable"]
[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"]
[node name="GeneratedWorldRegion" type="Node3D"]
script = ExtResource("1_region")
region_id = &"generated_world"
fishable_water_root = NodePath("WaterBodies")
water_recovery_root = NodePath("WaterBodies")
safe_respawns_root = NodePath("SafeRespawns")
diggable_area_root = NodePath("DiggableAreas")
gatherable_anchor_root = NodePath("GatherableAnchors")
[node name="Terrain" type="Node3D" parent="."]
[node name="TerrainChunkGenerator" type="Node3D" parent="Terrain"]
unique_name_in_owner = true
script = ExtResource("2_generator")
catalog = ExtResource("3_catalog")
grid_size = Vector2i(5, 5)
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_weight_multiplier = 64.0
maximum_backtracks = 100000
[node name="WaterBodies" type="Node3D" parent="."]
[node name="OceanWater" parent="WaterBodies" instance=ExtResource("4_water")]
unique_name_in_owner = true
surface_size = Vector2(10000, 10000)
fish_pool = ExtResource("10_ocean_pool")
water_type = 1
location_tags = Array[StringName]([&"coast", &"ocean", &"generated_ocean"])
[node name="FreshWaterBodies" type="Node3D" parent="WaterBodies"]
unique_name_in_owner = true
[node name="DiggableAreas" type="Node3D" parent="."]
[node name="DiggableBeach" type="Node3D" parent="DiggableAreas"]
unique_name_in_owner = true
script = ExtResource("8_diggable")
area_id = &"starter_beach"
terrain_source = NodePath("../../Terrain/TerrainChunkGenerator")
surface_materials = Array[StringName]([&"sand"])
generation_bounds = Rect2(-35, -35, 70, 70)
minimum_global_y = -0.24
maximum_global_y = 0.5
minimum_up_dot = 0.72
[node name="GatherableAnchors" type="Node3D" parent="."]
[node name="ReachableTreeTrunks" type="Node3D" parent="GatherableAnchors"]
unique_name_in_owner = true
script = ExtResource("9_anchors")
anchor_set_id = &"starter_reachable_tree_trunks"
[node name="SafeRespawns" type="Node3D" parent="."]
[node name="SafeSpawn" type="Marker3D" parent="SafeRespawns"]
unique_name_in_owner = true
script = ExtResource("7_safe")
[node name="PlayerSpawn" type="Marker3D" parent="."]
unique_name_in_owner = true
[node name="Interactables" type="Node3D" parent="."]
[node name="FishingShopWorld" parent="Interactables" instance=ExtResource("5_shop")]
unique_name_in_owner = true
[node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("6_storage")]
unique_name_in_owner = true
[node name="Decorations" type="Node3D" parent="."]
unique_name_in_owner = true
[node name="ShorelineReference" type="MeshInstance3D" parent="."]
unique_name_in_owner = true
visible = false
cast_shadow = 0

View file

@ -0,0 +1,216 @@
class_name TerrainChunkAnalyzer
extends RefCounted
const DEFAULT_EDGE_EPSILON := 0.005
const DEFAULT_PROFILE_QUANTIZATION := 0.001
static func create_variants(
definition: TerrainChunkDefinition,
chunk_size: float,
edge_epsilon := DEFAULT_EDGE_EPSILON,
profile_quantization := DEFAULT_PROFILE_QUANTIZATION,
) -> Array[TerrainChunkVariant]:
var variants: Array[TerrainChunkVariant] = []
if definition == null or definition.packed_scene == null:
return variants
var chunk_root := definition.packed_scene.instantiate()
var primary_mesh := find_primary_mesh(
chunk_root,
definition.primary_mesh_name,
)
if primary_mesh == null or primary_mesh.mesh == null:
push_error(
"Terrain chunk %s has no MeshInstance3D named %s."
% [definition.stable_id, definition.primary_mesh_name]
)
chunk_root.free()
return variants
var mesh_transform := _transform_relative_to(primary_mesh, chunk_root)
var boundary_points := _collect_boundary_points(
primary_mesh.mesh,
mesh_transform,
chunk_size,
edge_epsilon,
)
for quarter_turns: int in 4:
if not definition.allows_quarter_turn(quarter_turns):
continue
var rotated_points := _rotate_points(boundary_points, quarter_turns)
var variant := TerrainChunkVariant.new()
variant.definition = definition
variant.quarter_turns = quarter_turns
variant.edge_profiles = _build_profiles(
rotated_points,
chunk_size,
edge_epsilon,
profile_quantization,
)
variants.append(variant)
chunk_root.free()
return variants
static func find_primary_mesh(
root: Node,
primary_mesh_name: StringName,
) -> MeshInstance3D:
if root is MeshInstance3D and root.name == primary_mesh_name:
return root as MeshInstance3D
for candidate: Node in root.find_children(
"*",
"MeshInstance3D",
true,
false,
):
var mesh_instance := candidate as MeshInstance3D
if mesh_instance != null and mesh_instance.name == primary_mesh_name:
return mesh_instance
return null
static func _transform_relative_to(node: Node3D, root: Node) -> Transform3D:
var relative_transform := Transform3D.IDENTITY
var current: Node = node
while current != null and current != root:
if current is Node3D:
relative_transform = (
(current as Node3D).transform * relative_transform
)
current = current.get_parent()
return relative_transform
static func _collect_boundary_points(
mesh: Mesh,
mesh_transform: Transform3D,
chunk_size: float,
edge_epsilon: float,
) -> PackedVector3Array:
var result := PackedVector3Array()
var half_size := chunk_size * 0.5
var unique_points: Dictionary[Vector3i, bool] = {}
for surface_index: int in mesh.get_surface_count():
var arrays := mesh.surface_get_arrays(surface_index)
if arrays.is_empty():
continue
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
for source_vertex: Vector3 in vertices:
var vertex := mesh_transform * source_vertex
if not _is_boundary_point(vertex, half_size, edge_epsilon):
continue
var key := Vector3i(
roundi(vertex.x / edge_epsilon),
roundi(vertex.y / edge_epsilon),
roundi(vertex.z / edge_epsilon),
)
if unique_points.has(key):
continue
unique_points[key] = true
result.append(vertex)
return result
static func _is_boundary_point(
point: Vector3,
half_size: float,
tolerance: float,
) -> bool:
return (
absf(absf(point.x) - half_size) <= tolerance
or absf(absf(point.z) - half_size) <= tolerance
)
static func _rotate_points(
points: PackedVector3Array,
quarter_turns: int,
) -> PackedVector3Array:
var result := PackedVector3Array()
var angle := float(posmod(quarter_turns, 4)) * PI * 0.5
for point: Vector3 in points:
result.append(point.rotated(Vector3.UP, angle))
return result
static func _build_profiles(
boundary_points: PackedVector3Array,
chunk_size: float,
edge_epsilon: float,
profile_quantization: float,
) -> Array[TerrainChunkEdgeProfile]:
var profiles: Array[TerrainChunkEdgeProfile] = []
for edge_value: int in TerrainChunkTopology.Edge.values():
var edge := edge_value as TerrainChunkTopology.Edge
profiles.append(
_build_profile(
edge,
boundary_points,
chunk_size * 0.5,
edge_epsilon,
profile_quantization,
)
)
return profiles
static func _build_profile(
edge: TerrainChunkTopology.Edge,
boundary_points: PackedVector3Array,
half_size: float,
edge_epsilon: float,
profile_quantization: float,
) -> TerrainChunkEdgeProfile:
var unique_points: Dictionary[Vector2i, bool] = {}
var profile_points: Array[Vector2] = []
for point: Vector3 in boundary_points:
if not _point_is_on_edge(point, edge, half_size, edge_epsilon):
continue
var tangent := (
point.x
if edge in [
TerrainChunkTopology.Edge.NORTH,
TerrainChunkTopology.Edge.SOUTH,
]
else point.z
)
var key := Vector2i(
roundi(tangent / profile_quantization),
roundi(point.y / profile_quantization),
)
if unique_points.has(key):
continue
unique_points[key] = true
profile_points.append(
Vector2(key.x, key.y) * profile_quantization
)
profile_points.sort_custom(_profile_point_less_than)
return TerrainChunkEdgeProfile.new(
edge,
PackedVector2Array(profile_points),
)
static func _point_is_on_edge(
point: Vector3,
edge: TerrainChunkTopology.Edge,
half_size: float,
tolerance: float,
) -> bool:
match edge:
TerrainChunkTopology.Edge.NORTH:
return absf(point.z + half_size) <= tolerance
TerrainChunkTopology.Edge.EAST:
return absf(point.x - half_size) <= tolerance
TerrainChunkTopology.Edge.SOUTH:
return absf(point.z - half_size) <= tolerance
TerrainChunkTopology.Edge.WEST:
return absf(point.x + half_size) <= tolerance
return false
static func _profile_point_less_than(a: Vector2, b: Vector2) -> bool:
if is_equal_approx(a.x, b.x):
return a.y < b.y
return a.x < b.x

View file

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

View file

@ -0,0 +1,50 @@
class_name TerrainChunkCatalog
extends Resource
@export_range(1.0, 100.0, 0.5) var chunk_size := 10.0
@export var definitions: Array[TerrainChunkDefinition] = []
func definition_for_id(stable_id: StringName) -> TerrainChunkDefinition:
for definition: TerrainChunkDefinition in definitions:
if definition != null and definition.stable_id == stable_id:
return definition
return null
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("Definition %d is empty." % index)
continue
if definition.stable_id == &"":
errors.append("Definition %d has no stable ID." % index)
elif seen_ids.has(definition.stable_id):
errors.append("Stable 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.primary_mesh_name == &"":
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_fresh_water := "fresh_water" in definition.tags
var has_water_footprint := (
definition.water_surface_size.x > 0.0
and definition.water_surface_size.y > 0.0
)
if has_fresh_water and not has_water_footprint:
errors.append(
"%s is freshwater but has no water footprint."
% definition.stable_id
)
elif not has_fresh_water and has_water_footprint:
errors.append(
"%s has a water footprint without the freshwater tag."
% definition.stable_id
)
return errors

View file

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

View file

@ -0,0 +1,24 @@
class_name TerrainChunkDefinition
extends Resource
@export var stable_id: StringName
@export var label := ""
@export var packed_scene: PackedScene
@export var primary_mesh_name: StringName
@export_flags("0 degrees", "90 degrees", "180 degrees", "270 degrees")
var allowed_rotation_mask := 15
@export_range(0.01, 100.0, 0.01) var selection_weight := 1.0
## Negative values allow unlimited placements; zero disables random placement.
@export_range(-1, 1024, 1) var maximum_placements := -1
@export var tags := PackedStringArray()
@export_flags("North", "East", "South", "West") var water_inlet_edges := 0
@export_flags("North", "East", "South", "West") var water_outlet_edges := 0
## Optional generated water footprint in the chunk's unrotated local X/Z plane.
## Zero means this chunk contributes no standalone water surface.
@export var water_surface_size := Vector2.ZERO
@export var water_surface_offset := Vector2.ZERO
func allows_quarter_turn(quarter_turns: int) -> bool:
var normalized_turns := posmod(quarter_turns, 4)
return (allowed_rotation_mask & (1 << normalized_turns)) != 0

View file

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

View file

@ -0,0 +1,97 @@
class_name TerrainChunkEdgeProfile
extends RefCounted
var edge := TerrainChunkTopology.Edge.NORTH
var points := PackedVector2Array()
func _init(
profile_edge := TerrainChunkTopology.Edge.NORTH,
profile_points := PackedVector2Array(),
) -> void:
edge = profile_edge
points = profile_points
func matches(other: TerrainChunkEdgeProfile, tolerance: float) -> bool:
if other == null:
return false
var first_surface := _upper_surface_points()
var second_surface := other._upper_surface_points()
if first_surface.size() < 2 or second_surface.size() < 2:
return false
if (
absf(first_surface[0].x - second_surface[0].x) > tolerance
or absf(
first_surface[first_surface.size() - 1].x
- second_surface[second_surface.size() - 1].x
) > tolerance
):
return false
var sample_tangents := PackedFloat32Array()
for point: Vector2 in first_surface:
_append_unique_tangent(sample_tangents, point.x, tolerance)
for point: Vector2 in second_surface:
_append_unique_tangent(sample_tangents, point.x, tolerance)
sample_tangents.sort()
for tangent: float in sample_tangents:
var first_height := _sample_height(first_surface, tangent)
var second_height := _sample_height(second_surface, tangent)
if absf(first_height - second_height) > tolerance:
return false
return true
func _upper_surface_points() -> PackedVector2Array:
var result := PackedVector2Array()
for point: Vector2 in points:
if (
not result.is_empty()
and is_equal_approx(result[result.size() - 1].x, point.x)
):
var previous := result[result.size() - 1]
previous.y = maxf(previous.y, point.y)
result[result.size() - 1] = previous
continue
result.append(point)
return result
func _append_unique_tangent(
tangents: PackedFloat32Array,
tangent: float,
tolerance: float,
) -> void:
for existing: float in tangents:
if absf(existing - tangent) <= tolerance:
return
tangents.append(tangent)
func _sample_height(surface: PackedVector2Array, tangent: float) -> float:
if tangent <= surface[0].x:
return surface[0].y
for index: int in range(1, surface.size()):
var right := surface[index]
if tangent > right.x:
continue
var left := surface[index - 1]
var span := right.x - left.x
if is_zero_approx(span):
return maxf(left.y, right.y)
return lerpf(left.y, right.y, (tangent - left.x) / span)
return surface[surface.size() - 1].y
func signature(quantization: float) -> String:
var tokens := PackedStringArray()
for point: Vector2 in points:
tokens.append(
"%d:%d"
% [
roundi(point.x / quantization),
roundi(point.y / quantization),
]
)
return ",".join(tokens)

View file

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

View file

@ -0,0 +1,396 @@
class_name TerrainChunkGenerator
extends Node3D
signal generation_completed(summary: Dictionary)
const EDGE_PROFILE_QUANTIZATION := 0.001
@export var catalog: TerrainChunkCatalog
@export var grid_size := Vector2i(5, 5)
@export var generation_seed := 13001
@export var generate_on_ready := true
@export var build_collision := true
@export var show_chunk_labels := false
@export var force_center_chunk_id: StringName = &"chunk_0000"
@export var required_chunk_ids := PackedStringArray()
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
@export_range(0.001, 0.1, 0.001) var edge_match_tolerance := 0.01
@export_range(1, 100000, 1) var maximum_backtracks := 20000
var _variants: Array[TerrainChunkVariant] = []
var _placements: Array[TerrainChunkVariant] = []
var _random := RandomNumberGenerator.new()
var _generated_chunks: Node3D
var _backtrack_count := 0
func _ready() -> void:
if generate_on_ready:
call_deferred(&"generate")
func generate() -> bool:
_clear_generated_chunks()
if not _prepare_catalog():
return false
_random.seed = generation_seed
_backtrack_count = 0
_placements.clear()
_placements.resize(grid_size.x * grid_size.y)
if not _solve_cell(0):
push_error(
"Terrain generation could not solve a %dx%d grid with seed %d."
% [grid_size.x, grid_size.y, generation_seed]
)
return false
_instantiate_solution()
var summary := _build_summary()
generation_completed.emit(summary)
return true
func _prepare_catalog() -> bool:
if catalog == null:
push_error("TerrainChunkGenerator requires a catalog.")
return false
if grid_size.x <= 0 or grid_size.y <= 0:
push_error("TerrainChunkGenerator grid dimensions must be positive.")
return false
var catalog_errors := catalog.validation_errors()
if not catalog_errors.is_empty():
push_error("Terrain chunk catalog is invalid:\n" + "\n".join(catalog_errors))
return false
_variants.clear()
for definition: TerrainChunkDefinition in catalog.definitions:
var analyzed := TerrainChunkAnalyzer.create_variants(
definition,
catalog.chunk_size,
)
var seen_signatures: Dictionary[String, bool] = {}
for variant: TerrainChunkVariant in analyzed:
var signature := variant.topology_signature(
EDGE_PROFILE_QUANTIZATION
)
if seen_signatures.has(signature):
continue
seen_signatures[signature] = true
_variants.append(variant)
if _variants.is_empty():
push_error("Terrain chunk catalog produced no usable variants.")
return false
return true
func _solve_cell(index: int) -> bool:
if index >= _placements.size():
return _required_chunks_are_present()
if _backtrack_count >= maximum_backtracks:
return false
var candidates := _compatible_candidates(index)
for candidate: TerrainChunkVariant in _weighted_candidate_order(candidates):
_placements[index] = candidate
if _solve_cell(index + 1):
return true
_placements[index] = null
_backtrack_count += 1
if _backtrack_count >= maximum_backtracks:
break
return false
func _compatible_candidates(index: int) -> Array[TerrainChunkVariant]:
var result: Array[TerrainChunkVariant] = []
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
for candidate: TerrainChunkVariant in _variants:
if not _definition_has_capacity(candidate.definition):
continue
if (
coordinate == center
and force_center_chunk_id != &""
and candidate.definition.stable_id != force_center_chunk_id
):
continue
if coordinate.x > 0:
var west_neighbor := _placements[index - 1]
if (
west_neighbor != null
and not _edges_are_compatible(
candidate,
TerrainChunkTopology.Edge.WEST,
west_neighbor,
TerrainChunkTopology.Edge.EAST,
)
):
continue
if coordinate.y > 0:
var north_neighbor := _placements[index - grid_size.x]
if (
north_neighbor != null
and not _edges_are_compatible(
candidate,
TerrainChunkTopology.Edge.NORTH,
north_neighbor,
TerrainChunkTopology.Edge.SOUTH,
)
):
continue
result.append(candidate)
return result
func _definition_has_capacity(definition: TerrainChunkDefinition) -> bool:
if definition.maximum_placements < 0:
return true
var count := 0
for placement: TerrainChunkVariant in _placements:
if placement != null and placement.definition == definition:
count += 1
return count < definition.maximum_placements
func _edges_are_compatible(
first: TerrainChunkVariant,
first_edge: TerrainChunkTopology.Edge,
second: TerrainChunkVariant,
second_edge: TerrainChunkTopology.Edge,
) -> bool:
if not first.profile(first_edge).matches(
second.profile(second_edge),
edge_match_tolerance,
):
return false
var first_inlet := _variant_edge_has_connector(
first,
first.definition.water_inlet_edges,
first_edge,
)
var first_outlet := _variant_edge_has_connector(
first,
first.definition.water_outlet_edges,
first_edge,
)
var second_inlet := _variant_edge_has_connector(
second,
second.definition.water_inlet_edges,
second_edge,
)
var second_outlet := _variant_edge_has_connector(
second,
second.definition.water_outlet_edges,
second_edge,
)
var first_has_water := first_inlet or first_outlet
var second_has_water := second_inlet or second_outlet
if not first_has_water and not second_has_water:
return true
return (
(first_outlet and second_inlet)
or (first_inlet and second_outlet)
)
func _variant_edge_has_connector(
variant: TerrainChunkVariant,
source_mask: int,
edge: TerrainChunkTopology.Edge,
) -> bool:
return (variant.rotated_edge_mask(source_mask) & (1 << int(edge))) != 0
func _required_chunks_are_present() -> bool:
if required_chunk_ids.is_empty():
return true
var found: Dictionary[StringName, bool] = {}
for placement: TerrainChunkVariant in _placements:
if placement != null:
found[placement.definition.stable_id] = true
for required_id: String in required_chunk_ids:
if not found.has(StringName(required_id)):
return false
return true
func _weighted_candidate_order(
candidates: Array[TerrainChunkVariant],
) -> Array[TerrainChunkVariant]:
var scored: Array[Dictionary] = []
for candidate: TerrainChunkVariant in candidates:
var weight := maxf(candidate.definition.selection_weight, 0.01)
if _required_chunk_is_missing(candidate.definition.stable_id):
weight *= required_chunk_weight_multiplier
var sample := maxf(_random.randf(), 0.000001)
scored.append(
{
"variant": candidate,
"score": pow(sample, 1.0 / weight),
}
)
scored.sort_custom(_higher_candidate_score)
var result: Array[TerrainChunkVariant] = []
for entry: Dictionary in scored:
result.append(entry["variant"] as TerrainChunkVariant)
return result
func _required_chunk_is_missing(stable_id: StringName) -> bool:
if not required_chunk_ids.has(String(stable_id)):
return false
for placement: TerrainChunkVariant in _placements:
if (
placement != null
and placement.definition.stable_id == stable_id
):
return false
return true
func _instantiate_solution() -> void:
_generated_chunks = Node3D.new()
_generated_chunks.name = "GeneratedChunks"
add_child(_generated_chunks)
var half_grid := Vector2(
float(grid_size.x - 1) * 0.5,
float(grid_size.y - 1) * 0.5,
)
for index: int in _placements.size():
var variant := _placements[index]
if variant == null:
continue
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
var chunk_root := variant.definition.packed_scene.instantiate() as Node3D
if chunk_root == null:
push_error("%s does not instantiate as Node3D." % variant.stable_key())
continue
chunk_root.name = "%s_%d_%d" % [
variant.stable_key().replace("@", "r"),
coordinate.x,
coordinate.y,
]
chunk_root.position = Vector3(
(float(coordinate.x) - half_grid.x) * catalog.chunk_size,
0.0,
(float(coordinate.y) - half_grid.y) * catalog.chunk_size,
)
chunk_root.rotation.y = variant.rotation_radians()
_generated_chunks.add_child(chunk_root)
if build_collision:
_add_collision(chunk_root, variant.definition)
if show_chunk_labels:
_add_chunk_label(chunk_root, variant)
func _add_collision(
chunk_root: Node3D,
definition: TerrainChunkDefinition,
) -> void:
var primary_mesh := TerrainChunkAnalyzer.find_primary_mesh(
chunk_root,
definition.primary_mesh_name,
)
if primary_mesh == null or primary_mesh.mesh == null:
return
var terrain_shape := primary_mesh.mesh.create_trimesh_shape()
if terrain_shape == null or terrain_shape.get_faces().is_empty():
push_warning("%s produced no terrain collision." % definition.stable_id)
return
var collision_body := StaticBody3D.new()
collision_body.name = "TerrainCollision"
collision_body.collision_layer = 1
collision_body.collision_mask = 0
primary_mesh.add_child(collision_body)
var collision_shape := CollisionShape3D.new()
collision_shape.name = "TerrainShape"
collision_shape.shape = terrain_shape
collision_body.add_child(collision_shape)
func _add_chunk_label(
chunk_root: Node3D,
variant: TerrainChunkVariant,
) -> void:
var label := Label3D.new()
label.name = "ChunkLabel"
label.text = "%s r%d" % [
variant.definition.stable_id,
variant.quarter_turns,
]
label.position = Vector3(0.0, 1.25, 0.0)
label.font_size = 24
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
label.no_depth_test = true
chunk_root.add_child(label)
func _clear_generated_chunks() -> void:
if is_instance_valid(_generated_chunks):
remove_child(_generated_chunks)
_generated_chunks.free()
_generated_chunks = null
func _build_summary() -> Dictionary:
var counts: Dictionary[StringName, int] = {}
for variant: TerrainChunkVariant in _placements:
if variant == null:
continue
var stable_id := variant.definition.stable_id
counts[stable_id] = counts.get(stable_id, 0) + 1
return {
"seed": generation_seed,
"grid_size": grid_size,
"chunk_count": _placements.size(),
"variant_count": _variants.size(),
"backtracks": _backtrack_count,
"counts": counts,
"placements": placement_keys(),
}
func placement_keys() -> PackedStringArray:
var result := PackedStringArray()
for variant: TerrainChunkVariant in _placements:
result.append(variant.stable_key() if variant != null else "empty")
return result
func placement_records() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for index: int in _placements.size():
var variant: TerrainChunkVariant = _placements[index]
if variant == null:
continue
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
result.append({
"coordinate": coordinate,
"position": chunk_position(coordinate),
"rotation_quarters": variant.quarter_turns,
"stable_id": variant.definition.stable_id,
"tags": variant.definition.tags,
"water_surface_size": variant.definition.water_surface_size,
"water_surface_offset": variant.definition.water_surface_offset,
})
return result
func chunk_position(coordinate: Vector2i) -> Vector3:
if catalog == null:
return Vector3.ZERO
var half_grid := Vector2(
float(grid_size.x - 1) * 0.5,
float(grid_size.y - 1) * 0.5,
)
return Vector3(
(float(coordinate.x) - half_grid.x) * catalog.chunk_size,
0.0,
(float(coordinate.y) - half_grid.y) * catalog.chunk_size,
)
func get_generated_chunks_root() -> Node3D:
return _generated_chunks
static func _higher_candidate_score(a: Dictionary, b: Dictionary) -> bool:
return float(a["score"]) > float(b["score"])

View file

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

View file

@ -0,0 +1,53 @@
class_name TerrainChunkTopology
extends RefCounted
enum Edge {
NORTH,
EAST,
SOUTH,
WEST,
}
static func opposite_edge(edge: Edge) -> Edge:
return ((int(edge) + 2) % 4) as Edge
static func edge_name(edge: Edge) -> String:
match edge:
Edge.NORTH:
return "north"
Edge.EAST:
return "east"
Edge.SOUTH:
return "south"
Edge.WEST:
return "west"
return "unknown"
static func edge_normal(edge: Edge) -> Vector3:
match edge:
Edge.NORTH:
return Vector3.FORWARD
Edge.EAST:
return Vector3.RIGHT
Edge.SOUTH:
return Vector3.BACK
Edge.WEST:
return Vector3.LEFT
return Vector3.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)
var best_edge := Edge.NORTH
var best_dot := -INF
for candidate_value: int in Edge.values():
var candidate := candidate_value as Edge
var alignment := normal.dot(edge_normal(candidate))
if alignment > best_dot:
best_dot = alignment
best_edge = candidate
return best_edge

View file

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

View file

@ -0,0 +1,39 @@
class_name TerrainChunkVariant
extends RefCounted
var definition: TerrainChunkDefinition
var quarter_turns := 0
var edge_profiles: Array[TerrainChunkEdgeProfile] = []
func profile(edge: TerrainChunkTopology.Edge) -> TerrainChunkEdgeProfile:
return edge_profiles[int(edge)]
func rotation_radians() -> float:
return float(posmod(quarter_turns, 4)) * PI * 0.5
func stable_key() -> String:
return "%s@%d" % [definition.stable_id, posmod(quarter_turns, 4)]
func topology_signature(quantization: float) -> String:
var tokens := PackedStringArray()
for edge_value: int in TerrainChunkTopology.Edge.values():
var edge := edge_value as TerrainChunkTopology.Edge
tokens.append(profile(edge).signature(quantization))
return "|".join(tokens)
func rotated_edge_mask(source_mask: int) -> int:
var result := 0
for edge_value: int in TerrainChunkTopology.Edge.values():
if (source_mask & (1 << edge_value)) == 0:
continue
var rotated := TerrainChunkTopology.rotated_edge(
edge_value as TerrainChunkTopology.Edge,
quarter_turns,
)
result |= 1 << int(rotated)
return result

View file

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

View file

@ -10,6 +10,38 @@ extends Node3D
@export var gatherable_anchor_root: NodePath = ^"GatherableAnchors"
func get_player_spawn_transform() -> Transform3D:
return global_transform
func get_fishing_shop() -> FishingShopInteraction:
return null
func get_player_storage() -> PlayerStorageInteraction:
return null
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return null
func set_light_performance_profile(_enabled: bool) -> void:
pass
func get_playable_half_extents() -> Vector2:
return Vector2(50.0, 50.0)
func get_spawn_surface_triangles(
_material_names: Array[StringName],
_minimum_global_y: float,
_minimum_up_dot: float = 0.6,
) -> Array[PackedVector3Array]:
return []
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
var regions: Array[FishableWaterRegion] = []
var root: Node = get_node_or_null(fishable_water_root)

View file

@ -9,18 +9,10 @@ const PlayerStorageInteractionType = preload(
)
@onready var _regions_root: Node3D = $Regions
@onready var _starter_island: StarterIslandRegion = (
$Regions/StarterIslandRegion
)
@onready var _active_region: WorldRegion = _find_active_region()
@onready var _below_world_failsafe: PlayerWaterTrigger = (
$Safety/BelowWorldFailsafe
)
@onready var _fishing_shop: FishingShopInteractionType = (
_starter_island.get_fishing_shop()
)
@onready var _player_storage: PlayerStorageInteractionType = (
_starter_island.get_player_storage()
)
@onready var _world_environment: WorldEnvironment = $Environment/WorldEnvironment
@onready var _sun: DirectionalLight3D = $Environment/Sun
@ -41,15 +33,15 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
func get_fishing_shop() -> FishingShopInteractionType:
return _fishing_shop
return _active_region.get_fishing_shop()
func get_player_storage() -> PlayerStorageInteractionType:
return _player_storage
return _active_region.get_player_storage()
func get_player_spawn_transform() -> Transform3D:
return _starter_island.get_player_spawn_transform()
return _active_region.get_player_spawn_transform()
func get_world_environment() -> WorldEnvironment:
@ -61,7 +53,7 @@ func get_sun() -> DirectionalLight3D:
func set_light_performance_profile(enabled: bool) -> void:
_starter_island.set_light_performance_profile(enabled)
_active_region.set_light_performance_profile(enabled)
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
@ -72,19 +64,34 @@ func get_fishable_water_regions() -> Array[FishableWaterRegion]:
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return _starter_island.get_saltwater_shoreline_mesh()
return _active_region.get_saltwater_shoreline_mesh()
func get_spawn_surface_triangles(
material_names: Array[StringName],
minimum_global_y: float,
) -> Array[PackedVector3Array]:
return _starter_island.get_spawn_surface_triangles(
return _active_region.get_spawn_surface_triangles(
material_names,
minimum_global_y,
)
func generate_world(seed: int) -> bool:
if _active_region == null or not _active_region.has_method("generate_world"):
return false
var generated: bool = bool(_active_region.call("generate_world", seed))
if generated:
_configure_world_coverage()
return generated
func get_generation_seed() -> int:
if _active_region != null and _active_region.has_method("get_generation_seed"):
return int(_active_region.call("get_generation_seed"))
return PlayerSaveManager.DEFAULT_WORLD_SEED
func get_diggable_area_triangles(
area_id: StringName,
) -> Array[PackedVector3Array]:
@ -114,3 +121,51 @@ func _get_regions() -> Array[WorldRegion]:
if region != null:
regions.append(region)
return regions
func _ready() -> void:
_configure_world_coverage()
func _find_active_region() -> WorldRegion:
for child: Node in _regions_root.get_children():
var region := child as WorldRegion
if region != null:
return region
return null
func _configure_world_coverage() -> void:
if _active_region == null:
return
var half: Vector2 = _active_region.get_playable_half_extents()
var wall_margin := 2.0
var wall_height := 14.0
var bounds_root := $WorldBounds as Node3D
var north_south_size := Vector3(
half.x * 2.0 + wall_margin * 2.0,
wall_height,
2.0,
)
var east_west_size := Vector3(
2.0,
wall_height,
half.y * 2.0 + wall_margin * 2.0,
)
_set_bound(bounds_root.get_node("North"), Vector3(0.0, 5.0, half.y + wall_margin), north_south_size)
_set_bound(bounds_root.get_node("South"), Vector3(0.0, 5.0, -half.y - wall_margin), north_south_size)
_set_bound(bounds_root.get_node("West"), Vector3(-half.x - wall_margin, 5.0, 0.0), east_west_size)
_set_bound(bounds_root.get_node("East"), Vector3(half.x + wall_margin, 5.0, 0.0), east_west_size)
_below_world_failsafe.position = Vector3(0.0, -7.0, 0.0)
var coverage := _below_world_failsafe.get_node("Coverage") as CollisionShape3D
var coverage_shape := coverage.shape as BoxShape3D
if coverage_shape != null:
coverage_shape.size = Vector3(half.x * 2.0, 4.0, half.y * 2.0)
func _set_bound(body: Node3D, position: Vector3, size: Vector3) -> void:
body.position = position
var collision := body.get_node("Shape") as CollisionShape3D
var shape := collision.shape as BoxShape3D
if shape != null:
shape.size = size

View file

@ -1,7 +1,7 @@
[gd_scene load_steps=10 format=3]
[ext_resource type="Script" path="res://world/test_world.gd" id="1_world"]
[ext_resource type="PackedScene" path="res://world/regions/starter_island_region.tscn" id="2_island"]
[ext_resource type="PackedScene" path="res://world/generation/generated_world_region.tscn" id="2_island"]
[ext_resource type="Script" path="res://world/below_world_failsafe.gd" id="3_water_trigger"]
[ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="4_environment"]
@ -32,7 +32,7 @@ shadow_blur = 2.0
[node name="Regions" type="Node3D" parent="."]
[node name="StarterIslandRegion" parent="Regions" instance=ExtResource("2_island")]
[node name="GeneratedWorldRegion" parent="Regions" instance=ExtResource("2_island")]
[node name="Safety" type="Node3D" parent="."]

View file

@ -25,6 +25,12 @@ var surface_size: Vector2 = Vector2(10.0, 10.0):
set(value):
water_material = value
_sync_owned_nodes()
## Render the owned plane. Disable this when another shared water surface
## already provides visible water and this body only supplies gameplay data.
@export var visual_surface_enabled := true:
set(value):
visual_surface_enabled = value
_sync_owned_nodes()
@export_group("Fishing Coverage")
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
@ -85,6 +91,7 @@ func _sync_owned_nodes() -> void:
return
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
if visual_water != null:
visual_water.visible = visual_surface_enabled
var plane_mesh := visual_water.mesh as PlaneMesh
if plane_mesh != null and not derive_coverage_from_visual_mesh:
plane_mesh.size = surface_size

View file

@ -69,6 +69,37 @@ func setup(
)
func update_world_context(
initial_spawn_transform: Transform3D,
water_triggers: Array[PlayerWaterTrigger],
safe_points: Array[SafeRespawnPoint],
) -> void:
for water_trigger: PlayerWaterTrigger in _water_triggers:
if (
water_trigger != null
and is_instance_valid(water_trigger)
and water_trigger.recovery_requested.is_connected(
_on_recovery_requested
)
):
water_trigger.recovery_requested.disconnect(
_on_recovery_requested
)
_initial_spawn_transform = initial_spawn_transform
_water_triggers = water_triggers
_safe_points = safe_points
for water_trigger: PlayerWaterTrigger in _water_triggers:
if (
water_trigger != null
and not water_trigger.recovery_requested.is_connected(
_on_recovery_requested
)
):
water_trigger.recovery_requested.connect(
_on_recovery_requested
)
func _process(delta: float) -> void:
if state != RecoveryState.BOBBING or _player == null:
return