test(client): add sprint 18 test suites for examine display and minimap
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
## Sprint 18 — Examine result display (#174)
|
||||
## Spec refs: D-061 (adjacent to dialogue spec), D-041 (character-filtered observation)
|
||||
##
|
||||
## ExamineDisplay: non-interactive overlay, diegetic, auto-dismisses after DISMISS_DELAY.
|
||||
## Positioned in InsertOverlay (CanvasLayer 10).
|
||||
## GameState.current_examine_result: cleared every snapshot (unlike player_knowledge).
|
||||
##
|
||||
## Tests run against live Stig implementation (examine_display.gd).
|
||||
class_name TestExamineDisplaySprint18
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const EXAMINE_SCENE_PATH: String = "res://ui/examine_display.tscn"
|
||||
|
||||
func _make_examine_display() -> Control:
|
||||
if not ResourceLoader.exists(EXAMINE_SCENE_PATH):
|
||||
push_warning("TestExamineDisplaySprint18: examine_display.tscn not found — skip")
|
||||
return null
|
||||
var node: Control = load(EXAMINE_SCENE_PATH).instantiate()
|
||||
add_child(node)
|
||||
return node
|
||||
|
||||
|
||||
func _make_result(overrides: Dictionary = {}) -> Dictionary:
|
||||
var base: Dictionary = {
|
||||
"entity_id": 42,
|
||||
"text": "Kael Davan — nervous energy. He's scanning exits.",
|
||||
"confidence": "KnowsOf",
|
||||
}
|
||||
base.merge(overrides, true)
|
||||
return base
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func before_test() -> void:
|
||||
GameState.current_examine_result = null
|
||||
|
||||
func after_test() -> void:
|
||||
GameState.current_examine_result = null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GameState: current_examine_result parsing
|
||||
## (Now tests real implementation — not test-first stubs)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_gamestate_examine_result_field_exists() -> void:
|
||||
assert_bool(GameState.has("current_examine_result")).override_failure_message(
|
||||
"GameState must have 'current_examine_result' field (#174)"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_gamestate_examine_result_null_by_default() -> void:
|
||||
GameState.current_examine_result = null
|
||||
assert_that(GameState.current_examine_result).is_null()
|
||||
|
||||
|
||||
func test_gamestate_examine_result_set_from_snapshot() -> void:
|
||||
GameState.apply_snapshot({"tick": 5, "examine_result": _make_result()})
|
||||
assert_that(GameState.current_examine_result).is_not_null()
|
||||
assert_that(GameState.current_examine_result.get("text")).contains("Kael Davan")
|
||||
|
||||
|
||||
func test_gamestate_examine_result_null_when_absent() -> void:
|
||||
## CONTRAST with player_knowledge: examine_result DOES clear each snapshot.
|
||||
## The overlay must auto-dismiss — the server never re-sends the same result.
|
||||
GameState.current_examine_result = _make_result()
|
||||
GameState.apply_snapshot({"tick": 6})
|
||||
assert_that(GameState.current_examine_result).is_null()
|
||||
|
||||
|
||||
func test_gamestate_examine_result_null_when_non_dict() -> void:
|
||||
GameState.apply_snapshot({"tick": 1, "examine_result": "bad-value"})
|
||||
assert_that(GameState.current_examine_result).is_null()
|
||||
|
||||
|
||||
func test_gamestate_examine_result_entity_id_survives_roundtrip() -> void:
|
||||
GameState.apply_snapshot({"tick": 1, "examine_result": _make_result({"entity_id": 99})})
|
||||
assert_int(GameState.current_examine_result.get("entity_id", -1)).is_equal(99)
|
||||
|
||||
|
||||
func test_gamestate_examine_result_confidence_survives_roundtrip() -> void:
|
||||
GameState.apply_snapshot({"tick": 1, "examine_result": _make_result({"confidence": "Direct"})})
|
||||
assert_that(GameState.current_examine_result.get("confidence")).is_equal("Direct")
|
||||
|
||||
|
||||
func test_gamestate_examine_result_replaced_on_next_snapshot() -> void:
|
||||
## Two examine results in sequence — second replaces first.
|
||||
GameState.apply_snapshot({"tick": 1, "examine_result": _make_result({"text": "First observation."})})
|
||||
GameState.apply_snapshot({"tick": 2, "examine_result": _make_result({"text": "Second observation."})})
|
||||
assert_that(GameState.current_examine_result.get("text")).is_equal("Second observation.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ExamineDisplay scene
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_examine_display_scene_exists() -> void:
|
||||
assert_bool(ResourceLoader.exists(EXAMINE_SCENE_PATH)).override_failure_message(
|
||||
"ExamineDisplay scene must exist at res://ui/examine_display.tscn"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_examine_display_instantiates_without_crash() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_that(display).is_not_null()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_has_show_result_method() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_bool(display.has_method("show_result")).override_failure_message(
|
||||
"ExamineDisplay must have show_result(result: Dictionary) method"
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_has_dismiss_method() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_bool(display.has_method("dismiss")).override_failure_message(
|
||||
"ExamineDisplay must have dismiss() method"
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_has_is_active_method() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_bool(display.has_method("is_active")).override_failure_message(
|
||||
"ExamineDisplay must have is_active() method"
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ExamineDisplay behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_examine_display_not_active_on_init() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_bool(display.is_active()).override_failure_message(
|
||||
"ExamineDisplay must start inactive (no result showing)"
|
||||
).is_false()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_not_visible_on_init() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_bool(display.visible).override_failure_message(
|
||||
"ExamineDisplay must start invisible"
|
||||
).is_false()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_active_after_show_result() -> void:
|
||||
## show_result() with valid text sets is_active() = true.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.show_result(_make_result())
|
||||
assert_bool(display.is_active()).override_failure_message(
|
||||
"show_result() with text must set is_active() = true"
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_visible_after_show_result() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.show_result(_make_result())
|
||||
assert_bool(display.visible).override_failure_message(
|
||||
"show_result() must set visible = true"
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_empty_text_ignored() -> void:
|
||||
## show_result() with empty text must not activate (D-041: no empty observations).
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.show_result({"entity_id": 1, "text": "", "confidence": "KnowsOf"})
|
||||
assert_bool(display.is_active()).override_failure_message(
|
||||
"show_result() with empty text must not activate the display"
|
||||
).is_false()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_inactive_after_dismiss() -> void:
|
||||
## dismiss() immediately starts fade-out and sets _active = false.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.show_result(_make_result())
|
||||
assert_bool(display.is_active()).is_true()
|
||||
display.dismiss()
|
||||
assert_bool(display.is_active()).override_failure_message(
|
||||
"dismiss() must set is_active() = false immediately"
|
||||
).is_false()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_dismiss_when_inactive_no_crash() -> void:
|
||||
## dismiss() on an inactive display must be safe (no crash, no state corruption).
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.dismiss() # called when not active
|
||||
assert_bool(display.is_active()).is_false()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_examine_display_show_replaces_previous() -> void:
|
||||
## Second show_result() replaces first (only one result at a time).
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
display.show_result(_make_result({"text": "First observation."}))
|
||||
display.show_result(_make_result({"text": "Second observation."}))
|
||||
assert_bool(display.is_active()).override_failure_message(
|
||||
"show_result() called twice must leave display active"
|
||||
).is_true()
|
||||
## Text label should reflect the second result
|
||||
var text_label := display.get_node_or_null("PanelContainer/MarginContainer/TextLabel")
|
||||
if text_label is RichTextLabel:
|
||||
assert_that(text_label.text).override_failure_message(
|
||||
"Second show_result() must replace the displayed text"
|
||||
).is_equal("Second observation.")
|
||||
display.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ExamineDisplay: DISMISS_DELAY within spec
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_dismiss_delay_within_spec() -> void:
|
||||
## Spec (sprint-18/client.md): auto-dismisses after 4–6 seconds.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_float(display.DISMISS_DELAY).override_failure_message(
|
||||
"DISMISS_DELAY must be 4–6 seconds per spec"
|
||||
).is_between(4.0, 6.0)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ExamineDisplay: CONFIDENCE_ALPHA — confidence-based alpha modulation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_confidence_alpha_dict_covers_all_levels() -> void:
|
||||
## All four confidence levels must have alpha mappings.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
var alpha_dict: Dictionary = display.CONFIDENCE_ALPHA
|
||||
for level in ["Direct", "KnowsDetails", "KnowsOf", "Suspects"]:
|
||||
assert_bool(alpha_dict.has(level)).override_failure_message(
|
||||
"CONFIDENCE_ALPHA must map '%s'" % level
|
||||
).is_true()
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_confidence_alpha_direct_is_highest() -> void:
|
||||
## Direct confidence = brightest (alpha 1.0). Character fully trusts this observation.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
var alpha_dict: Dictionary = display.CONFIDENCE_ALPHA
|
||||
assert_float(alpha_dict.get("Direct", 0.0)).override_failure_message(
|
||||
"Direct confidence must have alpha 1.0 (brightest)"
|
||||
).is_equal_approx(1.0, 0.001)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_confidence_alpha_suspects_is_lowest() -> void:
|
||||
## Suspects = most dimmed (lowest alpha). Uncertainty is visually represented.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
var alpha_dict: Dictionary = display.CONFIDENCE_ALPHA
|
||||
var suspects_alpha: float = alpha_dict.get("Suspects", 1.0)
|
||||
var direct_alpha: float = alpha_dict.get("Direct", 0.0)
|
||||
assert_float(suspects_alpha).override_failure_message(
|
||||
"Suspects alpha must be less than Direct alpha (dimmer = less certain)"
|
||||
).is_less(direct_alpha)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_confidence_alpha_all_values_valid() -> void:
|
||||
## All alpha values must be in [0.0, 1.0].
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
for key in display.CONFIDENCE_ALPHA:
|
||||
var alpha: float = display.CONFIDENCE_ALPHA[key]
|
||||
assert_float(alpha).override_failure_message(
|
||||
"CONFIDENCE_ALPHA['%s'] = %.2f must be in [0, 1]" % [key, alpha]
|
||||
).is_between(0.0, 1.0)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-interactive: mouse_filter must be IGNORE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_examine_display_mouse_filter_ignore() -> void:
|
||||
## ExamineDisplay is non-interactive — must not consume mouse events.
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_int(display.mouse_filter).override_failure_message(
|
||||
"ExamineDisplay must have mouse_filter=IGNORE (non-interactive overlay)"
|
||||
).is_equal(Control.MOUSE_FILTER_IGNORE)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fade constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_fade_in_is_short() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_float(display.FADE_IN).is_between(0.0, 0.5)
|
||||
display.queue_free()
|
||||
|
||||
|
||||
func test_fade_out_is_short() -> void:
|
||||
var display := _make_examine_display()
|
||||
if display == null: return
|
||||
assert_float(display.FADE_OUT).is_between(0.0, 0.5)
|
||||
display.queue_free()
|
||||
@@ -0,0 +1,320 @@
|
||||
## Sprint 18 — Minimap rendering (#151)
|
||||
## Spec refs: D-013 (diegetic insert/POI system), D-015 (fixed-north, player-centered),
|
||||
## D-049 (z-layer 6 = InsertOverlay)
|
||||
##
|
||||
## MinimapRenderer: circular insert overlay, always renders frame, draws discovered POIs.
|
||||
## Scene: res://ui/minimap.tscn (class_name MinimapRenderer)
|
||||
## Positioned at InsertOverlay/Minimap in main.tscn.
|
||||
##
|
||||
## Tests run against live Stig implementation (minimap.gd).
|
||||
class_name TestMinimapSprint18
|
||||
extends GdUnitTestSuite
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const MINIMAP_SCENE_PATH: String = "res://ui/minimap.tscn"
|
||||
|
||||
func _make_minimap() -> Control:
|
||||
if not ResourceLoader.exists(MINIMAP_SCENE_PATH):
|
||||
push_warning("TestMinimapSprint18: minimap.tscn not found — skip")
|
||||
return null
|
||||
var node: Control = load(MINIMAP_SCENE_PATH).instantiate()
|
||||
add_child(node)
|
||||
return node
|
||||
|
||||
|
||||
func _make_poi(overrides: Dictionary = {}) -> Dictionary:
|
||||
var base: Dictionary = {
|
||||
"id": "poi_test_001",
|
||||
"x": 20,
|
||||
"y": 15,
|
||||
"poi_category": "location",
|
||||
"label": "Exit A",
|
||||
}
|
||||
base.merge(overrides, true)
|
||||
return base
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func before_test() -> void:
|
||||
GameState.discovered_pois = []
|
||||
GameState.player_position = Vector2(10.0, 10.0)
|
||||
GameState.insert_active = true
|
||||
|
||||
func after_test() -> void:
|
||||
GameState.discovered_pois = []
|
||||
GameState.player_position = Vector2.ZERO
|
||||
GameState.insert_active = true
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene and class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_minimap_scene_exists() -> void:
|
||||
assert_bool(ResourceLoader.exists(MINIMAP_SCENE_PATH)).override_failure_message(
|
||||
"Minimap scene must exist at res://ui/minimap.tscn (#151)"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_minimap_instantiates_without_crash() -> void:
|
||||
var mm := _make_minimap()
|
||||
if mm == null: return
|
||||
assert_that(mm).is_not_null()
|
||||
mm.queue_free()
|
||||
|
||||
|
||||
func test_minimap_is_minimap_renderer_class() -> void:
|
||||
## class_name MinimapRenderer in minimap.gd.
|
||||
var mm := _make_minimap()
|
||||
if mm == null: return
|
||||
assert_bool(mm is MinimapRenderer).override_failure_message(
|
||||
"Minimap node must be a MinimapRenderer instance (check class_name in minimap.gd)"
|
||||
).is_true()
|
||||
mm.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants: D-015, visual parameters
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_minimap_radius_constant() -> void:
|
||||
## MINIMAP_RADIUS defines the sim-tile distance of visible POI area.
|
||||
## Value is tuned to 24 tiles — reasonable coverage without map reveal.
|
||||
assert_float(MinimapRenderer.MINIMAP_RADIUS).override_failure_message(
|
||||
"MinimapRenderer.MINIMAP_RADIUS must be 24.0"
|
||||
).is_equal_approx(24.0, 0.01)
|
||||
|
||||
|
||||
func test_player_dot_radius_defined() -> void:
|
||||
## Player dot must be visible (> 0) and distinct from POI dot.
|
||||
assert_float(MinimapRenderer.PLAYER_DOT_RADIUS).is_greater(0.0)
|
||||
|
||||
|
||||
func test_poi_dot_radius_defined() -> void:
|
||||
## POI dot must be visible (> 0).
|
||||
assert_float(MinimapRenderer.POI_DOT_RADIUS).is_greater(0.0)
|
||||
|
||||
|
||||
func test_player_dot_larger_than_poi_dot() -> void:
|
||||
## D-015: Player is always centered and visually distinct.
|
||||
## Player dot should be at least as large as POI dot.
|
||||
assert_float(MinimapRenderer.PLAYER_DOT_RADIUS).is_greater_equal(MinimapRenderer.POI_DOT_RADIUS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _category_color() — D-013 POI category color mapping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_category_color_danger_is_hostile_color() -> void:
|
||||
## "danger", "threat", "hostile" → ENTITY_COLOR_HOSTILE (red)
|
||||
for cat in ["danger", "threat", "hostile"]:
|
||||
var color: Color = MinimapRenderer._category_color(cat)
|
||||
assert_that(color).override_failure_message(
|
||||
"Category '%s' must map to ENTITY_COLOR_HOSTILE" % cat
|
||||
).is_equal(Constants.ENTITY_COLOR_HOSTILE)
|
||||
|
||||
|
||||
func test_category_color_evidence_is_poi_color() -> void:
|
||||
## "evidence", "note", "clue" → ENTITY_COLOR_POI (amber)
|
||||
for cat in ["evidence", "note", "clue"]:
|
||||
var color: Color = MinimapRenderer._category_color(cat)
|
||||
assert_that(color).override_failure_message(
|
||||
"Category '%s' must map to ENTITY_COLOR_POI (amber)" % cat
|
||||
).is_equal(Constants.ENTITY_COLOR_POI)
|
||||
|
||||
|
||||
func test_category_color_contact_is_unknown_color() -> void:
|
||||
## "contact", "npc", "person" → ENTITY_COLOR_UNKNOWN (teal)
|
||||
for cat in ["contact", "npc", "person"]:
|
||||
var color: Color = MinimapRenderer._category_color(cat)
|
||||
assert_that(color).override_failure_message(
|
||||
"Category '%s' must map to ENTITY_COLOR_UNKNOWN (teal)" % cat
|
||||
).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
|
||||
|
||||
|
||||
func test_category_color_unknown_category_defaults_to_insert_text() -> void:
|
||||
## Unknown/unspecified categories → INSERT_COLOR_TEXT (white-blue default)
|
||||
var color: Color = MinimapRenderer._category_color("some_unknown_type")
|
||||
assert_that(color).override_failure_message(
|
||||
"Unknown category must default to INSERT_COLOR_TEXT"
|
||||
).is_equal(Constants.INSERT_COLOR_TEXT)
|
||||
|
||||
|
||||
func test_category_color_empty_string_defaults() -> void:
|
||||
## Empty category string → default color, no crash.
|
||||
var color: Color = MinimapRenderer._category_color("")
|
||||
assert_that(color).is_equal(Constants.INSERT_COLOR_TEXT)
|
||||
|
||||
|
||||
func test_category_color_case_insensitive() -> void:
|
||||
## Category matching is case-insensitive (uses to_lower()).
|
||||
var danger_lower := MinimapRenderer._category_color("danger")
|
||||
var danger_upper := MinimapRenderer._category_color("DANGER")
|
||||
var danger_mixed := MinimapRenderer._category_color("Danger")
|
||||
assert_that(danger_lower).is_equal(danger_upper)
|
||||
assert_that(danger_lower).is_equal(danger_mixed)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# set_insert_active() — D-049: insert layer visibility
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_set_insert_active_false_hides_minimap() -> void:
|
||||
## When insert is inactive, minimap must be hidden.
|
||||
var mm := _make_minimap()
|
||||
if mm == null: return
|
||||
mm.set_insert_active(false)
|
||||
assert_bool(mm.visible).override_failure_message(
|
||||
"set_insert_active(false) must hide the minimap"
|
||||
).is_false()
|
||||
mm.queue_free()
|
||||
|
||||
|
||||
func test_set_insert_active_true_shows_minimap() -> void:
|
||||
## When insert is active, minimap must be visible.
|
||||
var mm := _make_minimap()
|
||||
if mm == null: return
|
||||
mm.set_insert_active(false)
|
||||
mm.set_insert_active(true)
|
||||
assert_bool(mm.visible).override_failure_message(
|
||||
"set_insert_active(true) must show the minimap"
|
||||
).is_true()
|
||||
mm.queue_free()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main scene structural check: InsertOverlay/Minimap
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_minimap_in_main_scene_on_insert_overlay() -> void:
|
||||
## D-049: Minimap must be in InsertOverlay (CanvasLayer 10), not UILayer.
|
||||
## Scene path: Game/InsertOverlay/Minimap or InsertOverlay/Minimap.
|
||||
if not ResourceLoader.exists("res://scenes/main.tscn"):
|
||||
push_warning("TestMinimapSprint18: main.tscn not found — scene tree test skipped")
|
||||
return
|
||||
var scene: Node = load("res://scenes/main.tscn").instantiate()
|
||||
auto_free(scene)
|
||||
add_child(scene)
|
||||
|
||||
# Check for Minimap in InsertOverlay
|
||||
var insert_overlay := scene.get_node_or_null("InsertOverlay")
|
||||
assert_that(insert_overlay != null).override_failure_message(
|
||||
"InsertOverlay (CanvasLayer 10) must exist in main.tscn"
|
||||
).is_true()
|
||||
if insert_overlay == null: return
|
||||
|
||||
var minimap := insert_overlay.get_node_or_null("Minimap")
|
||||
assert_that(minimap != null).override_failure_message(
|
||||
"Minimap must be a child of InsertOverlay in main.tscn (D-049: insert layer)"
|
||||
).is_true()
|
||||
if minimap == null: return
|
||||
|
||||
assert_bool(minimap is MinimapRenderer).override_failure_message(
|
||||
"InsertOverlay/Minimap must be a MinimapRenderer instance"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_insert_overlay_is_canvas_layer_10() -> void:
|
||||
## InsertOverlay must be CanvasLayer 10 (CANVAS_INSERT per D-049).
|
||||
if not ResourceLoader.exists("res://scenes/main.tscn"):
|
||||
push_warning("TestMinimapSprint18: main.tscn not found — canvas layer test skipped")
|
||||
return
|
||||
var scene: Node = load("res://scenes/main.tscn").instantiate()
|
||||
auto_free(scene)
|
||||
add_child(scene)
|
||||
|
||||
var insert_overlay := scene.get_node_or_null("InsertOverlay") as CanvasLayer
|
||||
if insert_overlay == null: return
|
||||
assert_int(insert_overlay.layer).override_failure_message(
|
||||
"InsertOverlay must be CanvasLayer %d (CANVAS_INSERT)" % Constants.CANVAS_INSERT
|
||||
).is_equal(Constants.CANVAS_INSERT)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GameState.discovered_pois integration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_discovered_pois_field_exists_in_gamestate() -> void:
|
||||
assert_bool(GameState.has("discovered_pois")).override_failure_message(
|
||||
"GameState must have 'discovered_pois' field (#151)"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_discovered_pois_set_from_poi_list_snapshot() -> void:
|
||||
## Snapshot with "poi_list" key (Sprint 17 server wire name) populates discovered_pois.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"poi_list": [
|
||||
_make_poi({"id": "p1", "x": 50, "y": 30, "poi_category": "location"}),
|
||||
_make_poi({"id": "p2", "x": 80, "y": 15, "poi_category": "contact"}),
|
||||
],
|
||||
})
|
||||
assert_int(GameState.discovered_pois.size()).override_failure_message(
|
||||
"discovered_pois must be populated from snapshot 'poi_list' field"
|
||||
).is_equal(2)
|
||||
|
||||
|
||||
func test_discovered_pois_set_from_discovered_pois_snapshot() -> void:
|
||||
## Snapshot with "discovered_pois" key also works.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 2,
|
||||
"discovered_pois": [_make_poi()],
|
||||
})
|
||||
assert_int(GameState.discovered_pois.size()).is_equal(1)
|
||||
|
||||
|
||||
func test_discovered_pois_persists_when_absent_from_snapshot() -> void:
|
||||
## Like player_knowledge: POI list persists when server doesn't send an update.
|
||||
GameState.discovered_pois = [_make_poi()]
|
||||
GameState.apply_snapshot({"tick": 3})
|
||||
assert_int(GameState.discovered_pois.size()).override_failure_message(
|
||||
"discovered_pois must persist when absent from snapshot (not cleared each tick)"
|
||||
).is_equal(1)
|
||||
|
||||
|
||||
func test_discovered_pois_poi_category_field_present() -> void:
|
||||
## MinimapRenderer reads poi_category to determine shape/color.
|
||||
## Verify the wire format includes this field.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"poi_list": [_make_poi({"poi_category": "danger"})],
|
||||
})
|
||||
assert_int(GameState.discovered_pois.size()).is_greater(0)
|
||||
var first_poi: Dictionary = GameState.discovered_pois[0]
|
||||
assert_bool(first_poi.has("poi_category")).override_failure_message(
|
||||
"POI entries must have 'poi_category' field for MinimapRenderer shape selection"
|
||||
).is_true()
|
||||
|
||||
|
||||
func test_discovered_pois_x_y_fields_present() -> void:
|
||||
## MinimapRenderer reads x, y for position calculation.
|
||||
GameState.apply_snapshot({
|
||||
"tick": 1,
|
||||
"poi_list": [_make_poi({"x": 42, "y": 17})],
|
||||
})
|
||||
assert_int(GameState.discovered_pois.size()).is_greater(0)
|
||||
var first_poi: Dictionary = GameState.discovered_pois[0]
|
||||
assert_bool(first_poi.has("x") and first_poi.has("y")).override_failure_message(
|
||||
"POI entries must have 'x' and 'y' coordinate fields"
|
||||
).is_true()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Color constants: all distinct
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func test_category_colors_are_distinct() -> void:
|
||||
## All three primary category color groups must be visually distinct.
|
||||
var danger_color := MinimapRenderer._category_color("danger")
|
||||
var evidence_color := MinimapRenderer._category_color("evidence")
|
||||
var contact_color := MinimapRenderer._category_color("contact")
|
||||
assert_that(danger_color).is_not_equal(evidence_color)
|
||||
assert_that(evidence_color).is_not_equal(contact_color)
|
||||
assert_that(danger_color).is_not_equal(contact_color)
|
||||
Reference in New Issue
Block a user