Expand generated terrain and forest variation
This commit is contained in:
parent
779e3d983e
commit
b2752b1074
25 changed files with 898 additions and 145 deletions
|
|
@ -89,9 +89,8 @@ presented through a uniformly scaled SubViewport. Shared UI components and
|
|||
palette resources prevent page-specific geometry and style drift.
|
||||
|
||||
World presentation systems (time/weather visuals, procedural sky and water,
|
||||
shoreline ribbons, player blob shadows) do not own gameplay collision or
|
||||
network authority. Generated shoreline meshes are deterministic presentation
|
||||
resources baked from explicitly configured static terrain.
|
||||
shoreline treatment, player blob shadows) do not own gameplay collision or
|
||||
network authority.
|
||||
|
||||
### Engineering consequences
|
||||
|
||||
|
|
@ -129,9 +128,9 @@ remain authoritative in `.tres` resources and typed scripts.
|
|||
wallet exactly once.
|
||||
- Reserved assets are rejected atomically; mixed valid and reserved batches
|
||||
must not partially succeed.
|
||||
- Item effects, shop prices, cooler capacity, fishing upgrades, jobs, and
|
||||
experience are separate balance axes. Avoid changing several in an
|
||||
unrelated presentation pass.
|
||||
- Item effects, shop prices, inventory and storage capacity, fishing upgrades,
|
||||
jobs, and experience are separate balance axes. Avoid changing several in
|
||||
an unrelated presentation pass.
|
||||
|
||||
Record affected stable IDs and resource paths, old and new values, intended
|
||||
player-facing outcome, interactions with quality/availability/economy,
|
||||
|
|
@ -143,70 +142,57 @@ authored number changed.
|
|||
|
||||
### World composition
|
||||
|
||||
The active test world uses the canonical starter island through
|
||||
`world/regions/starter_island_region.tscn`.
|
||||
`world/test_world.tscn` is the shared gameplay stage. It owns the environment,
|
||||
sun, world bounds, and below-world failsafe, then hosts exactly one active
|
||||
`WorldRegion`. Two canonical region implementations are supported:
|
||||
|
||||
`world/test_world.tscn` owns the gameplay-wide environment, sun, world bounds,
|
||||
and below-world failsafe. `StarterIslandRegion` owns the placed island content:
|
||||
- the generated world, which is the default for new games and is rebuilt from
|
||||
the saved seed; and
|
||||
- the starter island, which remains an authored selectable layout and a useful
|
||||
compatibility/reference scene.
|
||||
|
||||
The stable layout IDs live in `world/world_layout.gd`. Do not infer a layout
|
||||
from a scene filename, display label, or region node name. Both implementations
|
||||
must satisfy the shared `WorldRegion` contract for player spawn, safe respawns,
|
||||
water regions, recovery, gathering, digging, the shop, and player storage.
|
||||
|
||||
A region owns the content that changes with the selected world:
|
||||
|
||||
```text
|
||||
StarterIslandRegion
|
||||
WorldRegion
|
||||
├── Terrain
|
||||
│ ├── Visual
|
||||
│ └── Collision
|
||||
├── WaterBodies
|
||||
│ ├── Pond
|
||||
│ │ ├── VisualWater
|
||||
│ │ ├── FishingRegion
|
||||
│ │ └── RecoveryRegion
|
||||
│ └── Ocean
|
||||
│ ├── VisualWater
|
||||
│ ├── FishingRegions
|
||||
│ └── RecoveryRegions
|
||||
├── PlayerSpawn
|
||||
├── SafeRespawns
|
||||
├── DiggableAreas
|
||||
├── GatherableAnchors
|
||||
└── Interactables
|
||||
├── FishingShopWorld
|
||||
└── PelicanCoolerPerch
|
||||
└── PlayerStorageBox
|
||||
```
|
||||
|
||||
Move a meaningful feature root rather than one of its implementation children.
|
||||
Child transforms are local offsets owned by that feature.
|
||||
The generated region owns its chunk, biome, and prop catalogs and derives
|
||||
terrain collision, water coverage, biome placement, and presentation reference
|
||||
geometry from the generated result. The starter-island region owns its imported
|
||||
terrain hierarchy and explicitly placed content. Code shared by both layouts
|
||||
must depend on `WorldRegion`, not a starter-island child path.
|
||||
|
||||
Every fishable region authors its `water_type` explicitly. The starter pond is
|
||||
`FRESH_WATER`; the starter coast is `SALT_WATER`. Fish species use the same
|
||||
central type through an allowed-water-type bitmask. Do not infer habitat from
|
||||
node names, pool filenames, coordinates, or water height.
|
||||
Every fishable water body authors its water type explicitly. Fish species use
|
||||
the same central type through an allowed-water-type bitmask. Do not infer
|
||||
habitat from node names, pool filenames, coordinates, or water height.
|
||||
|
||||
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
|
||||
authoritative surface position; surface size and fishing/recovery coverage are
|
||||
owned together. Select `WaterBodies/Ocean` to move the surrounding water as one
|
||||
feature. Its fishing coverage is derived from the visible water mesh and the
|
||||
terrain collision determines the shoreline. New or revised land meshes do not
|
||||
require hand-authored fishing exclusions. Recovery volumes remain explicit so
|
||||
they can follow the playable world bounds rather than the horizon-sized visual
|
||||
ocean.
|
||||
In the generated layout, seed and layout ID are persistent data. Chunk IDs,
|
||||
edge rules, biome definitions, and prop definitions are authored resources;
|
||||
changing them can change the world produced by an existing seed and therefore
|
||||
requires deliberate compatibility review.
|
||||
|
||||
Placed-feature ownership:
|
||||
For the authored starter island, move a meaningful feature root rather than one
|
||||
of its implementation children. Child transforms are local offsets owned by
|
||||
that feature. Its imported GLB hierarchy is the starter region's terrain and
|
||||
collision authority; this rule does not describe generated-world collision.
|
||||
|
||||
- `PlayerSpawn` defines the initial player transform and facing.
|
||||
- `SafeRespawns` contains authoritative recovery destinations. Recovery
|
||||
preserves current facing.
|
||||
- `Interactables/FishingShopWorld` owns the shop visual, collision, interaction
|
||||
area, prompt, and entrance marker.
|
||||
- `Interactables/PelicanCoolerPerch` owns the Pelican landmark visual; selling
|
||||
remains a Cooler action.
|
||||
- `Terrain` owns imported visuals and generated collision.
|
||||
- `BelowWorldFailsafe/Coverage` owns below-world recovery coverage.
|
||||
|
||||
The starter-island GLB hierarchy is the terrain-collision authority. The
|
||||
region rebuilds one concave collision shape from every imported
|
||||
`MeshInstance3D` when it loads, so newly exported terrain and props participate
|
||||
without a separate stale collision bake. Keep purely decorative meshes out of
|
||||
that hierarchy if they should not block players.
|
||||
|
||||
The active `Environment`, `Sun`, and starter-island grass material remain
|
||||
Inspector-authored and are not recreated by runtime scripts.
|
||||
The active `Environment` and `Sun` remain Inspector-authored on the shared
|
||||
stage and are not recreated by either region.
|
||||
|
||||
### Reusable world props
|
||||
|
||||
|
|
@ -226,29 +212,14 @@ root scaling is acceptable when visual and collision scale together; avoid
|
|||
non-uniform root scaling. Make mutable per-instance shapes, meshes, and
|
||||
materials local to the scene.
|
||||
|
||||
### Shoreline tide ribbons
|
||||
### Shoreline presentation
|
||||
|
||||
Maps using generated tide ribbons own a `ShorelineRibbonBaker` and one
|
||||
`ShorelineRibbonConfig` per water body. Each configuration selects only its
|
||||
static terrain source, water height, generation bounds, water-facing reference,
|
||||
smoothing overrides, and generated `.tres` path. Props, players, bobbers, and
|
||||
gameplay areas are not scanned unless selected explicitly. Only `SALT_WATER`
|
||||
bodies generate ribbons.
|
||||
|
||||
The editor automatically rebuilds configured shoreline resources when an
|
||||
authored terrain GLB is reimported. Select the baker and press **Rebuild
|
||||
Shoreline Ribbons** only when intentionally forcing a rebuild without a terrain
|
||||
reimport. The equivalent headless fallback and validation command is:
|
||||
|
||||
```sh
|
||||
godot --headless --path . --script scripts/bake_shoreline_ribbons.gd
|
||||
```
|
||||
|
||||
Use `debug_path_stage` only while comparing extraction stages and leave it Off
|
||||
in committed scenes. A rebuild is required when saltwater terrain at the
|
||||
waterline, water height, or generation bounds change; terrain reimports handle
|
||||
the usual case automatically. Normal gameplay loads committed generated meshes
|
||||
and never runs extraction.
|
||||
The visible shoreline treatment is currently produced by the water materials.
|
||||
The older smooth-ribbon baker and generated starter-island ribbon resource are
|
||||
retained as development history/tooling, but the ribbon mesh is disabled and
|
||||
is not part of the current rendered shoreline. Do not treat a ribbon rebuild
|
||||
as a normal terrain-authoring requirement or re-enable the mesh as incidental
|
||||
cleanup.
|
||||
|
||||
### Facial-feature textures
|
||||
|
||||
|
|
@ -299,12 +270,12 @@ with a -3 dBFS peak ceiling, mono, 48 kHz, 16-bit PCM. Import a supplied set
|
|||
through the normalization script rather than copying its WAV files directly:
|
||||
|
||||
```bash
|
||||
scripts/import_animalese_voice.sh /path/to/source_clips sample_set_directory
|
||||
scripts/import_animalese_voice.sh /path/to/source_clips <sample-set-id>
|
||||
```
|
||||
|
||||
The source directory may contain lowercase letter, number, and underscore WAV
|
||||
filenames. The script converts every top-level WAV and writes the results under
|
||||
`sound/dialogue/animalese/sample_set_directory/`. Add a new runtime set to
|
||||
`sound/dialogue/animalese/<sample-set-id>/`. Add a new runtime set to
|
||||
`player/animalese_voice_profiles.gd` after import, then run the quick validation
|
||||
suite. Keep the untouched submission and its original hashes in the private
|
||||
intake record; repository files are normalized derivatives.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
NETfishing
|
||||
v0.14.0-alpha
|
||||
Alpha 0.14.0
|
||||
Alpha playtest
|
||||
|
||||
Thank you for trying this early private playtest.
|
||||
|
||||
|
|
@ -30,13 +29,16 @@ Sprint Shift
|
|||
Sneak Ctrl or click right stick
|
||||
Slow walk Alt
|
||||
Rotate camera Hold right mouse, or use right stick
|
||||
Cycle active hotbar slot Mouse wheel
|
||||
Cycle active hotbar slot Mouse wheel or D-pad left/right
|
||||
Select hotbar slot 1 through 9
|
||||
Zoom camera Shift + mouse wheel
|
||||
Cast / withdraw / reel Left mouse
|
||||
Cooler, Bag, and Logbook Tab
|
||||
Primary tool / fishing Left mouse or right trigger
|
||||
Inventory and player pages Tab
|
||||
Game Menu / back Escape
|
||||
Open Fishing Shop E while near the shop
|
||||
Interact E
|
||||
Chat T
|
||||
|
||||
Keyboard and controller bindings can be reviewed and changed in Settings.
|
||||
|
||||
FISHING
|
||||
|
||||
|
|
@ -48,12 +50,14 @@ FISHING
|
|||
- At barriers, use distinct left-click presses to damage the barrier.
|
||||
- Do not let the red chase meter catch the green meter.
|
||||
- Accessibility auto-click can be enabled in Settings.
|
||||
- Approach the bright Fishing Shop booth and press E to open it.
|
||||
- The Fishing Shop buys one Cooler fish at a time for full base value.
|
||||
- Buy Coffee, Energy Drinks, Snacks, and Fish Finders as Bag supplies.
|
||||
- Drag supplies from Bag to the hotbar and left-click in READY to use one.
|
||||
- Approach the Fishing Shop and interact to open it.
|
||||
- Move sellable inventory entries into the shop's sell tray, review the total,
|
||||
and confirm the sale.
|
||||
- Buy supplies, equipment, and persistent upgrades from their shop tabs.
|
||||
- Move world-use items into the hotbar and select their slot to use or hold
|
||||
them.
|
||||
- Coffee improves movement; other supplies temporarily improve fishing.
|
||||
- The Cooler starts at 12 fish; buy permanent capacity expansions at the shop.
|
||||
- Inventory and storage capacity can be expanded permanently at the shop.
|
||||
- Reel Speed upgrades increase authoritative green reeling progress.
|
||||
- Rod Power upgrades increase damage per valid barrier action.
|
||||
- Shop upgrades persist in the local progression save.
|
||||
|
|
@ -71,28 +75,24 @@ FEATURES TO TRY
|
|||
- Barrier-and-chase catching
|
||||
- Accessibility auto-click settings
|
||||
- Catch showcase and fish size variation
|
||||
- Cooler, Bag, Logbook, favorites, and sorting
|
||||
- Basic Fishing Rod equipment and the 1–9 hotbar
|
||||
- Pelican selling and wallet updates
|
||||
- Physical Fishing Shop sales and persistent fishing upgrades
|
||||
- General inventory, separate tackle, hotbar storage, player storage, and the
|
||||
Logbook
|
||||
- Fishing, net gathering, and beach digging
|
||||
- Equipment and consumable use from the hotbar
|
||||
- Fishing Shop sell tray, wallet updates, and persistent upgrades
|
||||
- Seeded generated worlds and the authored starter-island layout
|
||||
- Save, Continue, New Game, and Delete Save
|
||||
- Game Menu and persistent camera settings
|
||||
- Water-entry recovery from several shores
|
||||
|
||||
SAVES AND SETTINGS
|
||||
|
||||
Progression and settings are stored in Godot's per-user application-data
|
||||
directory, outside this extracted game folder. Delete Save removes progression
|
||||
but preserves settings.
|
||||
|
||||
Windows:
|
||||
%APPDATA%\Godot\app_userdata\NETFISHING\
|
||||
|
||||
Linux:
|
||||
~/.local/share/godot/app_userdata/NETFISHING/
|
||||
Progression and settings are stored outside the extracted game folder. The
|
||||
Data & Identity page shows the active player-data location. Delete Save removes
|
||||
progression but preserves device settings.
|
||||
|
||||
This is an early save format. Keep expectations modest and report any failure
|
||||
to Continue, save, sell, favorite, or retain an individual fish.
|
||||
to Continue, save, sell, move, favorite, or retain an inventory entry.
|
||||
|
||||
FEEDBACK
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ const EXPECTED_PROP_IDS: Array[StringName] = [
|
|||
&"prop_mushroom",
|
||||
&"prop_palm",
|
||||
&"prop_pine",
|
||||
&"prop_pine_large",
|
||||
&"prop_timber_1",
|
||||
&"prop_timber_2",
|
||||
&"prop_tree_1",
|
||||
&"prop_tree_2",
|
||||
&"prop_tree_3",
|
||||
&"prop_tree_large",
|
||||
]
|
||||
const EXPECTED_BIOME_IDS: Array[StringName] = [
|
||||
&"biome_plains",
|
||||
|
|
@ -40,9 +42,11 @@ const PROCEDURAL_PROP_IDS: Array[StringName] = [
|
|||
&"prop_mushroom",
|
||||
&"prop_palm",
|
||||
&"prop_pine",
|
||||
&"prop_pine_large",
|
||||
&"prop_tree_1",
|
||||
&"prop_tree_2",
|
||||
&"prop_tree_3",
|
||||
&"prop_tree_large",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -53,6 +57,11 @@ func _initialize() -> void:
|
|||
func _run() -> void:
|
||||
var region := RegionScene.instantiate() as GeneratedWorldRegion
|
||||
assert(region != null)
|
||||
var configured_generator := region.get_node(
|
||||
"Terrain/TerrainChunkGenerator"
|
||||
) as TerrainChunkGenerator
|
||||
assert(configured_generator != null)
|
||||
configured_generator.elevated_cliff_third_level_chance = 1.0
|
||||
root.add_child(region)
|
||||
await process_frame
|
||||
await physics_frame
|
||||
|
|
@ -124,7 +133,10 @@ func _validate_generated_region(
|
|||
)
|
||||
assert(
|
||||
generator.get_primary_terrain_meshes().size()
|
||||
== expected_chunk_count
|
||||
== (
|
||||
expected_chunk_count
|
||||
+ generator.stacked_elevated_placement_keys().size()
|
||||
)
|
||||
)
|
||||
_validate_elevated_cliff_feature(generator)
|
||||
|
||||
|
|
@ -179,6 +191,16 @@ func _validate_generated_region(
|
|||
assert(decorations != null and decorations.get_child_count() > 0)
|
||||
assert(anchors != null)
|
||||
assert(anchors.get_spawn_positions().size() > 0)
|
||||
var palm_clusters: Dictionary[int, Array] = {}
|
||||
var tree_scales: Array[float] = []
|
||||
var tree_yaws: Dictionary[float, bool] = {}
|
||||
var ocean_edges_by_coordinate: Dictionary[Vector2i, int] = {}
|
||||
for record: Dictionary in generator.placement_records():
|
||||
var record_edges := int(record.get("ocean_facing_edges", 0))
|
||||
if record_edges != 0:
|
||||
ocean_edges_by_coordinate[
|
||||
record.get("coordinate", Vector2i.ZERO)
|
||||
] = record_edges
|
||||
for child: Node in decorations.get_children():
|
||||
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
||||
assert(PROCEDURAL_PROP_IDS.has(prop_id))
|
||||
|
|
@ -211,13 +233,80 @@ func _validate_generated_region(
|
|||
child as Node3D,
|
||||
definition,
|
||||
)
|
||||
if prop_id in [&"prop_tree_1", &"prop_tree_2", &"prop_tree_3"]:
|
||||
if prop_id == &"prop_palm":
|
||||
var cluster_id := int(
|
||||
child.get_meta(&"terrain_prop_cluster_id", -1)
|
||||
)
|
||||
var members: Array = palm_clusters.get(cluster_id, [])
|
||||
members.append(child)
|
||||
palm_clusters[cluster_id] = members
|
||||
var ocean_direction := _ocean_direction(
|
||||
ocean_edges_by_coordinate.get(coordinate, 0)
|
||||
)
|
||||
if ocean_direction.is_zero_approx():
|
||||
ocean_direction = _nearest_ocean_direction(
|
||||
region,
|
||||
generator,
|
||||
(child as Node3D).position,
|
||||
)
|
||||
var local_overhang := Vector3(
|
||||
definition.local_overhang_direction.x,
|
||||
0.0,
|
||||
definition.local_overhang_direction.y,
|
||||
).normalized()
|
||||
var actual_overhang := local_overhang.rotated(
|
||||
Vector3.UP,
|
||||
(child as Node3D).rotation.y,
|
||||
)
|
||||
assert(
|
||||
actual_overhang.dot(ocean_direction)
|
||||
>= cos(deg_to_rad(definition.ocean_facing_spread_degrees))
|
||||
- 0.001
|
||||
)
|
||||
elif prop_id in [
|
||||
&"prop_tree_1",
|
||||
&"prop_tree_2",
|
||||
&"prop_tree_3",
|
||||
&"prop_tree_large",
|
||||
&"prop_pine",
|
||||
&"prop_pine_large",
|
||||
]:
|
||||
tree_scales.append(
|
||||
float(child.get_meta(&"terrain_prop_visual_scale", 1.0))
|
||||
)
|
||||
tree_yaws[snappedf((child as Node3D).rotation.y, 0.01)] = true
|
||||
if prop_id in [
|
||||
&"prop_tree_1",
|
||||
&"prop_tree_2",
|
||||
&"prop_tree_3",
|
||||
&"prop_tree_large",
|
||||
]:
|
||||
assert(
|
||||
_has_material_variant_override(
|
||||
child.get_node_or_null("Visual"),
|
||||
definition.material_variants,
|
||||
)
|
||||
)
|
||||
assert(
|
||||
_has_material_variant_override(
|
||||
child.get_node_or_null("Visual"),
|
||||
definition.secondary_material_variants,
|
||||
)
|
||||
)
|
||||
assert(not palm_clusters.is_empty())
|
||||
for members: Array in palm_clusters.values():
|
||||
assert(members.size() >= 2 and members.size() <= 3)
|
||||
for first_index: int in members.size():
|
||||
for second_index: int in range(first_index + 1, members.size()):
|
||||
var first := members[first_index] as Node3D
|
||||
var second := members[second_index] as Node3D
|
||||
var separation := Vector2(
|
||||
first.position.x - second.position.x,
|
||||
first.position.z - second.position.z,
|
||||
).length()
|
||||
assert(separation > 0.5 and separation < 6.1)
|
||||
assert(tree_scales.max() - tree_scales.min() > 0.25)
|
||||
assert(tree_yaws.size() >= 8)
|
||||
assert(_decoration_group_count(region, &"grass_tree") > 0)
|
||||
assert(_decoration_group_count(region, &"sand_tree") > 0)
|
||||
assert(
|
||||
|
|
@ -297,6 +386,35 @@ func _validate_elevated_cliff_feature(
|
|||
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
|
||||
assert(overlay_mesh.has_node("TerrainCollision"))
|
||||
assert(layered_count == 9)
|
||||
var stacked_keys := generator.stacked_elevated_placement_keys()
|
||||
assert(stacked_keys.size() in [0, 4])
|
||||
if stacked_keys.is_empty():
|
||||
return
|
||||
for key: String in stacked_keys:
|
||||
assert(key.begins_with("chunk_0010@"))
|
||||
var stacked_count := 0
|
||||
for chunk_root: Node in generated.get_children():
|
||||
for child: Node in chunk_root.get_children():
|
||||
if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
|
||||
continue
|
||||
stacked_count += 1
|
||||
assert(
|
||||
StringName(child.get_meta(&"terrain_chunk_id", &""))
|
||||
== &"chunk_0010"
|
||||
)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
(child as Node3D).position.y,
|
||||
generator.elevated_cliff_level_height,
|
||||
)
|
||||
)
|
||||
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
child,
|
||||
&"chunk_0010",
|
||||
)
|
||||
assert(stacked_mesh != null and stacked_mesh.mesh != null)
|
||||
assert(stacked_mesh.has_node("TerrainCollision"))
|
||||
assert(stacked_count == 4)
|
||||
|
||||
|
||||
func _validate_ocean_facing_record(
|
||||
|
|
@ -323,6 +441,31 @@ func _validate_ocean_facing_record(
|
|||
)
|
||||
|
||||
|
||||
func _ocean_direction(ocean_edges: int) -> Vector3:
|
||||
var result := Vector3.ZERO
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
if (ocean_edges & (1 << edge_value)) != 0:
|
||||
result += TerrainChunkTopology.edge_normal(
|
||||
edge_value as TerrainChunkTopology.Edge
|
||||
)
|
||||
return result.normalized()
|
||||
|
||||
|
||||
func _nearest_ocean_direction(
|
||||
region: GeneratedWorldRegion,
|
||||
generator: TerrainChunkGenerator,
|
||||
position: Vector3,
|
||||
) -> Vector3:
|
||||
var half_extents := region.get_playable_half_extents()
|
||||
var distance_x := half_extents.x - absf(position.x)
|
||||
var distance_z := half_extents.y - absf(position.z)
|
||||
var direction_x := Vector3.RIGHT if position.x >= 0.0 else Vector3.LEFT
|
||||
var direction_z := Vector3.BACK if position.z >= 0.0 else Vector3.FORWARD
|
||||
if absf(distance_x - distance_z) <= generator.catalog.chunk_size * 0.35:
|
||||
return (direction_x + direction_z).normalized()
|
||||
return direction_x if distance_x < distance_z else direction_z
|
||||
|
||||
|
||||
func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
||||
var catalog := region.get_prop_catalog()
|
||||
assert(catalog != null)
|
||||
|
|
@ -343,16 +486,49 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
|||
&"prop_tree_1",
|
||||
&"prop_tree_2",
|
||||
&"prop_tree_3",
|
||||
&"prop_tree_large",
|
||||
]:
|
||||
var tree := catalog.definition_for_id(tree_id)
|
||||
assert(tree != null)
|
||||
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
|
||||
assert(tree.material_variants.size() >= 3)
|
||||
assert(not tree.variant_material_slot_names.is_empty())
|
||||
assert(tree.secondary_material_variants.size() >= 3)
|
||||
assert(not tree.secondary_variant_material_slot_names.is_empty())
|
||||
var pine := catalog.definition_for_id(&"prop_pine")
|
||||
assert(pine != null)
|
||||
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
|
||||
assert(pine.material_variants.is_empty())
|
||||
var large_pine := catalog.definition_for_id(&"prop_pine_large")
|
||||
assert(large_pine != null)
|
||||
assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale)
|
||||
assert(large_pine.material_variants.is_empty())
|
||||
var palm := catalog.definition_for_id(&"prop_palm")
|
||||
assert(palm != null)
|
||||
assert(is_equal_approx(palm.minimum_visual_scale, 0.5))
|
||||
assert(is_equal_approx(palm.maximum_visual_scale, 1.0))
|
||||
assert(palm.minimum_cluster_size == 2)
|
||||
assert(palm.maximum_cluster_size == 3)
|
||||
assert(palm.minimum_cluster_radius > palm.clearance_radius)
|
||||
assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius)
|
||||
assert(palm.prefer_ocean_facing)
|
||||
assert(not palm.local_overhang_direction.is_zero_approx())
|
||||
var forest := region.get_biome_catalog().definition_for_id(&"biome_forest")
|
||||
var forest_rule := forest.prop_rule_for_group(&"grass_tree")
|
||||
var large_tree := catalog.definition_for_id(&"prop_tree_large")
|
||||
var small_tree_weight := (
|
||||
catalog.definition_for_id(&"prop_tree_1").selection_weight
|
||||
+ catalog.definition_for_id(&"prop_tree_2").selection_weight
|
||||
+ catalog.definition_for_id(&"prop_tree_3").selection_weight
|
||||
)
|
||||
assert(forest_rule.allows_prop(large_tree))
|
||||
assert(large_tree.selection_weight > small_tree_weight)
|
||||
var pine_forest := region.get_biome_catalog().definition_for_id(
|
||||
&"biome_pine_forest"
|
||||
)
|
||||
var pine_rule := pine_forest.prop_rule_for_group(&"grass_tree")
|
||||
assert(pine_rule.allows_prop(large_pine))
|
||||
assert(large_pine.selection_weight > pine.selection_weight)
|
||||
|
||||
|
||||
func _validate_biome_catalog(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
[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
|
||||
placement_chance = 2200
|
||||
adjacency_bonus = 900
|
||||
maximum_density = 2800
|
||||
minimum_placements = 1
|
||||
allowed_prop_ids = PackedStringArray("prop_palm")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
|||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
allowed_prop_ids = PackedStringArray("prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||
allowed_prop_ids = PackedStringArray("prop_tree_large", "prop_tree_1", "prop_tree_2", "prop_tree_3")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
script = ExtResource("2_rule")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
|||
maximum_density = 8500
|
||||
minimum_placements = 2
|
||||
placement_attempts_per_chunk = 3
|
||||
allowed_prop_ids = PackedStringArray("prop_pine")
|
||||
allowed_prop_ids = PackedStringArray("prop_pine_large", "prop_pine")
|
||||
|
||||
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||
script = ExtResource("2_rule")
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const OCEAN_POOL: FishPool = preload(
|
|||
const WATER_HEIGHT := -0.25
|
||||
const PROP_EDGE_MARGIN := 2.2
|
||||
const PROP_PLACEMENT_ATTEMPTS := 12
|
||||
const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
|
||||
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||
const PROP_CHANCE_SCALE := 10000
|
||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||
|
|
@ -268,6 +269,7 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
record.get("position", Vector3.ZERO),
|
||||
coordinate,
|
||||
biome.stable_id,
|
||||
int(record.get("ocean_facing_edges", 0)),
|
||||
random,
|
||||
terrain_triangles,
|
||||
):
|
||||
|
|
@ -445,6 +447,7 @@ func _maybe_add_prop(
|
|||
chunk_center: Vector3,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
ocean_facing_edges: int,
|
||||
random: RandomNumberGenerator,
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
|
|
@ -460,6 +463,7 @@ func _maybe_add_prop(
|
|||
chunk_center,
|
||||
chunk_coordinate,
|
||||
biome_id,
|
||||
ocean_facing_edges,
|
||||
random,
|
||||
terrain_triangles,
|
||||
)
|
||||
|
|
@ -470,19 +474,26 @@ func _add_prop(
|
|||
chunk_center: Vector3,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
ocean_facing_edges: int,
|
||||
random: RandomNumberGenerator,
|
||||
terrain_triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
if definition == null or definition.packed_scene == null:
|
||||
return false
|
||||
var visual_scale := random.randf_range(
|
||||
definition.minimum_visual_scale,
|
||||
definition.maximum_visual_scale,
|
||||
var cluster_size := random.randi_range(
|
||||
definition.minimum_cluster_size,
|
||||
definition.maximum_cluster_size,
|
||||
)
|
||||
var scaled_clearance := definition.clearance_radius * visual_scale
|
||||
var visual_scales := _prop_visual_scales(
|
||||
definition,
|
||||
cluster_size,
|
||||
random,
|
||||
)
|
||||
var primary_scale := visual_scales[0]
|
||||
var scaled_clearance := definition.clearance_radius * primary_scale
|
||||
var placement := Vector3.ZERO
|
||||
var found_surface := false
|
||||
for attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||
var offset := Vector3(
|
||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||
0.0,
|
||||
|
|
@ -507,6 +518,118 @@ func _add_prop(
|
|||
break
|
||||
if not found_surface:
|
||||
return false
|
||||
var cluster_id := _decorations.get_child_count()
|
||||
if not _instantiate_prop(
|
||||
definition,
|
||||
placement,
|
||||
primary_scale,
|
||||
_prop_yaw(definition, ocean_facing_edges, placement, random),
|
||||
chunk_coordinate,
|
||||
biome_id,
|
||||
cluster_id,
|
||||
0,
|
||||
random,
|
||||
):
|
||||
return false
|
||||
if cluster_size <= 1:
|
||||
return true
|
||||
var cluster_angle := random.randf_range(-PI, PI)
|
||||
var companion_count := cluster_size - 1
|
||||
var chunk_half_extent := _generator.catalog.chunk_size * 0.5 - 0.25
|
||||
for member_index: int in range(1, cluster_size):
|
||||
var member_scale := visual_scales[member_index]
|
||||
var member_clearance := definition.clearance_radius * member_scale
|
||||
for _attempt: int in PROP_CLUSTER_PLACEMENT_ATTEMPTS:
|
||||
var sector_angle := (
|
||||
cluster_angle
|
||||
+ TAU * float(member_index - 1) / float(companion_count)
|
||||
+ random.randf_range(-0.45, 0.45)
|
||||
)
|
||||
var radius := random.randf_range(
|
||||
definition.minimum_cluster_radius,
|
||||
definition.maximum_cluster_radius,
|
||||
)
|
||||
var candidate := placement + Vector3(
|
||||
cos(sector_angle) * radius,
|
||||
0.0,
|
||||
sin(sector_angle) * radius,
|
||||
)
|
||||
if (
|
||||
absf(candidate.x - chunk_center.x) > chunk_half_extent
|
||||
or absf(candidate.z - chunk_center.z) > chunk_half_extent
|
||||
):
|
||||
continue
|
||||
var surface_height := _surface_height_at(
|
||||
candidate,
|
||||
terrain_triangles,
|
||||
)
|
||||
if (
|
||||
surface_height <= WATER_HEIGHT + PROP_MINIMUM_GROUND_CLEARANCE
|
||||
or not _has_prop_clearance(candidate, member_clearance)
|
||||
):
|
||||
continue
|
||||
candidate.y = surface_height
|
||||
if _instantiate_prop(
|
||||
definition,
|
||||
candidate,
|
||||
member_scale,
|
||||
_prop_yaw(
|
||||
definition,
|
||||
ocean_facing_edges,
|
||||
candidate,
|
||||
random,
|
||||
),
|
||||
chunk_coordinate,
|
||||
biome_id,
|
||||
cluster_id,
|
||||
member_index,
|
||||
random,
|
||||
):
|
||||
break
|
||||
return true
|
||||
|
||||
|
||||
func _prop_visual_scales(
|
||||
definition: TerrainPropDefinition,
|
||||
count: int,
|
||||
random: RandomNumberGenerator,
|
||||
) -> Array[float]:
|
||||
var result: Array[float] = []
|
||||
if count <= 1:
|
||||
result.append(
|
||||
random.randf_range(
|
||||
definition.minimum_visual_scale,
|
||||
definition.maximum_visual_scale,
|
||||
)
|
||||
)
|
||||
return result
|
||||
for index: int in count:
|
||||
var descending_band := count - index - 1
|
||||
var band_minimum := lerpf(
|
||||
definition.minimum_visual_scale,
|
||||
definition.maximum_visual_scale,
|
||||
float(descending_band) / float(count),
|
||||
)
|
||||
var band_maximum := lerpf(
|
||||
definition.minimum_visual_scale,
|
||||
definition.maximum_visual_scale,
|
||||
float(descending_band + 1) / float(count),
|
||||
)
|
||||
result.append(random.randf_range(band_minimum, band_maximum))
|
||||
return result
|
||||
|
||||
|
||||
func _instantiate_prop(
|
||||
definition: TerrainPropDefinition,
|
||||
placement: Vector3,
|
||||
visual_scale: float,
|
||||
yaw: float,
|
||||
chunk_coordinate: Vector2i,
|
||||
biome_id: StringName,
|
||||
cluster_id: int,
|
||||
cluster_member_index: int,
|
||||
random: RandomNumberGenerator,
|
||||
) -> bool:
|
||||
var packed_instance := definition.packed_scene.instantiate()
|
||||
var visual_root := packed_instance as Node3D
|
||||
if visual_root == null:
|
||||
|
|
@ -522,8 +645,10 @@ func _add_prop(
|
|||
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
|
||||
prop.set_meta(&"terrain_biome_id", biome_id)
|
||||
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
|
||||
prop.set_meta(&"terrain_prop_cluster_id", cluster_id)
|
||||
prop.set_meta(&"terrain_prop_cluster_member_index", cluster_member_index)
|
||||
prop.position = placement
|
||||
prop.rotation.y = random.randf_range(-PI, PI)
|
||||
prop.rotation.y = yaw
|
||||
_decorations.add_child(prop)
|
||||
visual_root.name = "Visual"
|
||||
prop.add_child(visual_root)
|
||||
|
|
@ -533,7 +658,9 @@ func _add_prop(
|
|||
_apply_prop_material_variant(visual_root, definition, random)
|
||||
_add_prop_collision(prop, definition, visual_scale)
|
||||
_placed_prop_positions.append(placement)
|
||||
_placed_prop_clearance_radii.append(scaled_clearance)
|
||||
_placed_prop_clearance_radii.append(
|
||||
definition.clearance_radius * visual_scale
|
||||
)
|
||||
_placed_prop_groups.append(definition.procedural_group)
|
||||
if definition.gatherable_anchor_height > 0.0:
|
||||
var anchor := Marker3D.new()
|
||||
|
|
@ -547,6 +674,46 @@ func _add_prop(
|
|||
return true
|
||||
|
||||
|
||||
func _prop_yaw(
|
||||
definition: TerrainPropDefinition,
|
||||
ocean_facing_edges: int,
|
||||
placement: Vector3,
|
||||
random: RandomNumberGenerator,
|
||||
) -> float:
|
||||
if not definition.prefer_ocean_facing:
|
||||
return random.randf_range(-PI, PI)
|
||||
var ocean_direction := Vector2.ZERO
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
if (ocean_facing_edges & (1 << edge_value)) == 0:
|
||||
continue
|
||||
var edge_normal := TerrainChunkTopology.edge_normal(
|
||||
edge_value as TerrainChunkTopology.Edge
|
||||
)
|
||||
ocean_direction += Vector2(edge_normal.x, edge_normal.z)
|
||||
if ocean_direction.is_zero_approx():
|
||||
ocean_direction = _nearest_ocean_direction(placement)
|
||||
var local_direction := definition.local_overhang_direction.normalized()
|
||||
var target_angle := atan2(ocean_direction.x, ocean_direction.y)
|
||||
var local_angle := atan2(local_direction.x, local_direction.y)
|
||||
var spread := deg_to_rad(definition.ocean_facing_spread_degrees)
|
||||
return (
|
||||
target_angle
|
||||
- local_angle
|
||||
+ random.randf_range(-spread, spread)
|
||||
)
|
||||
|
||||
|
||||
func _nearest_ocean_direction(position: Vector3) -> Vector2:
|
||||
var half_extents := get_playable_half_extents()
|
||||
var distance_x := half_extents.x - absf(position.x)
|
||||
var distance_z := half_extents.y - absf(position.z)
|
||||
var direction_x := Vector2.RIGHT if position.x >= 0.0 else Vector2.LEFT
|
||||
var direction_z := Vector2.DOWN if position.z >= 0.0 else Vector2.UP
|
||||
if absf(distance_x - distance_z) <= _generator.catalog.chunk_size * 0.35:
|
||||
return (direction_x + direction_z).normalized()
|
||||
return direction_x if distance_x < distance_z else direction_z
|
||||
|
||||
|
||||
func _validate_prop_catalog() -> void:
|
||||
if prop_catalog == null:
|
||||
push_error("Generated world terrain prop catalog is unavailable.")
|
||||
|
|
@ -776,6 +943,7 @@ func _ensure_minimum_props(
|
|||
record.get("position", Vector3.ZERO),
|
||||
coordinate,
|
||||
biome.stable_id,
|
||||
int(record.get("ocean_facing_edges", 0)),
|
||||
random,
|
||||
terrain_triangles,
|
||||
):
|
||||
|
|
@ -819,17 +987,32 @@ func _apply_prop_material_variant(
|
|||
definition: TerrainPropDefinition,
|
||||
random: RandomNumberGenerator,
|
||||
) -> void:
|
||||
if (
|
||||
definition.material_variants.is_empty()
|
||||
or definition.variant_material_slot_names.is_empty()
|
||||
):
|
||||
return
|
||||
var variant := definition.material_variants[
|
||||
random.randi_range(0, definition.material_variants.size() - 1)
|
||||
]
|
||||
_apply_material_variant_to_meshes(
|
||||
_apply_random_material_variant(
|
||||
root_node,
|
||||
definition.variant_material_slot_names,
|
||||
definition.material_variants,
|
||||
random,
|
||||
)
|
||||
_apply_random_material_variant(
|
||||
root_node,
|
||||
definition.secondary_variant_material_slot_names,
|
||||
definition.secondary_material_variants,
|
||||
random,
|
||||
)
|
||||
|
||||
|
||||
func _apply_random_material_variant(
|
||||
root_node: Node,
|
||||
target_material_names: PackedStringArray,
|
||||
variants: Array[Material],
|
||||
random: RandomNumberGenerator,
|
||||
) -> void:
|
||||
if variants.is_empty() or target_material_names.is_empty():
|
||||
return
|
||||
var variant := variants[random.randi_range(0, variants.size() - 1)]
|
||||
_apply_material_variant_to_meshes(
|
||||
root_node,
|
||||
target_material_names,
|
||||
variant,
|
||||
)
|
||||
|
||||
|
|
|
|||
BIN
world/generation/props/assets/prop_pine_large.glb
Normal file
BIN
world/generation/props/assets/prop_pine_large.glb
Normal file
Binary file not shown.
45
world/generation/props/assets/prop_pine_large.glb.import
Normal file
45
world/generation/props/assets/prop_pine_large.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bsislktcsck1y"
|
||||
path="res://.godot/imported/prop_pine_large.glb-97f0e7e162a6a624faaf2cc023211d3e.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://world/generation/props/assets/prop_pine_large.glb"
|
||||
dest_files=["res://.godot/imported/prop_pine_large.glb-97f0e7e162a6a624faaf2cc023211d3e.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
|
||||
BIN
world/generation/props/assets/prop_tree_large.glb
Normal file
BIN
world/generation/props/assets/prop_tree_large.glb
Normal file
Binary file not shown.
45
world/generation/props/assets/prop_tree_large.glb.import
Normal file
45
world/generation/props/assets/prop_tree_large.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://c46jcb2vo2fd"
|
||||
path="res://.godot/imported/prop_tree_large.glb-0a4ab94061e4876be025fa3d63aa7bd8.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://world/generation/props/assets/prop_tree_large.glb"
|
||||
dest_files=["res://.godot/imported/prop_tree_large.glb-0a4ab94061e4876be025fa3d63aa7bd8.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
|
||||
|
|
@ -11,6 +11,15 @@ packed_scene = ExtResource("2_scene")
|
|||
procedural_group = &"sand_tree"
|
||||
required_chunk_tags = PackedStringArray("sand")
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 2.0
|
||||
clearance_radius = 0.65
|
||||
minimum_visual_scale = 0.5
|
||||
maximum_visual_scale = 1.0
|
||||
minimum_cluster_size = 2
|
||||
maximum_cluster_size = 3
|
||||
minimum_cluster_radius = 1.4
|
||||
maximum_cluster_radius = 3.0
|
||||
prefer_ocean_facing = true
|
||||
local_overhang_direction = Vector2(-0.883, 0.469)
|
||||
ocean_facing_spread_degrees = 55.0
|
||||
collision_radius = 0.4
|
||||
collision_height = 6.0
|
||||
|
|
|
|||
|
|
@ -6,15 +6,15 @@
|
|||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_pine"
|
||||
label = "pine"
|
||||
label = "small 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
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.22
|
||||
minimum_visual_scale = 0.65
|
||||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.5
|
||||
collision_height = 4.0
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
|
|||
20
world/generation/props/definitions/prop_pine_large.tres
Normal file
20
world/generation/props/definitions/prop_pine_large.tres
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[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_large.glb" id="2_scene"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_pine_large"
|
||||
label = "large pine"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 3.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.65
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.2
|
||||
collision_radius = 0.65
|
||||
collision_height = 9.5
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 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"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_1"
|
||||
label = "small tree"
|
||||
label = "small deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.3
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.15
|
||||
minimum_visual_scale = 0.65
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.4
|
||||
collision_height = 3.2
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 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"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_2"
|
||||
label = "medium tree"
|
||||
label = "medium deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.5
|
||||
minimum_visual_scale = 0.8
|
||||
maximum_visual_scale = 1.18
|
||||
minimum_visual_scale = 0.7
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.5
|
||||
collision_height = 3.8
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=6 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 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"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_3"
|
||||
label = "large tree"
|
||||
label = "decorative deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 1.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 1.8
|
||||
minimum_visual_scale = 0.85
|
||||
maximum_visual_scale = 1.15
|
||||
minimum_visual_scale = 0.7
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.55
|
||||
collision_height = 4.2
|
||||
gatherable_anchor_height = 2.15
|
||||
|
|
|
|||
30
world/generation/props/definitions/prop_tree_large.tres
Normal file
30
world/generation/props/definitions/prop_tree_large.tres
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 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_large.glb" id="2_scene"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_light.tres" id="3_leaf_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_mid.tres" id="4_leaf_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/leaf_dark.tres" id="5_leaf_dark"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"]
|
||||
[ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_definition")
|
||||
stable_id = &"prop_tree_large"
|
||||
label = "large deciduous tree"
|
||||
packed_scene = ExtResource("2_scene")
|
||||
procedural_group = &"grass_tree"
|
||||
required_chunk_tags = PackedStringArray("grass")
|
||||
selection_weight = 12.0
|
||||
minimum_spawn_chunk_distance = 2
|
||||
clearance_radius = 2.5
|
||||
minimum_visual_scale = 0.75
|
||||
maximum_visual_scale = 1.2
|
||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
||||
material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")])
|
||||
secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid")
|
||||
secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")])
|
||||
collision_radius = 0.85
|
||||
collision_height = 7.5
|
||||
gatherable_anchor_height = 2.15
|
||||
9
world/generation/props/materials/wood_dark.tres
Normal file
9
world/generation/props/materials/wood_dark.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_dark"
|
||||
albedo_color = Color(0.163, 0.139, 0.0254, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
9
world/generation/props/materials/wood_light.tres
Normal file
9
world/generation/props/materials/wood_light.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_light"
|
||||
albedo_color = Color(0.644, 0.563, 0.356, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
9
world/generation/props/materials/wood_mid.tres
Normal file
9
world/generation/props/materials/wood_mid.tres
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[gd_resource type="StandardMaterial3D" format=3]
|
||||
|
||||
[resource]
|
||||
resource_name = "wood_variant_mid"
|
||||
albedo_color = Color(0.333, 0.271, 0.114, 1)
|
||||
metallic = 0.0
|
||||
roughness = 1.0
|
||||
shading_mode = 1
|
||||
texture_filter = 0
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=16 format=3]
|
||||
[gd_resource type="Resource" script_class="TerrainPropCatalog" load_steps=18 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"]
|
||||
|
|
@ -15,7 +15,9 @@
|
|||
[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"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_tree_large.tres" id="16_tree_large"]
|
||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_pine_large.tres" id="17_pine_large"]
|
||||
|
||||
[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")])
|
||||
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"), ExtResource("16_tree_large"), ExtResource("17_pine_large")])
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ const MAX_PACKED_SOLVER_VARIANTS := 62
|
|||
@export var elevated_cliff_edge_chunk_id: StringName = &"chunk_0011"
|
||||
@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012"
|
||||
@export_range(0.0, 1.0, 0.05) var elevated_cliff_ramp_chance := 0.75
|
||||
## A smaller third tier reuses the existing corner geometry at the authored
|
||||
## two-meter level interval. It is nested into the lower cliff assembly rather
|
||||
## than occupying or replacing additional terrain-grid cells.
|
||||
@export_range(0.0, 1.0, 0.05) var elevated_cliff_third_level_chance := 0.7
|
||||
@export_range(0.1, 10.0, 0.1) var elevated_cliff_level_height := 2.0
|
||||
@export_group("")
|
||||
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
|
||||
@export_range(1.0, 4.0, 0.05) var preferred_neighbor_weight_multiplier := 1.6
|
||||
|
|
@ -68,6 +73,7 @@ var _generated_chunks: Node3D
|
|||
var _backtrack_count := 0
|
||||
var _elevated_feature_center := Vector2i(-1, -1)
|
||||
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
||||
var _stacked_elevated_placements: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -100,6 +106,9 @@ func generate() -> bool:
|
|||
if not _apply_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
if not _configure_stacked_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
var solution_root := _build_solution_root()
|
||||
if solution_root == null:
|
||||
_assign_placements(previous_placements)
|
||||
|
|
@ -138,6 +147,9 @@ func generate_from_placement_keys(keys: PackedStringArray) -> bool:
|
|||
previous_placements.assign(_placements)
|
||||
_assign_placements(resolved)
|
||||
_backtrack_count = 0
|
||||
if not _configure_stacked_elevated_feature():
|
||||
_assign_placements(previous_placements)
|
||||
return false
|
||||
var solution_root := _build_solution_root()
|
||||
if solution_root == null:
|
||||
_assign_placements(previous_placements)
|
||||
|
|
@ -1184,6 +1196,7 @@ func _candidate_can_occupy_cell(
|
|||
func _prepare_elevated_feature_region() -> bool:
|
||||
_elevated_feature_center = Vector2i(-1, -1)
|
||||
_elevated_feature_reserved_grass_indices.clear()
|
||||
_stacked_elevated_placements.clear()
|
||||
if not elevated_cliff_feature_enabled:
|
||||
_build_grid_solver_caches()
|
||||
return true
|
||||
|
|
@ -1439,6 +1452,65 @@ func _apply_elevated_feature() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _configure_stacked_elevated_feature() -> bool:
|
||||
_stacked_elevated_placements.clear()
|
||||
if (
|
||||
not elevated_cliff_feature_enabled
|
||||
or elevated_cliff_third_level_chance <= 0.0
|
||||
):
|
||||
return true
|
||||
var top_coordinate := Vector2i(-1, -1)
|
||||
for index: int in _placements.size():
|
||||
var placement := _placements[index]
|
||||
if (
|
||||
placement != null
|
||||
and placement.definition.stable_id == elevated_cliff_top_chunk_id
|
||||
):
|
||||
top_coordinate = Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
break
|
||||
if top_coordinate.x < 0:
|
||||
return true
|
||||
var stack_random := RandomNumberGenerator.new()
|
||||
stack_random.seed = generation_seed ^ 0x7312DC11F
|
||||
if stack_random.randf() >= elevated_cliff_third_level_chance:
|
||||
return true
|
||||
var corner_definition := catalog.definition_for_id(
|
||||
elevated_cliff_corner_chunk_id
|
||||
)
|
||||
if corner_definition == null:
|
||||
push_error(
|
||||
"Stacked cliff feature references missing corner chunk %s."
|
||||
% elevated_cliff_corner_chunk_id
|
||||
)
|
||||
return false
|
||||
# Four nearly half-cell-offset corners form a closed 2x2 ring. A slight
|
||||
# inward overlap keeps the authored curved feet fully seated on the lower
|
||||
# plateau instead of exposing a hairline gap at its outermost vertices.
|
||||
var specs: Array[Dictionary] = [
|
||||
{"offset": Vector2(-0.45, -0.45), "turns": 2},
|
||||
{"offset": Vector2(0.45, -0.45), "turns": 1},
|
||||
{"offset": Vector2(-0.45, 0.45), "turns": 3},
|
||||
{"offset": Vector2(0.45, 0.45), "turns": 0},
|
||||
]
|
||||
for spec: Dictionary in specs:
|
||||
var variant := _authored_variant(
|
||||
corner_definition,
|
||||
int(spec["turns"]),
|
||||
)
|
||||
if variant == null:
|
||||
push_error(
|
||||
"Stacked cliff feature cannot resolve %s rotation %d."
|
||||
% [elevated_cliff_corner_chunk_id, spec["turns"]]
|
||||
)
|
||||
return false
|
||||
_stacked_elevated_placements.append({
|
||||
"support_coordinate": top_coordinate,
|
||||
"offset": spec["offset"],
|
||||
"variant": variant,
|
||||
})
|
||||
return true
|
||||
|
||||
|
||||
func _variant_respects_ocean_boundary(
|
||||
variant: TerrainChunkVariant,
|
||||
coordinate: Vector2i,
|
||||
|
|
@ -2040,9 +2112,74 @@ func _build_solution_root() -> Node3D:
|
|||
_add_collision(chunk_root, variant.definition)
|
||||
if show_chunk_labels:
|
||||
_add_chunk_label(chunk_root, variant)
|
||||
if not _add_stacked_elevated_chunks(solution_root):
|
||||
solution_root.free()
|
||||
return null
|
||||
return solution_root
|
||||
|
||||
|
||||
func _add_stacked_elevated_chunks(solution_root: Node3D) -> bool:
|
||||
for index: int in _stacked_elevated_placements.size():
|
||||
var record := _stacked_elevated_placements[index]
|
||||
var variant := record.get("variant") as TerrainChunkVariant
|
||||
var support_coordinate: Vector2i = record.get(
|
||||
"support_coordinate",
|
||||
Vector2i(-1, -1),
|
||||
)
|
||||
var support_root := _find_chunk_root_for_coordinate(
|
||||
solution_root,
|
||||
support_coordinate,
|
||||
)
|
||||
if variant == null or support_root == null:
|
||||
push_error("Stacked cliff feature lost its supporting terrain cell.")
|
||||
return false
|
||||
var stacked_root := variant.definition.packed_scene.instantiate() as Node3D
|
||||
if stacked_root == null:
|
||||
push_error(
|
||||
"%s does not instantiate as a stacked Node3D."
|
||||
% variant.stable_key()
|
||||
)
|
||||
return false
|
||||
stacked_root.name = "Stacked_%s_%d" % [
|
||||
variant.stable_key().replace("@", "r"),
|
||||
index,
|
||||
]
|
||||
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
||||
stacked_root.position = Vector3(
|
||||
offset.x * catalog.chunk_size,
|
||||
elevated_cliff_level_height,
|
||||
offset.y * catalog.chunk_size,
|
||||
)
|
||||
stacked_root.rotation.y = variant.rotation_radians()
|
||||
stacked_root.set_meta(&"terrain_stacked_elevation", true)
|
||||
stacked_root.set_meta(
|
||||
&"terrain_chunk_id",
|
||||
variant.definition.stable_id,
|
||||
)
|
||||
stacked_root.set_meta(
|
||||
&"terrain_chunk_coordinate",
|
||||
support_coordinate,
|
||||
)
|
||||
support_root.add_child(stacked_root)
|
||||
if build_collision:
|
||||
_add_collision(stacked_root, variant.definition)
|
||||
if show_chunk_labels:
|
||||
_add_chunk_label(stacked_root, variant)
|
||||
return true
|
||||
|
||||
|
||||
func _find_chunk_root_for_coordinate(
|
||||
solution_root: Node3D,
|
||||
coordinate: Vector2i,
|
||||
) -> Node3D:
|
||||
for child: Node in solution_root.get_children():
|
||||
if child.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)) == (
|
||||
coordinate
|
||||
):
|
||||
return child as Node3D
|
||||
return null
|
||||
|
||||
|
||||
func _instantiate_chunk_root(
|
||||
definition: TerrainChunkDefinition,
|
||||
) -> Node3D:
|
||||
|
|
@ -2171,6 +2308,7 @@ func _build_summary() -> Dictionary:
|
|||
"counts": counts,
|
||||
"variant_counts": variant_counts,
|
||||
"placements": placement_keys(),
|
||||
"stacked_elevated_placements": stacked_elevated_placement_keys(),
|
||||
"layout_fingerprint": placement_fingerprint(),
|
||||
}
|
||||
|
||||
|
|
@ -2214,9 +2352,29 @@ func placement_fingerprint() -> String:
|
|||
"chunk_size:%.6f" % (catalog.chunk_size if catalog != null else 0.0),
|
||||
])
|
||||
fingerprint_input.append_array(placement_keys())
|
||||
fingerprint_input.append_array(stacked_elevated_placement_keys())
|
||||
return "\n".join(fingerprint_input).sha256_text()
|
||||
|
||||
|
||||
func stacked_elevated_placement_keys() -> PackedStringArray:
|
||||
var result := PackedStringArray()
|
||||
for record: Dictionary in _stacked_elevated_placements:
|
||||
var variant := record.get("variant") as TerrainChunkVariant
|
||||
var offset: Vector2 = record.get("offset", Vector2.ZERO)
|
||||
if variant == null:
|
||||
continue
|
||||
result.append(
|
||||
"%s@%.2f,%.2f,+%.2fm"
|
||||
% [
|
||||
variant.stable_key(),
|
||||
offset.x,
|
||||
offset.y,
|
||||
elevated_cliff_level_height,
|
||||
]
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func placement_records() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for index: int in _placements.size():
|
||||
|
|
@ -2276,4 +2434,18 @@ func get_primary_terrain_meshes() -> Array[MeshInstance3D]:
|
|||
)
|
||||
if primary_mesh != null and primary_mesh.mesh != null:
|
||||
result.append(primary_mesh)
|
||||
for child: Node in chunk_root.get_children():
|
||||
if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
|
||||
continue
|
||||
var definition := catalog.definition_for_id(
|
||||
StringName(child.get_meta(&"terrain_chunk_id", &""))
|
||||
)
|
||||
if definition == null:
|
||||
continue
|
||||
var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
|
||||
child,
|
||||
definition.primary_mesh_name,
|
||||
)
|
||||
if stacked_mesh != null and stacked_mesh.mesh != null:
|
||||
result.append(stacked_mesh)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -87,6 +87,23 @@ func validation_errors() -> PackedStringArray:
|
|||
"%s has an inverted visual scale range."
|
||||
% definition.stable_id
|
||||
)
|
||||
if definition.minimum_cluster_size > definition.maximum_cluster_size:
|
||||
errors.append(
|
||||
"%s has an inverted cluster size range."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.maximum_cluster_size > 1
|
||||
and (
|
||||
definition.minimum_cluster_radius <= 0.0
|
||||
or definition.minimum_cluster_radius
|
||||
> definition.maximum_cluster_radius
|
||||
)
|
||||
):
|
||||
errors.append(
|
||||
"%s has an invalid loose-cluster radius."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
not definition.material_variants.is_empty()
|
||||
and definition.variant_material_slot_names.is_empty()
|
||||
|
|
@ -101,4 +118,26 @@ func validation_errors() -> PackedStringArray:
|
|||
"%s contains an empty material variant."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
not definition.secondary_material_variants.is_empty()
|
||||
and definition.secondary_variant_material_slot_names.is_empty()
|
||||
):
|
||||
errors.append(
|
||||
"%s has secondary variants but no target material slots."
|
||||
% definition.stable_id
|
||||
)
|
||||
for material: Material in definition.secondary_material_variants:
|
||||
if material == null:
|
||||
errors.append(
|
||||
"%s contains an empty secondary material variant."
|
||||
% definition.stable_id
|
||||
)
|
||||
if (
|
||||
definition.prefer_ocean_facing
|
||||
and definition.local_overhang_direction.is_zero_approx()
|
||||
):
|
||||
errors.append(
|
||||
"%s prefers the ocean but has no local overhang direction."
|
||||
% definition.stable_id
|
||||
)
|
||||
return errors
|
||||
|
|
|
|||
|
|
@ -21,12 +21,28 @@ extends Resource
|
|||
## Procedural instances use a deterministic uniform scale in this range.
|
||||
@export_range(0.1, 4.0, 0.05) var minimum_visual_scale := 1.0
|
||||
@export_range(0.1, 4.0, 0.05) var maximum_visual_scale := 1.0
|
||||
## Values above one create a loose group of separately anchored instances.
|
||||
## Cluster members are distributed across the configured scale range so a
|
||||
## three-member cluster naturally contains a small, medium, and large prop.
|
||||
@export_range(1, 4, 1) var minimum_cluster_size := 1
|
||||
@export_range(1, 4, 1) var maximum_cluster_size := 1
|
||||
@export_range(0.0, 10.0, 0.05) var minimum_cluster_radius := 0.0
|
||||
@export_range(0.0, 10.0, 0.05) var maximum_cluster_radius := 0.0
|
||||
## Presentation-only offset. The prop root remains exactly terrain-aligned.
|
||||
@export var visual_offset := Vector3.ZERO
|
||||
## A single material is chosen per prop and applied only to matching imported
|
||||
## material slots. This keeps trunks untouched while varying foliage.
|
||||
@export var variant_material_slot_names := PackedStringArray()
|
||||
@export var material_variants: Array[Material] = []
|
||||
## An independent second material channel, used when both foliage and wood
|
||||
## should vary without coupling their color choices.
|
||||
@export var secondary_variant_material_slot_names := PackedStringArray()
|
||||
@export var secondary_material_variants: Array[Material] = []
|
||||
## Some asymmetric props can loosely face an authored ocean edge. The local
|
||||
## direction describes the model's natural overhang in Godot X/Z space.
|
||||
@export var prefer_ocean_facing := false
|
||||
@export var local_overhang_direction := Vector2(0.0, -1.0)
|
||||
@export_range(0.0, 180.0, 1.0) var ocean_facing_spread_degrees := 45.0
|
||||
@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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue