Room reset (#502): amber reset_plate tile type in TileRenderer, 0.15s screen flash on room_reset monologue, 'Reset Room' verb via existing nearby_interactions. Insert pause (#518, D-058): explicit PauseSimulation on insert open, ResumeSimulation on close. Replaces toggle-style pause with idempotent pair per D-058. Auto-checklist (#503): ChecklistEvaluator parses room YAML, evaluates 7 condition types against GameState with latching. ChecklistOverlay renders progress in gauntlet mode only. 48 tests covering parser, evaluation, latching, visibility, and integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
693 lines
24 KiB
GDScript
693 lines
24 KiB
GDScript
## #503: Auto-checklist progress tracking — unit + integration tests.
|
|
##
|
|
## Tests cover:
|
|
## 1. YAML parser: basic types, conditions array, edge cases
|
|
## 2. Condition evaluation: all 7 condition types
|
|
## 3. Latching: conditions stay met once satisfied
|
|
## 4. Room change: per-room conditions reset, cross-room conditions persist
|
|
## 5. Overlay: visibility gating on gauntlet_mode
|
|
## 6. Integration: snapshot -> GameState -> evaluator -> overlay
|
|
##
|
|
## Spec ref: D-030 (testability), checklist.schema.json (#497), Sprint 10 Completion Proof.
|
|
class_name TestChecklist
|
|
extends GdUnitTestSuite
|
|
|
|
|
|
var ChecklistOverlayScript = load("res://ui/checklist_overlay.gd")
|
|
var ChecklistEvaluatorScript = load("res://scripts/checklist/checklist_evaluator.gd")
|
|
|
|
|
|
func before_test() -> void:
|
|
SimBridge.reset_test_state()
|
|
SimBridge._last_snapshot = null
|
|
GameState.current_tick = 0
|
|
GameState.player_position = Vector2.ZERO
|
|
GameState.visible_entities = []
|
|
GameState.visible_tiles = []
|
|
GameState.visible_positions = {}
|
|
GameState.current_monologue = null
|
|
GameState.current_dialogue = null
|
|
GameState.nearby_interactions = []
|
|
GameState.game_time = {}
|
|
GameState.pending_recognitions = []
|
|
GameState.room_id = null
|
|
GameState.gauntlet_mode = false
|
|
GameState.player_facing = "North"
|
|
GameState.player_stance = "Walk"
|
|
GameState.player_inventory = []
|
|
|
|
|
|
# -- YAML Parser Tests ---------------------------------------------------------
|
|
|
|
func test_parse_empty_yaml() -> void:
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml("")
|
|
assert_that(result.size()).is_equal(0)
|
|
|
|
|
|
func test_parse_top_level_string() -> void:
|
|
var yaml := "room_id: inventory_warehouse"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.get("room_id")).is_equal("inventory_warehouse")
|
|
|
|
|
|
func test_parse_top_level_quoted_string() -> void:
|
|
var yaml := 'description: "Tests D-065 (9-slot inventory)."'
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.get("description")).is_equal("Tests D-065 (9-slot inventory).")
|
|
|
|
|
|
func test_parse_single_condition() -> void:
|
|
var yaml := "conditions:\n - id: test-1\n description: \"Test condition\"\n condition_type: player_near\n x: 10\n y: 20\n radius: 3.0"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.has("conditions")).is_true()
|
|
var conditions: Array = result["conditions"]
|
|
assert_that(conditions.size()).is_equal(1)
|
|
assert_that(conditions[0]["id"]).is_equal("test-1")
|
|
assert_that(conditions[0]["condition_type"]).is_equal("player_near")
|
|
assert_that(conditions[0]["x"]).is_equal(10)
|
|
assert_that(conditions[0]["y"]).is_equal(20)
|
|
assert_that(conditions[0]["radius"]).is_equal_approx(3.0, 0.001)
|
|
|
|
|
|
func test_parse_multiple_conditions() -> void:
|
|
var yaml := "conditions:\n - id: cond-a\n condition_type: player_near\n x: 1\n y: 2\n radius: 1.0\n\n - id: cond-b\n condition_type: player_facing\n direction: East"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
var conditions: Array = result["conditions"]
|
|
assert_that(conditions.size()).is_equal(2)
|
|
assert_that(conditions[0]["id"]).is_equal("cond-a")
|
|
assert_that(conditions[1]["id"]).is_equal("cond-b")
|
|
assert_that(conditions[1]["direction"]).is_equal("East")
|
|
|
|
|
|
func test_parse_comments_ignored() -> void:
|
|
var yaml := "# This is a comment\nroom_id: test\n# Another comment\nconditions:\n - id: c1\n condition_type: entity_present\n entity_id: 5"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.get("room_id")).is_equal("test")
|
|
var conditions: Array = result["conditions"]
|
|
assert_that(conditions.size()).is_equal(1)
|
|
assert_that(conditions[0]["entity_id"]).is_equal(5)
|
|
|
|
|
|
func test_parse_integer_and_float_values() -> void:
|
|
var yaml := "conditions:\n - id: t\n condition_type: player_near\n x: 42\n y: -3\n radius: 2.5"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
var cond: Dictionary = result["conditions"][0]
|
|
assert_that(cond["x"]).is_equal(42)
|
|
assert_that(typeof(cond["radius"])).is_equal(TYPE_FLOAT)
|
|
|
|
|
|
func test_parse_scope_field() -> void:
|
|
var yaml := "scope: cross_room\nconditions:\n - id: cr-1\n condition_type: entity_present\n entity_id: 0"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.get("scope")).is_equal("cross_room")
|
|
|
|
|
|
func test_parse_inline_comment_stripped() -> void:
|
|
var yaml := "room_id: test # this is a comment"
|
|
var result: Dictionary = ChecklistEvaluatorScript.parse_checklist_yaml(yaml)
|
|
assert_that(result.get("room_id")).is_equal("test")
|
|
|
|
|
|
# -- Condition Evaluation Tests ------------------------------------------------
|
|
|
|
func _make_evaluator(conditions: Array):
|
|
var evaluator = ChecklistEvaluatorScript.new()
|
|
evaluator._room_conditions = conditions
|
|
evaluator._loaded = true
|
|
return evaluator
|
|
|
|
|
|
func test_eval_player_near_within_radius() -> void:
|
|
GameState.player_position = Vector2(10.0, 20.0)
|
|
var evaluator = _make_evaluator([{
|
|
"id": "near-1", "condition_type": "player_near",
|
|
"x": 10, "y": 21, "radius": 2.0,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"player_near: player at (10,20), target (10,21), radius 2.0 — should be met"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_eval_player_near_outside_radius() -> void:
|
|
GameState.player_position = Vector2(10.0, 20.0)
|
|
var evaluator = _make_evaluator([{
|
|
"id": "near-2", "condition_type": "player_near",
|
|
"x": 10, "y": 30, "radius": 2.0,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"player_near: player at (10,20), target (10,30), radius 2.0 — should NOT be met"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_player_near_exact_boundary() -> void:
|
|
GameState.player_position = Vector2(10.0, 20.0)
|
|
var evaluator = _make_evaluator([{
|
|
"id": "near-3", "condition_type": "player_near",
|
|
"x": 10, "y": 22, "radius": 2.0,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"player_near: distance exactly equals radius — should be met (<=)"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_eval_player_facing_match() -> void:
|
|
GameState.player_facing = "East"
|
|
var evaluator = _make_evaluator([{
|
|
"id": "face-1", "condition_type": "player_facing",
|
|
"direction": "East",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
|
|
func test_eval_player_facing_no_match() -> void:
|
|
GameState.player_facing = "North"
|
|
var evaluator = _make_evaluator([{
|
|
"id": "face-2", "condition_type": "player_facing",
|
|
"direction": "East",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
func test_eval_player_facing_diagonal_no_match() -> void:
|
|
# 8-directional facing "Northeast" should NOT match "East" or "North"
|
|
GameState.player_facing = "Northeast"
|
|
var evaluator = _make_evaluator([{
|
|
"id": "face-diag", "condition_type": "player_facing",
|
|
"direction": "East",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"player_facing: Northeast should NOT match East (exact match only)"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_entity_present_found() -> void:
|
|
GameState.visible_entities = [
|
|
{"entity_id": 5, "x": 1.0, "y": 1.0, "z": 0, "kind": "Npc"},
|
|
{"entity_id": 10, "x": 2.0, "y": 2.0, "z": 0, "kind": "Object"},
|
|
]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "present-1", "condition_type": "entity_present",
|
|
"entity_id": 10,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
|
|
func test_eval_entity_present_not_found() -> void:
|
|
GameState.visible_entities = [
|
|
{"entity_id": 5, "x": 1.0, "y": 1.0, "z": 0, "kind": "Npc"},
|
|
]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "present-2", "condition_type": "entity_present",
|
|
"entity_id": 99,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
func test_eval_entity_absent_when_not_visible() -> void:
|
|
GameState.visible_entities = [
|
|
{"entity_id": 5, "x": 1.0, "y": 1.0, "z": 0, "kind": "Npc"},
|
|
]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "absent-1", "condition_type": "entity_absent",
|
|
"entity_id": 99,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"entity_absent: entity 99 not in visible_entities — should be met"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_eval_entity_absent_when_visible() -> void:
|
|
GameState.visible_entities = [
|
|
{"entity_id": 10, "x": 1.0, "y": 1.0, "z": 0, "kind": "Npc"},
|
|
]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "absent-2", "condition_type": "entity_absent",
|
|
"entity_id": 10,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"entity_absent: entity 10 IS visible — should NOT be met"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_expected_monologue_match() -> void:
|
|
GameState.current_monologue = {"id": "m1", "text": "Something is wrong here.", "duration_seconds": 5.0}
|
|
var evaluator = _make_evaluator([{
|
|
"id": "mono-1", "condition_type": "expected_monologue",
|
|
"contains": "wrong here",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
|
|
func test_eval_expected_monologue_no_match() -> void:
|
|
GameState.current_monologue = {"id": "m1", "text": "All clear.", "duration_seconds": 5.0}
|
|
var evaluator = _make_evaluator([{
|
|
"id": "mono-2", "condition_type": "expected_monologue",
|
|
"contains": "wrong here",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
func test_eval_expected_monologue_null() -> void:
|
|
GameState.current_monologue = null
|
|
var evaluator = _make_evaluator([{
|
|
"id": "mono-3", "condition_type": "expected_monologue",
|
|
"contains": "test",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"expected_monologue: null monologue should not match"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_expected_dialogue_match() -> void:
|
|
GameState.current_dialogue = {"npc_name": "Kael", "speech": "Who are you?", "options": []}
|
|
var evaluator = _make_evaluator([{
|
|
"id": "dlg-1", "condition_type": "expected_dialogue",
|
|
"contains": "Who are you",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
|
|
func test_eval_expected_dialogue_no_match() -> void:
|
|
GameState.current_dialogue = {"npc_name": "Kael", "speech": "Hello.", "options": []}
|
|
var evaluator = _make_evaluator([{
|
|
"id": "dlg-2", "condition_type": "expected_dialogue",
|
|
"contains": "Goodbye",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
func test_eval_expected_dialogue_null() -> void:
|
|
GameState.current_dialogue = null
|
|
var evaluator = _make_evaluator([{
|
|
"id": "dlg-3", "condition_type": "expected_dialogue",
|
|
"contains": "test",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"expected_dialogue: null dialogue should not match"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_interaction_verb_match() -> void:
|
|
GameState.nearby_interactions = [{
|
|
"entity_id": 13,
|
|
"entity_type": "Object",
|
|
"distance": 1,
|
|
"verbs": [
|
|
{"kind": "Take", "label": "Pickup", "priority": 1, "available": true},
|
|
{"kind": "Observe", "label": "Examine", "priority": 2, "available": true},
|
|
],
|
|
}]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "verb-1", "condition_type": "expected_interaction_verb",
|
|
"entity_id": 13, "verb": "Pickup",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
|
|
func test_eval_interaction_verb_by_kind() -> void:
|
|
GameState.nearby_interactions = [{
|
|
"entity_id": 13,
|
|
"entity_type": "Object",
|
|
"distance": 1,
|
|
"verbs": [
|
|
{"kind": "Take", "label": "Pickup", "priority": 1, "available": true},
|
|
],
|
|
}]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "verb-kind", "condition_type": "expected_interaction_verb",
|
|
"entity_id": 13, "verb": "Take",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"expected_interaction_verb: should match by kind='Take' as well as label"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_eval_interaction_verb_wrong_entity() -> void:
|
|
GameState.nearby_interactions = [{
|
|
"entity_id": 13,
|
|
"entity_type": "Object",
|
|
"distance": 1,
|
|
"verbs": [{"kind": "Take", "label": "Pickup", "priority": 1, "available": true}],
|
|
}]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "verb-wrong", "condition_type": "expected_interaction_verb",
|
|
"entity_id": 99, "verb": "Pickup",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"expected_interaction_verb: wrong entity_id should not match"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_interaction_verb_unavailable() -> void:
|
|
GameState.nearby_interactions = [{
|
|
"entity_id": 13,
|
|
"entity_type": "Object",
|
|
"distance": 1,
|
|
"verbs": [{"kind": "Take", "label": "Pickup", "priority": 1, "available": false}],
|
|
}]
|
|
var evaluator = _make_evaluator([{
|
|
"id": "verb-unavail", "condition_type": "expected_interaction_verb",
|
|
"entity_id": 13, "verb": "Pickup",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"expected_interaction_verb: unavailable verb should not match"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_eval_interaction_verb_no_interactions() -> void:
|
|
GameState.nearby_interactions = []
|
|
var evaluator = _make_evaluator([{
|
|
"id": "verb-none", "condition_type": "expected_interaction_verb",
|
|
"entity_id": 13, "verb": "Pickup",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
# -- Latching Tests ------------------------------------------------------------
|
|
|
|
func test_latching_condition_stays_met() -> void:
|
|
# Condition met on first evaluate, stays met even when state changes.
|
|
GameState.player_facing = "East"
|
|
var evaluator = _make_evaluator([{
|
|
"id": "latch-1", "condition_type": "player_facing",
|
|
"direction": "East",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
# Change state so condition would be false if re-evaluated fresh
|
|
GameState.player_facing = "North"
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"Latched condition should stay met even after state changes"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_latching_monologue_transient() -> void:
|
|
# Monologue appears for one tick, then disappears. Condition should latch.
|
|
var evaluator = _make_evaluator([{
|
|
"id": "mono-latch", "condition_type": "expected_monologue",
|
|
"contains": "recalibrated",
|
|
}])
|
|
|
|
# Tick 1: no monologue
|
|
GameState.current_monologue = null
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
# Tick 2: monologue fires
|
|
GameState.current_monologue = {"id": "m1", "text": "Systems recalibrated.", "duration_seconds": 3.0}
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
# Tick 3: monologue consumed (null)
|
|
GameState.current_monologue = null
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"Monologue condition should stay latched after monologue disappears"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_multiple_conditions_partial_latching() -> void:
|
|
var evaluator = _make_evaluator([
|
|
{"id": "c1", "condition_type": "player_facing", "direction": "East"},
|
|
{"id": "c2", "condition_type": "entity_present", "entity_id": 5},
|
|
{"id": "c3", "condition_type": "player_near", "x": 50, "y": 50, "radius": 1.0},
|
|
])
|
|
|
|
# Tick 1: only facing matches
|
|
GameState.player_facing = "East"
|
|
GameState.visible_entities = []
|
|
GameState.player_position = Vector2(0, 0)
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
assert_that(evaluator.get_total_count()).is_equal(3)
|
|
|
|
# Tick 2: entity also visible
|
|
GameState.visible_entities = [{"entity_id": 5, "x": 1.0, "y": 1.0, "z": 0, "kind": "Npc"}]
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(2)
|
|
|
|
# Tick 3: player moves to target
|
|
GameState.player_position = Vector2(50.0, 50.0)
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(3)
|
|
assert_that(evaluator.is_complete()).is_true()
|
|
|
|
|
|
# -- Room Change Tests ---------------------------------------------------------
|
|
|
|
func test_room_change_resets_per_room_conditions() -> void:
|
|
var evaluator = ChecklistEvaluatorScript.new()
|
|
# Manually set conditions to avoid file loading
|
|
evaluator._room_conditions = [
|
|
{"id": "r1-c1", "condition_type": "player_facing", "direction": "East"},
|
|
]
|
|
evaluator._loaded = true
|
|
GameState.player_facing = "East"
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
|
|
# Simulate room change by loading a new "room"
|
|
evaluator._current_room_id = "old_room"
|
|
evaluator._room_conditions = [
|
|
{"id": "r2-c1", "condition_type": "player_facing", "direction": "North"},
|
|
]
|
|
# Clear latches for the new room (simulating load_room behavior)
|
|
evaluator._latched.clear()
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"After room change, old latches should be cleared; new condition not met"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_reset_clears_all_state() -> void:
|
|
var evaluator = _make_evaluator([
|
|
{"id": "c1", "condition_type": "player_facing", "direction": "East"},
|
|
])
|
|
GameState.player_facing = "East"
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
evaluator.reset()
|
|
assert_that(evaluator.is_loaded()).is_false()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
assert_that(evaluator.get_total_count()).is_equal(0)
|
|
|
|
|
|
# -- get_results Tests ---------------------------------------------------------
|
|
|
|
func test_get_results_structure() -> void:
|
|
var evaluator = _make_evaluator([
|
|
{"id": "c1", "description": "Test condition", "condition_type": "player_facing", "direction": "North"},
|
|
])
|
|
GameState.player_facing = "North"
|
|
evaluator.evaluate()
|
|
var results: Array = evaluator.get_results()
|
|
assert_that(results.size()).is_equal(1)
|
|
assert_that(results[0]["id"]).is_equal("c1")
|
|
assert_that(results[0]["description"]).is_equal("Test condition")
|
|
assert_that(results[0]["condition_type"]).is_equal("player_facing")
|
|
assert_that(results[0]["met"]).is_true()
|
|
|
|
|
|
func test_get_results_unmet() -> void:
|
|
var evaluator = _make_evaluator([
|
|
{"id": "c1", "description": "Test", "condition_type": "player_facing", "direction": "South"},
|
|
])
|
|
GameState.player_facing = "North"
|
|
evaluator.evaluate()
|
|
var results: Array = evaluator.get_results()
|
|
assert_that(results[0]["met"]).is_false()
|
|
|
|
|
|
# -- Overlay Visibility Tests --------------------------------------------------
|
|
|
|
func _make_checklist_overlay() -> Control:
|
|
var overlay = Control.new()
|
|
overlay.set_script(ChecklistOverlayScript)
|
|
auto_free(overlay)
|
|
add_child(overlay)
|
|
return overlay
|
|
|
|
|
|
func test_overlay_hidden_in_non_gauntlet_mode() -> void:
|
|
var overlay := _make_checklist_overlay()
|
|
GameState.gauntlet_mode = false
|
|
overlay.update_from_state()
|
|
assert_that(overlay.visible).override_failure_message(
|
|
"Checklist overlay must be hidden in non-gauntlet mode"
|
|
).is_false()
|
|
|
|
|
|
func test_overlay_visible_in_gauntlet_mode() -> void:
|
|
var overlay := _make_checklist_overlay()
|
|
GameState.gauntlet_mode = true
|
|
GameState.room_id = "test_room"
|
|
overlay.update_from_state()
|
|
assert_that(overlay.visible).override_failure_message(
|
|
"Checklist overlay must be visible in gauntlet mode"
|
|
).is_true()
|
|
|
|
|
|
func test_overlay_hides_on_gauntlet_deactivation() -> void:
|
|
var overlay := _make_checklist_overlay()
|
|
GameState.gauntlet_mode = true
|
|
GameState.room_id = "test_room"
|
|
overlay.update_from_state()
|
|
assert_that(overlay.visible).is_true()
|
|
GameState.gauntlet_mode = false
|
|
overlay.update_from_state()
|
|
assert_that(overlay.visible).override_failure_message(
|
|
"Overlay must hide when gauntlet mode deactivates"
|
|
).is_false()
|
|
|
|
|
|
func test_overlay_evaluator_accessible() -> void:
|
|
var overlay := _make_checklist_overlay()
|
|
var evaluator = overlay.get_evaluator()
|
|
assert_that(evaluator).override_failure_message(
|
|
"Overlay should expose evaluator via get_evaluator()"
|
|
).is_not_null()
|
|
|
|
|
|
func test_overlay_in_main_scene() -> void:
|
|
var scene: PackedScene = load("res://scenes/main.tscn")
|
|
var instance: Node = scene.instantiate()
|
|
auto_free(instance)
|
|
add_child(instance)
|
|
instance._process(0.016)
|
|
|
|
var overlay: Node = _find_node_recursive(instance, "ChecklistOverlay")
|
|
assert_that(overlay).override_failure_message(
|
|
"ChecklistOverlay node should exist in main scene tree"
|
|
).is_not_null()
|
|
if overlay is CanvasItem:
|
|
assert_that((overlay as CanvasItem).visible).override_failure_message(
|
|
"ChecklistOverlay should be hidden by default (non-gauntlet mode)"
|
|
).is_false()
|
|
|
|
|
|
# -- Integration: Snapshot -> Evaluation ----------------------------------------
|
|
|
|
func test_integration_snapshot_to_evaluator() -> void:
|
|
# Integration test: GameState snapshot data -> evaluator -> correct results.
|
|
# Tests the evaluator directly (overlay wiring tested separately).
|
|
var evaluator = ChecklistEvaluatorScript.new()
|
|
evaluator._room_conditions = [
|
|
{"id": "int-1", "description": "Player entity present", "condition_type": "entity_present", "entity_id": 1},
|
|
{"id": "int-2", "description": "Player faces East", "condition_type": "player_facing", "direction": "East"},
|
|
]
|
|
evaluator._loaded = true
|
|
|
|
# Simulate gauntlet snapshot with player entity
|
|
GameState.visible_entities = [
|
|
{"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": "Player"},
|
|
]
|
|
GameState.player_facing = "North"
|
|
|
|
evaluator.evaluate()
|
|
|
|
# Entity present should be met, facing should not
|
|
assert_that(evaluator.get_met_count()).is_equal(1)
|
|
assert_that(evaluator.get_total_count()).is_equal(2)
|
|
|
|
# Change facing — second condition should also latch
|
|
GameState.player_facing = "East"
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(2)
|
|
assert_that(evaluator.is_complete()).is_true()
|
|
|
|
# Verify results array contains both conditions as met
|
|
var results: Array = evaluator.get_results()
|
|
for r in results:
|
|
assert_that(r["met"]).override_failure_message(
|
|
"Condition '%s' should be met after snapshot sequence" % r["id"]
|
|
).is_true()
|
|
|
|
|
|
# -- Edge Cases ----------------------------------------------------------------
|
|
|
|
func test_empty_entity_list_entity_present() -> void:
|
|
GameState.visible_entities = []
|
|
var evaluator = _make_evaluator([{
|
|
"id": "edge-empty", "condition_type": "entity_present",
|
|
"entity_id": 0,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
|
|
|
|
func test_empty_entity_list_entity_absent() -> void:
|
|
GameState.visible_entities = []
|
|
var evaluator = _make_evaluator([{
|
|
"id": "edge-absent-empty", "condition_type": "entity_absent",
|
|
"entity_id": 99,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"entity_absent with empty visible_entities should be met"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_zero_radius_player_near() -> void:
|
|
GameState.player_position = Vector2(10.0, 20.0)
|
|
var evaluator = _make_evaluator([{
|
|
"id": "edge-zero-radius", "condition_type": "player_near",
|
|
"x": 10, "y": 20, "radius": 0.0,
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"player_near with radius 0 at exact position should be met"
|
|
).is_equal(1)
|
|
|
|
|
|
func test_unknown_condition_type() -> void:
|
|
var evaluator = _make_evaluator([{
|
|
"id": "edge-unknown", "condition_type": "nonexistent_type",
|
|
}])
|
|
evaluator.evaluate()
|
|
assert_that(evaluator.get_met_count()).override_failure_message(
|
|
"Unknown condition type should not be met"
|
|
).is_equal(0)
|
|
|
|
|
|
func test_no_conditions_loaded() -> void:
|
|
var evaluator = ChecklistEvaluatorScript.new()
|
|
assert_that(evaluator.is_loaded()).is_false()
|
|
assert_that(evaluator.get_total_count()).is_equal(0)
|
|
assert_that(evaluator.get_met_count()).is_equal(0)
|
|
assert_that(evaluator.is_complete()).is_false()
|
|
|
|
|
|
# -- Helper: recursive node search --------------------------------------------
|
|
|
|
func _find_node_recursive(root: Node, target_name: String) -> Node:
|
|
if root.name == target_name:
|
|
return root
|
|
for child in root.get_children():
|
|
var found := _find_node_recursive(child, target_name)
|
|
if found != null:
|
|
return found
|
|
return null
|