feat: add exact player context actions
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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
|
||||
var manager: Node = main_scene.get_node("SimulationManager")
|
||||
manager.set_process(false)
|
||||
var player := main_scene.get_node("Player")
|
||||
player.set_physics_process(false)
|
||||
for _frame in 3:
|
||||
await physics_frame
|
||||
|
||||
var dunja_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_node := AnimalNode.get_by_id(SimulationIds.ANIMAL_ZORA)
|
||||
var dunja_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
var zora_state: AnimalStateRecord = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
var hud := main_scene.get_node_or_null("PlayerInteractionLayer/PlayerInteractionHud")
|
||||
_check(
|
||||
(
|
||||
dunja_node != null
|
||||
and zora_node != null
|
||||
and dunja_state != null
|
||||
and zora_state != null
|
||||
and hud != null
|
||||
),
|
||||
"The real main scene should expose both named goats and the player interaction HUD",
|
||||
)
|
||||
if (
|
||||
dunja_node == null
|
||||
or zora_node == null
|
||||
or dunja_state == null
|
||||
or zora_state == null
|
||||
or hud == null
|
||||
):
|
||||
_finish()
|
||||
return
|
||||
|
||||
dunja_node.set_physics_process(false)
|
||||
zora_node.set_physics_process(false)
|
||||
dunja_state.set_hunger(72.0)
|
||||
zora_state.set_hunger(72.0)
|
||||
var dunja_interaction_position := dunja_node.get_interaction_position()
|
||||
var zora_interaction_position := zora_node.get_interaction_position()
|
||||
var tie_position := (dunja_interaction_position + zora_interaction_position) * 0.5
|
||||
var pair_range := dunja_interaction_position.distance_to(zora_interaction_position) + 0.5
|
||||
player.set("interaction_range", pair_range)
|
||||
|
||||
player.global_position = zora_interaction_position
|
||||
var nearest_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||
_check(
|
||||
(
|
||||
nearest_context != null
|
||||
and nearest_context.kind == PlayerInteractionResult.KIND_ANIMAL
|
||||
and nearest_context.action_id == SimulationIds.ACTION_FEED_ANIMAL
|
||||
and nearest_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and nearest_context.display_name == "Zora"
|
||||
and nearest_context.target_node == zora_node
|
||||
and nearest_context.is_available()
|
||||
and "Zora" in nearest_context.prompt_text
|
||||
),
|
||||
"With both hungry goats in range, the context should name exact nearest goat Zora",
|
||||
)
|
||||
hud.call("refresh_prompt")
|
||||
var action_label := hud.get_node("Copy/Action") as Label
|
||||
var detail_label := hud.get_node("Copy/Detail") as Label
|
||||
_check(
|
||||
nearest_context != null and action_label != null and detail_label != null,
|
||||
"The prompt proof needs a resolved context and both public HUD labels",
|
||||
)
|
||||
if nearest_context == null or action_label == null or detail_label == null:
|
||||
_finish()
|
||||
return
|
||||
_check(
|
||||
(
|
||||
nearest_context.prompt_text in action_label.text
|
||||
and detail_label.text == nearest_context.detail_text
|
||||
),
|
||||
"The HUD prompt should render the same exact context that execution will use",
|
||||
)
|
||||
|
||||
player.global_position = tie_position
|
||||
var tie_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||
_check(
|
||||
(
|
||||
tie_context != null
|
||||
and tie_position.distance_to(dunja_interaction_position) <= pair_range
|
||||
and tie_position.distance_to(zora_interaction_position) <= pair_range
|
||||
and tie_context.target_id == SimulationIds.ANIMAL_DUNJA
|
||||
and tie_context.display_name == "Dunja"
|
||||
and tie_context.target_node == dunja_node
|
||||
),
|
||||
"An equal-distance pair should resolve by stable animal ID, choosing Dunja",
|
||||
)
|
||||
|
||||
player.global_position = zora_interaction_position
|
||||
var execution_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
var pantry_before := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
var dunja_hunger_before := dunja_state.get_hunger()
|
||||
var zora_hunger_before := zora_state.get_hunger()
|
||||
var events_before: int = manager.economic_events.size()
|
||||
player.call("try_interact")
|
||||
var feed_event := _latest_feed_event(manager)
|
||||
_check(
|
||||
(
|
||||
execution_context != null
|
||||
and execution_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and is_equal_approx(dunja_state.get_hunger(), dunja_hunger_before)
|
||||
and is_equal_approx(
|
||||
zora_state.get_hunger(), zora_hunger_before - AnimalStateRecord.HUNGER_RELIEF
|
||||
)
|
||||
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), pantry_before - 1.0)
|
||||
and manager.economic_events.size() == events_before + 1
|
||||
),
|
||||
"Interaction should feed the prompted Zora only and consume one real pantry food",
|
||||
)
|
||||
_check(
|
||||
(
|
||||
feed_event != null
|
||||
and int(feed_event.data["actor_id"]) == -1
|
||||
and StringName(feed_event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and StringName(feed_event.data["destination_id"]) == SimulationIds.ANIMAL_ZORA
|
||||
and StringName(feed_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and is_equal_approx(float(feed_event.data["amount"]), 1.0)
|
||||
and feed_event.get_world_position().is_equal_approx(zora_state.get_position())
|
||||
),
|
||||
"The player feed event should preserve its exact pantry, named target, amount, and position",
|
||||
)
|
||||
_check(
|
||||
bool(hud.get("feedback_active")) and bool(hud.get("feedback_succeeded")),
|
||||
"A successful exact interaction should surface transient positive HUD feedback",
|
||||
)
|
||||
|
||||
dunja_state.set_hunger(72.0)
|
||||
zora_state.set_hunger(72.0)
|
||||
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
player.set("interaction_range", 100.0)
|
||||
player.global_position = zora_node.get_interaction_position()
|
||||
var blocked_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||
hud.call("refresh_prompt")
|
||||
_check(
|
||||
(
|
||||
blocked_context != null
|
||||
and blocked_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and blocked_context.display_name == "Zora"
|
||||
and not blocked_context.is_available()
|
||||
and not blocked_context.blocked_reason.is_empty()
|
||||
and "Zora" in (blocked_context.prompt_text + blocked_context.detail_text)
|
||||
and (
|
||||
"pantry"
|
||||
in (blocked_context.blocked_reason + blocked_context.detail_text).to_lower()
|
||||
)
|
||||
and blocked_context.prompt_text in action_label.text
|
||||
and detail_label.text == blocked_context.detail_text
|
||||
),
|
||||
"An empty pantry should show a named blocked Zora prompt before input",
|
||||
)
|
||||
|
||||
var blocked_dunja_hunger := dunja_state.get_hunger()
|
||||
var blocked_zora_hunger := zora_state.get_hunger()
|
||||
var blocked_event_count: int = manager.economic_events.size()
|
||||
var blocked_safety: float = manager.village.safety
|
||||
var blocked_knowledge: float = manager.village.knowledge
|
||||
player.call("try_interact")
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger)
|
||||
and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger)
|
||||
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
||||
and manager.economic_events.size() == blocked_event_count
|
||||
and is_equal_approx(manager.village.safety, blocked_safety)
|
||||
and is_equal_approx(manager.village.knowledge, blocked_knowledge)
|
||||
),
|
||||
"A blocked animal interaction should consume input without mutation, events, or fallthrough",
|
||||
)
|
||||
_check(
|
||||
bool(hud.get("feedback_active")) and not bool(hud.get("feedback_succeeded")),
|
||||
"The blocked exact interaction should surface transient negative HUD feedback",
|
||||
)
|
||||
|
||||
var blocked_json: String = manager.serialize_state()
|
||||
dunja_state.set_hunger(20.0)
|
||||
zora_state.set_hunger(20.0)
|
||||
pantry.deposit(SimulationIds.RESOURCE_FOOD, 2.0)
|
||||
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||
_check(
|
||||
manager.restore_state_from_json(blocked_json),
|
||||
"The blocked two-goat context should restore through the complete state schema",
|
||||
)
|
||||
dunja_state = manager.animal_care.get_state(SimulationIds.ANIMAL_DUNJA)
|
||||
zora_state = manager.animal_care.get_state(SimulationIds.ANIMAL_ZORA)
|
||||
pantry = manager.get_pantry()
|
||||
var restored_context := player.call("get_interaction_context") as PlayerInteractionResult
|
||||
_check(
|
||||
(
|
||||
not bool(hud.get("feedback_active"))
|
||||
and restored_context != null
|
||||
and restored_context.target_id == SimulationIds.ANIMAL_ZORA
|
||||
and not restored_context.is_available()
|
||||
and is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0)
|
||||
and is_equal_approx(dunja_state.get_hunger(), blocked_dunja_hunger)
|
||||
and is_equal_approx(zora_state.get_hunger(), blocked_zora_hunger)
|
||||
and restored_context.prompt_text in action_label.text
|
||||
and detail_label.text == restored_context.detail_text
|
||||
),
|
||||
"Restore should clear transient feedback and re-derive the named blocked prompt",
|
||||
)
|
||||
|
||||
var demo_controller := main_scene.get_node("DemoController")
|
||||
var debug_ui := main_scene.get_node("UI") as CanvasLayer
|
||||
demo_controller.call("set_debug_overlay_visible", false)
|
||||
_check(
|
||||
(
|
||||
not debug_ui.visible
|
||||
and bool(hud.get("visible"))
|
||||
and action_label.visible
|
||||
and not action_label.text.is_empty()
|
||||
),
|
||||
"The player interaction HUD should remain visible when development overlays are hidden",
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
func _latest_feed_event(manager: Node) -> EconomicEventRecord:
|
||||
for index in range(manager.economic_events.size() - 1, -1, -1):
|
||||
var event := manager.economic_events[index] as EconomicEventRecord
|
||||
if StringName(event.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
|
||||
return event
|
||||
return null
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Player context interaction passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://47l78j2djkv0
|
||||
Reference in New Issue
Block a user