diff --git a/docs/BUILD_IN_PUBLIC_PLAN.md b/docs/BUILD_IN_PUBLIC_PLAN.md index 8fdc568..8106a72 100644 --- a/docs/BUILD_IN_PUBLIC_PLAN.md +++ b/docs/BUILD_IN_PUBLIC_PLAN.md @@ -468,7 +468,10 @@ Tab-cycled inspector shows authoritative need values, task state, destination, decision branch, and utility scores. The existing carried-food prop and village panel expose immediate consequences. `F10` toggles the debug/cinematic presentation layer, while `F12` reloads the current scene as a repeatable -simulation-garden demo reset. +simulation-garden demo reset. The capsule NPCs have now been replaced by +multi-part stylized villager placeholders with tunics, heads, hair, limbs, +feet, and state-driven walk motion. Carried wood displays as a log bundle from +authoritative inventory, matching the existing carried-food presentation. ### Exit condition @@ -653,12 +656,20 @@ Completed: and waterfall widened; the fortress ridge landmark now has crenellations, a dedicated banner pole with larger banner, and three turrets for a more readable silhouette against the sky. +16. Stylized villager readability and VFX cleanup: NPCs now use compact + multi-part silhouettes with state-driven walk motion and visible carried + logs. Waterfall mist is anchored to the waterfall instead of the village + origin, duplicate wind layers are separated, and Metal smoke/wind + materials retain restrained transparency. Next: -1. Continue growing resource discovery with finite ResourceNode instances - placed in meaningful contexts (from LEARNING_ROADMAP). See the roadmap - for the next systems priority. +1. Replace the remaining player capsule with a matching stylized presentation, + then revisit runtime camera framing so the waterfall and village action + remain readable in the same authored shot. +2. Continue with the action precondition/material-cost contract and + relationship consequence work in `LEARNING_ROADMAP.md` before expanding + visual scope further. Do not start with GIS data, a full city, a large asset pack, or more NPC mechanics. The next proof is a beautiful stage for the systems that already diff --git a/docs/baselines/SIMULATION_GARDEN_01.md b/docs/baselines/SIMULATION_GARDEN_01.md index 15142a6..4a48733 100644 --- a/docs/baselines/SIMULATION_GARDEN_01.md +++ b/docs/baselines/SIMULATION_GARDEN_01.md @@ -1,6 +1,6 @@ # Simulation Garden 01 -Captured: 2026-07-08 +Captured: 2026-07-09 ## Files @@ -50,3 +50,14 @@ The frame also makes the next presentation risks plain: Use this baseline as the first runtime comparison image before adding more systems or expanding the terrain. + +## July 9 visual follow-up + +The baseline images now include the first stylized-villager pass. NPCs use +multi-part silhouettes and real travel-driven limb motion instead of capsules; +profession colors and props remain definition-driven, and authoritative wood +inventory now appears as carried logs. The capture also fixes three effects +that obscured the simulation on Metal: waterfall mist was incorrectly emitted +at the village origin, ridge/valley wind systems overlapped, and chimney smoke +ignored particle transparency. The player capsule and camera composition are +the next visible presentation gaps. diff --git a/docs/baselines/jajce_lookdev_01.png.import b/docs/baselines/jajce_lookdev_01.png.import new file mode 100644 index 0000000..6fd7fe5 --- /dev/null +++ b/docs/baselines/jajce_lookdev_01.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clc1x0yydurdk" +path="res://.godot/imported/jajce_lookdev_01.png-090cdbef692a973d175bce2e4bf10776.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/baselines/jajce_lookdev_01.png" +dest_files=["res://.godot/imported/jajce_lookdev_01.png-090cdbef692a973d175bce2e4bf10776.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 diff --git a/docs/baselines/simulation_garden_01_cinematic.png b/docs/baselines/simulation_garden_01_cinematic.png index 63a451a..629be65 100644 Binary files a/docs/baselines/simulation_garden_01_cinematic.png and b/docs/baselines/simulation_garden_01_cinematic.png differ diff --git a/docs/baselines/simulation_garden_01_cinematic.png.import b/docs/baselines/simulation_garden_01_cinematic.png.import new file mode 100644 index 0000000..79d192b --- /dev/null +++ b/docs/baselines/simulation_garden_01_cinematic.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtr1bl6xe3pnk" +path="res://.godot/imported/simulation_garden_01_cinematic.png-39560816a2b107dac16e5ba4e2c5dbb8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/baselines/simulation_garden_01_cinematic.png" +dest_files=["res://.godot/imported/simulation_garden_01_cinematic.png-39560816a2b107dac16e5ba4e2c5dbb8.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 diff --git a/docs/baselines/simulation_garden_01_debug.png b/docs/baselines/simulation_garden_01_debug.png index c2e7c44..5fd1ef2 100644 Binary files a/docs/baselines/simulation_garden_01_debug.png and b/docs/baselines/simulation_garden_01_debug.png differ diff --git a/docs/baselines/simulation_garden_01_debug.png.import b/docs/baselines/simulation_garden_01_debug.png.import new file mode 100644 index 0000000..662adab --- /dev/null +++ b/docs/baselines/simulation_garden_01_debug.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://wmk4fykdlafb" +path="res://.godot/imported/simulation_garden_01_debug.png-c2c8d5bb4081c8fa4b5fa05e512fa49e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/baselines/simulation_garden_01_debug.png" +dest_files=["res://.godot/imported/simulation_garden_01_debug.png-c2c8d5bb4081c8fa4b5fa05e512fa49e.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 diff --git a/player/npc/NpcVisual.gd b/player/npc/NpcVisual.gd index 8688b10..0011bdd 100644 --- a/player/npc/NpcVisual.gd +++ b/player/npc/NpcVisual.gd @@ -13,9 +13,16 @@ const DEBUG_LOGS := true @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var body_mesh: MeshInstance3D = $MeshInstance3D +@onready var head_mesh: MeshInstance3D = $Head +@onready var hair_mesh: MeshInstance3D = $Hair +@onready var left_arm: MeshInstance3D = $ArmLeft +@onready var right_arm: MeshInstance3D = $ArmRight +@onready var left_leg: MeshInstance3D = $LegLeft +@onready var right_leg: MeshInstance3D = $LegRight @onready var profession_prop: MeshInstance3D = $ProfessionProp @onready var profession_label: Label3D = $ProfessionLabel @onready var carried_food_visual: MeshInstance3D = $CarriedFood +@onready var carried_wood_visual: Node3D = $CarriedWood @onready var task_glyph_root: Node3D = $TaskGlyphRoot @onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph @@ -35,6 +42,8 @@ var path_pending := false var is_dead_visual := false var dead_material := StandardMaterial3D.new() var debug_overlay_visible := true +var walk_phase := 0.0 +var glyph_phase := 0.0 func debug_log(message: String) -> void: @@ -46,16 +55,62 @@ func _ready() -> void: dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0) +func _process(delta: float) -> void: + if is_dead_visual: + return + glyph_phase = fmod(glyph_phase + delta * 2.2, TAU) + if task_glyph_root.visible: + task_glyph_root.position.y = 2.35 + sin(glyph_phase) * 0.045 + task_glyph_root.rotation.y = glyph_phase * 0.35 + + var horizontal_speed := Vector2(velocity.x, velocity.z).length() + var blend := minf(delta * 9.0, 1.0) + if horizontal_speed > 0.1: + walk_phase = fmod(walk_phase + delta * horizontal_speed * 3.8, TAU) + var swing := sin(walk_phase) + left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, swing * 28.0, blend) + right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, -swing * 28.0, blend) + left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, -swing * 20.0, blend) + right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, swing * 20.0, blend) + var step_bob := absf(sin(walk_phase * 2.0)) * 0.035 + body_mesh.position.y = lerpf(body_mesh.position.y, 1.05 + step_bob, blend) + head_mesh.position.y = lerpf(head_mesh.position.y, 1.68 + step_bob, blend) + hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9 + step_bob, blend) + else: + left_leg.rotation_degrees.x = lerpf(left_leg.rotation_degrees.x, 0.0, blend) + right_leg.rotation_degrees.x = lerpf(right_leg.rotation_degrees.x, 0.0, blend) + left_arm.rotation_degrees.x = lerpf(left_arm.rotation_degrees.x, 0.0, blend) + right_arm.rotation_degrees.x = lerpf(right_arm.rotation_degrees.x, 0.0, blend) + body_mesh.position.y = lerpf(body_mesh.position.y, 1.05, blend) + head_mesh.position.y = lerpf(head_mesh.position.y, 1.68, blend) + hair_mesh.position.y = lerpf(hair_mesh.position.y, 1.9, blend) + + func setup_from_sim(npc: SimNPC) -> void: sim_id = npc.id npc_name = npc.npc_name profession = npc.profession name = "NPC_%s_%s" % [sim_id, npc_name] + _apply_personal_details() _apply_profession_presentation() set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0) + set_carried_wood_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) > 0.0) set_task_presentation(npc.current_task, npc.task_state) +func _apply_personal_details() -> void: + var hair_colors := [ + Color(0.16, 0.09, 0.055, 1.0), + Color(0.29, 0.16, 0.075, 1.0), + Color(0.09, 0.065, 0.05, 1.0), + Color(0.42, 0.27, 0.12, 1.0), + ] + var hair_material := StandardMaterial3D.new() + hair_material.albedo_color = hair_colors[maxi(sim_id, 0) % hair_colors.size()] + hair_material.roughness = 0.9 + hair_mesh.material_override = hair_material + + func _apply_profession_presentation() -> void: var definition := SimulationDefinitions.get_profession(profession) if definition == null: @@ -67,6 +122,8 @@ func _apply_profession_presentation() -> void: body_material.albedo_color = definition.visual_color body_material.roughness = 0.82 body_mesh.material_override = body_material + left_arm.material_override = body_material + right_arm.material_override = body_material var prop_material := StandardMaterial3D.new() prop_material.albedo_color = definition.prop_color @@ -117,6 +174,10 @@ func set_carried_food_visible(is_visible: bool) -> void: carried_food_visual.visible = is_visible and not is_dead_visual +func set_carried_wood_visible(is_visible: bool) -> void: + carried_wood_visual.visible = is_visible and not is_dead_visual + + func set_task_presentation(action_id: StringName, task_state: StringName) -> void: if task_glyph_root == null or task_glyph == null: return @@ -264,6 +325,7 @@ func _report_navigation_failure_once() -> void: func apply_dead_visual_state() -> void: is_dead_visual = true carried_food_visual.visible = false + carried_wood_visual.visible = false profession_prop.visible = false profession_label.visible = false task_glyph_root.visible = false @@ -278,9 +340,8 @@ func apply_dead_visual_state() -> void: scale = Vector3(0.8, 0.25, 1.2) rotation_degrees.x = 90.0 - for child in get_children(): - if child is MeshInstance3D: - child.material_override = dead_material + for child in find_children("*", "MeshInstance3D", true, false): + (child as MeshInstance3D).material_override = dead_material collision_layer = 0 collision_mask = 0 @@ -305,7 +366,7 @@ func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh: food.radius = 0.18 food.height = 0.26 return food - SimulationIds.ACTION_GATHER_WOOD: + SimulationIds.ACTION_GATHER_WOOD, SimulationIds.ACTION_DEPOSIT_WOOD: var wood := BoxMesh.new() wood.size = Vector3(0.18, 0.42, 0.18) task_glyph.rotation_degrees = Vector3(0, 0, -18) @@ -325,7 +386,7 @@ func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh: var book := BoxMesh.new() book.size = Vector3(0.36, 0.08, 0.24) return book - SimulationIds.ACTION_REST: + SimulationIds.ACTION_REST, SimulationIds.ACTION_SLEEP: var rest := SphereMesh.new() rest.radius = 0.2 rest.height = 0.12 @@ -337,7 +398,7 @@ func _create_task_glyph_material(action_id: StringName) -> StandardMaterial3D: var material := StandardMaterial3D.new() material.roughness = 0.65 material.emission_enabled = true - material.emission_energy_multiplier = 0.35 + material.emission_energy_multiplier = 0.12 var color := _get_task_glyph_color(action_id) material.albedo_color = color material.emission = color @@ -348,7 +409,7 @@ func _get_task_glyph_color(action_id: StringName) -> Color: match action_id: SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD: return Color(0.95, 0.42, 0.25, 1.0) - SimulationIds.ACTION_GATHER_WOOD: + SimulationIds.ACTION_GATHER_WOOD, SimulationIds.ACTION_DEPOSIT_WOOD: return Color(0.58, 0.34, 0.16, 1.0) SimulationIds.ACTION_DEPOSIT_FOOD: return Color(0.95, 0.72, 0.32, 1.0) @@ -356,6 +417,6 @@ func _get_task_glyph_color(action_id: StringName) -> Color: return Color(0.36, 0.58, 0.95, 1.0) SimulationIds.ACTION_STUDY: return Color(0.62, 0.46, 0.9, 1.0) - SimulationIds.ACTION_REST: + SimulationIds.ACTION_REST, SimulationIds.ACTION_SLEEP: return Color(0.4, 0.78, 0.72, 1.0) return Color(0.9, 0.9, 0.85, 1.0) diff --git a/player/npc/NpcVisual.tscn b/player/npc/NpcVisual.tscn index 577bea2..dcb7530 100644 --- a/player/npc/NpcVisual.tscn +++ b/player/npc/NpcVisual.tscn @@ -1,16 +1,69 @@ -[gd_scene load_steps=8 format=3 uid="uid://dhxxyprqflotq"] +[gd_scene load_steps=18 format=3 uid="uid://dhxxyprqflotq"] [ext_resource type="Script" uid="uid://bha8uf40p3sa0" path="res://player/npc/NpcVisual.gd" id="1_ixfoq"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_d844k"] -[sub_resource type="CapsuleMesh" id="CapsuleMesh_d844k"] - [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_d844k"] -albedo_color = Color(1, 0.22352941, 1, 1) +albedo_color = Color(0.46, 0.58, 0.33, 1) +roughness = 0.82 -[sub_resource type="BoxMesh" id="BoxMesh_d844k"] +[sub_resource type="CylinderMesh" id="CylinderMesh_body"] material = SubResource("StandardMaterial3D_d844k") +top_radius = 0.28 +bottom_radius = 0.42 +height = 0.9 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_skin"] +albedo_color = Color(0.84, 0.61, 0.39, 1) +roughness = 0.88 + +[sub_resource type="SphereMesh" id="SphereMesh_head"] +material = SubResource("StandardMaterial3D_skin") +radius = 0.29 +height = 0.56 + +[sub_resource type="CylinderMesh" id="CylinderMesh_hair"] +top_radius = 0.24 +bottom_radius = 0.29 +height = 0.18 + +[sub_resource type="CylinderMesh" id="CylinderMesh_arm"] +material = SubResource("StandardMaterial3D_d844k") +top_radius = 0.095 +bottom_radius = 0.11 +height = 0.58 + +[sub_resource type="SphereMesh" id="SphereMesh_hand"] +material = SubResource("StandardMaterial3D_skin") +radius = 0.12 +height = 0.22 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pants"] +albedo_color = Color(0.16, 0.22, 0.25, 1) +roughness = 0.9 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_leg"] +material = SubResource("StandardMaterial3D_pants") +radius = 0.115 +height = 0.62 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_boot"] +albedo_color = Color(0.13, 0.075, 0.04, 1) +roughness = 0.95 + +[sub_resource type="BoxMesh" id="BoxMesh_foot"] +material = SubResource("StandardMaterial3D_boot") +size = Vector3(0.23, 0.15, 0.36) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_eye"] +albedo_color = Color(0.06, 0.045, 0.035, 1) +roughness = 0.8 + +[sub_resource type="SphereMesh" id="SphereMesh_eye"] +material = SubResource("StandardMaterial3D_eye") +radius = 0.035 +height = 0.06 [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_food"] albedo_color = Color(0.38, 0.22, 0.08, 1) @@ -21,6 +74,16 @@ material = SubResource("StandardMaterial3D_food") radius = 0.28 height = 0.48 +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wood"] +albedo_color = Color(0.34, 0.17, 0.065, 1) +roughness = 0.96 + +[sub_resource type="CylinderMesh" id="CylinderMesh_wood"] +material = SubResource("StandardMaterial3D_wood") +top_radius = 0.09 +bottom_radius = 0.11 +height = 0.62 + [node name="NpcVisual" type="CharacterBody3D"] collision_layer = 2 collision_mask = 1 @@ -31,12 +94,58 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) shape = SubResource("CapsuleShape3D_d844k") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) -mesh = SubResource("CapsuleMesh_d844k") +position = Vector3(0, 1.05, 0) +mesh = SubResource("CylinderMesh_body") -[node name="FaceMarker" type="MeshInstance3D" parent="."] -transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.35, 0, 1.2, 0.59052056) -mesh = SubResource("BoxMesh_d844k") +[node name="Head" type="MeshInstance3D" parent="."] +position = Vector3(0, 1.68, 0) +mesh = SubResource("SphereMesh_head") + +[node name="Hair" type="MeshInstance3D" parent="."] +position = Vector3(0, 1.9, -0.015) +mesh = SubResource("CylinderMesh_hair") + +[node name="EyeLeft" type="MeshInstance3D" parent="."] +position = Vector3(-0.095, 1.72, 0.274) +mesh = SubResource("SphereMesh_eye") + +[node name="EyeRight" type="MeshInstance3D" parent="."] +position = Vector3(0.095, 1.72, 0.274) +mesh = SubResource("SphereMesh_eye") + +[node name="ArmLeft" type="MeshInstance3D" parent="."] +position = Vector3(-0.39, 1.08, 0) +rotation = Vector3(0, 0, -0.139626) +mesh = SubResource("CylinderMesh_arm") + +[node name="Hand" type="MeshInstance3D" parent="ArmLeft"] +position = Vector3(0, -0.35, 0) +mesh = SubResource("SphereMesh_hand") + +[node name="ArmRight" type="MeshInstance3D" parent="."] +position = Vector3(0.39, 1.08, 0) +rotation = Vector3(0, 0, 0.139626) +mesh = SubResource("CylinderMesh_arm") + +[node name="Hand" type="MeshInstance3D" parent="ArmRight"] +position = Vector3(0, -0.35, 0) +mesh = SubResource("SphereMesh_hand") + +[node name="LegLeft" type="MeshInstance3D" parent="."] +position = Vector3(-0.18, 0.43, 0) +mesh = SubResource("CapsuleMesh_leg") + +[node name="Foot" type="MeshInstance3D" parent="LegLeft"] +position = Vector3(0, -0.28, 0.09) +mesh = SubResource("BoxMesh_foot") + +[node name="LegRight" type="MeshInstance3D" parent="."] +position = Vector3(0.18, 0.43, 0) +mesh = SubResource("CapsuleMesh_leg") + +[node name="Foot" type="MeshInstance3D" parent="LegRight"] +position = Vector3(0, -0.28, 0.09) +mesh = SubResource("BoxMesh_foot") [node name="ProfessionProp" type="MeshInstance3D" parent="."] @@ -55,9 +164,22 @@ visible = false transform = Transform3D(0.75, 0, 0, 0, 0.75, 0, 0, 0, 0.75, 0.48, 0.7, 0) mesh = SubResource("SphereMesh_food") +[node name="CarriedWood" type="Node3D" parent="."] +visible = false +position = Vector3(-0.48, 0.88, -0.04) +rotation = Vector3(0, 0, 1.5708) + +[node name="LogA" type="MeshInstance3D" parent="CarriedWood"] +position = Vector3(0, -0.11, -0.08) +mesh = SubResource("CylinderMesh_wood") + +[node name="LogB" type="MeshInstance3D" parent="CarriedWood"] +position = Vector3(0, 0.11, 0.08) +mesh = SubResource("CylinderMesh_wood") + [node name="TaskGlyphRoot" type="Node3D" parent="."] visible = false -position = Vector3(0, 2.25, 0) +position = Vector3(0, 2.35, 0) [node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"] diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index 19ed620..1138c61 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -78,6 +78,10 @@ func _run() -> void: waterfall_mist.draw_pass_1 != null, "Waterfall mist particles should have visible billboard geometry" ) + _check( + waterfall_mist.position.distance_to(Vector3(25.0, 0.8, -24.0)) < 0.01, + "Waterfall mist should stay at the waterfall base instead of obscuring the village" + ) _check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface") var environment: Environment = ( (world.get_node("WorldEnvironment") as WorldEnvironment).environment @@ -97,16 +101,25 @@ func _run() -> void: "VillageRoot should include the mill and bridge blockouts" ) var smoke_count := 0 + var smoke_is_translucent := true for suffix in ["House_01", "House_02", "House_03"]: if world.has_node("VillageRoot/%s/Smoke" % suffix): var smoke: GPUParticles3D = world.get_node("VillageRoot/%s/Smoke" % suffix) if smoke.draw_pass_1 == null: continue + var smoke_material := (smoke.draw_pass_1 as QuadMesh).material as StandardMaterial3D + smoke_is_translucent = ( + smoke_is_translucent + and smoke_material != null + and smoke_material.transparency != BaseMaterial3D.TRANSPARENCY_DISABLED + and smoke_material.albedo_color.a < 0.5 + ) smoke_count += 1 _check( smoke_count >= 2, "At least two houses should have chimney smoke particles (found %s)" % smoke_count ) + _check(smoke_is_translucent, "Chimney smoke should not render as opaque white quads") var lookdev: Node3D = load("res://world/jajce/JajceLookdev.tscn").instantiate() _check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld") _check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera") diff --git a/tests/npc_visual_lifecycle_test.gd b/tests/npc_visual_lifecycle_test.gd index c0a0e6a..2d6da4a 100644 --- a/tests/npc_visual_lifecycle_test.gd +++ b/tests/npc_visual_lifecycle_test.gd @@ -27,6 +27,15 @@ func _run() -> void: body.material_override.albedo_color == profession_definition.visual_color, "NPC body color should come from its profession definition" ) + _check( + ( + body.mesh is CylinderMesh + and visual.has_node("Head") + and visual.has_node("ArmLeft") + and visual.has_node("LegRight/Foot") + ), + "NPC presentation should use a readable multi-part villager silhouette" + ) _check( visual.get_node("ProfessionProp").mesh != null and profession_definition.display_name in visual.get_node("ProfessionLabel").text, @@ -74,6 +83,14 @@ func _run() -> void: npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 0.0) _check(not carried_food.visible, "Deposited or eaten food should be hidden") + var carried_wood: Node3D = visual.get_node("CarriedWood") + _check(not carried_wood.visible, "NPC should begin without carried logs") + npc.add_inventory(SimulationIds.RESOURCE_WOOD, 2.0) + manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_WOOD, 2.0) + _check(carried_wood.visible, "Carried wood should show a visible log bundle") + npc.remove_inventory(SimulationIds.RESOURCE_WOOD, 2.0) + manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_WOOD, 0.0) + _check(not carried_wood.visible, "Deposited wood should hide the log bundle") var resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01") npc.set_task(SimulationIds.ACTION_GATHER_FOOD) diff --git a/tools/bake_jajce_navigation.gd.uid b/tools/bake_jajce_navigation.gd.uid new file mode 100644 index 0000000..bb74e5c --- /dev/null +++ b/tools/bake_jajce_navigation.gd.uid @@ -0,0 +1 @@ +uid://bts3v8ybsqche diff --git a/tools/capture_jajce_lookdev.gd.uid b/tools/capture_jajce_lookdev.gd.uid new file mode 100644 index 0000000..bd39a60 --- /dev/null +++ b/tools/capture_jajce_lookdev.gd.uid @@ -0,0 +1 @@ +uid://cr22fjs3577fa diff --git a/tools/capture_simulation_garden.gd.uid b/tools/capture_simulation_garden.gd.uid new file mode 100644 index 0000000..0ead870 --- /dev/null +++ b/tools/capture_simulation_garden.gd.uid @@ -0,0 +1 @@ +uid://dcquhx5tin7a diff --git a/world/jajce/ChimneySmoke.tscn b/world/jajce/ChimneySmoke.tscn index 192496c..960734b 100644 --- a/world/jajce/ChimneySmoke.tscn +++ b/world/jajce/ChimneySmoke.tscn @@ -11,19 +11,21 @@ scale_max = 0.6 color = Color(0.8, 0.8, 0.8, 0.6) [sub_resource type="StandardMaterial3D" id="4"] -albedo_color = Color(1, 1, 1, 1) +albedo_color = Color(0.5, 0.52, 0.5, 0.24) transparency = 1 shading_mode = 0 +vertex_color_use_as_albedo = true +vertex_color_is_srgb = true billboard_mode = 1 cull_mode = 0 [sub_resource type="QuadMesh" id="5"] -size = Vector2(1, 1) +size = Vector2(0.8, 0.8) material = SubResource("4") [node name="ChimneySmoke" type="GPUParticles3D"] emitting = true -amount = 16 +amount = 12 lifetime = 3.0 one_shot = false local_coords = true diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index 8a1a6ee..f8fbfb3 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -610,10 +610,12 @@ directional_shadow_max_distance = 180.0 [node name="WindStrokes_Valley" parent="." instance=ExtResource("19_windstrokes")] [node name="WindStrokes_Ridge" parent="." instance=ExtResource("19_windstrokes")] +position = Vector3(-25, 7, 9) [node name="WindSpecks_Valley" parent="." instance=ExtResource("20_windspecks")] [node name="WindSpecks_Ridge" parent="." instance=ExtResource("20_windspecks")] +position = Vector3(-25, 7, 9) [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_greybox") diff --git a/world/jajce/WaterfallMist.tscn b/world/jajce/WaterfallMist.tscn index 970d906..7d290c9 100644 --- a/world/jajce/WaterfallMist.tscn +++ b/world/jajce/WaterfallMist.tscn @@ -24,6 +24,7 @@ material = SubResource("Material_mist") size = Vector2(1.8, 1.8) [node name="WaterfallMist" type="GPUParticles3D"] +position = Vector3(25, 0.8, -24) emitting = true amount = 72 lifetime = 3.0 diff --git a/world/jajce/WindStrokes.tscn b/world/jajce/WindStrokes.tscn index 84ba6c7..31c70f3 100644 --- a/world/jajce/WindStrokes.tscn +++ b/world/jajce/WindStrokes.tscn @@ -7,7 +7,7 @@ shader = ExtResource("1_shader") [sub_resource type="QuadMesh" id="Mesh_stroke"] material = SubResource("ShaderMaterial_stroke") -size = Vector2(2.4, 0.14) +size = Vector2(1.5, 0.07) [sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_wind"] lifetime_randomness = 0.6 @@ -30,7 +30,7 @@ emission_box_extents = Vector3(28.0, 8.0, 28.0) [node name="WindStrokes" type="GPUParticles3D"] emitting = true -amount = 24 +amount = 10 lifetime = 5.0 one_shot = false preprocess = 3.0 diff --git a/world/jajce/materials/water.gdshader b/world/jajce/materials/water.gdshader index 4f85114..3d91248 100644 --- a/world/jajce/materials/water.gdshader +++ b/world/jajce/materials/water.gdshader @@ -10,7 +10,7 @@ uniform vec3 foam_color : source_color = vec3(0.82, 0.88, 0.86); uniform float rim_power : hint_range(0.5, 5.0) = 2.5; uniform float sparkle_strength : hint_range(0.0, 1.0) = 0.12; -global uniform vec3 sun_direction = vec3(0.4, 0.8, 0.4); +uniform vec3 sun_direction = vec3(0.4, 0.8, 0.4); void vertex() { vec3 pos = VERTEX; diff --git a/world/jajce/materials/wind_stroke.gdshader b/world/jajce/materials/wind_stroke.gdshader index 93df93b..ed7e085 100644 --- a/world/jajce/materials/wind_stroke.gdshader +++ b/world/jajce/materials/wind_stroke.gdshader @@ -1,5 +1,5 @@ shader_type spatial; -render_mode blend_mix, depth_draw_opaque, cull_disabled, unshaded; +render_mode blend_mix, depth_draw_never, cull_disabled, unshaded; uniform float softness : hint_range(0.0, 1.0) = 0.45; uniform vec3 tint : source_color = vec3(0.95, 0.93, 0.88); @@ -8,7 +8,7 @@ void fragment() { float edge_dist = abs(UV.y - 0.5) * 2.0; float alpha = 1.0 - smoothstep(0.0, 1.0, edge_dist); alpha *= smoothstep(0.0, 0.2, UV.x) * smoothstep(0.0, 0.2, 1.0 - UV.x); - alpha *= softness * 0.35; + alpha *= softness * 0.1; ALBEDO = tint; ALPHA = alpha; }