- Fix stale test path UILayer/HUD → InsertOverlay/HUD - Fix D-record citation D-051 → D-049 in main.tscn - Add null guards to hud.gd update methods (pre-_ready safety) - Minimap: dirty-flag queue_redraw instead of per-frame - Remove redundant _panel.size.x, debug print - Fix DebugOverlay/GauntletHUD positioning comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
228 lines
7.4 KiB
GDScript
228 lines
7.4 KiB
GDScript
class_name MinimapRenderer
|
|
extends Control
|
|
|
|
## 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)
|
|
|
|
## 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
|
|
# D-169: StyleBoxFlat matching ImplantPanel aesthetic — drawn as container frame behind the circle.
|
|
var _container_style: StyleBoxFlat = null
|
|
|
|
# Dirty flag — only queue_redraw() when player position or POI list changes.
|
|
var _dirty: bool = true
|
|
var _last_player_pos: Vector2 = Vector2(INF, INF)
|
|
var _last_poi_count: int = -1
|
|
|
|
|
|
func _ready() -> void:
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
set_process(true)
|
|
_init_container_style()
|
|
|
|
|
|
func _init_container_style() -> void:
|
|
var t := load("res://ui/implant/default_implant.tres") as ImplantTheme
|
|
if not t:
|
|
return
|
|
_container_style = StyleBoxFlat.new()
|
|
_container_style.bg_color = t.panel_bg
|
|
_container_style.border_color = t.separator
|
|
_container_style.set_border_width_all(int(t.border_width))
|
|
_container_style.set_content_margin_all(0)
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if not _insert_active:
|
|
return
|
|
var pos := GameState.player_position
|
|
var poi_count := GameState.discovered_pois.size()
|
|
if pos != _last_player_pos or poi_count != _last_poi_count:
|
|
_last_player_pos = pos
|
|
_last_poi_count = poi_count
|
|
_dirty = true
|
|
if _dirty:
|
|
_dirty = false
|
|
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
|
|
if active:
|
|
_dirty = true
|
|
|
|
|
|
func _draw() -> void:
|
|
# D-169: ImplantPanel-style container frame behind the circular minimap.
|
|
# Gives the minimap the same panel border aesthetic as other implant components.
|
|
if _container_style:
|
|
_container_style.draw(get_canvas_item(), Rect2(Vector2.ZERO, get_rect().size))
|
|
|
|
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
|