diff --git a/.gitignore b/.gitignore index 0af181c..1856418 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Godot 4+ specific ignores .godot/ /android/ +*.tmp \ No newline at end of file diff --git a/Entities/Bush/assets/bush_large.res b/Entities/Bush/assets/bush_large.res new file mode 100644 index 0000000..dcb20f2 Binary files /dev/null and b/Entities/Bush/assets/bush_large.res differ diff --git a/Entities/Bush/assets/bush_small.res b/Entities/Bush/assets/bush_small.res new file mode 100644 index 0000000..dbab793 Binary files /dev/null and b/Entities/Bush/assets/bush_small.res differ diff --git a/Entities/Tree/assets/temp/plant_bush.glb b/Entities/Bush/assets/plant_bush.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bush.glb rename to Entities/Bush/assets/plant_bush.glb diff --git a/Entities/Tree/assets/temp/plant_bush.glb.import b/Entities/Bush/assets/plant_bush.glb.import similarity index 75% rename from Entities/Tree/assets/temp/plant_bush.glb.import rename to Entities/Bush/assets/plant_bush.glb.import index 77a7f96..f1a6e3a 100644 --- a/Entities/Tree/assets/temp/plant_bush.glb.import +++ b/Entities/Bush/assets/plant_bush.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://c3objh5he4fv7" -path="res://.godot/imported/plant_bush.glb-e9806d63c67fa60c2bf9125a46d34412.scn" +path="res://.godot/imported/plant_bush.glb-4eaf384fbf61c7e3b82505202bc9874e.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bush.glb" -dest_files=["res://.godot/imported/plant_bush.glb-e9806d63c67fa60c2bf9125a46d34412.scn"] +source_file="res://Entities/Bush/assets/plant_bush.glb" +dest_files=["res://.godot/imported/plant_bush.glb-4eaf384fbf61c7e3b82505202bc9874e.scn"] [params] diff --git a/Entities/Tree/assets/temp/plant_bushDetailed.glb b/Entities/Bush/assets/plant_bushDetailed.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bushDetailed.glb rename to Entities/Bush/assets/plant_bushDetailed.glb diff --git a/Entities/Tree/assets/temp/plant_bushDetailed.glb.import b/Entities/Bush/assets/plant_bushDetailed.glb.import similarity index 56% rename from Entities/Tree/assets/temp/plant_bushDetailed.glb.import rename to Entities/Bush/assets/plant_bushDetailed.glb.import index 8ca63b1..fa88344 100644 --- a/Entities/Tree/assets/temp/plant_bushDetailed.glb.import +++ b/Entities/Bush/assets/plant_bushDetailed.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://cefyfwcx88gn8" -path="res://.godot/imported/plant_bushDetailed.glb-b49c41eb141c3e76fb272adf14466b5a.scn" +path="res://.godot/imported/plant_bushDetailed.glb-8633db2639023d65ffc045b449119f56.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bushDetailed.glb" -dest_files=["res://.godot/imported/plant_bushDetailed.glb-b49c41eb141c3e76fb272adf14466b5a.scn"] +source_file="res://Entities/Bush/assets/plant_bushDetailed.glb" +dest_files=["res://.godot/imported/plant_bushDetailed.glb-8633db2639023d65ffc045b449119f56.scn"] [params] @@ -32,6 +32,17 @@ animation/trimming=false animation/remove_immutable_tracks=true animation/import_rest_as_RESET=false import_script/path="" -_subresources={} +_subresources={ +"meshes": { +"plant_bushDetailed_Mesh plant_bushDetailed": { +"generate/lightmap_uv": 0, +"generate/lods": 0, +"generate/shadow_meshes": 0, +"lods/normal_merge_angle": 60.0, +"save_to_file/enabled": true, +"save_to_file/path": "res://Entities/Bush/assets/bush_large.res" +} +} +} gltf/naming_version=1 gltf/embedded_image_handling=1 diff --git a/Entities/Tree/assets/temp/plant_bushLarge.glb b/Entities/Bush/assets/plant_bushLarge.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bushLarge.glb rename to Entities/Bush/assets/plant_bushLarge.glb diff --git a/Entities/Tree/assets/temp/plant_bushLarge.glb.import b/Entities/Bush/assets/plant_bushLarge.glb.import similarity index 74% rename from Entities/Tree/assets/temp/plant_bushLarge.glb.import rename to Entities/Bush/assets/plant_bushLarge.glb.import index 7095403..e70d184 100644 --- a/Entities/Tree/assets/temp/plant_bushLarge.glb.import +++ b/Entities/Bush/assets/plant_bushLarge.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://bqh742nyfy67t" -path="res://.godot/imported/plant_bushLarge.glb-444f7e2fefa42d9967360c2460aba973.scn" +path="res://.godot/imported/plant_bushLarge.glb-034f46e010fc48537badbba708af03b9.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bushLarge.glb" -dest_files=["res://.godot/imported/plant_bushLarge.glb-444f7e2fefa42d9967360c2460aba973.scn"] +source_file="res://Entities/Bush/assets/plant_bushLarge.glb" +dest_files=["res://.godot/imported/plant_bushLarge.glb-034f46e010fc48537badbba708af03b9.scn"] [params] diff --git a/Entities/Tree/assets/temp/plant_bushLargeTriangle.glb b/Entities/Bush/assets/plant_bushLargeTriangle.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bushLargeTriangle.glb rename to Entities/Bush/assets/plant_bushLargeTriangle.glb diff --git a/Entities/Tree/assets/temp/plant_bushLargeTriangle.glb.import b/Entities/Bush/assets/plant_bushLargeTriangle.glb.import similarity index 72% rename from Entities/Tree/assets/temp/plant_bushLargeTriangle.glb.import rename to Entities/Bush/assets/plant_bushLargeTriangle.glb.import index ae7acfb..0f60668 100644 --- a/Entities/Tree/assets/temp/plant_bushLargeTriangle.glb.import +++ b/Entities/Bush/assets/plant_bushLargeTriangle.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://xj45kysvl047" -path="res://.godot/imported/plant_bushLargeTriangle.glb-9e831461ae4497bcbdb95549623cbaf6.scn" +path="res://.godot/imported/plant_bushLargeTriangle.glb-4c2a6e5fedd13bffdd9cfb0a212c89c8.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bushLargeTriangle.glb" -dest_files=["res://.godot/imported/plant_bushLargeTriangle.glb-9e831461ae4497bcbdb95549623cbaf6.scn"] +source_file="res://Entities/Bush/assets/plant_bushLargeTriangle.glb" +dest_files=["res://.godot/imported/plant_bushLargeTriangle.glb-4c2a6e5fedd13bffdd9cfb0a212c89c8.scn"] [params] diff --git a/Entities/Tree/assets/temp/plant_bushSmall.glb b/Entities/Bush/assets/plant_bushSmall.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bushSmall.glb rename to Entities/Bush/assets/plant_bushSmall.glb diff --git a/Entities/Tree/assets/temp/plant_bushSmall.glb.import b/Entities/Bush/assets/plant_bushSmall.glb.import similarity index 56% rename from Entities/Tree/assets/temp/plant_bushSmall.glb.import rename to Entities/Bush/assets/plant_bushSmall.glb.import index f5b1ace..c42fef4 100644 --- a/Entities/Tree/assets/temp/plant_bushSmall.glb.import +++ b/Entities/Bush/assets/plant_bushSmall.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://cjoyr61xdo3hx" -path="res://.godot/imported/plant_bushSmall.glb-636059e56114bd2f2c7902ac6a574e3a.scn" +path="res://.godot/imported/plant_bushSmall.glb-2218874f9d8b685dac1fd79232f82be8.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bushSmall.glb" -dest_files=["res://.godot/imported/plant_bushSmall.glb-636059e56114bd2f2c7902ac6a574e3a.scn"] +source_file="res://Entities/Bush/assets/plant_bushSmall.glb" +dest_files=["res://.godot/imported/plant_bushSmall.glb-2218874f9d8b685dac1fd79232f82be8.scn"] [params] @@ -32,6 +32,17 @@ animation/trimming=false animation/remove_immutable_tracks=true animation/import_rest_as_RESET=false import_script/path="" -_subresources={} +_subresources={ +"meshes": { +"plant_bushSmall_Mesh plant_bushSmall": { +"generate/lightmap_uv": 0, +"generate/lods": 0, +"generate/shadow_meshes": 0, +"lods/normal_merge_angle": 60.0, +"save_to_file/enabled": true, +"save_to_file/path": "res://Entities/Bush/assets/bush_small.res" +} +} +} gltf/naming_version=1 gltf/embedded_image_handling=1 diff --git a/Entities/Tree/assets/temp/plant_bushTriangle.glb b/Entities/Bush/assets/plant_bushTriangle.glb similarity index 100% rename from Entities/Tree/assets/temp/plant_bushTriangle.glb rename to Entities/Bush/assets/plant_bushTriangle.glb diff --git a/Entities/Tree/assets/temp/plant_bushTriangle.glb.import b/Entities/Bush/assets/plant_bushTriangle.glb.import similarity index 73% rename from Entities/Tree/assets/temp/plant_bushTriangle.glb.import rename to Entities/Bush/assets/plant_bushTriangle.glb.import index 399a21b..54f225f 100644 --- a/Entities/Tree/assets/temp/plant_bushTriangle.glb.import +++ b/Entities/Bush/assets/plant_bushTriangle.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://dsch27pqxgud5" -path="res://.godot/imported/plant_bushTriangle.glb-68d6459f20861ebce284d042b5ea0419.scn" +path="res://.godot/imported/plant_bushTriangle.glb-518273ba96c29be659e875adddc31676.scn" [deps] -source_file="res://Entities/Tree/assets/temp/plant_bushTriangle.glb" -dest_files=["res://.godot/imported/plant_bushTriangle.glb-68d6459f20861ebce284d042b5ea0419.scn"] +source_file="res://Entities/Bush/assets/plant_bushTriangle.glb" +dest_files=["res://.godot/imported/plant_bushTriangle.glb-518273ba96c29be659e875adddc31676.scn"] [params] diff --git a/Entities/GroundTile/GroundTile.tscn b/Entities/GroundTile/GroundTile.tscn index cd4bf43..047c3bc 100644 --- a/Entities/GroundTile/GroundTile.tscn +++ b/Entities/GroundTile/GroundTile.tscn @@ -1,11 +1,15 @@ -[gd_scene load_steps=13 format=3 uid="uid://bwcevwwphdvq"] +[gd_scene load_steps=17 format=3 uid="uid://bwcevwwphdvq"] -[ext_resource type="Script" uid="uid://bq7hia2dit80y" path="res://Entities/GroundTile/ground_tile.gd" id="1_uwxqs"] +[ext_resource type="Script" uid="uid://bq7hia2dit80y" path="res://Entities/GroundTile/scripts/ground_tile.gd" id="1_uwxqs"] [ext_resource type="ArrayMesh" uid="uid://duj6747nq4qsk" path="res://Stages/Test3D/assets/stylizedGrassMeshes/grass.res" id="3_8mhad"] [ext_resource type="Script" uid="uid://cacp8ncwuofuj" path="res://Entities/GroundTile/scripts/grass.gd" id="3_224hx"] [ext_resource type="Material" uid="uid://b1miqvl8lus75" path="res://Stages/Test3D/GrassMaterialOverride.tres" id="3_f37ob"] [ext_resource type="Script" uid="uid://btju6b83mvgvk" path="res://Entities/GroundTile/scripts/grass_multimesh.gd" id="4_3wpcb"] +[ext_resource type="Script" uid="uid://bt67yhdkwtqy5" path="res://Entities/GroundTile/scripts/bushes.gd" id="6_224hx"] [ext_resource type="Script" uid="uid://cqko4m7cbxsfb" path="res://Entities/GroundTile/scripts/trees.gd" id="7_7lc7k"] +[ext_resource type="Script" uid="uid://dri5tubavplji" path="res://Entities/GroundTile/scripts/bush_multimesh.gd" id="7_jysav"] +[ext_resource type="Script" uid="uid://d3s0u7rm1y7i6" path="res://Entities/GroundTile/scripts/flower_multimesh.gd" id="8_jysav"] +[ext_resource type="Script" uid="uid://18vxtm3ua4x0" path="res://Entities/GroundTile/scripts/flowers.gd" id="8_q0r4p"] [sub_resource type="ViewportTexture" id="ViewportTexture_h4g11"] viewport_path = NodePath("DebugText/DebugTextViewport") @@ -67,5 +71,33 @@ script = ExtResource("4_3wpcb") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0111763, 0) mesh = SubResource("PlaneMesh_f37ob") +[node name="Bushes" type="Node3D" parent="."] +script = ExtResource("6_224hx") + +[node name="BushMultimesh" type="MultiMeshInstance3D" parent="Bushes"] +cast_shadow = 0 +multimesh = SubResource("MultiMesh_3wpcb") +script = ExtResource("7_jysav") + +[node name="BushTarget" type="MeshInstance3D" parent="Bushes"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0111763, 0) +mesh = SubResource("PlaneMesh_f37ob") + +[node name="Flowers" type="Node3D" parent="."] +script = ExtResource("8_q0r4p") + +[node name="FlowerMultimesh" type="MultiMeshInstance3D" parent="Flowers"] +cast_shadow = 0 +multimesh = SubResource("MultiMesh_3wpcb") +script = ExtResource("8_jysav") + +[node name="FloewerTarget" type="MeshInstance3D" parent="Flowers"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0111763, 0) +mesh = SubResource("PlaneMesh_f37ob") + [node name="Trees" type="Node3D" parent="."] script = ExtResource("7_7lc7k") + +[node name="Special" type="Node3D" parent="."] + +[node name="Rocks" type="Node3D" parent="Special"] diff --git a/Entities/GroundTile/scripts/bush_multimesh.gd b/Entities/GroundTile/scripts/bush_multimesh.gd new file mode 100644 index 0000000..9153bbf --- /dev/null +++ b/Entities/GroundTile/scripts/bush_multimesh.gd @@ -0,0 +1,56 @@ +extends MultiMeshInstance3D +var mm: MultiMesh +var parent_node: BushController +static var bush_mesh: Mesh = null +var material: ShaderMaterial + +func _ready() -> void: + parent_node = get_parent() as BushController + if parent_node == null: + Log.pr("Error: Parent node is not a BushController!") + + # Load mesh once and reuse + if bush_mesh == null: + bush_mesh = load("res://Entities/Bush/assets/bush_large.res") + +func setup_multimesh() -> void: + if parent_node == null: + return + + if bush_mesh == null: + Log.pr("Error: Could not load bush mesh") + return + + # Reuse existing MultiMesh if possible, or create new one + if mm == null: + mm = MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_3D + mm.mesh = bush_mesh + + material = ColorData.get_random_bush_material(Season.current) + mm.mesh.surface_set_material(0, material) + + # Configure instance count + mm.instance_count = parent_node.bush_instance_range + + # Get shared RNG from GroundTile + var rng = parent_node.parent_node.get_rng() + + # Generate positions using shared RNG + for i in range(mm.instance_count): + var random_pos = Vector3( + rng.randf_range(-1.0, 1.0), + 0.0, + rng.randf_range(-1.0, 1.0) + ) + + var random_rotation = rng.randf_range(0.0, TAU) + var basis = Basis(Vector3.UP, random_rotation) + + var random_scale = rng.randf_range(0.3, 0.9) + basis = basis.scaled(Vector3(random_scale, random_scale, random_scale)) + + var tx = Transform3D(basis, random_pos) + mm.set_instance_transform(i, tx) + + multimesh = mm diff --git a/Entities/GroundTile/scripts/bush_multimesh.gd.uid b/Entities/GroundTile/scripts/bush_multimesh.gd.uid new file mode 100644 index 0000000..d832095 --- /dev/null +++ b/Entities/GroundTile/scripts/bush_multimesh.gd.uid @@ -0,0 +1 @@ +uid://dri5tubavplji diff --git a/Entities/GroundTile/scripts/bushes.gd b/Entities/GroundTile/scripts/bushes.gd new file mode 100644 index 0000000..6cbdb58 --- /dev/null +++ b/Entities/GroundTile/scripts/bushes.gd @@ -0,0 +1,36 @@ +# GrassController.gd +extends Node3D +class_name BushController + +@onready var bush_multimesh: MultiMeshInstance3D = $BushMultimesh +var parent_node: GroundTile = null +var bush_density: float = 0.5 +var bush_instance_range: int = 10 + +func _ready() -> void: + parent_node = get_parent() as GroundTile + +func spawn_bushes_for_cell(value): + if value == null: + return + + bush_density = value.vegetation_density + update_bush_density() + + if bush_multimesh and bush_multimesh.has_method("setup_multimesh"): + bush_multimesh.setup_multimesh() + +func update_bush_density() -> void: + if parent_node == null: + return + + var rng = parent_node.get_rng() + + if bush_density > 0.8: + bush_instance_range = rng.randi_range(5, 10) + elif bush_density > 0.6: + bush_instance_range = rng.randi_range(2, 7) + elif bush_density > 0.3: + bush_instance_range = rng.randi_range(0, 1) + else: + bush_instance_range = rng.randi_range(0, 0) diff --git a/Entities/GroundTile/scripts/bushes.gd.uid b/Entities/GroundTile/scripts/bushes.gd.uid new file mode 100644 index 0000000..905ac13 --- /dev/null +++ b/Entities/GroundTile/scripts/bushes.gd.uid @@ -0,0 +1 @@ +uid://bt67yhdkwtqy5 diff --git a/Entities/GroundTile/scripts/flower_multimesh.gd b/Entities/GroundTile/scripts/flower_multimesh.gd new file mode 100644 index 0000000..6ca351a --- /dev/null +++ b/Entities/GroundTile/scripts/flower_multimesh.gd @@ -0,0 +1,59 @@ +extends MultiMeshInstance3D + +var mm: MultiMesh +var parent_node: FlowerController +static var flower_mesh: Mesh = null +var material: StandardMaterial3D + +func _ready() -> void: + parent_node = get_parent() as FlowerController + if parent_node == null: + Log.pr("Error: Parent node is not a FlowerController!") + + # Load mesh once and reuse + if flower_mesh == null: + flower_mesh = load("res://Entities/Plant/assets/flower_tall.res") + +func setup_multimesh() -> void: + if parent_node == null: + return + + if flower_mesh == null: + Log.pr("Error: Could not load flower mesh") + return + + # Reuse existing MultiMesh if possible, or create new one + if mm == null: + mm = MultiMesh.new() + mm.transform_format = MultiMesh.TRANSFORM_3D + mm.mesh = flower_mesh + + var flower_material = ColorData.flower_materials[Season.current]["flower"] + var stem_material = ColorData.flower_materials[Season.current]["stem"] + mm.mesh.surface_set_material(0, stem_material) + mm.mesh.surface_set_material(1, flower_material) + + # Configure instance count + mm.instance_count = parent_node.flower_instance_range + + # Get shared RNG from GroundTile + var rng = parent_node.parent_node.get_rng() + + # Generate positions using shared RNG + for i in range(mm.instance_count): + var random_pos = Vector3( + rng.randf_range(-1.0, 1.0), + 0.0, + rng.randf_range(-1.0, 1.0) + ) + + var random_rotation = rng.randf_range(0.0, TAU) + var basis = Basis(Vector3.UP, random_rotation) + + var random_scale = rng.randf_range(0.2, 0.4) + basis = basis.scaled(Vector3(random_scale, random_scale, random_scale)) + + var tx = Transform3D(basis, random_pos) + mm.set_instance_transform(i, tx) + + multimesh = mm diff --git a/Entities/GroundTile/scripts/flower_multimesh.gd.uid b/Entities/GroundTile/scripts/flower_multimesh.gd.uid new file mode 100644 index 0000000..0f711a3 --- /dev/null +++ b/Entities/GroundTile/scripts/flower_multimesh.gd.uid @@ -0,0 +1 @@ +uid://d3s0u7rm1y7i6 diff --git a/Entities/GroundTile/scripts/flowers.gd b/Entities/GroundTile/scripts/flowers.gd new file mode 100644 index 0000000..f66aa62 --- /dev/null +++ b/Entities/GroundTile/scripts/flowers.gd @@ -0,0 +1,36 @@ +# GrassController.gd +extends Node3D +class_name FlowerController + +@onready var flower_multimesh: MultiMeshInstance3D = $FlowerMultimesh +var parent_node: GroundTile = null +var flower_density: float = 0.5 +var flower_instance_range: int = 10 + +func _ready() -> void: + parent_node = get_parent() as GroundTile + +func spawn_flowers_for_cell(value): + if value == null: + return + + flower_density = value.vegetation_density + update_flower_density() + + if flower_multimesh and flower_multimesh.has_method("setup_multimesh"): + flower_multimesh.setup_multimesh() + +func update_flower_density() -> void: + if parent_node == null: + return + + var rng = parent_node.get_rng() + + if flower_density > 0.8: + flower_instance_range = rng.randi_range(1, 3) + elif flower_density > 0.6: + flower_instance_range = rng.randi_range(3, 4) + elif flower_density > 0.3: + flower_instance_range = rng.randi_range(4, 7) + else: + flower_instance_range = rng.randi_range(5, 10) diff --git a/Entities/GroundTile/scripts/flowers.gd.uid b/Entities/GroundTile/scripts/flowers.gd.uid new file mode 100644 index 0000000..bb5f79f --- /dev/null +++ b/Entities/GroundTile/scripts/flowers.gd.uid @@ -0,0 +1 @@ +uid://18vxtm3ua4x0 diff --git a/Entities/GroundTile/scripts/grass_multimesh.gd b/Entities/GroundTile/scripts/grass_multimesh.gd index e05c4f4..a289661 100644 --- a/Entities/GroundTile/scripts/grass_multimesh.gd +++ b/Entities/GroundTile/scripts/grass_multimesh.gd @@ -25,6 +25,8 @@ func setup_multimesh() -> void: mm = MultiMesh.new() mm.transform_format = MultiMesh.TRANSFORM_3D mm.mesh = grass_mesh + update_shader_parameter('top_color', ColorData.grass_materials[Season.current]['top']) + update_shader_parameter('bottom_color', ColorData.grass_materials[Season.current]['base']) # Configure instance count mm.instance_count = parent_node.grass_instance_range @@ -51,3 +53,18 @@ func setup_multimesh() -> void: multimesh = mm cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + + +func update_shader_parameter(param_name: String, value) -> void: + if material_override == null: + Log.pr("Error: No material override found") + return + + # Check if it's a ShaderMaterial + if material_override is ShaderMaterial: + var shader_material = material_override as ShaderMaterial + shader_material.set_shader_parameter(param_name, value) + else: + Log.pr("Error: Material override is not a ShaderMaterial") + + return diff --git a/Entities/GroundTile/ground_tile.gd b/Entities/GroundTile/scripts/ground_tile.gd similarity index 75% rename from Entities/GroundTile/ground_tile.gd rename to Entities/GroundTile/scripts/ground_tile.gd index 86f29b5..639ab11 100644 --- a/Entities/GroundTile/ground_tile.gd +++ b/Entities/GroundTile/scripts/ground_tile.gd @@ -3,6 +3,9 @@ extends Node3D @onready var debug_text: Label = $DebugText/DebugTextViewport/DebugTextLabel @onready var tree_spawner = $Trees @onready var grass_spawner = $Grass +@onready var bush_spawner = $Bushes +@onready var flower_spawner = $Flowers +@onready var ground = $Ground var grid_x: int var grid_z: int var cell_info: CellDataResource = null @@ -16,8 +19,10 @@ func _ready() -> void: spawners_ready = true if cell_info != null: - spawn_content() - update_text_label() + spawn_content() + update_text_label() + + ground.material_override.albedo_color = ColorData.grass_materials[Season.current]['base'] func get_rng() -> RandomClass: if cached_rng == null and cell_info: @@ -44,6 +49,11 @@ func spawn_content(): tree_spawner.spawn_trees_for_cell(cell_info) if grass_spawner: grass_spawner.spawn_grass_for_cell(cell_info) + if bush_spawner: + bush_spawner.spawn_bushes_for_cell(cell_info) + if flower_spawner: + flower_spawner.spawn_flowers_for_cell(cell_info) + func update_text_label() -> void: debug_text.text = str(cell_info.vegetation_density) diff --git a/Entities/GroundTile/ground_tile.gd.uid b/Entities/GroundTile/scripts/ground_tile.gd.uid similarity index 100% rename from Entities/GroundTile/ground_tile.gd.uid rename to Entities/GroundTile/scripts/ground_tile.gd.uid diff --git a/Entities/Plant/assets/flower.res b/Entities/Plant/assets/flower.res new file mode 100644 index 0000000..869b4f6 Binary files /dev/null and b/Entities/Plant/assets/flower.res differ diff --git a/Entities/Tree/assets/temp/flower_purpleB.glb b/Entities/Plant/assets/flower_tall.glb similarity index 100% rename from Entities/Tree/assets/temp/flower_purpleB.glb rename to Entities/Plant/assets/flower_tall.glb diff --git a/Entities/Tree/assets/temp/flower_purpleB.glb.import b/Entities/Plant/assets/flower_tall.glb.import similarity index 57% rename from Entities/Tree/assets/temp/flower_purpleB.glb.import rename to Entities/Plant/assets/flower_tall.glb.import index 34273c4..aaeb895 100644 --- a/Entities/Tree/assets/temp/flower_purpleB.glb.import +++ b/Entities/Plant/assets/flower_tall.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://c8kl0a1nstbuc" -path="res://.godot/imported/flower_purpleB.glb-69a5b0606a51601fdd91762f296ce88b.scn" +path="res://.godot/imported/flower_tall.glb-105aa1c03f7501793bbf8ad0e150a0ed.scn" [deps] -source_file="res://Entities/Tree/assets/temp/flower_purpleB.glb" -dest_files=["res://.godot/imported/flower_purpleB.glb-69a5b0606a51601fdd91762f296ce88b.scn"] +source_file="res://Entities/Plant/assets/flower_tall.glb" +dest_files=["res://.godot/imported/flower_tall.glb-105aa1c03f7501793bbf8ad0e150a0ed.scn"] [params] @@ -32,6 +32,17 @@ animation/trimming=false animation/remove_immutable_tracks=true animation/import_rest_as_RESET=false import_script/path="" -_subresources={} +_subresources={ +"meshes": { +"flower_tall_Mesh flower_purpleB": { +"generate/lightmap_uv": 0, +"generate/lods": 0, +"generate/shadow_meshes": 0, +"lods/normal_merge_angle": 60.0, +"save_to_file/enabled": true, +"save_to_file/path": "res://Entities/Plant/assets/flower.res" +} +} +} gltf/naming_version=1 gltf/embedded_image_handling=1 diff --git a/Entities/Plant/assets/flower_tall.res b/Entities/Plant/assets/flower_tall.res new file mode 100644 index 0000000..9ae3080 Binary files /dev/null and b/Entities/Plant/assets/flower_tall.res differ diff --git a/Entities/Tree/scripts/color_default.gd b/Entities/Tree/scripts/color_default.gd index b91a5ab..ef63019 100644 --- a/Entities/Tree/scripts/color_default.gd +++ b/Entities/Tree/scripts/color_default.gd @@ -104,5 +104,5 @@ func update_material_in_mesh_instance(mesh_instance: MeshInstance3D, material_na func apply_material_with_queue(mesh_instance: MeshInstance3D, index: int, material: StandardMaterial3D): - ColorData.queue_material_application(mesh_instance, index, material) + MaterialManager.queue_material_application(mesh_instance, index, material) return diff --git a/Entities/Tree/scripts/tree.gd b/Entities/Tree/scripts/tree.gd index f780a49..3c8eb24 100644 --- a/Entities/Tree/scripts/tree.gd +++ b/Entities/Tree/scripts/tree.gd @@ -33,7 +33,7 @@ func spawn_model(): # Instantiate the model from the TreeDataResource model_instance = tree_data.model.instantiate() - tree_data.apply_seasonal_colors(model_instance, "spring") + tree_data.apply_seasonal_colors(model_instance, Season.current) add_child(model_instance) diff --git a/Utilities/ColorStorage/ColorStorage.gd b/Utilities/ColorStorage/ColorStorage.gd index 3da66a7..2a3a4bc 100644 --- a/Utilities/ColorStorage/ColorStorage.gd +++ b/Utilities/ColorStorage/ColorStorage.gd @@ -3,48 +3,44 @@ extends Node var tree_collection = preload("res://Entities/Tree/Resources/TreeData.tres") as TreeDataCollection var tree_materials: Dictionary = {} -var grass_materials: Dictionary = {"spring": {"base": Vector3(0.196, 0.392, 0.196), "top": Vector3(0.253, 0.492, 0.253), "bottom": Vector3(0.196, 0.392, 0.196)}, - "summer": {"base": null, "top": null, "bottom": null}, - "autumn": {"base": null, "top": null, "bottom": null}, - "winter": {"base": null, "top": null, "bottom": null}} - - -## TODO - Move this out into its own class -var material_application_queue: Array = [] -var max_materials_per_frame: int = 10 # Adjust based on performance +var grass_materials: Dictionary = { + "spring": { + "bottom": Color(0.18, 0.35, 0.12), # #2e5920 - Dark forest base + "top": Color(0.18, 0.35, 0.12), # Color(0.35, 0.65, 0.25), # #59a640 - Bright spring green + "base": Color(0.12, 0.25, 0.08), # #1f4014 - Deep shadow green + "flower": Color(0.776, 0.835, 0.855) # rgb(198, 213, 218) - Spring flower color + }, + "summer": { + "bottom": Color(0.22, 0.45, 0.18), # #38732e - Rich summer base + "top": Color(0.22, 0.45, 0.18), # Color(0.4, 0.75, 0.3), # #66bf4d - Sun-kissed green + "base": Color(0.15, 0.3, 0.12), # #264d1f - Shaded summer base + "flower": Color(0.408, 0.537, 0.886) # rgb(104, 137, 226) - Summer flower color + }, + "autumn": { + "bottom": Color(0.35, 0.4, 0.15), # #596626 - Yellowing grass + "top": Color(0.42, 0.45, 0.19), # #6b7330 - Muted autumn tips + "base": Color(0.25, 0.25, 0.1), # #40401a - Dying grass base + "flower": Color(0.820, 0.337, 0.318) # rgb(209, 86, 81) - Autumn flower color + }, + "winter": { + "bottom": Color(0.25, 0.3, 0.2), # #404d33 - Dormant green-brown + "top": Color(0.35, 0.4, 0.3), # #59664d - Frost-touched tips + "base": Color(0.2, 0.2, 0.15), # #333326 - Winter soil color + "flower": Color(1.0, 0.8, 0.6) # #ffd9b3 - Winter flower color + } +} +var bush_materials: Dictionary = {} +var flower_materials: Dictionary = {} @export var tree_colours: Array func _ready() -> void: populate_tree_colors() Log.pr("Tree colors populated: %d trees" % tree_materials.size()) - -func _process(_delta): - # Process material applications gradually over multiple frames - process_material_queue() - -func process_material_queue(): - var processed = 0 - while material_application_queue.size() > 0 and processed < max_materials_per_frame: - var task = material_application_queue.pop_front() - - # Check if the mesh instance is still valid before applying - if is_instance_valid(task.mesh_instance) and task.mesh_instance != null: - apply_material_immediately(task.mesh_instance, task.surface_index, task.material) - # If invalid, just skip this task (object was freed) - - processed += 1 - -func queue_material_application(mesh_instance: MeshInstance3D, surface_index: int, material: StandardMaterial3D): - material_application_queue.append({ - "mesh_instance": mesh_instance, - "surface_index": surface_index, - "material": material - }) - -func apply_material_immediately(mesh_instance: MeshInstance3D, surface_index: int, material: StandardMaterial3D): - if is_instance_valid(mesh_instance): - mesh_instance.set_surface_override_material(surface_index, material) + populate_bush_materials() + Log.pr("Bush materials populated: %d seasons" % bush_materials.size()) + populate_flower_materials() + Log.pr("Flower materials populated: %d seasons" % flower_materials.size()) # Create materials once at startup func populate_tree_colors() -> void: @@ -117,3 +113,53 @@ func get_random_trunk_material(tree_name: String, season: String) -> StandardMat return trunk_materials[random_key] return create_material(Color(0.6, 0.4, 0.2), "fallback_trunk") + +func populate_bush_materials() -> void: + for season in Season.seasons: + bush_materials[season] = { + "1": create_bush_material(grass_materials[season]["base"], grass_materials[season]["flower"].lightened(0.1), "bush_base_%s" % season) + } + Log.pr("Bush materials populated for seasons: %s" % bush_materials.keys()) + +func create_bush_material(color: Color, variation: Color, material_name: String) -> ShaderMaterial: + var material = ShaderMaterial.new() + + var shader = load("res://leaves.gdshader") + + material.shader = shader + material.resource_name = material_name + + material.set_shader_parameter("base_color", color) + material.set_shader_parameter("variation_color", variation) + + return material + +func get_random_bush_material(season: String) -> ShaderMaterial: + if bush_materials.has(season): + var bush_materials_season = bush_materials[season] + var material_keys = bush_materials_season.keys() + if material_keys.size() > 0: + var random_key = material_keys[randi() % material_keys.size()] + return bush_materials_season[random_key] + + Log.pr("No bush materials found for season: %s, returning fallback." % season) + return create_bush_material(Color.GREEN, Color.GREEN, "fallback_bush") + +func populate_flower_materials() -> void: + for season in Season.seasons: + flower_materials[season] = { + "flower": create_material(grass_materials[season]['flower'], "flower_base_%s" % season), + "stem": create_material(grass_materials[season]["top"], "flower_stem_%s" % season), + } + Log.pr("Flower materials populated for seasons: %s" % flower_materials.keys()) + +func get_random_flower_material(season: String) -> StandardMaterial3D: + if flower_materials.has(season): + var flower_materials_season = flower_materials[season] + var material_keys = flower_materials_season.keys() + if material_keys.size() > 0: + var random_key = material_keys[randi() % material_keys.size()] + return flower_materials_season[random_key] + + Log.pr("No flower materials found for season: %s, returning fallback." % season) + return create_material(Color(1.0, 1.0, 0.5), "fallback_flower") diff --git a/Utilities/MaterialManager/MaterialManager.gd b/Utilities/MaterialManager/MaterialManager.gd new file mode 100644 index 0000000..9a9f562 --- /dev/null +++ b/Utilities/MaterialManager/MaterialManager.gd @@ -0,0 +1,30 @@ +class_name MaterialManagerClass +extends Node + +var material_application_queue: Array = [] +var max_materials_per_frame: int = 20 # Adjust based on performance + +func _process(_delta): + # Process material applications gradually over multiple frames + process_material_queue() + +func process_material_queue(): + var processed = 0 + while material_application_queue.size() > 0 and processed < max_materials_per_frame: + var task = material_application_queue.pop_front() + + if is_instance_valid(task.mesh_instance) and task.mesh_instance != null: + apply_material_immediately(task.mesh_instance, task.surface_index, task.material) + + processed += 1 + +func queue_material_application(mesh_instance: MeshInstance3D, surface_index: int, material: StandardMaterial3D): + material_application_queue.append({ + "mesh_instance": mesh_instance, + "surface_index": surface_index, + "material": material + }) + +func apply_material_immediately(mesh_instance: MeshInstance3D, surface_index: int, material: StandardMaterial3D): + if is_instance_valid(mesh_instance): + mesh_instance.set_surface_override_material(surface_index, material) diff --git a/Utilities/MaterialManager/MaterialManager.gd.uid b/Utilities/MaterialManager/MaterialManager.gd.uid new file mode 100644 index 0000000..1c7917f --- /dev/null +++ b/Utilities/MaterialManager/MaterialManager.gd.uid @@ -0,0 +1 @@ +uid://hxkwqb71371n diff --git a/Utilities/Seasons/seasons.gd b/Utilities/Seasons/seasons.gd new file mode 100644 index 0000000..8cd6f8b --- /dev/null +++ b/Utilities/Seasons/seasons.gd @@ -0,0 +1,11 @@ +class_name SeasonController +extends Node + +var seasons: Array = [ + "spring", + "summer", + "autumn", + "winter" +] + +@export var current: String = "autumn" \ No newline at end of file diff --git a/Utilities/Seasons/seasons.gd.uid b/Utilities/Seasons/seasons.gd.uid new file mode 100644 index 0000000..f759b7d --- /dev/null +++ b/Utilities/Seasons/seasons.gd.uid @@ -0,0 +1 @@ +uid://buj63lgb7ckto diff --git a/leaves.gdshader b/leaves.gdshader new file mode 100644 index 0000000..4ba6f4e --- /dev/null +++ b/leaves.gdshader @@ -0,0 +1,23 @@ +shader_type spatial; +uniform vec4 base_color : source_color = vec4(0.2, 0.6, 0.1, 1.0); +uniform vec4 variation_color : source_color = vec4(0.4, 0.8, 0.2, 1.0); +uniform float gradient_height : hint_range(0.1, 5.0) = 2.0; +uniform float gradient_offset : hint_range(-2.0, 2.0) = 0.0; + +varying vec3 world_position; + +void vertex() { + world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; +} + +void fragment() { + // Use world Y position instead of UV + float gradient_factor = (world_position.y + gradient_offset) / gradient_height; + gradient_factor = clamp(gradient_factor, 0.0, 1.0); + + vec3 final_color = mix(base_color.rgb, variation_color.rgb, gradient_factor); + + ALBEDO = final_color; + ROUGHNESS = 0.8; + METALLIC = 0.0; +} \ No newline at end of file diff --git a/leaves.gdshader.uid b/leaves.gdshader.uid new file mode 100644 index 0000000..8d6b26d --- /dev/null +++ b/leaves.gdshader.uid @@ -0,0 +1 @@ +uid://d0e60a0hdk02j diff --git a/project.godot b/project.godot index b41fddb..fe1e629 100644 --- a/project.godot +++ b/project.godot @@ -25,6 +25,8 @@ BiomeData="*res://Utilities/BiomeGeneration/BiomeData.gd" MapData="*res://Utilities/MapData/MapData.gd" ColorData="*res://Utilities/ColorStorage/ColorStorage.gd" MapPopulation="*res://Utilities/MapData/MapPopulation.gd" +MaterialManager="*res://Utilities/MaterialManager/MaterialManager.gd" +Season="*res://Utilities/Seasons/seasons.gd" [editor_plugins] diff --git a/stages/Test3D/Test3d.tscn b/stages/Test3D/Test3d.tscn index 518349c..05f23fe 100644 --- a/stages/Test3D/Test3d.tscn +++ b/stages/Test3D/Test3d.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=45 format=4 uid="uid://bwsugg4p50fjr"] +[gd_scene load_steps=46 format=4 uid="uid://bwsugg4p50fjr"] [ext_resource type="Environment" uid="uid://cm77bbr0io118" path="res://Stages/Test3D/new_environment.tres" id="1_8ph61"] [ext_resource type="Script" uid="uid://bwed2dwogfmxv" path="res://Entities/Player/scripts/player.gd" id="1_d602n"] [ext_resource type="AnimationLibrary" uid="uid://bwnn7vpd0dqds" path="res://Common/animations/basic-movement.res" id="1_tfa5t"] [ext_resource type="Script" uid="uid://bbjv6a7yg7m02" path="res://Stages/Test3D/camera_pivot.gd" id="2_sdmks"] [ext_resource type="Shader" uid="uid://bsemnmdracd4m" path="res://Common/shaders/outline.gdshader" id="4_feu7y"] +[ext_resource type="Script" uid="uid://bjco8musjqog4" path="res://Stages/Test3D/particles.gd" id="9_oiyue"] [ext_resource type="Texture2D" uid="uid://c78jcjh8fjndd" path="res://Stages/Test3D/assets/3d/particles/flamelet_smooth.png" id="21_xvexm"] [ext_resource type="Script" uid="uid://dglvt140rhg00" path="res://Stages/Test3D/omni_light_3d.gd" id="22_ukp6m"] [ext_resource type="PackedScene" uid="uid://mdxkaqaoybjv" path="res://Stages/Test3D/assets/tent-canvas.glb" id="23_5r2bu"] @@ -737,11 +738,11 @@ transform = Transform3D(0.707107, -0.408607, 0.577096, 0, 0.816138, 0.577857, -0 script = ExtResource("2_sdmks") [node name="Camera3D" type="Camera3D" parent="SubViewportContainer/SubViewport/Player/CameraPivot"] -transform = Transform3D(1, 0, 0, 0, 1, -1.49012e-07, 0, 1.19209e-07, 1, 0.0410548, 0.237644, 3.45114) +transform = Transform3D(1, 0, 0, 0, 1, 1.78814e-07, 0, -3.27826e-07, 1, 0, 0, 7) projection = 1 current = true size = 3.0 -near = 0.005 +near = 0.001 far = 100.0 [node name="PostProcessing" type="MeshInstance3D" parent="SubViewportContainer/SubViewport/Player/CameraPivot/Camera3D"] @@ -759,10 +760,10 @@ mesh = SubResource("QuadMesh_tfa5t") shape = SubResource("BoxShape3D_tfa5t") [node name="VFX" type="Node3D" parent="SubViewportContainer/SubViewport"] -visible = false [node name="Fire" type="GPUParticles3D" parent="SubViewportContainer/SubViewport/VFX"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.08083, 0.0837402, 0.501403) +visible = false amount = 50 lifetime = 0.4 speed_scale = 0.4 @@ -772,7 +773,7 @@ draw_pass_1 = SubResource("QuadMesh_hvb1l") [node name="OmniLight3D" type="OmniLight3D" parent="SubViewportContainer/SubViewport/VFX/Fire"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.000509977, 0.121094, -0.00151992) light_color = Color(0.89, 0.461613, 0.2136, 1) -light_energy = 0.802091 +light_energy = 0.590552 light_indirect_energy = 1.084 light_volumetric_fog_energy = 3.764 light_size = 0.105 @@ -794,6 +795,9 @@ trail_lifetime = 0.1 process_material = SubResource("ParticleProcessMaterial_p5fn2") draw_pass_1 = SubResource("RibbonTrailMesh_5r2bu") +[node name="FloatingParticles" type="GPUParticles3D" parent="SubViewportContainer/SubViewport/VFX"] +script = ExtResource("9_oiyue") + [node name="TileGround" type="Node3D" parent="SubViewportContainer/SubViewport"] unique_name_in_owner = true script = ExtResource("24_vyi1v") diff --git a/stages/Test3D/assets/stylizedGrassMeshes/grass.res b/stages/Test3D/assets/stylizedGrassMeshes/grass.res index 598ca86..416568c 100644 Binary files a/stages/Test3D/assets/stylizedGrassMeshes/grass.res and b/stages/Test3D/assets/stylizedGrassMeshes/grass.res differ diff --git a/stages/Test3D/new_environment.tres b/stages/Test3D/new_environment.tres index 970049f..0cfe14c 100644 --- a/stages/Test3D/new_environment.tres +++ b/stages/Test3D/new_environment.tres @@ -2,7 +2,6 @@ [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_lg8b7"] sky_horizon_color = Color(0.67451, 0.682353, 0.698039, 1) -sky_curve = 0.0175 ground_bottom_color = Color(1, 1, 1, 1) ground_curve = 0.171484 @@ -10,7 +9,7 @@ ground_curve = 0.171484 sky_material = SubResource("ProceduralSkyMaterial_lg8b7") [resource] -background_mode = 1 +background_mode = 2 background_color = Color(0.752941, 0.776471, 0.827451, 1) sky = SubResource("Sky_7bk1c") ambient_light_source = 2 @@ -18,12 +17,12 @@ ambient_light_color = Color(0.662745, 0.694118, 0.772549, 1) ambient_light_energy = 0.5 reflected_light_source = 2 tonemap_mode = 2 -ssao_enabled = true -ssao_radius = 0.3 -ssao_intensity = 0.5 -ssao_power = 15.0 ssil_enabled = true +sdfgi_use_occlusion = true glow_levels/2 = 0.6 glow_levels/3 = 0.6 glow_levels/5 = 0.0 glow_intensity = 2.0 +fog_density = 0.0635 +volumetric_fog_emission = Color(0.821925, 0.333878, 0.419207, 1) +volumetric_fog_ambient_inject = 16.0 diff --git a/stages/Test3D/particles.gd b/stages/Test3D/particles.gd new file mode 100644 index 0000000..3046ca8 --- /dev/null +++ b/stages/Test3D/particles.gd @@ -0,0 +1,79 @@ +extends GPUParticles3D + +@onready var player: Node3D = get_node("%Player") +var last_player_position: Vector3 +var update_distance: float = 1 + +func _ready(): + setup_floating_particles() + +func _process(delta): + if player: + var current_player_pos = player.global_position + + var target_pos = Vector3( + current_player_pos.x, + 1.0, + current_player_pos.z + ) + + global_position = global_position.lerp(target_pos, delta * 3.0) + +func setup_floating_particles(): + # Basic particle setup + emitting = true + amount = 100 + lifetime = 8.0 + visibility_aabb = AABB(Vector3(-20, 0, -20), Vector3(40, 10, 40)) + + # Create material + var material = ParticleProcessMaterial.new() + + # Emission + material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX + material.emission_box_extents = Vector3(5, 2, 5) + + # Movement + material.direction = Vector3(0.1, 0.8, 0.1) + material.initial_velocity_min = 0.2 + material.initial_velocity_max = 0.8 + material.gravity = Vector3(0, -0.3, 0) + + # Floating motion + material.orbit_velocity_min = 0.1 + material.orbit_velocity_max = 0.3 + material.radial_velocity_min = -0.2 + material.radial_velocity_max = 0.2 + + # Size and fade + material.scale_min = 0.01 + material.scale_max = 0.03 + material.scale_over_velocity_min = 0.0 + material.scale_over_velocity_max = 2.0 + + # Color (golden dust particles) + var gradient = Gradient.new() + gradient.add_point(0.0, Color(1.0, 0.9, 0.6, 0.0)) # Fade in + gradient.add_point(0.2, Color(1.0, 0.9, 0.6, 0.5)) # Full opacity + gradient.add_point(0.8, Color(1.0, 0.8, 0.4, 0.3)) # Slight color shift + gradient.add_point(1.0, Color(1.0, 0.7, 0.3, 0.0)) # Fade out + + var gradient_texture = GradientTexture1D.new() + gradient_texture.gradient = gradient + material.color_ramp = gradient_texture + + # Assign the material to the particle system + process_material = material + + # Create and assign visual mesh (small billboard) + var quad_mesh = QuadMesh.new() + quad_mesh.size = Vector2(0.01, 0.01) + draw_pass_1 = quad_mesh + + # Create a basic material for the particles to be visible + var particle_material = StandardMaterial3D.new() + particle_material.albedo_color = ColorData.grass_materials[Season.current]['top'] + particle_material.flags_transparent = true + particle_material.flags_unshaded = true + particle_material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED + material_override = particle_material diff --git a/stages/Test3D/particles.gd.uid b/stages/Test3D/particles.gd.uid new file mode 100644 index 0000000..469c48b --- /dev/null +++ b/stages/Test3D/particles.gd.uid @@ -0,0 +1 @@ +uid://bjco8musjqog4