350 lines
12 KiB
GDScript
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
|