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,
|
||||
reservations, or persistence. `ResourceNode` enter/exit, bind, and transform
|
||||
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,
|
||||
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
|
||||
the landform. A matched Metal capture records the change, and the rebaked
|
||||
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:
|
||||
|
||||
1. Prove one bounded foliage/resource authoring workflow at a riverbank or
|
||||
forest edge. Use decorative Terrain3D/intentional instances for tree, fern,
|
||||
flower, and rock masses, but create simulation-owned finite resources only
|
||||
through selected stable-ID `ResourceNode` anchors.
|
||||
2. Validate that villagers discover both nearby and farther authored anchors,
|
||||
preserve reachable interaction points, and keep the visual batch inside a
|
||||
measured presentation budget. Add livestock only with a real identity and
|
||||
1. Give the authored berry/tree resources state-driven low/depleted visuals
|
||||
sourced from their real `ResourceStateRecord` amount. Prove NPC and player
|
||||
extraction plus save/restore rebuild the same presentation without adding
|
||||
visual fields to simulation state.
|
||||
2. Apply the workflow to a second forest-edge consumer only after that contract
|
||||
is honest and reusable. Add livestock only with a real identity and
|
||||
interaction contract.
|
||||
|
||||
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
|
||||
against required village/resource routes.
|
||||
|
||||
The immediate next slice should prove one bounded foliage/resource authoring
|
||||
workflow in the real world: build a restrained riverbank or forest-edge cluster
|
||||
with decorative Terrain3D/intentional instances, pair only selected harvestable
|
||||
trees or berry patches with stable-ID `ResourceNode` anchors, and verify nearby
|
||||
and farther loaded anchors remain discoverable through `ActiveWorldAdapter`.
|
||||
Keep decorative instances out of simulation state, measure the bounded visual
|
||||
cost, and do not generalize the resource grid to people/buildings/events or
|
||||
The first authored foliage/resource workflow is complete. One riverbank cluster
|
||||
uses three intentional MultiMesh batches for 28 fern, flower, and stone
|
||||
instances plus two decorative trees, while only an existing stable-ID berry and
|
||||
tree are finite `ResourceNode` anchors. Both anchors preserve the save contract
|
||||
of eighteen resources, sit above the carved channel, and expose separate
|
||||
terrain-snapped approach points. A forced village query expands beyond the
|
||||
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.
|
||||
|
||||
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
|
||||
stream, raised waterfall shelf, plunge basin, and curved downstream channel;
|
||||
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;
|
||||
- one sparse terrain-aware calligraphic gust field with two or three soft
|
||||
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;
|
||||
- a Terrain3D-derived baked navigation mesh covering the current playable loop;
|
||||
- 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)
|
||||
and [Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md).
|
||||
|
||||
The authored Jajce watercourse and its Terrain3D-derived navigation rebake are
|
||||
complete. The next slice should prove a bounded stable-ID resource-anchor
|
||||
placement workflow beside decorative Terrain3D/intentional foliage instances
|
||||
at one riverbank or forest edge. It should validate nearby and farther loaded
|
||||
resource discovery, reachable interaction points, and a measured presentation
|
||||
budget without making decorative instances authoritative. Do not yet
|
||||
generalize the resource grid or introduce active/abstract simulation LOD.
|
||||
The authored Jajce watercourse, navigation rebake, and first bounded
|
||||
foliage/resource placement proof are complete. The next slice should derive
|
||||
low/depleted berry and tree visuals from authoritative resource amount, cover
|
||||
real NPC/player extraction and save/restore reconstruction, and keep that
|
||||
presentation out of serialized simulation state. Apply the cluster workflow to
|
||||
a second forest edge only after the depletion contract supplies a real second
|
||||
consumer. Do not yet generalize the resource grid or introduce active/abstract
|
||||
simulation LOD.
|
||||
|
||||
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
|
||||
[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
|
||||
|
||||
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
|
||||
|
||||
The next pass should author one bounded riverbank or forest-edge foliage mass,
|
||||
using decorative Terrain3D/intentional instances for visual density and only a
|
||||
small selected set of stable-ID `ResourceNode` anchors for finite harvestable
|
||||
resources. Bank stones, ferns, flowers, and tree masses can improve the channel
|
||||
edge during that proof. Bridge, buildings, and fortress geometry remain
|
||||
That proof is complete in
|
||||
[Jajce Resource Grove 04](JAJCE_RESOURCE_GROVE_04.md): a bounded riverbank mass
|
||||
keeps decorative density separate from two stable-ID finite anchors. The next
|
||||
pass should make those berry/tree visuals reflect real depletion before adding
|
||||
a second forest-edge cluster. Bridge, buildings, and fortress geometry remain
|
||||
prototype blockouts and should be replaced gradually rather than through a
|
||||
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
|
||||
`ResourceNode` anchor binds simulation-owned state. A later placement workflow
|
||||
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
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes")
|
||||
var runtime_resources := _collect_resource_nodes(main_scene.get_node("JajceWorld/WorldObjects"))
|
||||
_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(
|
||||
simulation_manager.resource_states.size() == 18,
|
||||
@@ -597,8 +598,8 @@ func _run() -> void:
|
||||
for site in activity_root.get_children():
|
||||
destinations.append((site as ActivitySite).get_interaction_position())
|
||||
|
||||
for resource in resource_root.get_children():
|
||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
||||
for resource in runtime_resources:
|
||||
destinations.append(resource.interaction_point.global_position)
|
||||
|
||||
for origin in fixed_origins:
|
||||
for destination in destinations:
|
||||
@@ -673,6 +674,16 @@ func _check(condition: bool, message: String) -> void:
|
||||
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(
|
||||
terrain: Terrain3D, path: PackedVector3Array, message: String
|
||||
) -> void:
|
||||
|
||||
@@ -324,7 +324,7 @@ func _run() -> void:
|
||||
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
|
||||
lookdev.free()
|
||||
|
||||
var resources := world.get_node("WorldObjects/ResourceNodes").get_children()
|
||||
var resources := _collect_resource_nodes(world.get_node("WorldObjects"))
|
||||
var ids: Array[String] = []
|
||||
for resource in resources:
|
||||
ids.append(String((resource as ResourceNode).node_id))
|
||||
@@ -561,6 +561,16 @@ func _check(condition: bool, message: String) -> void:
|
||||
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(
|
||||
terrain: Terrain3D, path: PackedVector3Array, message: String
|
||||
) -> 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="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://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="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
@@ -410,10 +411,6 @@ position = Vector3(-4, 0, -30)
|
||||
position = Vector3(19, 0, 18)
|
||||
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")]
|
||||
position = Vector3(-39, 0, 13)
|
||||
rotation_degrees = Vector3(0, 18, 0)
|
||||
@@ -439,11 +436,6 @@ position = Vector3(38, 0, -8)
|
||||
rotation_degrees = Vector3(0, 28, 0)
|
||||
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")]
|
||||
position = Vector3(-21, 0, -29)
|
||||
rotation_degrees = Vector3(0, 44, 0)
|
||||
@@ -566,17 +558,6 @@ yield_per_action = 2.0
|
||||
safety_risk = 0.24
|
||||
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")]
|
||||
position = Vector3(5, 0, -13)
|
||||
node_id = &"wood_pile_village_01"
|
||||
@@ -597,15 +578,6 @@ safety_risk = 0.02
|
||||
comfort_distance = 16.0
|
||||
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")]
|
||||
position = Vector3(-8, 0, -22)
|
||||
node_id = &"tree_forest_grove_01"
|
||||
@@ -648,6 +620,11 @@ safety_risk = 0.1
|
||||
comfort_distance = 20.0
|
||||
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="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