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
|
11. `Simulation Garden 01` capture and review: a reproducible runtime capture
|
||||||
tool writes debug and cinematic `main.tscn` baselines to `docs/baselines/`
|
tool writes debug and cinematic `main.tscn` baselines to `docs/baselines/`
|
||||||
and verifies live NPC visuals in the terrain-backed village.
|
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:
|
Next:
|
||||||
|
|
||||||
1. Improve the runtime first-read: camera staging, readable path/landmark
|
1. Improve camera staging plus readable path and landmark composition without
|
||||||
composition, and diegetic task communication without adding new simulation
|
adding new simulation mechanics.
|
||||||
mechanics.
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -683,6 +683,8 @@ Recently completed:
|
|||||||
- `Simulation Garden 01` is captured and reviewed from `main.tscn` with paired
|
- `Simulation Garden 01` is captured and reviewed from `main.tscn` with paired
|
||||||
debug/cinematic baselines that verify live NPC visuals in the Terrain3D-backed
|
debug/cinematic baselines that verify live NPC visuals in the Terrain3D-backed
|
||||||
village.
|
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
|
This order strengthens the simulation while regularly producing visible
|
||||||
progress suitable for public development updates.
|
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,
|
- a Tab-cycled NPC inspector with profession, needs, task state, destination,
|
||||||
decision reason, and utility scores.
|
decision reason, and utility scores.
|
||||||
|
|
||||||
NPC name/profession labels, definition-driven colors and props, and the
|
NPC name/profession labels, definition-driven colors and props, carried-food
|
||||||
carried-food visual make active simulation state readable in the world.
|
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
|
## 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
|
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
|
village, the selected-NPC reason inspector is fed by real task scoring, and the
|
||||||
cinematic/debug toggle produces a shareable view without changing simulation
|
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:
|
The frame also makes the next presentation risks plain:
|
||||||
|
|
||||||
@@ -37,8 +38,9 @@ The frame also makes the next presentation risks plain:
|
|||||||
prototype-grade;
|
prototype-grade;
|
||||||
- terrain texture variation reads better than the old flat map, but paths,
|
- terrain texture variation reads better than the old flat map, but paths,
|
||||||
landmarks, water, and foreground silhouettes need stronger first-read staging;
|
landmarks, water, and foreground silhouettes need stronger first-read staging;
|
||||||
- labels explain the system well, while the cinematic view still needs more
|
- task glyphs help, but the cinematic view still needs clearer work-site props
|
||||||
diegetic task communication.
|
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
|
Use this baseline as the first runtime comparison image before adding more
|
||||||
systems or expanding the terrain.
|
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_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 task_glyph_root: Node3D = $TaskGlyphRoot
|
||||||
|
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
|
||||||
|
|
||||||
var has_reported_arrival := false
|
var has_reported_arrival := false
|
||||||
var sim_id: int = -1
|
var sim_id: int = -1
|
||||||
@@ -51,6 +53,7 @@ func setup_from_sim(npc: SimNPC) -> void:
|
|||||||
name = "NPC_%s_%s" % [sim_id, npc_name]
|
name = "NPC_%s_%s" % [sim_id, npc_name]
|
||||||
_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_task_presentation(npc.current_task, npc.task_state)
|
||||||
|
|
||||||
|
|
||||||
func _apply_profession_presentation() -> void:
|
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
|
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:
|
func set_debug_overlay_visible(is_visible: bool) -> void:
|
||||||
debug_overlay_visible = is_visible
|
debug_overlay_visible = is_visible
|
||||||
if profession_label != null:
|
if profession_label != null:
|
||||||
@@ -251,6 +266,7 @@ func apply_dead_visual_state() -> void:
|
|||||||
carried_food_visual.visible = false
|
carried_food_visual.visible = false
|
||||||
profession_prop.visible = false
|
profession_prop.visible = false
|
||||||
profession_label.visible = false
|
profession_label.visible = false
|
||||||
|
task_glyph_root.visible = false
|
||||||
path_request_id += 1
|
path_request_id += 1
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
current_path = PackedVector3Array()
|
current_path = PackedVector3Array()
|
||||||
@@ -269,3 +285,77 @@ func apply_dead_visual_state() -> void:
|
|||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
debug_log("Applied dead visual state")
|
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)
|
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="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="."]
|
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
|
||||||
path_desired_distance = 0.3
|
path_desired_distance = 0.3
|
||||||
target_desired_distance = 0.5
|
target_desired_distance = 0.5
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ func simulate_tick() -> void:
|
|||||||
npc.current_task = SimulationIds.ACTION_IDLE
|
npc.current_task = SimulationIds.ACTION_IDLE
|
||||||
npc.has_travel_target = false
|
npc.has_travel_target = false
|
||||||
npc.target_id = &""
|
npc.target_id = &""
|
||||||
|
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print(
|
print(
|
||||||
|
|||||||
@@ -32,6 +32,24 @@ func _run() -> void:
|
|||||||
and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
|
and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
|
||||||
"NPC visual should expose a profession prop and readable label"
|
"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(
|
var test_decision := ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_GATHER_FOOD,
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
-1.0,
|
-1.0,
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func initialize_world_view() -> void:
|
|||||||
simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed)
|
simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed)
|
||||||
else:
|
else:
|
||||||
push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal")
|
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"):
|
if simulation_manager.has_signal("state_restored"):
|
||||||
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
simulation_manager.state_restored.connect(_on_simulation_state_restored)
|
||||||
else:
|
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")
|
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:
|
func _on_simulation_state_restored() -> void:
|
||||||
for visual in active_npc_visuals.values():
|
for visual in active_npc_visuals.values():
|
||||||
visual.queue_free()
|
visual.queue_free()
|
||||||
|
|||||||
Reference in New Issue
Block a user