feat: add stylized villager presentation

This commit is contained in:
Rijad Zuzo
2026-07-09 23:47:46 +02:00
parent 8419116008
commit 39989002ca
20 changed files with 395 additions and 32 deletions
+15 -4
View File
@@ -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 decision branch, and utility scores. The existing carried-food prop and village
panel expose immediate consequences. `F10` toggles the debug/cinematic panel expose immediate consequences. `F10` toggles the debug/cinematic
presentation layer, while `F12` reloads the current scene as a repeatable 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 ### Exit condition
@@ -653,12 +656,20 @@ Completed:
and waterfall widened; the fortress ridge landmark now has crenellations, and waterfall widened; the fortress ridge landmark now has crenellations,
a dedicated banner pole with larger banner, and three turrets for a more a dedicated banner pole with larger banner, and three turrets for a more
readable silhouette against the sky. 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: Next:
1. Continue growing resource discovery with finite ResourceNode instances 1. Replace the remaining player capsule with a matching stylized presentation,
placed in meaningful contexts (from LEARNING_ROADMAP). See the roadmap then revisit runtime camera framing so the waterfall and village action
for the next systems priority. 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 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 mechanics. The next proof is a beautiful stage for the systems that already
+12 -1
View File
@@ -1,6 +1,6 @@
# Simulation Garden 01 # Simulation Garden 01
Captured: 2026-07-08 Captured: 2026-07-09
## Files ## 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 Use this baseline as the first runtime comparison image before adding more
systems or expanding the terrain. 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.
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 419 KiB

@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

After

Width:  |  Height:  |  Size: 428 KiB

