feat: derive resource visuals from state

This commit is contained in:
Rijad Zuzo
2026-07-17 18:39:14 +02:00
parent 04c5371b6b
commit 2719b0398b
21 changed files with 686 additions and 18 deletions
@@ -0,0 +1,41 @@
class_name HarvestableTreePresentation
extends ResourceAmountVisual
const THINNED_CANOPIES := [&"CanopyLeft", &"CanopyRight"]
@onready var standing_tree: Node3D = $StandingTree
@onready var stump: Node3D = $Stump
func _ready() -> void:
super()
func is_standing_visible() -> bool:
return standing_tree.visible
func is_stump_visible() -> bool:
return stump.visible
func get_visible_canopy_count() -> int:
if not standing_tree.visible:
return 0
var visible_count := 0
for child in standing_tree.get_children():
if String(child.name).begins_with("Canopy") and child.visible:
visible_count += 1
return visible_count
func _apply_resource_amount_visual(
_amount_remaining: float, _remaining_ratio: float, tier: AmountTier
) -> void:
var depleted := tier == AmountTier.DEPLETED
standing_tree.visible = not depleted
stump.visible = depleted
for canopy_name in THINNED_CANOPIES:
var canopy := standing_tree.get_node_or_null(NodePath(canopy_name)) as Node3D
if canopy != null:
canopy.visible = tier == AmountTier.FULL
@@ -0,0 +1 @@
uid://cpue05wbg6roj
@@ -0,0 +1,42 @@
[gd_scene load_steps=7 format=3]
[ext_resource type="Script" path="res://world/jajce/HarvestableTreePresentation.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="2_tree"]
[sub_resource type="StandardMaterial3D" id="Material_stump_bark"]
albedo_color = Color(0.29, 0.17, 0.09, 1)
roughness = 0.96
[sub_resource type="CylinderMesh" id="Mesh_stump"]
material = SubResource("Material_stump_bark")
top_radius = 0.38
bottom_radius = 0.48
height = 0.82
radial_segments = 10
[sub_resource type="StandardMaterial3D" id="Material_stump_cut"]
albedo_color = Color(0.69, 0.49, 0.27, 1)
roughness = 0.9
[sub_resource type="CylinderMesh" id="Mesh_stump_cut"]
material = SubResource("Material_stump_cut")
top_radius = 0.36
bottom_radius = 0.37
height = 0.055
radial_segments = 10
[node name="HarvestableTreePresentation" type="Node3D"]
script = ExtResource("1_script")
[node name="StandingTree" parent="." instance=ExtResource("2_tree")]
[node name="Stump" type="Node3D" parent="."]
visible = false
[node name="Bark" type="MeshInstance3D" parent="Stump"]
position = Vector3(0, 0.41, 0)
mesh = SubResource("Mesh_stump")
[node name="CutFace" type="MeshInstance3D" parent="Stump"]
position = Vector3(0, 0.835, 0)
mesh = SubResource("Mesh_stump_cut")
@@ -0,0 +1,16 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
[node name="JajceResourceStateLookdev" type="Node3D"]
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
[node name="ResourceCamera" type="Camera3D" parent="."]
position = Vector3(14, 9, 7)
current = true
fov = 40.0
script = ExtResource("2_camera")
focus_point = Vector3(34, 3.3, 7)
animate_orbit = false
+3 -2
View File
@@ -1,10 +1,11 @@
[gd_scene load_steps=6 format=3]
[gd_scene load_steps=7 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"]
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="6_harvestable_tree"]
[node name="RiverbankResourceCluster" type="Node3D"]
script = ExtResource("1_script")
@@ -66,7 +67,7 @@ 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")]
[node name="TreePresentation" parent="ResourceAnchors/Tree_River_Resource_01/Visual" instance=ExtResource("6_harvestable_tree")]
scale = Vector3(1.08, 1.08, 1.08)
[node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"]
+26 -1
View File
@@ -1,5 +1,5 @@
class_name StylizedBerryPatch
extends Node3D
extends ResourceAmountVisual
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
const LEAF_POSITIONS := [
@@ -31,12 +31,37 @@ const BERRY_POSITIONS := [
func _ready() -> void:
_build_leaves()
_build_berries()
super()
func get_presentation_instance_count() -> int:
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
func get_visible_leaf_count() -> int:
return $Leaves.multimesh.visible_instance_count
func get_visible_berry_count() -> int:
return $Berries.multimesh.visible_instance_count
func _apply_resource_amount_visual(
_amount_remaining: float, remaining_ratio: float, tier: AmountTier
) -> void:
var visible_leaves := LEAF_POSITIONS.size()
if tier == AmountTier.LOW:
visible_leaves = 3
elif tier == AmountTier.DEPLETED:
visible_leaves = 2
$Leaves.multimesh.visible_instance_count = visible_leaves
var visible_berries := ceili(remaining_ratio * BERRY_POSITIONS.size())
if tier == AmountTier.DEPLETED:
visible_berries = 0
$Berries.multimesh.visible_instance_count = visible_berries
func _build_leaves() -> void:
var mesh := SphereMesh.new()
mesh.radius = 1.0
@@ -0,0 +1,84 @@
class_name ResourceAmountVisual
extends Node3D
enum AmountTier {
FULL,
LOW,
DEPLETED,
}
@export_range(0.0, 1.0, 0.05) var low_amount_ratio := 0.5
var resource_node: ResourceNode
var amount_ratio := 1.0
var amount_tier := AmountTier.FULL
func _ready() -> void:
call_deferred("_bind_resource_node")
func _exit_tree() -> void:
if (
resource_node != null
and resource_node.amount_changed.is_connected(_on_resource_amount_changed)
):
resource_node.amount_changed.disconnect(_on_resource_amount_changed)
resource_node = null
func refresh_from_resource() -> void:
if resource_node == null:
return
_apply_amount(resource_node.get_amount_remaining())
func get_amount_tier() -> AmountTier:
return amount_tier
func get_amount_ratio() -> float:
return amount_ratio
func _bind_resource_node() -> void:
resource_node = _find_resource_node()
if resource_node == null:
return
if not resource_node.amount_changed.is_connected(_on_resource_amount_changed):
resource_node.amount_changed.connect(_on_resource_amount_changed)
refresh_from_resource()
func _find_resource_node() -> ResourceNode:
var candidate := get_parent()
while candidate != null:
if candidate is ResourceNode:
return candidate as ResourceNode
candidate = candidate.get_parent()
return null
func _on_resource_amount_changed(changed_id: StringName, amount_remaining: float) -> void:
if resource_node != null and changed_id == resource_node.node_id:
_apply_amount(amount_remaining)
func _apply_amount(amount_remaining: float) -> void:
var initial_amount := maxf(resource_node.initial_amount, 0.0)
amount_ratio = (
clampf(amount_remaining / initial_amount, 0.0, 1.0) if initial_amount > 0.0 else 0.0
)
if amount_remaining <= 0.0:
amount_tier = AmountTier.DEPLETED
elif amount_ratio <= low_amount_ratio:
amount_tier = AmountTier.LOW
else:
amount_tier = AmountTier.FULL
_apply_resource_amount_visual(amount_remaining, amount_ratio, amount_tier)
func _apply_resource_amount_visual(
_amount_remaining: float, _remaining_ratio: float, _tier: AmountTier
) -> void:
pass
@@ -0,0 +1 @@
uid://bls5qs3s3omgh
+1
View File
@@ -74,6 +74,7 @@ func bind_state(resource_state: ResourceStateRecord) -> bool:
if not state.depleted.is_connected(_on_state_depleted):
state.depleted.connect(_on_state_depleted)
_update_presentation()
amount_changed.emit(node_id, state.get_amount_remaining())
_notify_world_adapters()
return true