feat(ui): minimap rendering with POI dots and border arrows (#151)
Circular 160px diegetic insert overlay on CanvasLayer 10. Nearby POIs as colored dots (diamond=danger, square=evidence, circle=default), distant POIs as directional border arrows. Player centered, fixed-north. Moved from UILayer to InsertOverlay per D-013/D-049. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+12
-3
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+166
-9
@@ -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
|
||||
|
||||
+11
-23
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user