148 lines
5.1 KiB
GDScript
148 lines
5.1 KiB
GDScript
extends SceneTree
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
var main_scene: Node = load("res://main.tscn").instantiate()
|
|
root.add_child(main_scene)
|
|
await process_frame
|
|
await physics_frame
|
|
await physics_frame
|
|
|
|
var manager: Node = main_scene.get_node("SimulationManager")
|
|
var view: Node = main_scene.get_node("WorldViewManager")
|
|
var ui: CanvasLayer = main_scene.get_node("UI")
|
|
manager.set_process(false)
|
|
var npc: SimNPC = manager.npcs[0]
|
|
var visual: Node3D = view.active_npc_visuals[npc.id]
|
|
visual.set_physics_process(false)
|
|
var profession_definition := SimulationDefinitions.get_profession(npc.profession)
|
|
var body: MeshInstance3D = visual.get_node("MeshInstance3D")
|
|
_check(
|
|
body.material_override.albedo_color == profession_definition.visual_color,
|
|
"NPC body color should come from its profession definition"
|
|
)
|
|
_check(
|
|
visual.get_node("ProfessionProp").mesh != null
|
|
and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
|
|
"NPC visual should expose a profession prop and readable label"
|
|
)
|
|
var test_decision := ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD,
|
|
-1.0,
|
|
"Test shortage reason",
|
|
{SimulationIds.ACTION_GATHER_FOOD: 4.5}
|
|
)
|
|
manager.latest_decisions[npc.id] = test_decision
|
|
manager.emit_signal("npc_decision_recorded", npc, test_decision)
|
|
var inspector_label: Label = ui.get_node(
|
|
"NpcInspectorPanel/MarginContainer/NpcInspectorLabel"
|
|
)
|
|
_check(
|
|
"Test shortage reason" in inspector_label.text
|
|
and npc.npc_name in inspector_label.text,
|
|
"NPC inspector should show the selected villager's real decision reason"
|
|
)
|
|
var carried_food: MeshInstance3D = visual.get_node("CarriedFood")
|
|
_check(not carried_food.visible, "NPC should begin without a carried-food prop")
|
|
npc.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
|
manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
|
_check(carried_food.visible, "Carried food should be visible on the NPC")
|
|
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 resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01")
|
|
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
|
|
npc.target_id = resource_state.get_node_id()
|
|
npc.travel_target_position = Vector3(-6.0, 0.0, -8.0)
|
|
npc.has_travel_target = true
|
|
resource_state.reserve(npc.id)
|
|
manager.request_current_travel(npc.id)
|
|
|
|
var saved_action := npc.current_task
|
|
var saved_target_id := npc.target_id
|
|
var saved_target_position := npc.travel_target_position
|
|
var authoritative_position := Vector3(-2.5, 0.5, 1.75)
|
|
visual.global_position = authoritative_position
|
|
|
|
_check(view.despawn_npc_visual(npc.id), "Visual should unload")
|
|
await process_frame
|
|
_check(
|
|
npc.position.is_equal_approx(authoritative_position),
|
|
"Unload should synchronize the last active position"
|
|
)
|
|
_check(
|
|
(
|
|
npc.current_task == saved_action
|
|
and npc.target_id == saved_target_id
|
|
and npc.travel_target_position.is_equal_approx(saved_target_position)
|
|
),
|
|
"Unload should preserve action and target state"
|
|
)
|
|
_check(
|
|
resource_state.get_reserved_by() == npc.id,
|
|
"Unload should preserve the resource reservation"
|
|
)
|
|
var unloaded_checksum: String = manager.get_state_checksum()
|
|
|
|
_check(view.reload_npc_visual(npc.id), "Visual should reload")
|
|
var reloaded: Node3D = view.active_npc_visuals[npc.id]
|
|
reloaded.set_physics_process(false)
|
|
_check(
|
|
reloaded.global_position.is_equal_approx(authoritative_position),
|
|
"Reloaded visual should use authoritative position"
|
|
)
|
|
var resumed_target: Vector3 = reloaded.get("current_target")
|
|
_check(
|
|
resumed_target.is_equal_approx(saved_target_position),
|
|
"Reloaded visual should resume the persisted travel target"
|
|
)
|
|
_check(
|
|
manager.get_state_checksum() == unloaded_checksum,
|
|
"Loading a visual must not mutate simulation state"
|
|
)
|
|
_check(resource_state.get_reserved_by() == npc.id, "Reload should preserve the reservation")
|
|
|
|
var moved_position := authoritative_position + Vector3(0.5, 0.0, 0.25)
|
|
reloaded.emit_signal("position_changed", npc.id, moved_position)
|
|
_check(
|
|
npc.position.is_equal_approx(moved_position),
|
|
"Active position callback should synchronize simulation state"
|
|
)
|
|
|
|
var saved_state: String = manager.serialize_state()
|
|
var visual_before_restore: Node3D = view.active_npc_visuals[npc.id]
|
|
npc.hunger = 99.0
|
|
_check(
|
|
manager.restore_state_from_json(saved_state),
|
|
"Visual lifecycle scenario should restore valid state"
|
|
)
|
|
var visual_after_restore: Node3D = view.active_npc_visuals[npc.id]
|
|
_check(
|
|
visual_after_restore != visual_before_restore,
|
|
"State restoration should rebuild active NPC presentation"
|
|
)
|
|
_check(
|
|
visual_after_restore.global_position.is_equal_approx(npc.position),
|
|
"Rebuilt presentation should use the restored authoritative position"
|
|
)
|
|
|
|
if failures.is_empty():
|
|
print("[TEST] NPC visual lifecycle passed")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|