Files
settled-reach/client/ui/implant/atlas_marker_overlay.gd
T

350 lines
12 KiB
GDScript

class_name AtlasMarkerOverlay
extends Node2D
## Draws heightmap + markers for #835 AtlasViewer. Child of AtlasViewer._canvas
## so it inherits the pan/zoom transform. Draws in texture-native coordinates.
##
## Overlay layers (#836 toggles via viewer._overlay_visibility):
## terrain heightmap texture itself
## political_zones currency-zone color bands (coarse)
## infrastructure roads + railroads
## named_features labels for rivers, oceans, mountain ranges
## gate_markers gate-terminal POIs
## population_density (toggleable) density heatmap placeholder
## production_zones (toggleable) production zone outlines placeholder
## shadow_economy (toggleable) shadow-zone broad bands placeholder
## corp_presence (toggleable) Tier 1 corp dots placeholder
## stockpile_weeks (locked) gated by corporate contact
## production_vs_baseline (locked) gated by insider access
const COLOR_HEIGHTMAP_TINT: Color = Color(0.85, 0.88, 0.95, 1.0)
const COLOR_POLITICAL: Color = Color(0.25, 0.50, 0.75, 0.14)
const COLOR_ROAD: Color = Color(0.85, 0.60, 0.30, 0.85)
const COLOR_RAIL: Color = Color(0.45, 0.55, 0.70, 0.85)
const COLOR_CITY: Color = Color(0.94, 0.82, 0.38, 1.0)
const COLOR_CITY_HOVER: Color = Color(1.0, 1.0, 1.0, 1.0)
const COLOR_CITY_SELECTED: Color = Color(1.0, 0.95, 0.60, 1.0)
const COLOR_GATE: Color = Color(0.70, 0.88, 1.0, 1.0)
const COLOR_POI: Color = Color(0.45, 0.75, 0.85, 0.90)
const COLOR_FEATURE_LABEL: Color = Color(0.78, 0.82, 0.92, 0.65)
const COLOR_POP_HEAT: Color = Color(0.95, 0.35, 0.25, 0.22)
const COLOR_PRODUCTION: Color = Color(0.40, 0.80, 0.55, 0.18)
const COLOR_SHADOW: Color = Color(0.35, 0.20, 0.50, 0.22)
const COLOR_CORP: Color = Color(0.85, 0.65, 0.20, 0.75)
const RAIL_DASH_ON: float = 6.0
const RAIL_DASH_OFF: float = 4.0
const PRODUCTION_FUNCTIONS: Array = [
"industrial",
"industry",
"manufacturing",
"extraction",
"mining",
"refinery",
"foundry",
"shipyard",
"production",
]
var viewer = null # AtlasViewer (untyped to avoid cyclic ref)
func _draw() -> void:
if viewer == null:
return
var tex: Texture2D = viewer.get_heightmap_texture()
if tex == null:
return
var tex_w: float = float(tex.get_width())
var tex_h: float = float(tex.get_height())
var markers: Dictionary = viewer.get_markers()
# Terrain (heightmap) — always first
if viewer.is_overlay_visible("terrain"):
draw_texture_rect(
tex, Rect2(Vector2.ZERO, Vector2(tex_w, tex_h)), false, COLOR_HEIGHTMAP_TINT
)
else:
draw_rect(Rect2(Vector2.ZERO, Vector2(tex_w, tex_h)), Color(0.05, 0.07, 0.10, 1.0))
# Political zone tint (currency zone band — single tint over whole body for MVP)
if viewer.is_overlay_visible("political_zones"):
draw_rect(Rect2(Vector2.ZERO, Vector2(tex_w, tex_h)), COLOR_POLITICAL)
# Infrastructure (roads + rail)
if viewer.is_overlay_visible("infrastructure"):
_draw_roads(markers)
_draw_rails(markers)
# Named features (rivers, oceans, mountain ranges)
if viewer.is_overlay_visible("named_features"):
_draw_named_features(markers)
# Toggleable placeholders — drawn only when populated
if viewer.is_overlay_visible("population_density"):
_draw_population_density(markers)
if viewer.is_overlay_visible("production_zones"):
_draw_production_zones(markers)
if viewer.is_overlay_visible("shadow_economy"):
_draw_shadow_economy(markers)
if viewer.is_overlay_visible("corp_presence"):
_draw_corp_presence(markers)
# POIs (non-gate first, then gates on top if enabled)
_draw_pois(markers)
# Gate markers
if viewer.is_overlay_visible("gate_markers"):
_draw_gate_markers(markers)
# Cities last so labels sit on top
_draw_cities(markers)
# =============================================================================
# Feature drawing
# =============================================================================
func _draw_roads(markers: Dictionary) -> void:
var roads: Array = markers.get("roads", [])
for r: Dictionary in roads:
var path: Array = r.get("path", [])
if path.size() < 2:
continue
var points: PackedVector2Array = _path_to_canvas(path)
draw_polyline(points, COLOR_ROAD, 1.5, true)
func _draw_rails(markers: Dictionary) -> void:
var rails: Array = markers.get("railroads", [])
for r: Dictionary in rails:
var path: Array = r.get("path", [])
if path.size() < 2:
continue
var points: PackedVector2Array = _path_to_canvas(path)
_draw_dashed_polyline(points, COLOR_RAIL, 1.0)
func _draw_named_features(markers: Dictionary) -> void:
var font := ThemeDB.fallback_font
var fs: int = 8
for ocean: Dictionary in markers.get("oceans", []):
var label: String = ocean.get("name", "") if ocean.get("name") else ""
if label.is_empty():
continue
var p: Vector2 = _center_to_canvas(ocean.get("center"))
draw_string(
font, p, label.to_upper(), HORIZONTAL_ALIGNMENT_CENTER, -1, fs, COLOR_FEATURE_LABEL
)
for mtn: Dictionary in markers.get("mountain_ranges", []):
var label: String = mtn.get("name", "") if mtn.get("name") else ""
if label.is_empty():
continue
var p: Vector2 = _center_to_canvas(mtn.get("center"))
draw_string(font, p, label, HORIZONTAL_ALIGNMENT_CENTER, -1, fs, COLOR_FEATURE_LABEL)
for river: Dictionary in markers.get("rivers", []):
var label: String = river.get("name", "") if river.get("name") else ""
if label.is_empty():
continue
var p: Vector2 = _center_to_canvas(river.get("center"))
draw_string(font, p, label, HORIZONTAL_ALIGNMENT_CENTER, -1, fs, COLOR_FEATURE_LABEL)
func _draw_pois(markers: Dictionary) -> void:
var pois: Array = markers.get("pois", [])
for p: Dictionary in pois:
if bool(p.get("gate_terminal", false)):
continue # drawn by gate_markers layer
var pos: Vector2 = _poi_pos(p)
_draw_diamond(pos, 4.0, COLOR_POI)
func _draw_gate_markers(markers: Dictionary) -> void:
# Gates show up in both pois[] (kind="gate") and cities[].gate_terminal
for p: Dictionary in markers.get("pois", []):
if not bool(p.get("gate_terminal", false)):
continue
_draw_diamond(_poi_pos(p), 6.0, COLOR_GATE)
for c: Dictionary in markers.get("cities", []):
if not bool(c.get("gate_terminal", false)):
continue
var pos: Vector2 = viewer.city_canvas_pos(c)
draw_arc(pos, 9.0, 0.0, TAU, 18, COLOR_GATE, 1.2, true)
func _draw_cities(markers: Dictionary) -> void:
var cities: Array = markers.get("cities", [])
if cities.is_empty():
return
var font := ThemeDB.fallback_font
# Compare by stable key, not Dictionary.== — deep equality was O(fields)
# per city per redraw (review #13). `name` is unique per body in the
# D-191 §8 schema; fall back to `body_id` or the hash of the dict for
# un-named markers so the comparison still works in transitional data.
var hovered_key: String = _city_key(viewer.get_hovered_city())
var selected_key: String = _city_key(viewer.get_selected_city())
for c: Dictionary in cities:
var pos: Vector2 = viewer.city_canvas_pos(c)
var tier: int = int(c.get("population_tier", 1))
var r: float = 2.5 + float(tier) * 0.8
var key: String = _city_key(c)
var is_selected: bool = not selected_key.is_empty() and key == selected_key
var is_hovered: bool = not hovered_key.is_empty() and key == hovered_key
var col: Color = COLOR_CITY
if is_selected:
col = COLOR_CITY_SELECTED
elif is_hovered:
col = COLOR_CITY_HOVER
draw_circle(pos, r + 1.0, Color(0.0, 0.0, 0.0, 0.55))
draw_circle(pos, r, col)
var name_str: String = c.get("name", "") if c.get("name") else ""
if not name_str.is_empty() and (tier >= 3 or is_hovered or is_selected):
var lcolor: Color = (
COLOR_CITY_HOVER if is_hovered or is_selected else Color(0.88, 0.90, 0.96, 0.85)
)
draw_string(
font,
pos + Vector2(r + 2.0, r * 0.4),
name_str,
HORIZONTAL_ALIGNMENT_LEFT,
-1,
8,
lcolor
)
static func _city_key(city: Dictionary) -> String:
if city.is_empty():
return ""
var name_val: Variant = city.get("name")
if name_val != null and not str(name_val).is_empty():
return "n:" + str(name_val)
var id_val: Variant = city.get("city_id")
if id_val != null and not str(id_val).is_empty():
return "i:" + str(id_val)
# Last resort: positional key from pos/lat/lon so two unnamed cities at
# different coordinates still compare unequal.
var pos: Variant = city.get("pos")
if pos != null:
return "p:" + str(pos)
if city.has("lat") and city.has("lon"):
return "ll:%s:%s" % [city["lat"], city["lon"]]
return "h:%d" % city.hash()
# =============================================================================
# Overlay placeholders (populated by server side signals eventually)
# =============================================================================
func _draw_population_density(markers: Dictionary) -> void:
# Placeholder: soft blobs at city positions scaled by population_tier
for c: Dictionary in markers.get("cities", []):
var pos: Vector2 = viewer.city_canvas_pos(c)
var tier: int = int(c.get("population_tier", 1))
var r: float = 12.0 + float(tier) * 6.0
draw_circle(pos, r, COLOR_POP_HEAT)
# Review #3: derive overlays from existing cities[] fields. The original
# code read production_zones / shadow_zones / corp_presence from keys that
# are not in the D-191 §8 markers schema, so toggling was a silent no-op.
# When the server schema lands with explicit zone polygons we can promote
# these back to dedicated arrays; until then the MVP reads what's there.
func _draw_production_zones(markers: Dictionary) -> void:
for city: Dictionary in markers.get("cities", []):
var func_id: String = str(city.get("primary_function", "")).to_lower()
if not PRODUCTION_FUNCTIONS.has(func_id):
continue
var pos: Vector2 = viewer.city_canvas_pos(city)
var tier: int = int(city.get("population_tier", 1))
var radius: float = 10.0 + float(tier) * 4.0
draw_circle(pos, radius, COLOR_PRODUCTION)
func _draw_shadow_economy(markers: Dictionary) -> void:
# Broad bands around cities outside the Commission's reach. Intentionally
# soft + overlapping — D-181 treats shadow zones as "broad bands", not
# precise polygons.
for city: Dictionary in markers.get("cities", []):
var commission: bool = bool(city.get("commission_presence", false))
if commission:
continue
var pos: Vector2 = viewer.city_canvas_pos(city)
var tier: int = int(city.get("population_tier", 1))
var radius: float = 18.0 + float(tier) * 6.0
draw_circle(pos, radius, COLOR_SHADOW)
func _draw_corp_presence(markers: Dictionary) -> void:
# Tier 1 corp presence — keyed off the Commission-presence flag on cities
# until D-191 §8 schema explicitly lists per-corp dots.
for city: Dictionary in markers.get("cities", []):
if not bool(city.get("commission_presence", false)):
continue
var pos: Vector2 = viewer.city_canvas_pos(city)
draw_rect(Rect2(pos - Vector2(3, 3), Vector2(6, 6)), COLOR_CORP)
# =============================================================================
# Helpers
# =============================================================================
func _path_to_canvas(path: Array) -> PackedVector2Array:
var out: PackedVector2Array = PackedVector2Array()
for pt: Variant in path:
if pt is Array and pt.size() >= 2:
out.append(viewer.grid_to_canvas(Vector2(float(pt[0]), float(pt[1]))))
return out
func _center_to_canvas(center: Variant) -> Vector2:
# Legacy markers.json encodes center as [row, col]
if center is Array and center.size() >= 2:
return viewer.grid_to_canvas(Vector2(float(center[1]), float(center[0])))
return Vector2.ZERO
func _poi_pos(poi: Dictionary) -> Vector2:
if poi.has("pos") and poi["pos"] is Array and poi["pos"].size() >= 2:
return viewer.grid_to_canvas(Vector2(float(poi["pos"][0]), float(poi["pos"][1])))
return _center_to_canvas(poi.get("center"))
func _draw_diamond(pos: Vector2, size: float, color: Color) -> void:
var pts: PackedVector2Array = PackedVector2Array(
[
pos + Vector2(0, -size),
pos + Vector2(size, 0),
pos + Vector2(0, size),
pos + Vector2(-size, 0),
]
)
draw_colored_polygon(pts, color)
func _draw_dashed_polyline(points: PackedVector2Array, color: Color, width: float) -> void:
if points.size() < 2:
return
# Simple per-segment dashing — good enough for the MVP.
for i: int in range(points.size() - 1):
var a: Vector2 = points[i]
var b: Vector2 = points[i + 1]
var seg: Vector2 = b - a
var seg_len: float = seg.length()
if seg_len <= 0.001:
continue
var dir: Vector2 = seg / seg_len
var t: float = 0.0
while t < seg_len:
var t_end: float = minf(t + RAIL_DASH_ON, seg_len)
draw_line(a + dir * t, a + dir * t_end, color, width, true)
t = t_end + RAIL_DASH_OFF