diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn index 5362fca82..30c3381b7 100644 --- a/client/scenes/main.tscn +++ b/client/scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=3 uid="uid://bswrmh7w8dbgm"] +[gd_scene load_steps=26 format=3 uid="uid://bswrmh7w8dbgm"] [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] [ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"] @@ -23,6 +23,8 @@ [ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"] [ext_resource type="Script" path="res://scripts/ui/debug_overlay.gd" id="22_debug"] [ext_resource type="PackedScene" path="res://ui/time_display.tscn" id="23_tdisplay"] +[ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"] +[ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"] [node name="Game" type="Node2D"] script = ExtResource("1_main") @@ -134,6 +136,15 @@ layer = 10 ; D-061: Dialogue box — bottom screen, max 20% height, diegetic insert UI [node name="DialogueBox" parent="InsertOverlay" instance=ExtResource("15_dialogue")] +; #151: Minimap — diegetic insert overlay, top-right, 160px circle (D-013, D-049 z-layer 6) +[node name="Minimap" parent="InsertOverlay" instance=ExtResource("7_minimap")] + +; #174: Examine result — non-interactive observe text overlay, auto-dismisses 5s (D-061 adjacent) +[node name="ExamineDisplay" parent="InsertOverlay" instance=ExtResource("24_examine")] + +; #264: Journal panel — knowledge graph review, toggle J key, read-only (D-041) +[node name="JournalPanel" parent="InsertOverlay" instance=ExtResource("25_journal")] + ; --- UI layer (CanvasLayer 20) --- ; HUD, monologue, cursor — always visible, not affected by fog or camera. [node name="UILayer" type="CanvasLayer" parent="."] @@ -141,8 +152,6 @@ layer = 20 [node name="HUD" parent="UILayer" instance=ExtResource("6_hud")] -[node name="Minimap" parent="UILayer" instance=ExtResource("7_minimap")] - [node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")] ; D-053: Stance indicator — top-right, color-coded diff --git a/client/tests/test_ui_framework_sprint15.gd b/client/tests/test_ui_framework_sprint15.gd index 0efb6929e..6b77cf4a9 100644 --- a/client/tests/test_ui_framework_sprint15.gd +++ b/client/tests/test_ui_framework_sprint15.gd @@ -110,13 +110,14 @@ func test_stance_indicator_exists_in_ui_layer() -> void: func test_minimap_placeholder_exists_in_ui_layer() -> void: - # D-013: Minimap/insert placeholder must be in UILayer (not implemented yet). + # D-013/D-049: Minimap is on InsertOverlay (z-layer 6), NOT UILayer. + # Sprint 18 #151 (Stig): moved from UILayer to InsertOverlay per D-049 spec. var scene := load("res://scenes/main.tscn") _instance = scene.instantiate() auto_free(_instance) add_child(_instance) - assert_that(_instance.get_node_or_null("UILayer/Minimap")).is_not_null() + assert_that(_instance.get_node_or_null("InsertOverlay/Minimap")).is_not_null() func test_hud_exists_in_ui_layer() -> void: diff --git a/client/ui/minimap.gd b/client/ui/minimap.gd index 327471267..0f27ee575 100644 --- a/client/ui/minimap.gd +++ b/client/ui/minimap.gd @@ -1,15 +1,172 @@ +class_name MinimapRenderer extends Control -# Minimap — small viewport showing local area -# Placeholder implementation for now +## Minimap — diegetic neural insert overlay. D-013, D-049 z-layer 6. +## +## Player always centered. Fixed-north (no rotation per D-015). +## Nearby POIs (within MINIMAP_RADIUS sim tiles): colored dot/shape at scaled position. +## Distant POIs (beyond radius): directional arrow at circle border pointing toward POI. +## Frame renders always — the insert is on even when no POIs are discovered. +## +## POI categories → shapes: +## danger / threat / hostile → diamond (ENTITY_COLOR_HOSTILE, red) +## evidence / note / clue → square (ENTITY_COLOR_POI, amber) +## contact / npc / person → circle (ENTITY_COLOR_UNKNOWN, teal) +## location / place / venue → circle (INSERT_COLOR_TEXT, white-blue) +## (default) → circle (INSERT_COLOR_TEXT) -@onready var viewport_container: SubViewportContainer = $SubViewportContainer +## Sim tiles visible within the minimap circle. POIs beyond this show as border arrows. +const MINIMAP_RADIUS: float = 24.0 + +# Visual parameters +const FRAME_WIDTH: float = 1.2 +const PLAYER_DOT_RADIUS: float = 3.5 +const POI_DOT_RADIUS: float = 3.0 +const ARROW_HALF: float = 4.5 +const ARROW_LEN: float = 7.0 +const NORTH_TICK_LEN: float = 8.0 +const CARDINAL_TICK_LEN: float = 4.0 + +# Colors — insert palette from Constants, tuned for the circular minimap frame +const COLOR_BG: Color = Color(0.04, 0.07, 0.12, 0.82) +const COLOR_FRAME: Color = Color(0.784, 0.816, 0.878, 0.55) # INSERT_COLOR_TEXT at reduced alpha +const COLOR_NORTH: Color = Color(0.784, 0.816, 0.878, 0.9) # Brighter for N tick +const COLOR_CARDINAL: Color = Color(0.784, 0.816, 0.878, 0.4) # Dimmer E/S/W ticks +const COLOR_PLAYER: Color = Constants.ENTITY_COLOR_PLAYER + +var _insert_active: bool = true func _ready() -> void: - print("Minimap: Initialized") + mouse_filter = Control.MOUSE_FILTER_IGNORE + set_process(true) -# Update minimap view (stub) -func update_minimap(player_pos: Vector2, entities: Array) -> void: - # TODO: Render minimap view - # This will show a top-down view of the local area - pass +func _process(_delta: float) -> void: + if _insert_active: + queue_redraw() + +## Called from main.gd when GameState.insert_active changes. +## Hides the minimap overlay when the neural insert is inactive. +func set_insert_active(active: bool) -> void: + _insert_active = active + visible = active + +func _draw() -> void: + var sz: Vector2 = get_rect().size + var center := sz / 2.0 + # Outer radius: fill control with 1px edge padding + var outer_r: float = minf(sz.x, sz.y) / 2.0 - 1.0 + + # --- Background fill --- + draw_circle(center, outer_r, COLOR_BG) + + # --- Frame ring --- + draw_arc(center, outer_r, 0.0, TAU, 64, COLOR_FRAME, FRAME_WIDTH, true) + + # --- Cardinal ticks (N brighter, E/S/W dimmer) --- + _draw_cardinal_ticks(center, outer_r) + + # --- Player dot at center --- + draw_circle(center, PLAYER_DOT_RADIUS, COLOR_PLAYER) + # Soft bloom ring + draw_arc(center, PLAYER_DOT_RADIUS + 1.5, 0.0, TAU, 32, + Color(COLOR_PLAYER.r, COLOR_PLAYER.g, COLOR_PLAYER.b, 0.22), 1.0, true) + + # --- POIs --- + var pois: Array = GameState.discovered_pois + if pois.is_empty(): + return + + var px: float = GameState.player_position.x + var py: float = GameState.player_position.y + # Pixels per sim tile within the inner drawable area + var inner_r: float = outer_r - FRAME_WIDTH + var scale: float = inner_r / MINIMAP_RADIUS + + for poi in pois: + if not poi is Dictionary: + continue + if not poi.has("x") or not poi.has("y"): + continue + + var dx: float = float(poi.x) - px + var dy: float = float(poi.y) - py + var dist: float = sqrt(dx * dx + dy * dy) + var category: String = poi.get("poi_category", "") + var color: Color = _category_color(category) + + if dist < 0.01: + # POI at exact player position — draw at center offset slightly + _draw_poi_shape(center + Vector2(0.0, -POI_DOT_RADIUS - 2.0), color, category) + elif dist <= MINIMAP_RADIUS: + # Nearby: project to screen position within circle + var poi_screen := center + Vector2(dx, dy) * scale + # Hard-clamp to inner circle boundary (guards floating-point edge cases) + var rel := poi_screen - center + if rel.length() > inner_r - POI_DOT_RADIUS - 1.0: + poi_screen = center + rel.normalized() * (inner_r - POI_DOT_RADIUS - 1.0) + _draw_poi_shape(poi_screen, color, category) + else: + # Distant: arrow at border pointing toward POI direction + var dir := Vector2(dx, dy).normalized() + var arrow_tip := center + dir * (inner_r - 2.0) + _draw_border_arrow(arrow_tip, dir, color) + + +func _draw_cardinal_ticks(center: Vector2, outer_r: float) -> void: + # North tick — longer, brighter, the fixed-north indicator + var n_dir := Vector2(0.0, -1.0) + draw_line( + center + n_dir * (outer_r - NORTH_TICK_LEN), + center + n_dir * outer_r, + COLOR_NORTH, FRAME_WIDTH + 0.5, true + ) + # East (PI/2), South (PI), West (3PI/2) — shorter, dimmer + for angle in [PI / 2.0, PI, 3.0 * PI / 2.0]: + var dir := Vector2(cos(angle), sin(angle)) + draw_line( + center + dir * (outer_r - CARDINAL_TICK_LEN), + center + dir * outer_r, + COLOR_CARDINAL, FRAME_WIDTH, true + ) + + +func _draw_poi_shape(pos: Vector2, color: Color, category: String) -> void: + match category.to_lower(): + "danger", "threat", "hostile": + # Diamond for danger + var s: float = POI_DOT_RADIUS + 1.0 + draw_polygon( + PackedVector2Array([ + pos + Vector2(0.0, -s), pos + Vector2(s, 0.0), + pos + Vector2(0.0, s), pos + Vector2(-s, 0.0) + ]), + PackedColorArray([color, color, color, color]) + ) + "evidence", "note", "clue": + # Square for evidence/clue + var s: float = POI_DOT_RADIUS - 0.5 + draw_rect(Rect2(pos - Vector2(s, s), Vector2(s * 2.0, s * 2.0)), color) + _: + draw_circle(pos, POI_DOT_RADIUS, color) + + +## Arrow tip at `tip`, pointing in `dir`. Arrow body extends ARROW_LEN back from tip. +func _draw_border_arrow(tip: Vector2, dir: Vector2, color: Color) -> void: + var perp := Vector2(-dir.y, dir.x) + var base_center := tip - dir * ARROW_LEN + draw_polygon( + PackedVector2Array([tip, base_center - perp * ARROW_HALF, base_center + perp * ARROW_HALF]), + PackedColorArray([color, color, color]) + ) + + +func _category_color(category: String) -> Color: + match category.to_lower(): + "danger", "threat", "hostile": + return Constants.ENTITY_COLOR_HOSTILE # #d45d5d — red + "evidence", "note", "clue": + return Constants.ENTITY_COLOR_POI # #e8c547 — amber + "contact", "npc", "person": + return Constants.ENTITY_COLOR_UNKNOWN # #4a9ebb — teal + _: + return Constants.INSERT_COLOR_TEXT # #c8d0e0 — white-blue diff --git a/client/ui/minimap.tscn b/client/ui/minimap.tscn index 9ce330777..8e34b766b 100644 --- a/client/ui/minimap.tscn +++ b/client/ui/minimap.tscn @@ -2,33 +2,21 @@ [ext_resource type="Script" path="res://ui/minimap.gd" id="1_minimap"] -[node name="Minimap" type="Control"] +; MinimapRenderer — diegetic neural insert overlay (D-013, D-049 z-layer 6). +; 160x160px circle, top-right corner with 16px margin. +; Rendered via _draw() — no SubViewport needed. +; Positioned in InsertOverlay (CanvasLayer 10) by main.tscn. + +[node name="MinimapRenderer" type="Control"] +custom_minimum_size = Vector2(160, 160) layout_mode = 3 anchors_preset = 1 anchor_left = 1.0 anchor_right = 1.0 -offset_left = -200.0 -offset_bottom = 200.0 +offset_left = -176.0 +offset_top = 16.0 +offset_right = -16.0 +offset_bottom = 176.0 grow_horizontal = 0 mouse_filter = 2 script = ExtResource("1_minimap") - -[node name="SubViewportContainer" type="SubViewportContainer" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="SubViewport" type="SubViewport" parent="SubViewportContainer"] -size = Vector2i(200, 200) -render_target_update_mode = 4 - -[node name="Background" type="ColorRect" parent="SubViewportContainer/SubViewport"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0.1, 0.1, 0.1, 0.7)