@@ -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
+69 -8
View File
@@ -13,9 +13,16 @@ const DEBUG_LOGS := true
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
@onready var body_mesh: MeshInstance3D = $MeshInstance3D @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_prop: MeshInstance3D = $ProfessionProp
@onready var profession_label: Label3D = $ProfessionLabel @onready var profession_label: Label3D = $ProfessionLabel
@onready var carried_food_visual: MeshInstance3D = $CarriedFood @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_root: Node3D = $TaskGlyphRoot
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph @onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
@@ -35,6 +42,8 @@ var path_pending := false
var is_dead_visual := false var is_dead_visual := false
var dead_material := StandardMaterial3D.new() var dead_material := StandardMaterial3D.new()
var debug_overlay_visible := true var debug_overlay_visible := true
var walk_phase := 0.0
var glyph_phase := 0.0
func debug_log(message: String) -> void: 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) 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: func setup_from_sim(npc: SimNPC) -> void:
sim_id = npc.id sim_id = npc.id
npc_name = npc.npc_name npc_name = npc.npc_name
profession = npc.profession profession = npc.profession
name = "NPC_%s_%s" % [sim_id, npc_name] name = "NPC_%s_%s" % [sim_id, npc_name]
_apply_personal_details()
_apply_profession_presentation() _apply_profession_presentation()
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0) 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) 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: func _apply_profession_presentation() -> void:
var definition := SimulationDefinitions.get_profession(profession) var definition := SimulationDefinitions.get_profession(profession)
if definition == null: if definition == null:
@@ -67,6 +122,8 @@ func _apply_profession_presentation() -> void:
body_material.albedo_color = definition.visual_color body_material.albedo_color = definition.visual_color
body_material.roughness = 0.82 body_material.roughness = 0.82
body_mesh.material_override = body_material body_mesh.material_override = body_material
left_arm.material_override = body_material
right_arm.material_override = body_material
var prop_material := StandardMaterial3D.new() var prop_material := StandardMaterial3D.new()
prop_material.albedo_color = definition.prop_color 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 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: func set_task_presentation(action_id: StringName, task_state: StringName) -> void:
if task_glyph_root == null or task_glyph == null: if task_glyph_root == null or task_glyph == null:
return return
@@ -264,6 +325,7 @@ func _report_navigation_failure_once() -> void:
func apply_dead_visual_state() -> void: func apply_dead_visual_state() -> void:
is_dead_visual = true is_dead_visual = true
carried_food_visual.visible = false carried_food_visual.visible = false
carried_wood_visual.visible = false
profession_prop.visible = false profession_prop.visible = false
profession_label.visible = false profession_label.visible = false
task_glyph_root.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) scale = Vector3(0.8, 0.25, 1.2)
rotation_degrees.x = 90.0 rotation_degrees.x = 90.0
for child in get_children(): for child in find_children("*", "MeshInstance3D", true, false):
if child is MeshInstance3D: (child as MeshInstance3D).material_override = dead_material
child.material_override = dead_material
collision_layer = 0 collision_layer = 0
collision_mask = 0 collision_mask = 0
@@ -305,7 +366,7 @@ func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh:
food.radius = 0.18 food.radius = 0.18
food.height = 0.26 food.height = 0.26
return food return food
SimulationIds.ACTION_GATHER_WOOD: SimulationIds.ACTION_GATHER_WOOD, SimulationIds.ACTION_DEPOSIT_WOOD:
var wood := BoxMesh.new() var wood := BoxMesh.new()
wood.size = Vector3(0.18, 0.42, 0.18) wood.size = Vector3(0.18, 0.42, 0.18)
task_glyph.rotation_degrees = Vector3(0, 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() var book := BoxMesh.new()
book.size = Vector3(0.36, 0.08, 0.24) book.size = Vector3(0.36, 0.08, 0.24)
return book return book
SimulationIds.ACTION_REST: SimulationIds.ACTION_REST, SimulationIds.ACTION_SLEEP:
var rest := SphereMesh.new() var rest := SphereMesh.new()
rest.radius = 0.2 rest.radius = 0.2
rest.height = 0.12 rest.height = 0.12
@@ -337,7 +398,7 @@ func _create_task_glyph_material(action_id: StringName) -> StandardMaterial3D:
var material := StandardMaterial3D.new() var material := StandardMaterial3D.new()
material.roughness = 0.65 material.roughness = 0.65
material.emission_enabled = true material.emission_enabled = true
material.emission_energy_multiplier = 0.35 material.emission_energy_multiplier = 0.12
var color := _get_task_glyph_color(action_id) var color := _get_task_glyph_color(action_id)
material.albedo_color = color material.albedo_color = color
material.emission = color material.emission = color
@@ -348,7 +409,7 @@ func _get_task_glyph_color(action_id: StringName) -> Color:
match action_id: match action_id:
SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD: SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD:
return Color(0.95, 0.42, 0.25, 1.0) 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) return Color(0.58, 0.34, 0.16, 1.0)
SimulationIds.ACTION_DEPOSIT_FOOD: SimulationIds.ACTION_DEPOSIT_FOOD:
return Color(0.95, 0.72, 0.32, 1.0) 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) return Color(0.36, 0.58, 0.95, 1.0)
SimulationIds.ACTION_STUDY: SimulationIds.ACTION_STUDY:
return Color(0.62, 0.46, 0.9, 1.0) 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.4, 0.78, 0.72, 1.0)
return Color(0.9, 0.9, 0.85, 1.0) return Color(0.9, 0.9, 0.85, 1.0)
+133 -11
View File
@@ -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"] [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="CapsuleShape3D" id="CapsuleShape3D_d844k"]
[sub_resource type="CapsuleMesh" id="CapsuleMesh_d844k"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_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") 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"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_food"]
albedo_color = Color(0.38, 0.22, 0.08, 1) albedo_color = Color(0.38, 0.22, 0.08, 1)
@@ -21,6 +74,16 @@ material = SubResource("StandardMaterial3D_food")
radius = 0.28 radius = 0.28
height = 0.48 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"] [node name="NpcVisual" type="CharacterBody3D"]
collision_layer = 2 collision_layer = 2
collision_mask = 1 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") shape = SubResource("CapsuleShape3D_d844k")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."] [node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0) position = Vector3(0, 1.05, 0)
mesh = SubResource("CapsuleMesh_d844k") mesh = SubResource("CylinderMesh_body")
[node name="FaceMarker" type="MeshInstance3D" parent="."] [node name="Head" type="MeshInstance3D" parent="."]
transform = Transform3D(0.15, 0, 0, 0, 0.15, 0, 0, 0, 0.35, 0, 1.2, 0.59052056) position = Vector3(0, 1.68, 0)
mesh = SubResource("BoxMesh_d844k") 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="."] [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) transform = Transform3D(0.75, 0, 0, 0, 0.75, 0, 0, 0, 0.75, 0.48, 0.7, 0)
mesh = SubResource("SphereMesh_food") 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="."] [node name="TaskGlyphRoot" type="Node3D" parent="."]
visible = false visible = false
position = Vector3(0, 2.25, 0) position = Vector3(0, 2.35, 0)
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"] [node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
+13
View File
@@ -78,6 +78,10 @@ func _run() -> void:
waterfall_mist.draw_pass_1 != null, waterfall_mist.draw_pass_1 != null,
"Waterfall mist particles should have visible billboard geometry" "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") _check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface")
var environment: Environment = ( var environment: Environment = (
(world.get_node("WorldEnvironment") as WorldEnvironment).environment (world.get_node("WorldEnvironment") as WorldEnvironment).environment
@@ -97,16 +101,25 @@ func _run() -> void:
"VillageRoot should include the mill and bridge blockouts" "VillageRoot should include the mill and bridge blockouts"
) )
var smoke_count := 0 var smoke_count := 0
var smoke_is_translucent := true
for suffix in ["House_01", "House_02", "House_03"]: for suffix in ["House_01", "House_02", "House_03"]:
if world.has_node("VillageRoot/%s/Smoke" % suffix): if world.has_node("VillageRoot/%s/Smoke" % suffix):
var smoke: GPUParticles3D = world.get_node("VillageRoot/%s/Smoke" % suffix) var smoke: GPUParticles3D = world.get_node("VillageRoot/%s/Smoke" % suffix)
if smoke.draw_pass_1 == null: if smoke.draw_pass_1 == null:
continue 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 smoke_count += 1
_check( _check(
smoke_count >= 2, smoke_count >= 2,
"At least two houses should have chimney smoke particles (found %s)" % smoke_count "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() var lookdev: Node3D = load("res://world/jajce/JajceLookdev.tscn").instantiate()
_check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld") _check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld")
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera") _check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
+17
View File
@@ -27,6 +27,15 @@ func _run() -> void:
body.material_override.albedo_color == profession_definition.visual_color, body.material_override.albedo_color == profession_definition.visual_color,
"NPC body color should come from its profession definition" "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( _check(
visual.get_node("ProfessionProp").mesh != null visual.get_node("ProfessionProp").mesh != null
and profession_definition.display_name in visual.get_node("ProfessionLabel").text, 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) npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 0.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") _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") var resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01")
npc.set_task(SimulationIds.ACTION_GATHER_FOOD) npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
+1
View File
@@ -0,0 +1 @@
uid://bts3v8ybsqche
+1
View File
@@ -0,0 +1 @@
uid://cr22fjs3577fa
+1
View File
@@ -0,0 +1 @@
uid://dcquhx5tin7a
+5 -3
View File
@@ -11,19 +11,21 @@ scale_max = 0.6
color = Color(0.8, 0.8, 0.8, 0.6) color = Color(0.8, 0.8, 0.8, 0.6)
[sub_resource type="StandardMaterial3D" id="4"] [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 transparency = 1
shading_mode = 0 shading_mode = 0
vertex_color_use_as_albedo = true
vertex_color_is_srgb = true
billboard_mode = 1 billboard_mode = 1
cull_mode = 0 cull_mode = 0
[sub_resource type="QuadMesh" id="5"] [sub_resource type="QuadMesh" id="5"]
size = Vector2(1, 1) size = Vector2(0.8, 0.8)
material = SubResource("4") material = SubResource("4")
[node name="ChimneySmoke" type="GPUParticles3D"] [node name="ChimneySmoke" type="GPUParticles3D"]
emitting = true emitting = true
amount = 16 amount = 12
lifetime = 3.0 lifetime = 3.0
one_shot = false one_shot = false
local_coords = true local_coords = true
+2
View File
@@ -610,10 +610,12 @@ directional_shadow_max_distance = 180.0
[node name="WindStrokes_Valley" parent="." instance=ExtResource("19_windstrokes")] [node name="WindStrokes_Valley" parent="." instance=ExtResource("19_windstrokes")]
[node name="WindStrokes_Ridge" 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_Valley" parent="." instance=ExtResource("20_windspecks")]
[node name="WindSpecks_Ridge" 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="."] [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_greybox") environment = SubResource("Environment_greybox")
+1
View File
@@ -24,6 +24,7 @@ material = SubResource("Material_mist")
size = Vector2(1.8, 1.8) size = Vector2(1.8, 1.8)
[node name="WaterfallMist" type="GPUParticles3D"] [node name="WaterfallMist" type="GPUParticles3D"]
position = Vector3(25, 0.8, -24)
emitting = true emitting = true
amount = 72 amount = 72
lifetime = 3.0 lifetime = 3.0
+2 -2
View File
@@ -7,7 +7,7 @@ shader = ExtResource("1_shader")
[sub_resource type="QuadMesh" id="Mesh_stroke"] [sub_resource type="QuadMesh" id="Mesh_stroke"]
material = SubResource("ShaderMaterial_stroke") material = SubResource("ShaderMaterial_stroke")
size = Vector2(2.4, 0.14) size = Vector2(1.5, 0.07)
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_wind"] [sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_wind"]
lifetime_randomness = 0.6 lifetime_randomness = 0.6
@@ -30,7 +30,7 @@ emission_box_extents = Vector3(28.0, 8.0, 28.0)
[node name="WindStrokes" type="GPUParticles3D"] [node name="WindStrokes" type="GPUParticles3D"]
emitting = true emitting = true
amount = 24 amount = 10
lifetime = 5.0 lifetime = 5.0
one_shot = false one_shot = false
preprocess = 3.0 preprocess = 3.0
+1 -1
View File
@@ -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 rim_power : hint_range(0.5, 5.0) = 2.5;
uniform float sparkle_strength : hint_range(0.0, 1.0) = 0.12; 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() { void vertex() {
vec3 pos = VERTEX; vec3 pos = VERTEX;
+2 -2
View File
@@ -1,5 +1,5 @@
shader_type spatial; 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 float softness : hint_range(0.0, 1.0) = 0.45;
uniform vec3 tint : source_color = vec3(0.95, 0.93, 0.88); 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 edge_dist = abs(UV.y - 0.5) * 2.0;
float alpha = 1.0 - smoothstep(0.0, 1.0, edge_dist); 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 *= 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; ALBEDO = tint;
ALPHA = alpha; ALPHA = alpha;
} }