feat: author resource-aware riverbank grove
This commit is contained in:
@@ -69,6 +69,10 @@ interaction positions by action and horizontal cell, plus authoritative
|
|||||||
metadata bounds copied at bind time. It does not own amounts, enabled state,
|
metadata bounds copied at bind time. It does not own amounts, enabled state,
|
||||||
reservations, or persistence. `ResourceNode` enter/exit, bind, and transform
|
reservations, or persistence. `ResourceNode` enter/exit, bind, and transform
|
||||||
notifications keep it synchronized with the loaded presentation lifecycle.
|
notifications keep it synchronized with the loaded presentation lifecycle.
|
||||||
|
Resource anchors may be nested inside authored presentation clusters: runtime
|
||||||
|
discovery uses their stable IDs and registration lifecycle, never a parent
|
||||||
|
path. Decorative siblings without a `ResourceNode` remain outside the index
|
||||||
|
and simulation state.
|
||||||
|
|
||||||
Outside the runtime lifecycle, `simulation/benchmark/` owns reusable,
|
Outside the runtime lifecycle, `simulation/benchmark/` owns reusable,
|
||||||
schema-valid workload fixtures. CLI tools and headless scenarios consume those
|
schema-valid workload fixtures. CLI tools and headless scenarios consume those
|
||||||
|
|||||||
@@ -780,16 +780,22 @@ Completed:
|
|||||||
aligned; softened foam/mist and a smaller rock frame make the falls part of
|
aligned; softened foam/mist and a smaller rock frame make the falls part of
|
||||||
the landform. A matched Metal capture records the change, and the rebaked
|
the landform. A matched Metal capture records the change, and the rebaked
|
||||||
navigation mesh passes terrain-height and runtime route checks.
|
navigation mesh passes terrain-height and runtime route checks.
|
||||||
|
37. `Jajce Resource Grove 04`: the right riverbank now groups 28 intentionally
|
||||||
|
instanced fern/flower/stone details and two decorative trees around two
|
||||||
|
existing finite resources. The berry and tree keep their stable IDs and
|
||||||
|
metadata, use specific visuals instead of boxes, and remain reachable from
|
||||||
|
separate terrain-snapped approach points. Matched captures and a focused
|
||||||
|
scenario prove three-batch visual cost, nested discovery, and a forced
|
||||||
|
query beyond the first 24 m range without changing the 18-resource state.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Prove one bounded foliage/resource authoring workflow at a riverbank or
|
1. Give the authored berry/tree resources state-driven low/depleted visuals
|
||||||
forest edge. Use decorative Terrain3D/intentional instances for tree, fern,
|
sourced from their real `ResourceStateRecord` amount. Prove NPC and player
|
||||||
flower, and rock masses, but create simulation-owned finite resources only
|
extraction plus save/restore rebuild the same presentation without adding
|
||||||
through selected stable-ID `ResourceNode` anchors.
|
visual fields to simulation state.
|
||||||
2. Validate that villagers discover both nearby and farther authored anchors,
|
2. Apply the workflow to a second forest-edge consumer only after that contract
|
||||||
preserve reachable interaction points, and keep the visual batch inside a
|
is honest and reusable. Add livestock only with a real identity and
|
||||||
measured presentation budget. Add livestock only with a real identity and
|
|
||||||
interaction contract.
|
interaction contract.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||||
|
|||||||
@@ -896,17 +896,30 @@ plunge basin, and curved downstream channel. A matched Metal capture records
|
|||||||
the result, and the Terrain3D-derived navigation mesh was rebaked and validated
|
the result, and the Terrain3D-derived navigation mesh was rebaked and validated
|
||||||
against required village/resource routes.
|
against required village/resource routes.
|
||||||
|
|
||||||
The immediate next slice should prove one bounded foliage/resource authoring
|
The first authored foliage/resource workflow is complete. One riverbank cluster
|
||||||
workflow in the real world: build a restrained riverbank or forest-edge cluster
|
uses three intentional MultiMesh batches for 28 fern, flower, and stone
|
||||||
with decorative Terrain3D/intentional instances, pair only selected harvestable
|
instances plus two decorative trees, while only an existing stable-ID berry and
|
||||||
trees or berry patches with stable-ID `ResourceNode` anchors, and verify nearby
|
tree are finite `ResourceNode` anchors. Both anchors preserve the save contract
|
||||||
and farther loaded anchors remain discoverable through `ActiveWorldAdapter`.
|
of eighteen resources, sit above the carved channel, and expose separate
|
||||||
Keep decorative instances out of simulation state, measure the bounded visual
|
terrain-snapped approach points. A forced village query expands beyond the
|
||||||
cost, and do not generalize the resource grid to people/buildings/events or
|
initial 24 m range and selects the nested berry exactly by ID.
|
||||||
|
|
||||||
|
The immediate next slice should make those two resource-specific visuals
|
||||||
|
reflect authoritative amount/depletion: thin the berry patch and leave a stump
|
||||||
|
or equivalent readable tree state after real NPC/player extraction, then prove
|
||||||
|
save/restore rebuilds the same presentation without persisting visual-only
|
||||||
|
state. Only after both the berry and tree consume that contract should shared
|
||||||
|
cluster/depletion tooling be extracted or the workflow copied to a forest
|
||||||
|
edge. Do not generalize the resource grid to people/buildings/events or
|
||||||
introduce active/abstract LOD before those consumers need it.
|
introduce active/abstract LOD before those consumers need it.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
- `Jajce Resource Grove 04`: two existing river resources now live as nested
|
||||||
|
stable-ID anchors inside a restrained decorative bank cluster. Twenty-eight
|
||||||
|
understory instances use three MultiMesh batches, bespoke berry/tree visuals
|
||||||
|
replace generic boxes, terrain-snapped approach points remain reachable, and
|
||||||
|
a real far-source query proves parent-path-independent expanding discovery.
|
||||||
- `Jajce River 03`: deterministic Terrain3D generation now carves the upper
|
- `Jajce River 03`: deterministic Terrain3D generation now carves the upper
|
||||||
stream, raised waterfall shelf, plunge basin, and curved downstream channel;
|
stream, raised waterfall shelf, plunge basin, and curved downstream channel;
|
||||||
shared watercourse geometry keeps both water ribbons aligned, soft masked
|
shared watercourse geometry keeps both water ribbons aligned, soft masked
|
||||||
|
|||||||
+11
-8
@@ -354,7 +354,9 @@ but retain far sources whenever their authoritative metadata can still win.
|
|||||||
transforms, authored conifer clusters, and oversized ambient butterflies;
|
transforms, authored conifer clusters, and oversized ambient butterflies;
|
||||||
- one sparse terrain-aware calligraphic gust field with two or three soft
|
- one sparse terrain-aware calligraphic gust field with two or three soft
|
||||||
tapered strokes per burst;
|
tapered strokes per burst;
|
||||||
- eighteen ResourceNodes preserving stable food/wood discovery IDs;
|
- eighteen ResourceNodes preserving stable food/wood discovery IDs, including
|
||||||
|
one riverbank cluster that nests two finite anchors among 28 decorative
|
||||||
|
fern/flower/stone instances in three bounded MultiMesh batches;
|
||||||
- typed village pantry storage and typed guard, study, and rest activity sites;
|
- typed village pantry storage and typed guard, study, and rest activity sites;
|
||||||
- a Terrain3D-derived baked navigation mesh covering the current playable loop;
|
- a Terrain3D-derived baked navigation mesh covering the current playable loop;
|
||||||
- base collision/navigation guardrails that keep Terrain3D collision enabled,
|
- base collision/navigation guardrails that keep Terrain3D collision enabled,
|
||||||
@@ -980,13 +982,14 @@ Raw samples and workload exclusions live in
|
|||||||
[Simulation scaling baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md)
|
[Simulation scaling baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md)
|
||||||
and [Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md).
|
and [Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md).
|
||||||
|
|
||||||
The authored Jajce watercourse and its Terrain3D-derived navigation rebake are
|
The authored Jajce watercourse, navigation rebake, and first bounded
|
||||||
complete. The next slice should prove a bounded stable-ID resource-anchor
|
foliage/resource placement proof are complete. The next slice should derive
|
||||||
placement workflow beside decorative Terrain3D/intentional foliage instances
|
low/depleted berry and tree visuals from authoritative resource amount, cover
|
||||||
at one riverbank or forest edge. It should validate nearby and farther loaded
|
real NPC/player extraction and save/restore reconstruction, and keep that
|
||||||
resource discovery, reachable interaction points, and a measured presentation
|
presentation out of serialized simulation state. Apply the cluster workflow to
|
||||||
budget without making decorative instances authoritative. Do not yet
|
a second forest edge only after the depletion contract supplies a real second
|
||||||
generalize the resource grid or introduce active/abstract simulation LOD.
|
consumer. Do not yet generalize the resource grid or introduce active/abstract
|
||||||
|
simulation LOD.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -567,6 +567,16 @@ existing `ResourceStateRecord`. The reviewed 18/180/1,800-source benchmark and
|
|||||||
exact-selection contract live in
|
exact-selection contract live in
|
||||||
[Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md).
|
[Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md).
|
||||||
|
|
||||||
|
The first authored placement proof now nests `berry_bush_river_02` and
|
||||||
|
`tree_river_resource_01` inside one riverbank cluster without changing their
|
||||||
|
IDs, metadata, or the eighteen-resource save contract. Twenty-eight decorative
|
||||||
|
understory instances and two silhouette trees remain presentation-only. The
|
||||||
|
finite anchors have resource-specific visuals and separate terrain-snapped
|
||||||
|
approach points; recursive navigation checks cover both, while a forced query
|
||||||
|
selects the farther berry only after expanding beyond the first 24 m range.
|
||||||
|
The matched review lives in
|
||||||
|
[Jajce Resource Grove 04](baselines/JAJCE_RESOURCE_GROVE_04.md).
|
||||||
|
|
||||||
## Test scenarios
|
## Test scenarios
|
||||||
|
|
||||||
At minimum, exercise:
|
At minimum, exercise:
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Jajce Resource Grove 04
|
||||||
|
|
||||||
|
Captured: 2026-07-17
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- [before overview](jajce_resource_grove_04_before.png)
|
||||||
|
- [after overview](jajce_resource_grove_04_after.png)
|
||||||
|
- `world/jajce/RiverbankResourceCluster.tscn`
|
||||||
|
- `tests/jajce_resource_cluster_test.gd`
|
||||||
|
|
||||||
|
The images use the same 1280×720 camera, fixed daylight value, warmup, and
|
||||||
|
Metal renderer. The new cluster is on the right bank beside and beyond the
|
||||||
|
bridge; the comparison intentionally keeps the broader citadel/waterfall
|
||||||
|
composition unchanged.
|
||||||
|
|
||||||
|
## Visual reduction
|
||||||
|
|
||||||
|
The former river edge had one isolated deciduous tree and conifer, while two
|
||||||
|
generic-box resource anchors sat separately from the foliage language. The
|
||||||
|
after frame gathers that silhouette into one restrained bank mass:
|
||||||
|
|
||||||
|
- 12 low-poly fern clumps;
|
||||||
|
- 9 oversized warm flower heads;
|
||||||
|
- 7 flattened bank stones;
|
||||||
|
- 2 decorative silhouette trees; and
|
||||||
|
- 2 finite anchors with distinct berry-patch and harvestable-tree visuals.
|
||||||
|
|
||||||
|
The 28 understory details use three `MultiMeshInstance3D` batches. The berry
|
||||||
|
visual adds five wind-reactive leaf clumps and eight deliberately readable
|
||||||
|
berries. This is a bounded authored accent, not a world-wide scatter pass.
|
||||||
|
|
||||||
|
## Authority and authoring contract
|
||||||
|
|
||||||
|
`DecorativeInstances` contains no `ResourceNode` descendants. Its trees,
|
||||||
|
ferns, flowers, and stones are transient presentation and never enter saves,
|
||||||
|
checksums, reservations, or resource conservation.
|
||||||
|
|
||||||
|
`ResourceAnchors` contains only `berry_bush_river_02` and
|
||||||
|
`tree_river_resource_01`. These are the same existing IDs and definitions, now
|
||||||
|
nested inside the authored cluster. `ResourceNode` registration and
|
||||||
|
`ActiveWorldAdapter` discovery do not depend on a parent path, so simulation
|
||||||
|
authority still binds exactly 18 resources.
|
||||||
|
|
||||||
|
The visible plants sit on the raised Terrain3D bank. Each anchor has a separate
|
||||||
|
approach marker snapped to the lower bank surface, keeping the visual context
|
||||||
|
honest without asking NPCs to climb an unreachable slope. All required
|
||||||
|
navigation routes reach those markers and remain close to Terrain3D height.
|
||||||
|
|
||||||
|
## Far-discovery proof
|
||||||
|
|
||||||
|
The berry interaction point is approximately 30.5 m from the village origin,
|
||||||
|
beyond the adapter's initial 24 m query radius. The focused scenario first
|
||||||
|
confirms ordinary nearby food remains available, then disables every other food
|
||||||
|
source in authoritative state. `ActionTargetResolver` expands through more than
|
||||||
|
one range pass and selects `berry_bush_river_02` by stable ID. The test also
|
||||||
|
checks that direct and nested anchors produce the same 18-entry index.
|
||||||
|
|
||||||
|
## Remaining work
|
||||||
|
|
||||||
|
The resource-specific visuals currently identify finite sources but do not yet
|
||||||
|
thin or change shape as their authoritative amount falls. The next vertical
|
||||||
|
slice should show low/depleted berries and a stump or equivalent tree state
|
||||||
|
after real NPC/player extraction, then verify save/load reconstructs the same
|
||||||
|
visual from `ResourceStateRecord` without serializing presentation fields.
|
||||||
|
|
||||||
|
## Capture command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HOME="$PWD/logs/quality/godot_profile" \
|
||||||
|
APPDATA="$PWD/logs/quality/godot_profile" \
|
||||||
|
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot \
|
||||||
|
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
|
||||||
|
--output=res://docs/baselines/jajce_resource_grove_04_after.png
|
||||||
|
```
|
||||||
@@ -60,11 +60,11 @@ player resource gathering with all 18 finite resource anchors loaded.
|
|||||||
|
|
||||||
## Next visual/system proof
|
## Next visual/system proof
|
||||||
|
|
||||||
The next pass should author one bounded riverbank or forest-edge foliage mass,
|
That proof is complete in
|
||||||
using decorative Terrain3D/intentional instances for visual density and only a
|
[Jajce Resource Grove 04](JAJCE_RESOURCE_GROVE_04.md): a bounded riverbank mass
|
||||||
small selected set of stable-ID `ResourceNode` anchors for finite harvestable
|
keeps decorative density separate from two stable-ID finite anchors. The next
|
||||||
resources. Bank stones, ferns, flowers, and tree masses can improve the channel
|
pass should make those berry/tree visuals reflect real depletion before adding
|
||||||
edge during that proof. Bridge, buildings, and fortress geometry remain
|
a second forest-edge cluster. Bridge, buildings, and fortress geometry remain
|
||||||
prototype blockouts and should be replaced gradually rather than through a
|
prototype blockouts and should be replaced gradually rather than through a
|
||||||
broad asset dump.
|
broad asset dump.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 745 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://drip3uanxmac0"
|
||||||
|
path="res://.godot/imported/jajce_resource_grove_04_after.png-400fd71bda2bd7202805c1ba66aa8c28.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/baselines/jajce_resource_grove_04_after.png"
|
||||||
|
dest_files=["res://.godot/imported/jajce_resource_grove_04_after.png-400fd71bda2bd7202805c1ba66aa8c28.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 743 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ch6nx7rj4elit"
|
||||||
|
path="res://.godot/imported/jajce_resource_grove_04_before.png-153834f7053f143a34b090dc21ac0d16.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/baselines/jajce_resource_grove_04_before.png"
|
||||||
|
dest_files=["res://.godot/imported/jajce_resource_grove_04_before.png-153834f7053f143a34b090dc21ac0d16.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -68,7 +68,10 @@ simulation-LOD framework yet.
|
|||||||
Terrain3D foliage instances remain decorative unless an intentional stable-ID
|
Terrain3D foliage instances remain decorative unless an intentional stable-ID
|
||||||
`ResourceNode` anchor binds simulation-owned state. A later placement workflow
|
`ResourceNode` anchor binds simulation-owned state. A later placement workflow
|
||||||
can author those anchors alongside visual instancing without making every tree
|
can author those anchors alongside visual instancing without making every tree
|
||||||
an authoritative resource.
|
an authoritative resource. That authored proof is now complete in
|
||||||
|
[Jajce Resource Grove 04](../baselines/JAJCE_RESOURCE_GROVE_04.md): two nested
|
||||||
|
finite anchors remain discoverable among three bounded decorative MultiMesh
|
||||||
|
batches, including a real query that expands beyond its first local range.
|
||||||
|
|
||||||
## Capture command
|
## Capture command
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const FAR_BERRY_ID := &"berry_bush_river_02"
|
||||||
|
const CLUSTER_TREE_ID := &"tree_river_resource_01"
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var camera := Camera3D.new()
|
||||||
|
camera.current = true
|
||||||
|
root.add_child(camera)
|
||||||
|
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||||
|
root.add_child(world)
|
||||||
|
await process_frame
|
||||||
|
for _frame in 5:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
var terrain := world.get_node("TerrainRoot/Terrain3D") as Terrain3D
|
||||||
|
var cluster := (
|
||||||
|
world.get_node("WorldObjects/ResourceClusters/RiverbankResourceCluster")
|
||||||
|
as RiverbankResourceCluster
|
||||||
|
)
|
||||||
|
_check(cluster != null, "Jajce should expose the authored riverbank resource cluster")
|
||||||
|
if cluster == null:
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
var stats := cluster.get_presentation_stats()
|
||||||
|
_check(
|
||||||
|
int(stats["understory_instance_count"]) == 28,
|
||||||
|
"Riverbank understory should keep its reviewed 28-instance budget"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(stats["multimesh_batch_count"]) == 3,
|
||||||
|
"Riverbank understory should use exactly three intentional MultiMesh batches"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(stats["decorative_tree_count"]) == 2,
|
||||||
|
"Riverbank cluster should keep two non-authoritative silhouette trees"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(stats["resource_anchor_count"]) == 2,
|
||||||
|
"Riverbank cluster should pair density with only two finite resource anchors"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
_collect_resource_nodes(cluster.get_node("DecorativeInstances")).is_empty(),
|
||||||
|
"Decorative riverbank instances must not become simulation resources"
|
||||||
|
)
|
||||||
|
|
||||||
|
var anchors := cluster.get_resource_anchors()
|
||||||
|
var anchor_ids: Array[StringName] = []
|
||||||
|
for anchor in anchors:
|
||||||
|
anchor_ids.append(anchor.node_id)
|
||||||
|
var terrain_height := terrain.data.get_height(anchor.global_position)
|
||||||
|
_check(
|
||||||
|
absf(anchor.global_position.y - terrain_height) < 0.05,
|
||||||
|
"Resource anchor %s should snap to Terrain3D" % anchor.node_id
|
||||||
|
)
|
||||||
|
var channel_distance := absf(
|
||||||
|
(
|
||||||
|
anchor.global_position.x
|
||||||
|
- JajceWatercourse.downstream_center_x(anchor.global_position.z)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
channel_distance
|
||||||
|
> JajceWatercourse.downstream_half_width(anchor.global_position.z) + 1.5
|
||||||
|
),
|
||||||
|
"Resource anchor %s should sit on the riverbank rather than in water" % anchor.node_id
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
anchor_ids.size() == 2 and anchor_ids.has(FAR_BERRY_ID) and anchor_ids.has(CLUSTER_TREE_ID),
|
||||||
|
"Authored cluster should preserve the existing river berry/tree stable IDs"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
cluster.has_node("ResourceAnchors/BerryBush_River_02/Visual/BerryPatchPresentation")
|
||||||
|
and cluster.has_node("ResourceAnchors/Tree_River_Resource_01/Visual/TreePresentation")
|
||||||
|
),
|
||||||
|
"Finite cluster anchors should have resource-specific visuals instead of boxes"
|
||||||
|
)
|
||||||
|
var berry_presentation := (
|
||||||
|
cluster.get_node("ResourceAnchors/BerryBush_River_02/Visual/BerryPatchPresentation")
|
||||||
|
as StylizedBerryPatch
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
berry_presentation.get_presentation_instance_count() == 13,
|
||||||
|
"Berry resource visual should keep its bounded leaf-and-fruit instance count"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resources := _collect_resource_nodes(world.get_node("WorldObjects"))
|
||||||
|
_check(resources.size() == 18, "Clustering should preserve all eighteen finite resources")
|
||||||
|
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||||
|
var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
||||||
|
var adapter := ActiveWorldAdapter.new()
|
||||||
|
adapter.pantry_storage = pantry
|
||||||
|
adapter.woodpile_storage = woodpile
|
||||||
|
root.add_child(adapter)
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.debug_logs = false
|
||||||
|
manager.active_world_adapter = adapter
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
_check(
|
||||||
|
int(adapter.get_resource_index_stats()["candidate_count"]) == 18,
|
||||||
|
"ActiveWorldAdapter should index direct and nested resource anchors equally"
|
||||||
|
)
|
||||||
|
var nearby_food := adapter.get_resource_candidates_in_radius(
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD, Vector3.ZERO, 24.0
|
||||||
|
)
|
||||||
|
var farther_food := adapter.get_resource_candidates_in_radius(
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD, Vector3.ZERO, 64.0, 24.0
|
||||||
|
)
|
||||||
|
_check(not nearby_food.is_empty(), "The authored world should retain nearby food choices")
|
||||||
|
_check(
|
||||||
|
_candidate_ids(farther_food).has(FAR_BERRY_ID),
|
||||||
|
"The nested riverbank berry should occupy a discoverable farther range"
|
||||||
|
)
|
||||||
|
|
||||||
|
for resource in resources:
|
||||||
|
if resource.resource_id != SimulationIds.RESOURCE_FOOD:
|
||||||
|
continue
|
||||||
|
var state: ResourceStateRecord = manager.get_resource_state(resource.node_id)
|
||||||
|
state.set_enabled(resource.node_id == FAR_BERRY_ID)
|
||||||
|
var npc: SimNPC = manager.npcs[0]
|
||||||
|
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
|
_check(
|
||||||
|
manager.resolve_npc_target(npc.id, Vector3.ZERO),
|
||||||
|
"Resolver should expand from the village to the farther authored berry"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
npc.target_id == FAR_BERRY_ID,
|
||||||
|
"Farther discovery should return the nested stable-ID berry anchor"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
int(manager.target_resolver.last_resource_query_stats["range_pass_count"]) > 1,
|
||||||
|
"Farther authored discovery should expand beyond the initial 24 m range"
|
||||||
|
)
|
||||||
|
manager.release_resource(FAR_BERRY_ID, npc.id)
|
||||||
|
for resource in resources:
|
||||||
|
if resource.resource_id == SimulationIds.RESOURCE_FOOD:
|
||||||
|
manager.get_resource_state(resource.node_id).set_enabled(resource.initial_enabled)
|
||||||
|
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _candidate_ids(candidates: Array[Dictionary]) -> Array[StringName]:
|
||||||
|
var ids: Array[StringName] = []
|
||||||
|
for candidate in candidates:
|
||||||
|
ids.append(StringName(candidate["target_id"]))
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_resource_nodes(parent: Node) -> Array[ResourceNode]:
|
||||||
|
var resources: Array[ResourceNode] = []
|
||||||
|
for child in parent.get_children():
|
||||||
|
if child is ResourceNode:
|
||||||
|
resources.append(child)
|
||||||
|
else:
|
||||||
|
resources.append_array(_collect_resource_nodes(child))
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Jajce resource cluster passed: bounded density -> far stable-ID discovery")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbl3wqsud1gsh
|
||||||
@@ -519,9 +519,10 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Runtime should not retain duplicate flat-world objects"
|
"Runtime should not retain duplicate flat-world objects"
|
||||||
)
|
)
|
||||||
var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes")
|
var runtime_resources := _collect_resource_nodes(main_scene.get_node("JajceWorld/WorldObjects"))
|
||||||
_check(
|
_check(
|
||||||
resource_root.get_child_count() == 18, "Runtime should contain eighteen Jajce ResourceNodes"
|
runtime_resources.size() == 18,
|
||||||
|
"Runtime should contain eighteen Jajce ResourceNodes across authored clusters"
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
simulation_manager.resource_states.size() == 18,
|
simulation_manager.resource_states.size() == 18,
|
||||||
@@ -597,8 +598,8 @@ func _run() -> void:
|
|||||||
for site in activity_root.get_children():
|
for site in activity_root.get_children():
|
||||||
destinations.append((site as ActivitySite).get_interaction_position())
|
destinations.append((site as ActivitySite).get_interaction_position())
|
||||||
|
|
||||||
for resource in resource_root.get_children():
|
for resource in runtime_resources:
|
||||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
destinations.append(resource.interaction_point.global_position)
|
||||||
|
|
||||||
for origin in fixed_origins:
|
for origin in fixed_origins:
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
@@ -673,6 +674,16 @@ func _check(condition: bool, message: String) -> void:
|
|||||||
failures.append(message)
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_resource_nodes(parent: Node) -> Array[ResourceNode]:
|
||||||
|
var resources: Array[ResourceNode] = []
|
||||||
|
for child in parent.get_children():
|
||||||
|
if child is ResourceNode:
|
||||||
|
resources.append(child)
|
||||||
|
else:
|
||||||
|
resources.append_array(_collect_resource_nodes(child))
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
func _check_path_tracks_terrain(
|
func _check_path_tracks_terrain(
|
||||||
terrain: Terrain3D, path: PackedVector3Array, message: String
|
terrain: Terrain3D, path: PackedVector3Array, message: String
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ func _run() -> void:
|
|||||||
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
|
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
|
||||||
lookdev.free()
|
lookdev.free()
|
||||||
|
|
||||||
var resources := world.get_node("WorldObjects/ResourceNodes").get_children()
|
var resources := _collect_resource_nodes(world.get_node("WorldObjects"))
|
||||||
var ids: Array[String] = []
|
var ids: Array[String] = []
|
||||||
for resource in resources:
|
for resource in resources:
|
||||||
ids.append(String((resource as ResourceNode).node_id))
|
ids.append(String((resource as ResourceNode).node_id))
|
||||||
@@ -561,6 +561,16 @@ func _check(condition: bool, message: String) -> void:
|
|||||||
failures.append(message)
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_resource_nodes(parent: Node) -> Array[ResourceNode]:
|
||||||
|
var resources: Array[ResourceNode] = []
|
||||||
|
for child in parent.get_children():
|
||||||
|
if child is ResourceNode:
|
||||||
|
resources.append(child)
|
||||||
|
else:
|
||||||
|
resources.append_array(_collect_resource_nodes(child))
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
func _check_path_tracks_terrain(
|
func _check_path_tracks_terrain(
|
||||||
terrain: Terrain3D, path: PackedVector3Array, message: String
|
terrain: Terrain3D, path: PackedVector3Array, message: String
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=57 format=3]
|
[gd_scene load_steps=58 format=3]
|
||||||
|
|
||||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
[ext_resource type="PackedScene" path="res://assets/foliage/tree-high.glb" id="23_conifer_high"]
|
[ext_resource type="PackedScene" path="res://assets/foliage/tree-high.glb" id="23_conifer_high"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
|
[ext_resource type="PackedScene" path="res://world/jajce/CozyButterfly.tscn" id="24_butterfly"]
|
||||||
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
|
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
|
||||||
|
|
||||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
@@ -410,10 +411,6 @@ position = Vector3(-4, 0, -30)
|
|||||||
position = Vector3(19, 0, 18)
|
position = Vector3(19, 0, 18)
|
||||||
scale = Vector3(0.9, 0.9, 0.9)
|
scale = Vector3(0.9, 0.9, 0.9)
|
||||||
|
|
||||||
[node name="Tree_River_02" parent="FoliageRoot" instance=ExtResource("4_tree")]
|
|
||||||
position = Vector3(31, 0, 14)
|
|
||||||
scale = Vector3(1.25, 1.25, 1.25)
|
|
||||||
|
|
||||||
[node name="Conifer_Citadel_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
[node name="Conifer_Citadel_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||||
position = Vector3(-39, 0, 13)
|
position = Vector3(-39, 0, 13)
|
||||||
rotation_degrees = Vector3(0, 18, 0)
|
rotation_degrees = Vector3(0, 18, 0)
|
||||||
@@ -439,11 +436,6 @@ position = Vector3(38, 0, -8)
|
|||||||
rotation_degrees = Vector3(0, 28, 0)
|
rotation_degrees = Vector3(0, 28, 0)
|
||||||
scale = Vector3(2.7, 2.7, 2.7)
|
scale = Vector3(2.7, 2.7, 2.7)
|
||||||
|
|
||||||
[node name="Conifer_River_01" parent="FoliageRoot" instance=ExtResource("22_conifer")]
|
|
||||||
position = Vector3(38, 0, 20)
|
|
||||||
rotation_degrees = Vector3(0, -51, 0)
|
|
||||||
scale = Vector3(2.55, 2.55, 2.55)
|
|
||||||
|
|
||||||
[node name="Conifer_South_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
[node name="Conifer_South_01" parent="FoliageRoot" instance=ExtResource("23_conifer_high")]
|
||||||
position = Vector3(-21, 0, -29)
|
position = Vector3(-21, 0, -29)
|
||||||
rotation_degrees = Vector3(0, 44, 0)
|
rotation_degrees = Vector3(0, 44, 0)
|
||||||
@@ -566,17 +558,6 @@ yield_per_action = 2.0
|
|||||||
safety_risk = 0.24
|
safety_risk = 0.24
|
||||||
comfort_distance = 26.0
|
comfort_distance = 26.0
|
||||||
|
|
||||||
[node name="Tree_River_Resource_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
|
||||||
position = Vector3(20, 0, 18)
|
|
||||||
node_id = &"tree_river_resource_01"
|
|
||||||
action_id = &"gather_wood"
|
|
||||||
resource_id = &"wood"
|
|
||||||
initial_amount = 8.0
|
|
||||||
yield_per_action = 2.0
|
|
||||||
safety_risk = 0.22
|
|
||||||
comfort_distance = 30.0
|
|
||||||
discovery_priority = 0.1
|
|
||||||
|
|
||||||
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="WoodPile_Village_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(5, 0, -13)
|
position = Vector3(5, 0, -13)
|
||||||
node_id = &"wood_pile_village_01"
|
node_id = &"wood_pile_village_01"
|
||||||
@@ -597,15 +578,6 @@ safety_risk = 0.02
|
|||||||
comfort_distance = 16.0
|
comfort_distance = 16.0
|
||||||
discovery_priority = 0.85
|
discovery_priority = 0.85
|
||||||
|
|
||||||
[node name="BerryBush_River_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
|
||||||
position = Vector3(28, 0, -2)
|
|
||||||
node_id = &"berry_bush_river_02"
|
|
||||||
initial_amount = 5.0
|
|
||||||
yield_per_action = 1.5
|
|
||||||
safety_risk = 0.22
|
|
||||||
comfort_distance = 28.0
|
|
||||||
discovery_priority = 0.2
|
|
||||||
|
|
||||||
[node name="Tree_Forest_Grove_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="Tree_Forest_Grove_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(-8, 0, -22)
|
position = Vector3(-8, 0, -22)
|
||||||
node_id = &"tree_forest_grove_01"
|
node_id = &"tree_forest_grove_01"
|
||||||
@@ -648,6 +620,11 @@ safety_risk = 0.1
|
|||||||
comfort_distance = 20.0
|
comfort_distance = 20.0
|
||||||
discovery_priority = 0.45
|
discovery_priority = 0.45
|
||||||
|
|
||||||
|
[node name="ResourceClusters" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
|
[node name="RiverbankResourceCluster" parent="WorldObjects/ResourceClusters" instance=ExtResource("26_resource_cluster")]
|
||||||
|
position = Vector3(33, 0, 7)
|
||||||
|
|
||||||
[node name="StorageSites" type="Node3D" parent="WorldObjects"]
|
[node name="StorageSites" type="Node3D" parent="WorldObjects"]
|
||||||
|
|
||||||
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
class_name RiverbankResourceCluster
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const FERN_OFFSETS := [
|
||||||
|
Vector3(-0.8, 0.0, -10.8),
|
||||||
|
Vector3(1.1, 0.0, -8.7),
|
||||||
|
Vector3(2.9, 0.0, -6.8),
|
||||||
|
Vector3(-1.4, 0.0, -4.6),
|
||||||
|
Vector3(1.8, 0.0, -2.7),
|
||||||
|
Vector3(3.6, 0.0, -0.5),
|
||||||
|
Vector3(-0.5, 0.0, 1.2),
|
||||||
|
Vector3(2.4, 0.0, 3.4),
|
||||||
|
Vector3(-1.8, 0.0, 5.7),
|
||||||
|
Vector3(1.4, 0.0, 7.6),
|
||||||
|
Vector3(3.8, 0.0, 9.4),
|
||||||
|
Vector3(0.2, 0.0, 11.8),
|
||||||
|
]
|
||||||
|
const FLOWER_OFFSETS := [
|
||||||
|
Vector3(0.2, 0.0, -10.2),
|
||||||
|
Vector3(2.2, 0.0, -7.5),
|
||||||
|
Vector3(-0.4, 0.0, -5.5),
|
||||||
|
Vector3(3.4, 0.0, -3.2),
|
||||||
|
Vector3(0.8, 0.0, 0.1),
|
||||||
|
Vector3(2.8, 0.0, 2.1),
|
||||||
|
Vector3(-0.7, 0.0, 4.4),
|
||||||
|
Vector3(2.0, 0.0, 6.4),
|
||||||
|
Vector3(0.4, 0.0, 9.8),
|
||||||
|
]
|
||||||
|
const STONE_OFFSETS := [
|
||||||
|
Vector3(3.9, 0.0, -9.6),
|
||||||
|
Vector3(-1.2, 0.0, -7.2),
|
||||||
|
Vector3(3.2, 0.0, -4.3),
|
||||||
|
Vector3(-0.7, 0.0, -1.2),
|
||||||
|
Vector3(3.7, 0.0, 4.6),
|
||||||
|
Vector3(-1.1, 0.0, 8.4),
|
||||||
|
Vector3(2.7, 0.0, 11.2),
|
||||||
|
]
|
||||||
|
|
||||||
|
@export var terrain_path := NodePath("../../../TerrainRoot/Terrain3D")
|
||||||
|
|
||||||
|
var terrain: Terrain3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
call_deferred("_initialize_cluster")
|
||||||
|
|
||||||
|
|
||||||
|
func get_resource_anchors() -> Array[ResourceNode]:
|
||||||
|
var anchors: Array[ResourceNode] = []
|
||||||
|
for child in $ResourceAnchors.get_children():
|
||||||
|
if child is ResourceNode:
|
||||||
|
anchors.append(child)
|
||||||
|
return anchors
|
||||||
|
|
||||||
|
|
||||||
|
func get_presentation_stats() -> Dictionary:
|
||||||
|
var understory_instances := 0
|
||||||
|
var multimesh_batches := 0
|
||||||
|
for child in $DecorativeInstances/Understory.get_children():
|
||||||
|
var instance := child as MultiMeshInstance3D
|
||||||
|
if instance == null or instance.multimesh == null:
|
||||||
|
continue
|
||||||
|
understory_instances += instance.multimesh.instance_count
|
||||||
|
multimesh_batches += 1
|
||||||
|
return {
|
||||||
|
"understory_instance_count": understory_instances,
|
||||||
|
"multimesh_batch_count": multimesh_batches,
|
||||||
|
"decorative_tree_count": $DecorativeInstances/Trees.get_child_count(),
|
||||||
|
"resource_anchor_count": get_resource_anchors().size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_cluster() -> void:
|
||||||
|
terrain = get_node_or_null(terrain_path) as Terrain3D
|
||||||
|
if terrain == null:
|
||||||
|
push_warning("RiverbankResourceCluster: Terrain3D is unavailable")
|
||||||
|
return
|
||||||
|
_snap_authored_children($DecorativeInstances/Trees)
|
||||||
|
for anchor in get_resource_anchors():
|
||||||
|
_snap_resource_anchor(anchor)
|
||||||
|
_build_understory()
|
||||||
|
|
||||||
|
|
||||||
|
func _snap_authored_children(parent: Node) -> void:
|
||||||
|
for child in parent.get_children():
|
||||||
|
if child is Node3D:
|
||||||
|
_snap_to_terrain(child)
|
||||||
|
|
||||||
|
|
||||||
|
func _snap_to_terrain(node: Node3D) -> void:
|
||||||
|
var position := node.global_position
|
||||||
|
var height := terrain.data.get_height(position)
|
||||||
|
if not is_nan(height):
|
||||||
|
node.global_position.y = height
|
||||||
|
|
||||||
|
|
||||||
|
func _snap_resource_anchor(anchor: ResourceNode) -> void:
|
||||||
|
var anchor_position := anchor.global_position
|
||||||
|
var anchor_height := terrain.data.get_height(anchor_position)
|
||||||
|
if is_nan(anchor_height):
|
||||||
|
return
|
||||||
|
var interaction_position := anchor.global_transform * anchor.interaction_point.position
|
||||||
|
var interaction_height := terrain.data.get_height(interaction_position)
|
||||||
|
if not is_nan(interaction_height):
|
||||||
|
anchor.interaction_point.position.y = interaction_height - anchor_height + 0.05
|
||||||
|
anchor.global_position.y = anchor_height
|
||||||
|
|
||||||
|
|
||||||
|
func _build_understory() -> void:
|
||||||
|
var fern_material := StandardMaterial3D.new()
|
||||||
|
fern_material.albedo_color = Color("#315f35")
|
||||||
|
fern_material.roughness = 0.94
|
||||||
|
fern_material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
_assign_multimesh(
|
||||||
|
$DecorativeInstances/Understory/FernClumps,
|
||||||
|
_create_fern_mesh(fern_material),
|
||||||
|
FERN_OFFSETS,
|
||||||
|
Vector3(0.8, 0.8, 0.8),
|
||||||
|
0.03
|
||||||
|
)
|
||||||
|
|
||||||
|
var flower_mesh := SphereMesh.new()
|
||||||
|
flower_mesh.radius = 0.13
|
||||||
|
flower_mesh.height = 0.22
|
||||||
|
flower_mesh.radial_segments = 7
|
||||||
|
flower_mesh.rings = 4
|
||||||
|
var flower_material := StandardMaterial3D.new()
|
||||||
|
flower_material.albedo_color = Color("#efc55a")
|
||||||
|
flower_material.roughness = 0.82
|
||||||
|
flower_mesh.material = flower_material
|
||||||
|
_assign_multimesh(
|
||||||
|
$DecorativeInstances/Understory/FlowerClumps, flower_mesh, FLOWER_OFFSETS, Vector3.ONE, 0.2
|
||||||
|
)
|
||||||
|
|
||||||
|
var stone_mesh := SphereMesh.new()
|
||||||
|
stone_mesh.radius = 1.0
|
||||||
|
stone_mesh.height = 2.0
|
||||||
|
stone_mesh.radial_segments = 8
|
||||||
|
stone_mesh.rings = 4
|
||||||
|
var stone_material := StandardMaterial3D.new()
|
||||||
|
stone_material.albedo_color = Color("#77786b")
|
||||||
|
stone_material.roughness = 0.96
|
||||||
|
stone_mesh.material = stone_material
|
||||||
|
_assign_multimesh(
|
||||||
|
$DecorativeInstances/Understory/BankStones,
|
||||||
|
stone_mesh,
|
||||||
|
STONE_OFFSETS,
|
||||||
|
Vector3(0.62, 0.24, 0.44),
|
||||||
|
0.22
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _assign_multimesh(
|
||||||
|
target: MultiMeshInstance3D,
|
||||||
|
mesh: Mesh,
|
||||||
|
offsets: Array,
|
||||||
|
base_scale: Vector3,
|
||||||
|
vertical_offset: float
|
||||||
|
) -> void:
|
||||||
|
var multimesh := MultiMesh.new()
|
||||||
|
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
multimesh.mesh = mesh
|
||||||
|
multimesh.instance_count = offsets.size()
|
||||||
|
for index in offsets.size():
|
||||||
|
var offset: Vector3 = offsets[index]
|
||||||
|
var world_sample := to_global(offset)
|
||||||
|
var height := terrain.data.get_height(world_sample)
|
||||||
|
if is_nan(height):
|
||||||
|
height = global_position.y
|
||||||
|
var local_position := to_local(Vector3(world_sample.x, height, world_sample.z))
|
||||||
|
local_position.y += vertical_offset
|
||||||
|
var scale_variation := 0.84 + float((index * 37) % 5) * 0.07
|
||||||
|
var rotation := deg_to_rad(float((index * 53) % 360))
|
||||||
|
var basis := Basis(Vector3.UP, rotation).scaled(base_scale * scale_variation)
|
||||||
|
multimesh.set_instance_transform(index, Transform3D(basis, local_position))
|
||||||
|
target.multimesh = multimesh
|
||||||
|
|
||||||
|
|
||||||
|
func _create_fern_mesh(material: Material) -> ArrayMesh:
|
||||||
|
var vertices := PackedVector3Array()
|
||||||
|
var normals := PackedVector3Array()
|
||||||
|
var uvs := PackedVector2Array()
|
||||||
|
for frond_index in 5:
|
||||||
|
var angle := float(frond_index) / 5.0 * TAU + 0.3
|
||||||
|
var direction := Vector3(cos(angle), 0.0, sin(angle))
|
||||||
|
var side := Vector3(-direction.z, 0.0, direction.x) * 0.16
|
||||||
|
var base := Vector3(0.0, 0.02, 0.0)
|
||||||
|
var middle := direction * 0.38 + Vector3.UP * 0.14
|
||||||
|
var tip := direction * 0.82 + Vector3.UP * 0.24
|
||||||
|
vertices.append_array([base, middle + side, tip, base, tip, middle - side])
|
||||||
|
for _vertex in 6:
|
||||||
|
normals.append(Vector3.UP)
|
||||||
|
for uv in [
|
||||||
|
Vector2(0.5, 0.0),
|
||||||
|
Vector2(1.0, 0.5),
|
||||||
|
Vector2(0.5, 1.0),
|
||||||
|
Vector2(0.5, 0.0),
|
||||||
|
Vector2(0.5, 1.0),
|
||||||
|
Vector2(0.0, 0.5),
|
||||||
|
]:
|
||||||
|
uvs.append(uv)
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
mesh.surface_set_material(0, material)
|
||||||
|
return mesh
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b20nnq0w3x0co
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
[gd_scene load_steps=6 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/jajce/RiverbankResourceCluster.gd" id="1_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"]
|
||||||
|
|
||||||
|
[node name="RiverbankResourceCluster" type="Node3D"]
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="DecorativeInstances" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Understory" type="Node3D" parent="DecorativeInstances"]
|
||||||
|
|
||||||
|
[node name="FernClumps" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
|
||||||
|
cast_shadow = 0
|
||||||
|
|
||||||
|
[node name="FlowerClumps" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
|
||||||
|
cast_shadow = 0
|
||||||
|
|
||||||
|
[node name="BankStones" type="MultiMeshInstance3D" parent="DecorativeInstances/Understory"]
|
||||||
|
|
||||||
|
[node name="Trees" type="Node3D" parent="DecorativeInstances"]
|
||||||
|
|
||||||
|
[node name="DeciduousBankTree" parent="DecorativeInstances/Trees" instance=ExtResource("3_tree")]
|
||||||
|
position = Vector3(3, 0, 4)
|
||||||
|
scale = Vector3(0.92, 0.92, 0.92)
|
||||||
|
|
||||||
|
[node name="ConiferBankTree" parent="DecorativeInstances/Trees" instance=ExtResource("5_conifer")]
|
||||||
|
position = Vector3(5, 0, 13)
|
||||||
|
rotation_degrees = Vector3(0, -51, 0)
|
||||||
|
scale = Vector3(2.55, 2.55, 2.55)
|
||||||
|
|
||||||
|
[node name="ResourceAnchors" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="BerryBush_River_02" parent="ResourceAnchors" instance=ExtResource("2_resource")]
|
||||||
|
position = Vector3(0, 0, -9)
|
||||||
|
node_id = &"berry_bush_river_02"
|
||||||
|
initial_amount = 5.0
|
||||||
|
yield_per_action = 1.5
|
||||||
|
safety_risk = 0.22
|
||||||
|
comfort_distance = 28.0
|
||||||
|
discovery_priority = 0.2
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="ResourceAnchors/BerryBush_River_02/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BerryPatchPresentation" parent="ResourceAnchors/BerryBush_River_02/Visual" instance=ExtResource("4_berry")]
|
||||||
|
scale = Vector3(1.15, 1.15, 1.15)
|
||||||
|
|
||||||
|
[node name="InteractionPoint" parent="ResourceAnchors/BerryBush_River_02"]
|
||||||
|
position = Vector3(-2.5, 0, 2)
|
||||||
|
|
||||||
|
[node name="Tree_River_Resource_01" parent="ResourceAnchors" instance=ExtResource("2_resource")]
|
||||||
|
position = Vector3(0, 0, 8)
|
||||||
|
node_id = &"tree_river_resource_01"
|
||||||
|
action_id = &"gather_wood"
|
||||||
|
resource_id = &"wood"
|
||||||
|
initial_amount = 8.0
|
||||||
|
yield_per_action = 2.0
|
||||||
|
safety_risk = 0.22
|
||||||
|
comfort_distance = 30.0
|
||||||
|
discovery_priority = 0.1
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" parent="ResourceAnchors/Tree_River_Resource_01/Visual"]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TreePresentation" parent="ResourceAnchors/Tree_River_Resource_01/Visual" instance=ExtResource("3_tree")]
|
||||||
|
scale = Vector3(1.08, 1.08, 1.08)
|
||||||
|
|
||||||
|
[node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"]
|
||||||
|
position = Vector3(-4, 0, 2)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
class_name StylizedBerryPatch
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||||
|
const LEAF_POSITIONS := [
|
||||||
|
Vector3(-0.52, 0.42, 0.08),
|
||||||
|
Vector3(0.08, 0.48, -0.14),
|
||||||
|
Vector3(0.58, 0.4, 0.18),
|
||||||
|
Vector3(-0.18, 0.7, 0.18),
|
||||||
|
Vector3(0.38, 0.67, -0.22),
|
||||||
|
]
|
||||||
|
const LEAF_SCALES := [
|
||||||
|
Vector3(0.72, 0.38, 0.62),
|
||||||
|
Vector3(0.78, 0.44, 0.68),
|
||||||
|
Vector3(0.66, 0.36, 0.58),
|
||||||
|
Vector3(0.56, 0.32, 0.52),
|
||||||
|
Vector3(0.5, 0.28, 0.46),
|
||||||
|
]
|
||||||
|
const BERRY_POSITIONS := [
|
||||||
|
Vector3(-0.7, 0.64, 0.2),
|
||||||
|
Vector3(-0.43, 0.82, -0.34),
|
||||||
|
Vector3(-0.14, 0.7, 0.46),
|
||||||
|
Vector3(0.08, 0.88, -0.08),
|
||||||
|
Vector3(0.34, 0.7, 0.42),
|
||||||
|
Vector3(0.48, 0.86, -0.3),
|
||||||
|
Vector3(0.74, 0.6, 0.12),
|
||||||
|
Vector3(-0.02, 1.02, 0.22),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_leaves()
|
||||||
|
_build_berries()
|
||||||
|
|
||||||
|
|
||||||
|
func get_presentation_instance_count() -> int:
|
||||||
|
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_leaves() -> void:
|
||||||
|
var mesh := SphereMesh.new()
|
||||||
|
mesh.radius = 1.0
|
||||||
|
mesh.height = 2.0
|
||||||
|
mesh.radial_segments = 9
|
||||||
|
mesh.rings = 5
|
||||||
|
var material := ShaderMaterial.new()
|
||||||
|
material.shader = WIND_SHADER
|
||||||
|
material.set_shader_parameter("albedo", Color("#426c32"))
|
||||||
|
material.set_shader_parameter("roughness", 0.92)
|
||||||
|
material.set_shader_parameter("wind_strength", 0.045)
|
||||||
|
material.set_shader_parameter("wind_speed", 0.48)
|
||||||
|
material.set_shader_parameter("wind_phase", 1.7)
|
||||||
|
mesh.material = material
|
||||||
|
var transforms: Array[Transform3D] = []
|
||||||
|
for index in LEAF_POSITIONS.size():
|
||||||
|
transforms.append(
|
||||||
|
Transform3D(Basis.IDENTITY.scaled(LEAF_SCALES[index]), LEAF_POSITIONS[index])
|
||||||
|
)
|
||||||
|
$Leaves.multimesh = _create_multimesh(mesh, transforms)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_berries() -> void:
|
||||||
|
var mesh := SphereMesh.new()
|
||||||
|
mesh.radius = 0.12
|
||||||
|
mesh.height = 0.21
|
||||||
|
mesh.radial_segments = 8
|
||||||
|
mesh.rings = 4
|
||||||
|
var material := StandardMaterial3D.new()
|
||||||
|
material.albedo_color = Color("#8f3148")
|
||||||
|
material.roughness = 0.78
|
||||||
|
mesh.material = material
|
||||||
|
var transforms: Array[Transform3D] = []
|
||||||
|
for position in BERRY_POSITIONS:
|
||||||
|
transforms.append(Transform3D(Basis.IDENTITY, position))
|
||||||
|
$Berries.multimesh = _create_multimesh(mesh, transforms)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_multimesh(mesh: Mesh, transforms: Array) -> MultiMesh:
|
||||||
|
var multimesh := MultiMesh.new()
|
||||||
|
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
multimesh.mesh = mesh
|
||||||
|
multimesh.instance_count = transforms.size()
|
||||||
|
for index in transforms.size():
|
||||||
|
multimesh.set_instance_transform(index, transforms[index])
|
||||||
|
return multimesh
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ctb2v3prn7ovc
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/jajce/StylizedBerryPatch.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="StylizedBerryPatch" type="Node3D"]
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Leaves" type="MultiMeshInstance3D" parent="."]
|
||||||
|
cast_shadow = 0
|
||||||
|
|
||||||
|
[node name="Berries" type="MultiMeshInstance3D" parent="."]
|
||||||
|
cast_shadow = 0
|
||||||
Reference in New Issue
Block a user