Fix 354 gdlint warnings across 65 files: 194 class-definitions-order (reorder declarations), 138 max-line-length (split long lines), 22 code issues (unused args, no-else-return, naming). Update .gdlintrc to exclude addons/ and raise max-public-methods for test files. No logic changes — declaration order, whitespace, and naming only. Ticket: #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
322 lines
12 KiB
GDScript
322 lines
12 KiB
GDScript
## 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"
|
|
const MAIN_SCENE = preload("res://scenes/main.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 = MAIN_SCENE.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 = MAIN_SCENE.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)
|