feat: add exact player context actions

This commit is contained in:
Rijad Zuzo
2026-07-30 23:27:46 +02:00
parent d950c3da68
commit 7d410d8193
12 changed files with 762 additions and 67 deletions
+254
View File
@@ -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