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.
|
palette resources prevent page-specific geometry and style drift.
|
||||||
|
|
||||||
World presentation systems (time/weather visuals, procedural sky and water,
|
World presentation systems (time/weather visuals, procedural sky and water,
|
||||||
shoreline ribbons, player blob shadows) do not own gameplay collision or
|
shoreline treatment, player blob shadows) do not own gameplay collision or
|
||||||
network authority. Generated shoreline meshes are deterministic presentation
|
network authority.
|
||||||
resources baked from explicitly configured static terrain.
|
|
||||||
|
|
||||||
### Engineering consequences
|
### Engineering consequences
|
||||||
|
|
||||||
|
|
@ -129,9 +128,9 @@ remain authoritative in `.tres` resources and typed scripts.
|
||||||
wallet exactly once.
|
wallet exactly once.
|
||||||
- Reserved assets are rejected atomically; mixed valid and reserved batches
|
- Reserved assets are rejected atomically; mixed valid and reserved batches
|
||||||
must not partially succeed.
|
must not partially succeed.
|
||||||
- Item effects, shop prices, cooler capacity, fishing upgrades, jobs, and
|
- Item effects, shop prices, inventory and storage capacity, fishing upgrades,
|
||||||
experience are separate balance axes. Avoid changing several in an
|
jobs, and experience are separate balance axes. Avoid changing several in
|
||||||
unrelated presentation pass.
|
an unrelated presentation pass.
|
||||||
|
|
||||||
Record affected stable IDs and resource paths, old and new values, intended
|
Record affected stable IDs and resource paths, old and new values, intended
|
||||||
player-facing outcome, interactions with quality/availability/economy,
|
player-facing outcome, interactions with quality/availability/economy,
|
||||||
|
|
@ -143,70 +142,57 @@ authored number changed.
|
||||||
|
|
||||||
### World composition
|
### World composition
|
||||||
|
|
||||||
The active test world uses the canonical starter island through
|
`world/test_world.tscn` is the shared gameplay stage. It owns the environment,
|
||||||
`world/regions/starter_island_region.tscn`.
|
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,
|
- the generated world, which is the default for new games and is rebuilt from
|
||||||
and below-world failsafe. `StarterIslandRegion` owns the placed island content:
|
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
|
```text
|
||||||
StarterIslandRegion
|
WorldRegion
|
||||||
├── Terrain
|
├── Terrain
|
||||||
│ ├── Visual
|
|
||||||
│ └── Collision
|
|
||||||
├── WaterBodies
|
├── WaterBodies
|
||||||
│ ├── Pond
|
|
||||||
│ │ ├── VisualWater
|
|
||||||
│ │ ├── FishingRegion
|
|
||||||
│ │ └── RecoveryRegion
|
|
||||||
│ └── Ocean
|
|
||||||
│ ├── VisualWater
|
|
||||||
│ ├── FishingRegions
|
|
||||||
│ └── RecoveryRegions
|
|
||||||
├── PlayerSpawn
|
├── PlayerSpawn
|
||||||
├── SafeRespawns
|
├── SafeRespawns
|
||||||
|
├── DiggableAreas
|
||||||
|
├── GatherableAnchors
|
||||||
└── Interactables
|
└── Interactables
|
||||||
├── FishingShopWorld
|
├── FishingShopWorld
|
||||||
└── PelicanCoolerPerch
|
└── PlayerStorageBox
|
||||||
```
|
```
|
||||||
|
|
||||||
Move a meaningful feature root rather than one of its implementation children.
|
The generated region owns its chunk, biome, and prop catalogs and derives
|
||||||
Child transforms are local offsets owned by that feature.
|
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
|
Every fishable water body authors its water type explicitly. Fish species use
|
||||||
`FRESH_WATER`; the starter coast is `SALT_WATER`. Fish species use the same
|
the same central type through an allowed-water-type bitmask. Do not infer
|
||||||
central type through an allowed-water-type bitmask. Do not infer habitat from
|
habitat from node names, pool filenames, coordinates, or water height.
|
||||||
node names, pool filenames, coordinates, or water height.
|
|
||||||
|
|
||||||
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
|
In the generated layout, seed and layout ID are persistent data. Chunk IDs,
|
||||||
authoritative surface position; surface size and fishing/recovery coverage are
|
edge rules, biome definitions, and prop definitions are authored resources;
|
||||||
owned together. Select `WaterBodies/Ocean` to move the surrounding water as one
|
changing them can change the world produced by an existing seed and therefore
|
||||||
feature. Its fishing coverage is derived from the visible water mesh and the
|
requires deliberate compatibility review.
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
The active `Environment` and `Sun` remain Inspector-authored on the shared
|
||||||
- `SafeRespawns` contains authoritative recovery destinations. Recovery
|
stage and are not recreated by either region.
|
||||||
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.
|
|
||||||
|
|
||||||
### Reusable world props
|
### 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
|
non-uniform root scaling. Make mutable per-instance shapes, meshes, and
|
||||||
materials local to the scene.
|
materials local to the scene.
|
||||||
|
|
||||||
### Shoreline tide ribbons
|
### Shoreline presentation
|
||||||
|
|
||||||
Maps using generated tide ribbons own a `ShorelineRibbonBaker` and one
|
The visible shoreline treatment is currently produced by the water materials.
|
||||||
`ShorelineRibbonConfig` per water body. Each configuration selects only its
|
The older smooth-ribbon baker and generated starter-island ribbon resource are
|
||||||
static terrain source, water height, generation bounds, water-facing reference,
|
retained as development history/tooling, but the ribbon mesh is disabled and
|
||||||
smoothing overrides, and generated `.tres` path. Props, players, bobbers, and
|
is not part of the current rendered shoreline. Do not treat a ribbon rebuild
|
||||||
gameplay areas are not scanned unless selected explicitly. Only `SALT_WATER`
|
as a normal terrain-authoring requirement or re-enable the mesh as incidental
|
||||||
bodies generate ribbons.
|
cleanup.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Facial-feature textures
|
### 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:
|
through the normalization script rather than copying its WAV files directly:
|
||||||
|
|
||||||
```bash
|
```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
|
The source directory may contain lowercase letter, number, and underscore WAV
|
||||||
filenames. The script converts every top-level WAV and writes the results under
|
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
|
`player/animalese_voice_profiles.gd` after import, then run the quick validation
|
||||||
suite. Keep the untouched submission and its original hashes in the private
|
suite. Keep the untouched submission and its original hashes in the private
|
||||||
intake record; repository files are normalized derivatives.
|
intake record; repository files are normalized derivatives.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
NETfishing
|
NETfishing
|
||||||
v0.14.0-alpha
|
Alpha playtest
|
||||||
Alpha 0.14.0
|
|
||||||
|
|
||||||
Thank you for trying this early private playtest.
|
Thank you for trying this early private playtest.
|
||||||
|
|
||||||
|
|
@ -30,13 +29,16 @@ Sprint Shift
|
||||||
Sneak Ctrl or click right stick
|
Sneak Ctrl or click right stick
|
||||||
Slow walk Alt
|
Slow walk Alt
|
||||||
Rotate camera Hold right mouse, or use right stick
|
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
|
Select hotbar slot 1 through 9
|
||||||
Zoom camera Shift + mouse wheel
|
Zoom camera Shift + mouse wheel
|
||||||
Cast / withdraw / reel Left mouse
|
Primary tool / fishing Left mouse or right trigger
|
||||||
Cooler, Bag, and Logbook Tab
|
Inventory and player pages Tab
|
||||||
Game Menu / back Escape
|
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
|
FISHING
|
||||||
|
|
||||||
|
|
@ -48,12 +50,14 @@ FISHING
|
||||||
- At barriers, use distinct left-click presses to damage the barrier.
|
- At barriers, use distinct left-click presses to damage the barrier.
|
||||||
- Do not let the red chase meter catch the green meter.
|
- Do not let the red chase meter catch the green meter.
|
||||||
- Accessibility auto-click can be enabled in Settings.
|
- Accessibility auto-click can be enabled in Settings.
|
||||||
- Approach the bright Fishing Shop booth and press E to open it.
|
- Approach the Fishing Shop and interact to open it.
|
||||||
- The Fishing Shop buys one Cooler fish at a time for full base value.
|
- Move sellable inventory entries into the shop's sell tray, review the total,
|
||||||
- Buy Coffee, Energy Drinks, Snacks, and Fish Finders as Bag supplies.
|
and confirm the sale.
|
||||||
- Drag supplies from Bag to the hotbar and left-click in READY to use one.
|
- 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.
|
- 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.
|
- Reel Speed upgrades increase authoritative green reeling progress.
|
||||||
- Rod Power upgrades increase damage per valid barrier action.
|
- Rod Power upgrades increase damage per valid barrier action.
|
||||||
- Shop upgrades persist in the local progression save.
|
- Shop upgrades persist in the local progression save.
|
||||||
|
|
@ -71,28 +75,24 @@ FEATURES TO TRY
|
||||||
- Barrier-and-chase catching
|
- Barrier-and-chase catching
|
||||||
- Accessibility auto-click settings
|
- Accessibility auto-click settings
|
||||||
- Catch showcase and fish size variation
|
- Catch showcase and fish size variation
|
||||||
- Cooler, Bag, Logbook, favorites, and sorting
|
- General inventory, separate tackle, hotbar storage, player storage, and the
|
||||||
- Basic Fishing Rod equipment and the 1–9 hotbar
|
Logbook
|
||||||
- Pelican selling and wallet updates
|
- Fishing, net gathering, and beach digging
|
||||||
- Physical Fishing Shop sales and persistent fishing upgrades
|
- 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
|
- Save, Continue, New Game, and Delete Save
|
||||||
- Game Menu and persistent camera settings
|
- Game Menu and persistent camera settings
|
||||||
- Water-entry recovery from several shores
|
- Water-entry recovery from several shores
|
||||||
|
|
||||||
SAVES AND SETTINGS
|
SAVES AND SETTINGS
|
||||||
|
|
||||||
Progression and settings are stored in Godot's per-user application-data
|
Progression and settings are stored outside the extracted game folder. The
|
||||||
directory, outside this extracted game folder. Delete Save removes progression
|
Data & Identity page shows the active player-data location. Delete Save removes
|
||||||
but preserves settings.
|
progression but preserves device settings.
|
||||||
|
|
||||||
Windows:
|
|
||||||
%APPDATA%\Godot\app_userdata\NETFISHING\
|
|
||||||
|
|
||||||
Linux:
|
|
||||||
~/.local/share/godot/app_userdata/NETFISHING/
|
|
||||||
|
|
||||||
This is an early save format. Keep expectations modest and report any failure
|
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
|
FEEDBACK
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,13 @@ const EXPECTED_PROP_IDS: Array[StringName] = [
|
||||||
&"prop_mushroom",
|
&"prop_mushroom",
|
||||||
&"prop_palm",
|
&"prop_palm",
|
||||||
&"prop_pine",
|
&"prop_pine",
|
||||||
|
&"prop_pine_large",
|
||||||
&"prop_timber_1",
|
&"prop_timber_1",
|
||||||
&"prop_timber_2",
|
&"prop_timber_2",
|
||||||
&"prop_tree_1",
|
&"prop_tree_1",
|
||||||
&"prop_tree_2",
|
&"prop_tree_2",
|
||||||
&"prop_tree_3",
|
&"prop_tree_3",
|
||||||
|
&"prop_tree_large",
|
||||||
]
|
]
|
||||||
const EXPECTED_BIOME_IDS: Array[StringName] = [
|
const EXPECTED_BIOME_IDS: Array[StringName] = [
|
||||||
&"biome_plains",
|
&"biome_plains",
|
||||||
|
|
@ -40,9 +42,11 @@ const PROCEDURAL_PROP_IDS: Array[StringName] = [
|
||||||
&"prop_mushroom",
|
&"prop_mushroom",
|
||||||
&"prop_palm",
|
&"prop_palm",
|
||||||
&"prop_pine",
|
&"prop_pine",
|
||||||
|
&"prop_pine_large",
|
||||||
&"prop_tree_1",
|
&"prop_tree_1",
|
||||||
&"prop_tree_2",
|
&"prop_tree_2",
|
||||||
&"prop_tree_3",
|
&"prop_tree_3",
|
||||||
|
&"prop_tree_large",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,6 +57,11 @@ func _initialize() -> void:
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
var region := RegionScene.instantiate() as GeneratedWorldRegion
|
var region := RegionScene.instantiate() as GeneratedWorldRegion
|
||||||
assert(region != null)
|
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)
|
root.add_child(region)
|
||||||
await process_frame
|
await process_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -124,7 +133,10 @@ func _validate_generated_region(
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
generator.get_primary_terrain_meshes().size()
|
generator.get_primary_terrain_meshes().size()
|
||||||
== expected_chunk_count
|
== (
|
||||||
|
expected_chunk_count
|
||||||
|
+ generator.stacked_elevated_placement_keys().size()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_validate_elevated_cliff_feature(generator)
|
_validate_elevated_cliff_feature(generator)
|
||||||
|
|
||||||
|
|
@ -179,6 +191,16 @@ func _validate_generated_region(
|
||||||
assert(decorations != null and decorations.get_child_count() > 0)
|
assert(decorations != null and decorations.get_child_count() > 0)
|
||||||
assert(anchors != null)
|
assert(anchors != null)
|
||||||
assert(anchors.get_spawn_positions().size() > 0)
|
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():
|
for child: Node in decorations.get_children():
|
||||||
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
|
||||||
assert(PROCEDURAL_PROP_IDS.has(prop_id))
|
assert(PROCEDURAL_PROP_IDS.has(prop_id))
|
||||||
|
|
@ -211,13 +233,80 @@ func _validate_generated_region(
|
||||||
child as Node3D,
|
child as Node3D,
|
||||||
definition,
|
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(
|
assert(
|
||||||
_has_material_variant_override(
|
_has_material_variant_override(
|
||||||
child.get_node_or_null("Visual"),
|
child.get_node_or_null("Visual"),
|
||||||
definition.material_variants,
|
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, &"grass_tree") > 0)
|
||||||
assert(_decoration_group_count(region, &"sand_tree") > 0)
|
assert(_decoration_group_count(region, &"sand_tree") > 0)
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -297,6 +386,35 @@ func _validate_elevated_cliff_feature(
|
||||||
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
|
assert(base_mesh.has_node("TerrainBaseLayerCollision"))
|
||||||
assert(overlay_mesh.has_node("TerrainCollision"))
|
assert(overlay_mesh.has_node("TerrainCollision"))
|
||||||
assert(layered_count == 9)
|
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(
|
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:
|
func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
||||||
var catalog := region.get_prop_catalog()
|
var catalog := region.get_prop_catalog()
|
||||||
assert(catalog != null)
|
assert(catalog != null)
|
||||||
|
|
@ -343,16 +486,49 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
|
||||||
&"prop_tree_1",
|
&"prop_tree_1",
|
||||||
&"prop_tree_2",
|
&"prop_tree_2",
|
||||||
&"prop_tree_3",
|
&"prop_tree_3",
|
||||||
|
&"prop_tree_large",
|
||||||
]:
|
]:
|
||||||
var tree := catalog.definition_for_id(tree_id)
|
var tree := catalog.definition_for_id(tree_id)
|
||||||
assert(tree != null)
|
assert(tree != null)
|
||||||
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
|
assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
|
||||||
assert(tree.material_variants.size() >= 3)
|
assert(tree.material_variants.size() >= 3)
|
||||||
assert(not tree.variant_material_slot_names.is_empty())
|
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")
|
var pine := catalog.definition_for_id(&"prop_pine")
|
||||||
assert(pine != null)
|
assert(pine != null)
|
||||||
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
|
assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
|
||||||
assert(pine.material_variants.is_empty())
|
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(
|
func _validate_biome_catalog(
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
[sub_resource type="Resource" id="Rule_sand_tree"]
|
[sub_resource type="Resource" id="Rule_sand_tree"]
|
||||||
script = ExtResource("2_rule")
|
script = ExtResource("2_rule")
|
||||||
procedural_group = &"sand_tree"
|
procedural_group = &"sand_tree"
|
||||||
placement_chance = 4200
|
placement_chance = 2200
|
||||||
adjacency_bonus = 1200
|
adjacency_bonus = 900
|
||||||
maximum_density = 5000
|
maximum_density = 2800
|
||||||
minimum_placements = 1
|
minimum_placements = 1
|
||||||
allowed_prop_ids = PackedStringArray("prop_palm")
|
allowed_prop_ids = PackedStringArray("prop_palm")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
||||||
maximum_density = 8500
|
maximum_density = 8500
|
||||||
minimum_placements = 2
|
minimum_placements = 2
|
||||||
placement_attempts_per_chunk = 3
|
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"]
|
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||||
script = ExtResource("2_rule")
|
script = ExtResource("2_rule")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ adjacency_bonus = 1200
|
||||||
maximum_density = 8500
|
maximum_density = 8500
|
||||||
minimum_placements = 2
|
minimum_placements = 2
|
||||||
placement_attempts_per_chunk = 3
|
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"]
|
[sub_resource type="Resource" id="Rule_grass_detail"]
|
||||||
script = ExtResource("2_rule")
|
script = ExtResource("2_rule")
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const OCEAN_POOL: FishPool = preload(
|
||||||
const WATER_HEIGHT := -0.25
|
const WATER_HEIGHT := -0.25
|
||||||
const PROP_EDGE_MARGIN := 2.2
|
const PROP_EDGE_MARGIN := 2.2
|
||||||
const PROP_PLACEMENT_ATTEMPTS := 12
|
const PROP_PLACEMENT_ATTEMPTS := 12
|
||||||
|
const PROP_CLUSTER_PLACEMENT_ATTEMPTS := 10
|
||||||
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||||
const PROP_CHANCE_SCALE := 10000
|
const PROP_CHANCE_SCALE := 10000
|
||||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||||
|
|
@ -268,6 +269,7 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
record.get("position", Vector3.ZERO),
|
record.get("position", Vector3.ZERO),
|
||||||
coordinate,
|
coordinate,
|
||||||
biome.stable_id,
|
biome.stable_id,
|
||||||
|
int(record.get("ocean_facing_edges", 0)),
|
||||||
random,
|
random,
|
||||||
terrain_triangles,
|
terrain_triangles,
|
||||||
):
|
):
|
||||||
|
|
@ -445,6 +447,7 @@ func _maybe_add_prop(
|
||||||
chunk_center: Vector3,
|
chunk_center: Vector3,
|
||||||
chunk_coordinate: Vector2i,
|
chunk_coordinate: Vector2i,
|
||||||
biome_id: StringName,
|
biome_id: StringName,
|
||||||
|
ocean_facing_edges: int,
|
||||||
random: RandomNumberGenerator,
|
random: RandomNumberGenerator,
|
||||||
terrain_triangles: Array[PackedVector3Array],
|
terrain_triangles: Array[PackedVector3Array],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
@ -460,6 +463,7 @@ func _maybe_add_prop(
|
||||||
chunk_center,
|
chunk_center,
|
||||||
chunk_coordinate,
|
chunk_coordinate,
|
||||||
biome_id,
|
biome_id,
|
||||||
|
ocean_facing_edges,
|
||||||
random,
|
random,
|
||||||
terrain_triangles,
|
terrain_triangles,
|
||||||
)
|
)
|
||||||
|
|
@ -470,19 +474,26 @@ func _add_prop(
|
||||||
chunk_center: Vector3,
|
chunk_center: Vector3,
|
||||||
chunk_coordinate: Vector2i,
|
chunk_coordinate: Vector2i,
|
||||||
biome_id: StringName,
|
biome_id: StringName,
|
||||||
|
ocean_facing_edges: int,
|
||||||
random: RandomNumberGenerator,
|
random: RandomNumberGenerator,
|
||||||
terrain_triangles: Array[PackedVector3Array],
|
terrain_triangles: Array[PackedVector3Array],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if definition == null or definition.packed_scene == null:
|
if definition == null or definition.packed_scene == null:
|
||||||
return false
|
return false
|
||||||
var visual_scale := random.randf_range(
|
var cluster_size := random.randi_range(
|
||||||
definition.minimum_visual_scale,
|
definition.minimum_cluster_size,
|
||||||
definition.maximum_visual_scale,
|
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 placement := Vector3.ZERO
|
||||||
var found_surface := false
|
var found_surface := false
|
||||||
for attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
for _attempt: int in PROP_PLACEMENT_ATTEMPTS:
|
||||||
var offset := Vector3(
|
var offset := Vector3(
|
||||||
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
random.randf_range(-PROP_EDGE_MARGIN, PROP_EDGE_MARGIN),
|
||||||
0.0,
|
0.0,
|
||||||
|
|
@ -507,6 +518,118 @@ func _add_prop(
|
||||||
break
|
break
|
||||||
if not found_surface:
|
if not found_surface:
|
||||||
return false
|
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 packed_instance := definition.packed_scene.instantiate()
|
||||||
var visual_root := packed_instance as Node3D
|
var visual_root := packed_instance as Node3D
|
||||||
if visual_root == null:
|
if visual_root == null:
|
||||||
|
|
@ -522,8 +645,10 @@ func _add_prop(
|
||||||
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
|
prop.set_meta(&"terrain_chunk_coordinate", chunk_coordinate)
|
||||||
prop.set_meta(&"terrain_biome_id", biome_id)
|
prop.set_meta(&"terrain_biome_id", biome_id)
|
||||||
prop.set_meta(&"terrain_prop_visual_scale", visual_scale)
|
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.position = placement
|
||||||
prop.rotation.y = random.randf_range(-PI, PI)
|
prop.rotation.y = yaw
|
||||||
_decorations.add_child(prop)
|
_decorations.add_child(prop)
|
||||||
visual_root.name = "Visual"
|
visual_root.name = "Visual"
|
||||||
prop.add_child(visual_root)
|
prop.add_child(visual_root)
|
||||||
|
|
@ -533,7 +658,9 @@ func _add_prop(
|
||||||
_apply_prop_material_variant(visual_root, definition, random)
|
_apply_prop_material_variant(visual_root, definition, random)
|
||||||
_add_prop_collision(prop, definition, visual_scale)
|
_add_prop_collision(prop, definition, visual_scale)
|
||||||
_placed_prop_positions.append(placement)
|
_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)
|
_placed_prop_groups.append(definition.procedural_group)
|
||||||
if definition.gatherable_anchor_height > 0.0:
|
if definition.gatherable_anchor_height > 0.0:
|
||||||
var anchor := Marker3D.new()
|
var anchor := Marker3D.new()
|
||||||
|
|
@ -547,6 +674,46 @@ func _add_prop(
|
||||||
return true
|
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:
|
func _validate_prop_catalog() -> void:
|
||||||
if prop_catalog == null:
|
if prop_catalog == null:
|
||||||
push_error("Generated world terrain prop catalog is unavailable.")
|
push_error("Generated world terrain prop catalog is unavailable.")
|
||||||
|
|
@ -776,6 +943,7 @@ func _ensure_minimum_props(
|
||||||
record.get("position", Vector3.ZERO),
|
record.get("position", Vector3.ZERO),
|
||||||
coordinate,
|
coordinate,
|
||||||
biome.stable_id,
|
biome.stable_id,
|
||||||
|
int(record.get("ocean_facing_edges", 0)),
|
||||||
random,
|
random,
|
||||||
terrain_triangles,
|
terrain_triangles,
|
||||||
):
|
):
|
||||||
|
|
@ -819,17 +987,32 @@ func _apply_prop_material_variant(
|
||||||
definition: TerrainPropDefinition,
|
definition: TerrainPropDefinition,
|
||||||
random: RandomNumberGenerator,
|
random: RandomNumberGenerator,
|
||||||
) -> void:
|
) -> void:
|
||||||
if (
|
_apply_random_material_variant(
|
||||||
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(
|
|
||||||
root_node,
|
root_node,
|
||||||
definition.variant_material_slot_names,
|
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,
|
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"
|
procedural_group = &"sand_tree"
|
||||||
required_chunk_tags = PackedStringArray("sand")
|
required_chunk_tags = PackedStringArray("sand")
|
||||||
minimum_spawn_chunk_distance = 2
|
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_radius = 0.4
|
||||||
collision_height = 6.0
|
collision_height = 6.0
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_definition")
|
script = ExtResource("1_definition")
|
||||||
stable_id = &"prop_pine"
|
stable_id = &"prop_pine"
|
||||||
label = "pine"
|
label = "small pine"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
procedural_group = &"grass_tree"
|
procedural_group = &"grass_tree"
|
||||||
required_chunk_tags = PackedStringArray("grass")
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
selection_weight = 0.7
|
selection_weight = 0.7
|
||||||
minimum_spawn_chunk_distance = 2
|
minimum_spawn_chunk_distance = 2
|
||||||
clearance_radius = 1.55
|
clearance_radius = 1.55
|
||||||
minimum_visual_scale = 0.75
|
minimum_visual_scale = 0.65
|
||||||
maximum_visual_scale = 1.22
|
maximum_visual_scale = 1.2
|
||||||
collision_radius = 0.5
|
collision_radius = 0.5
|
||||||
collision_height = 4.0
|
collision_height = 4.0
|
||||||
gatherable_anchor_height = 2.15
|
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="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="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_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_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/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]
|
[resource]
|
||||||
script = ExtResource("1_definition")
|
script = ExtResource("1_definition")
|
||||||
stable_id = &"prop_tree_1"
|
stable_id = &"prop_tree_1"
|
||||||
label = "small tree"
|
label = "small deciduous tree"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
procedural_group = &"grass_tree"
|
procedural_group = &"grass_tree"
|
||||||
required_chunk_tags = PackedStringArray("grass")
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 1.0
|
||||||
minimum_spawn_chunk_distance = 2
|
minimum_spawn_chunk_distance = 2
|
||||||
clearance_radius = 1.3
|
clearance_radius = 1.3
|
||||||
minimum_visual_scale = 0.75
|
minimum_visual_scale = 0.65
|
||||||
maximum_visual_scale = 1.15
|
maximum_visual_scale = 1.2
|
||||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
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")])
|
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_radius = 0.4
|
||||||
collision_height = 3.2
|
collision_height = 3.2
|
||||||
gatherable_anchor_height = 2.15
|
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="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="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_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_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/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]
|
[resource]
|
||||||
script = ExtResource("1_definition")
|
script = ExtResource("1_definition")
|
||||||
stable_id = &"prop_tree_2"
|
stable_id = &"prop_tree_2"
|
||||||
label = "medium tree"
|
label = "medium deciduous tree"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
procedural_group = &"grass_tree"
|
procedural_group = &"grass_tree"
|
||||||
required_chunk_tags = PackedStringArray("grass")
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 1.0
|
||||||
minimum_spawn_chunk_distance = 2
|
minimum_spawn_chunk_distance = 2
|
||||||
clearance_radius = 1.5
|
clearance_radius = 1.5
|
||||||
minimum_visual_scale = 0.8
|
minimum_visual_scale = 0.7
|
||||||
maximum_visual_scale = 1.18
|
maximum_visual_scale = 1.2
|
||||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
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")])
|
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_radius = 0.5
|
||||||
collision_height = 3.8
|
collision_height = 3.8
|
||||||
gatherable_anchor_height = 2.15
|
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="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="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_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_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/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]
|
[resource]
|
||||||
script = ExtResource("1_definition")
|
script = ExtResource("1_definition")
|
||||||
stable_id = &"prop_tree_3"
|
stable_id = &"prop_tree_3"
|
||||||
label = "large tree"
|
label = "decorative deciduous tree"
|
||||||
packed_scene = ExtResource("2_scene")
|
packed_scene = ExtResource("2_scene")
|
||||||
procedural_group = &"grass_tree"
|
procedural_group = &"grass_tree"
|
||||||
required_chunk_tags = PackedStringArray("grass")
|
required_chunk_tags = PackedStringArray("grass")
|
||||||
|
selection_weight = 1.0
|
||||||
minimum_spawn_chunk_distance = 2
|
minimum_spawn_chunk_distance = 2
|
||||||
clearance_radius = 1.8
|
clearance_radius = 1.8
|
||||||
minimum_visual_scale = 0.85
|
minimum_visual_scale = 0.7
|
||||||
maximum_visual_scale = 1.15
|
maximum_visual_scale = 1.2
|
||||||
variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark")
|
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")])
|
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_radius = 0.55
|
||||||
collision_height = 4.2
|
collision_height = 4.2
|
||||||
gatherable_anchor_height = 2.15
|
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="Script" path="res://world/generation/terrain_prop_catalog.gd" id="1_catalog"]
|
||||||
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_bridge.tres" id="2_bridge"]
|
[ext_resource type="Resource" path="res://world/generation/props/definitions/prop_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_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="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="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]
|
[resource]
|
||||||
script = ExtResource("1_catalog")
|
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_edge_chunk_id: StringName = &"chunk_0011"
|
||||||
@export var elevated_cliff_ramp_chunk_id: StringName = &"chunk_0012"
|
@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
|
@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_group("")
|
||||||
@export_range(1.0, 100.0, 1.0) var required_chunk_weight_multiplier := 64.0
|
@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
|
@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 _backtrack_count := 0
|
||||||
var _elevated_feature_center := Vector2i(-1, -1)
|
var _elevated_feature_center := Vector2i(-1, -1)
|
||||||
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
var _elevated_feature_reserved_grass_indices: Dictionary[int, bool] = {}
|
||||||
|
var _stacked_elevated_placements: Array[Dictionary] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -100,6 +106,9 @@ func generate() -> bool:
|
||||||
if not _apply_elevated_feature():
|
if not _apply_elevated_feature():
|
||||||
_assign_placements(previous_placements)
|
_assign_placements(previous_placements)
|
||||||
return false
|
return false
|
||||||
|
if not _configure_stacked_elevated_feature():
|
||||||
|
_assign_placements(previous_placements)
|
||||||
|
return false
|
||||||
var solution_root := _build_solution_root()
|
var solution_root := _build_solution_root()
|
||||||
if solution_root == null:
|
if solution_root == null:
|
||||||
_assign_placements(previous_placements)
|
_assign_placements(previous_placements)
|
||||||
|
|
@ -138,6 +147,9 @@ func generate_from_placement_keys(keys: PackedStringArray) -> bool:
|
||||||
previous_placements.assign(_placements)
|
previous_placements.assign(_placements)
|
||||||
_assign_placements(resolved)
|
_assign_placements(resolved)
|
||||||
_backtrack_count = 0
|
_backtrack_count = 0
|
||||||
|
if not _configure_stacked_elevated_feature():
|
||||||
|
_assign_placements(previous_placements)
|
||||||
|
return false
|
||||||
var solution_root := _build_solution_root()
|
var solution_root := _build_solution_root()
|
||||||
if solution_root == null:
|
if solution_root == null:
|
||||||
_assign_placements(previous_placements)
|
_assign_placements(previous_placements)
|
||||||
|
|
@ -1184,6 +1196,7 @@ func _candidate_can_occupy_cell(
|
||||||
func _prepare_elevated_feature_region() -> bool:
|
func _prepare_elevated_feature_region() -> bool:
|
||||||
_elevated_feature_center = Vector2i(-1, -1)
|
_elevated_feature_center = Vector2i(-1, -1)
|
||||||
_elevated_feature_reserved_grass_indices.clear()
|
_elevated_feature_reserved_grass_indices.clear()
|
||||||
|
_stacked_elevated_placements.clear()
|
||||||
if not elevated_cliff_feature_enabled:
|
if not elevated_cliff_feature_enabled:
|
||||||
_build_grid_solver_caches()
|
_build_grid_solver_caches()
|
||||||
return true
|
return true
|
||||||
|
|
@ -1439,6 +1452,65 @@ func _apply_elevated_feature() -> bool:
|
||||||
return true
|
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(
|
func _variant_respects_ocean_boundary(
|
||||||
variant: TerrainChunkVariant,
|
variant: TerrainChunkVariant,
|
||||||
coordinate: Vector2i,
|
coordinate: Vector2i,
|
||||||
|
|
@ -2040,9 +2112,74 @@ func _build_solution_root() -> Node3D:
|
||||||
_add_collision(chunk_root, variant.definition)
|
_add_collision(chunk_root, variant.definition)
|
||||||
if show_chunk_labels:
|
if show_chunk_labels:
|
||||||
_add_chunk_label(chunk_root, variant)
|
_add_chunk_label(chunk_root, variant)
|
||||||
|
if not _add_stacked_elevated_chunks(solution_root):
|
||||||
|
solution_root.free()
|
||||||
|
return null
|
||||||
return solution_root
|
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(
|
func _instantiate_chunk_root(
|
||||||
definition: TerrainChunkDefinition,
|
definition: TerrainChunkDefinition,
|
||||||
) -> Node3D:
|
) -> Node3D:
|
||||||
|
|
@ -2171,6 +2308,7 @@ func _build_summary() -> Dictionary:
|
||||||
"counts": counts,
|
"counts": counts,
|
||||||
"variant_counts": variant_counts,
|
"variant_counts": variant_counts,
|
||||||
"placements": placement_keys(),
|
"placements": placement_keys(),
|
||||||
|
"stacked_elevated_placements": stacked_elevated_placement_keys(),
|
||||||
"layout_fingerprint": placement_fingerprint(),
|
"layout_fingerprint": placement_fingerprint(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2214,9 +2352,29 @@ func placement_fingerprint() -> String:
|
||||||
"chunk_size:%.6f" % (catalog.chunk_size if catalog != null else 0.0),
|
"chunk_size:%.6f" % (catalog.chunk_size if catalog != null else 0.0),
|
||||||
])
|
])
|
||||||
fingerprint_input.append_array(placement_keys())
|
fingerprint_input.append_array(placement_keys())
|
||||||
|
fingerprint_input.append_array(stacked_elevated_placement_keys())
|
||||||
return "\n".join(fingerprint_input).sha256_text()
|
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]:
|
func placement_records() -> Array[Dictionary]:
|
||||||
var result: Array[Dictionary] = []
|
var result: Array[Dictionary] = []
|
||||||
for index: int in _placements.size():
|
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:
|
if primary_mesh != null and primary_mesh.mesh != null:
|
||||||
result.append(primary_mesh)
|
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
|
return result
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,23 @@ func validation_errors() -> PackedStringArray:
|
||||||
"%s has an inverted visual scale range."
|
"%s has an inverted visual scale range."
|
||||||
% definition.stable_id
|
% 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 (
|
if (
|
||||||
not definition.material_variants.is_empty()
|
not definition.material_variants.is_empty()
|
||||||
and definition.variant_material_slot_names.is_empty()
|
and definition.variant_material_slot_names.is_empty()
|
||||||
|
|
@ -101,4 +118,26 @@ func validation_errors() -> PackedStringArray:
|
||||||
"%s contains an empty material variant."
|
"%s contains an empty material variant."
|
||||||
% definition.stable_id
|
% 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
|
return errors
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,28 @@ extends Resource
|
||||||
## Procedural instances use a deterministic uniform scale in this range.
|
## 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 minimum_visual_scale := 1.0
|
||||||
@export_range(0.1, 4.0, 0.05) var maximum_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.
|
## Presentation-only offset. The prop root remains exactly terrain-aligned.
|
||||||
@export var visual_offset := Vector3.ZERO
|
@export var visual_offset := Vector3.ZERO
|
||||||
## A single material is chosen per prop and applied only to matching imported
|
## A single material is chosen per prop and applied only to matching imported
|
||||||
## material slots. This keeps trunks untouched while varying foliage.
|
## material slots. This keeps trunks untouched while varying foliage.
|
||||||
@export var variant_material_slot_names := PackedStringArray()
|
@export var variant_material_slot_names := PackedStringArray()
|
||||||
@export var material_variants: Array[Material] = []
|
@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, 5.0, 0.05) var collision_radius := 0.0
|
||||||
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0
|
@export_range(0.0, 20.0, 0.05) var collision_height := 0.0
|
||||||
@export var collision_box_size := Vector3.ZERO
|
@export var collision_box_size := Vector3.ZERO
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue