fix(ui): atlas review blockers — moon layout, stale grid dims, overlay schema (#128)

1. Moon placement on the orbital diagram divided by a hard-coded 4 — a gas
   giant with five or more moons would overlap satellites and make them
   unclickable. Count moons per parent and distribute them evenly.
2. AtlasViewer._load_markers seeded _grid_w/_grid_h from _tex_w/_tex_h
   before the terrain_reference null check, so opening a body with no
   heightmap after one that had a heightmap left the grid dimensions
   pointing at the previous texture and misplaced markers. Reset texture +
   grid dims to sentinel defaults at the top of _load_heightmap.
3. AtlasMarkerOverlay's three toggleable overlays (production_zones,
   shadow_economy, corp_presence) read markers.production_zones /
   markers.shadow_zones / markers.corp_presence — keys that don't exist in
   D-191 §8's markers schema, so toggling was a silent no-op. Derive them
   from cities[] instead: primary_function ∈ PRODUCTION_FUNCTIONS for
   production, absent Commission presence for shadow bands, Commission
   presence for corp dots. Follow-up ticket will formalise per-overlay
   arrays once the server schema lands.

Addresses PR #128 review blockers 1-3.
This commit is contained in:
2026-04-15 08:47:14 +02:00
parent 9afd6b53d2
commit 1328db3138
3 changed files with 67 additions and 18 deletions
+36 -11
View File
@@ -35,6 +35,11 @@ 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)
@@ -200,24 +205,44 @@ func _draw_population_density(markers: Dictionary) -> void:
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 zone: Dictionary in markers.get("production_zones", []):
var p: Vector2 = _center_to_canvas(zone.get("center"))
var radius: float = float(zone.get("radius", 12.0))
draw_circle(p, radius, COLOR_PRODUCTION)
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:
for zone: Dictionary in markers.get("shadow_zones", []):
var p: Vector2 = _center_to_canvas(zone.get("center"))
var radius: float = float(zone.get("radius", 16.0))
draw_circle(p, radius, COLOR_SHADOW)
# 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:
for corp: Dictionary in markers.get("corp_presence", []):
var p: Vector2 = _center_to_canvas(corp.get("pos"))
draw_rect(Rect2(p - Vector2(3, 3), Vector2(6, 6)), COLOR_CORP)
# 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)
# =============================================================================
+23 -7
View File
@@ -230,18 +230,34 @@ func _compute_body_positions() -> void:
angle = -PI / 2.0 + TAU * float(i) / float(count)
_body_positions[b["body_id"]] = center + Vector2(cos(angle), sin(angle)) * ring_r
# Place moons near their parent body
# Place moons near their parent body. Moons per parent count drives the
# angular spacing — a hard-coded divisor made the 5th+ moon overlap moon 1
# and become unclickable on gas giants with many satellites (review #1).
var moons_by_parent: Dictionary = {}
for b: Dictionary in _orbital_bodies:
var parent_id: Variant = b.get("parent_body_id")
if parent_id == null:
continue
if not _body_positions.has(str(parent_id)):
var key: String = str(parent_id)
if not moons_by_parent.has(key):
moons_by_parent[key] = []
moons_by_parent[key].append(b)
for parent_key: String in moons_by_parent:
if not _body_positions.has(parent_key):
continue
var parent_pos: Vector2 = _body_positions[str(parent_id)]
var moon_idx: int = int(b.get("orbit_index", 1))
# Distribute moons around parent at evenly spaced angles
var angle: float = -PI / 2.0 + TAU * float(moon_idx - 1) / 4.0
_body_positions[b["body_id"]] = parent_pos + Vector2(cos(angle), sin(angle)) * MOON_ORBIT_RADIUS
var siblings: Array = moons_by_parent[parent_key]
siblings.sort_custom(
func(a: Dictionary, b: Dictionary) -> bool:
return int(a.get("orbit_index", 0)) < int(b.get("orbit_index", 0))
)
var parent_pos: Vector2 = _body_positions[parent_key]
var count: int = siblings.size()
for i: int in range(count):
var moon: Dictionary = siblings[i]
var angle: float = -PI / 2.0 + TAU * float(i) / float(count)
_body_positions[moon["body_id"]] = (
parent_pos + Vector2(cos(angle), sin(angle)) * MOON_ORBIT_RADIUS
)
# Place stations near their parent body (offset right + slightly up)
for s: Dictionary in _orbital_stations:
+8
View File
@@ -177,7 +177,13 @@ func get_markers() -> Dictionary:
func _load_heightmap() -> void:
# Reset to sentinel defaults so a body with no heightmap (or that fails to
# load) doesn't inherit the previous body's dimensions — otherwise
# grid_to_canvas would project markers using the prior texture size
# (review #2).
_heightmap_texture = null
_tex_w = 1024.0
_tex_h = 512.0
var ref: Variant = _body.get("terrain_reference")
if ref == null or str(ref).is_empty():
return
@@ -196,6 +202,8 @@ func _load_heightmap() -> void:
func _load_markers() -> void:
_markers = {}
# Reset grid dims alongside _tex_* in _load_heightmap so we start from a
# known baseline regardless of which body ran previously.
_grid_w = _tex_w
_grid_h = _tex_h