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