feat: add cinematic npc task glyphs
This commit is contained in:
@@ -635,12 +635,13 @@ Completed:
|
||||
11. `Simulation Garden 01` capture and review: a reproducible runtime capture
|
||||
tool writes debug and cinematic `main.tscn` baselines to `docs/baselines/`
|
||||
and verifies live NPC visuals in the terrain-backed village.
|
||||
12. Runtime task glyphs: active NPCs now keep compact task-intent markers in
|
||||
cinematic mode while debug names, labels, and inspector UI remain optional.
|
||||
|
||||
Next:
|
||||
|
||||
1. Improve the runtime first-read: camera staging, readable path/landmark
|
||||
composition, and diegetic task communication without adding new simulation
|
||||
mechanics.
|
||||
1. Improve camera staging plus readable path and landmark composition without
|
||||
adding new simulation mechanics.
|
||||
|
||||
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
|
||||
|
||||
@@ -683,6 +683,8 @@ Recently completed:
|
||||
- `Simulation Garden 01` is captured and reviewed from `main.tscn` with paired
|
||||
debug/cinematic baselines that verify live NPC visuals in the Terrain3D-backed
|
||||
village.
|
||||
- Runtime task glyphs preserve active NPC task intent in cinematic mode while
|
||||
leaving the simulation task state authoritative.
|
||||
|
||||
This order strengthens the simulation while regularly producing visible
|
||||
progress suitable for public development updates.
|
||||
|
||||
@@ -361,8 +361,10 @@ A small village panel displays:
|
||||
- a Tab-cycled NPC inspector with profession, needs, task state, destination,
|
||||
decision reason, and utility scores.
|
||||
|
||||
NPC name/profession labels, definition-driven colors and props, and the
|
||||
carried-food visual make active simulation state readable in the world.
|
||||
NPC name/profession labels, definition-driven colors and props, carried-food
|
||||
visuals, and compact task glyphs make active simulation state readable in the
|
||||
world. The F10 cinematic mode hides debug labels while preserving the task
|
||||
glyphs.
|
||||
|
||||
## Runtime architecture
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ The milestone is valid as a runtime integration checkpoint: the same Jajce
|
||||
Terrain3D world is running in `main.tscn`, NPCs are alive in the terrain-backed
|
||||
village, the selected-NPC reason inspector is fed by real task scoring, and the
|
||||
cinematic/debug toggle produces a shareable view without changing simulation
|
||||
state.
|
||||
state. A follow-up pass adds small task glyphs above active NPCs, so the
|
||||
cinematic view now preserves task intent after the debug labels are hidden.
|
||||
|
||||
The frame also makes the next presentation risks plain:
|
||||
|
||||
@@ -37,8 +38,9 @@ The frame also makes the next presentation risks plain:
|
||||
prototype-grade;
|
||||
- terrain texture variation reads better than the old flat map, but paths,
|
||||
landmarks, water, and foreground silhouettes need stronger first-read staging;
|
||||
- labels explain the system well, while the cinematic view still needs more
|
||||
diegetic task communication.
|
||||
- task glyphs help, but the cinematic view still needs clearer work-site props
|
||||
and route composition so intent is readable before the viewer learns the icon
|
||||
language.
|
||||
|
||||
Use this baseline as the first runtime comparison image before adding more
|
||||
systems or expanding the terrain.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 505 KiB After Width: | Height: | Size: 506 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 530 KiB After Width: | Height: | Size: 530 KiB |
@@ -16,6 +16,8 @@ const DEBUG_LOGS := true
|
||||
@onready var profession_prop: MeshInstance3D = $ProfessionProp
|
||||
@onready var profession_label: Label3D = $ProfessionLabel
|
||||
@onready var carried_food_visual: MeshInstance3D = $CarriedFood
|
||||
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
|
||||
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
||||
|
||||
var has_reported_arrival := false
|
||||
var sim_id: int = -1
|
||||
@@ -51,6 +53,7 @@ func setup_from_sim(npc: SimNPC) -> void:
|
||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||
_apply_profession_presentation()
|
||||
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
|
||||
set_task_presentation(npc.current_task, npc.task_state)
|
||||
|
||||
|
||||
func _apply_profession_presentation() -> void:
|
||||
@@ -114,6 +117,18 @@ func set_carried_food_visible(is_visible: bool) -> void:
|
||||
carried_food_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
|
||||
if is_dead_visual or not _should_show_task_glyph(action_id, task_state):
|
||||
task_glyph_root.visible = false
|
||||
return
|
||||
|
||||
task_glyph.mesh = _create_task_glyph_mesh(action_id)
|
||||
task_glyph.material_override = _create_task_glyph_material(action_id)
|
||||
task_glyph_root.visible = task_glyph.mesh != null
|
||||
|
||||
|
||||
func set_debug_overlay_visible(is_visible: bool) -> void:
|
||||
debug_overlay_visible = is_visible
|
||||
if profession_label != null:
|
||||
@@ -251,6 +266,7 @@ func apply_dead_visual_state() -> void:
|
||||
carried_food_visual.visible = false
|
||||
profession_prop.visible = false
|
||||
profession_label.visible = false
|
||||
task_glyph_root.visible = false
|
||||
path_request_id += 1
|
||||
velocity = Vector3.ZERO
|
||||
current_path = PackedVector3Array()
|
||||
@@ -269,3 +285,77 @@ func apply_dead_visual_state() -> void:
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
debug_log("Applied dead visual state")
|
||||
|
||||
|
||||
func _should_show_task_glyph(action_id: StringName, task_state: StringName) -> bool:
|
||||
if task_state not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]:
|
||||
return false
|
||||
return action_id not in [
|
||||
SimulationIds.ACTION_IDLE,
|
||||
SimulationIds.ACTION_DEAD,
|
||||
SimulationIds.ACTION_WANDER,
|
||||
]
|
||||
|
||||
|
||||
func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh:
|
||||
task_glyph.rotation_degrees = Vector3.ZERO
|
||||
match action_id:
|
||||
SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD:
|
||||
var food := SphereMesh.new()
|
||||
food.radius = 0.18
|
||||
food.height = 0.26
|
||||
return food
|
||||
SimulationIds.ACTION_GATHER_WOOD:
|
||||
var wood := BoxMesh.new()
|
||||
wood.size = Vector3(0.18, 0.42, 0.18)
|
||||
task_glyph.rotation_degrees = Vector3(0, 0, -18)
|
||||
return wood
|
||||
SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||
var crate := BoxMesh.new()
|
||||
crate.size = Vector3(0.34, 0.22, 0.34)
|
||||
return crate
|
||||
SimulationIds.ACTION_PATROL:
|
||||
var shield := CylinderMesh.new()
|
||||
shield.top_radius = 0.2
|
||||
shield.bottom_radius = 0.2
|
||||
shield.height = 0.1
|
||||
task_glyph.rotation_degrees = Vector3(90, 0, 0)
|
||||
return shield
|
||||
SimulationIds.ACTION_STUDY:
|
||||
var book := BoxMesh.new()
|
||||
book.size = Vector3(0.36, 0.08, 0.24)
|
||||
return book
|
||||
SimulationIds.ACTION_REST:
|
||||
var rest := SphereMesh.new()
|
||||
rest.radius = 0.2
|
||||
rest.height = 0.12
|
||||
return rest
|
||||
return null
|
||||
|
||||
|
||||
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
|
||||
var color := _get_task_glyph_color(action_id)
|
||||
material.albedo_color = color
|
||||
material.emission = color
|
||||
return material
|
||||
|
||||
|
||||
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:
|
||||
return Color(0.58, 0.34, 0.16, 1.0)
|
||||
SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||
return Color(0.95, 0.72, 0.32, 1.0)
|
||||
SimulationIds.ACTION_PATROL:
|
||||
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:
|
||||
return Color(0.4, 0.78, 0.72, 1.0)
|
||||
return Color(0.9, 0.9, 0.85, 1.0)
|
||||
|
||||
@@ -55,6 +55,12 @@ 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="TaskGlyphRoot" type="Node3D" parent="."]
|
||||
visible = false
|
||||
position = Vector3(0, 2.25, 0)
|
||||
|
||||
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
|
||||
|
||||
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||
path_desired_distance = 0.3
|
||||
target_desired_distance = 0.5
|
||||
|
||||
@@ -217,6 +217,7 @@ func simulate_tick() -> void:
|
||||
npc.current_task = SimulationIds.ACTION_IDLE
|
||||
npc.has_travel_target = false
|
||||
npc.target_id = &""
|
||||
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||
|
||||
if debug_logs:
|
||||
print(
|
||||
|
||||
@@ -32,6 +32,24 @@ func _run() -> void:
|
||||
and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
|
||||
"NPC visual should expose a profession prop and readable label"
|
||||
)
|
||||
visual.set_task_presentation(SimulationIds.ACTION_GATHER_FOOD, SimNPC.TASK_STATE_TRAVELING)
|
||||
_check(
|
||||
visual.get_node("TaskGlyphRoot").visible
|
||||
and visual.get_node("TaskGlyphRoot/TaskGlyph").mesh is SphereMesh,
|
||||
"NPC visual should expose a compact task glyph for cinematic readability"
|
||||
)
|
||||
visual.set_debug_overlay_visible(false)
|
||||
_check(
|
||||
not visual.get_node("ProfessionLabel").visible
|
||||
and visual.get_node("TaskGlyphRoot").visible,
|
||||
"Cinematic mode should hide debug labels while preserving task glyphs"
|
||||
)
|
||||
visual.set_debug_overlay_visible(true)
|
||||
visual.set_task_presentation(SimulationIds.ACTION_WANDER, SimNPC.TASK_STATE_TRAVELING)
|
||||
_check(
|
||||
not visual.get_node("TaskGlyphRoot").visible,
|
||||
"Ambient wander should not display a task glyph"
|
||||
)
|
||||
var test_decision := ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_GATHER_FOOD,
|
||||
-1.0,
|
||||
|
||||
@@ -36,6 +36,10 @@ func initialize_world_view() -> void:
|
||||
simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed)
|
||||
else:
|
||||
push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal")
|
||||
if simulation_manager.has_signal("npc_task_changed"):
|
||||
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
|
||||
else:
|
||||
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
|
||||
if simulation_manager.has_signal("state_restored"):
|
||||
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
||||
else:
|
||||
@@ -168,6 +172,16 @@ func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float)
|
||||
push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method")
|
||||
|
||||
|
||||
func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void:
|
||||
var visual = active_npc_visuals.get(npc.id)
|
||||
if visual == null:
|
||||
return
|
||||
if visual.has_method("set_task_presentation"):
|
||||
visual.set_task_presentation(npc.current_task, npc.task_state)
|
||||
else:
|
||||
push_error("WorldViewManager: NPCVisual has no set_task_presentation method")
|
||||
|
||||
|
||||
func _on_simulation_state_restored() -> void:
|
||||
for visual in active_npc_visuals.values():
|
||||
visual.queue_free()
|
||||
|
||||
Reference in New Issue
Block a user