"""SVG world map generator — lush fantasy garden aesthetic.""" from __future__ import annotations import html import math import random from ui import assets from world.entities import get_entities_by_location from world.events import get_active_world_event from world.locations import get_all_locations ENTITY_TYPE_COLORS = { "character": "#d4a84b", "creature": "#e07b4a", "object": "#a8c8e8", "place": "#60a060", } LOCATION_ICONS = { "library": "M-6,-4 L6,-4 L6,4 L-6,4 Z M-4,-6 L4,-6", "sea": "M-8,2 Q-4,-2 0,2 T8,2", "clock-forest": "M0,-8 L2,0 L0,8 L-2,0 Z", "moon-market": "M0,-6 L5,6 L-5,6 Z", "valley": "M-8,4 L0,-6 L8,4", "crossroads": "M0,-7 L2,0 L0,7 L-2,0 M-7,0 L7,0", "mirror-bogs": "M-6,-6 A6,6 0 1,1 6,-6", "hollow-mountain": "M-8,6 L0,-8 L8,6", } LOCATION_PATHS = [ "M 120,120 Q 220,60 400,80 Q 520,40 640,100", "M 120,120 Q 80,220 160,320 Q 200,420 200,500", "M 640,100 Q 620,220 640,340", "M 400,80 Q 400,200 400,300", "M 400,300 Q 280,380 200,500", "M 400,300 Q 520,380 600,500", "M 400,300 Q 520,280 640,340", "M 200,500 Q 400,460 600,500", ] def render_world_map(selected_location_id: int | None = None) -> str: locations = get_all_locations() map_w, map_h = 800, 600 # Keep hit areas and labels safely inside the frame on all aspect ratios. margin_x = 62 margin_top = 62 margin_bottom = 90 active_event = get_active_world_event() affected_slugs = set() if active_event: from world.seed_data import LOCATION_NAME_TO_SLUG for name in active_event.get("affected_locations", []): slug = LOCATION_NAME_TO_SLUG.get(name, name) affected_slugs.add(slug) svg_parts = [ '', "", '', '', '', "", '', '', '', "", '', '', '', '', "", '', '', '', "", '', '', '', '', "", "", '', f'', '', '', ] # Fireflies / stars for i in range(55): random.seed(i * 13) cx = random.randint(20, 780) cy = random.randint(20, 580) r = random.uniform(0.4, 2.0) opacity = random.uniform(0.04, 0.18) color = "#d4a84b" if i % 3 == 0 else "#e8f0d8" svg_parts.append( f'' ) # Organic terrain blobs for i, (cx, cy, rx, ry, color) in enumerate([ (200, 450, 120, 60, "rgba(42,74,58,0.25)"), (550, 200, 90, 50, "rgba(42,74,58,0.2)"), (400, 350, 150, 80, "rgba(58,90,68,0.15)"), (650, 420, 80, 40, "rgba(42,74,58,0.18)"), ]): svg_parts.append( f'' ) for i, path in enumerate(LOCATION_PATHS): svg_parts.append( f'' ) for loc in locations: raw_x = loc["map_x"] / 100 * map_w raw_y = loc["map_y"] / 100 * map_h x = min(max(raw_x, margin_x), map_w - margin_x) y = min(max(raw_y, margin_top), map_h - margin_bottom) is_affected = loc["slug"] in affected_slugs is_selected = selected_location_id == loc["id"] pulse_class = " location-pulse" if is_affected else " location-glow" selected_class = " location-selected" if is_selected else "" entities = get_entities_by_location(loc["id"], limit=8) count = loc["entity_count"] icon_path = LOCATION_ICONS.get(loc["slug"], "M0,-6 A6,6 0 1,1 0,6") img = assets.location_image_url(loc.get("slug")) or "" special = loc.get("special_property", "") or "" desc = loc.get("short_description", "") or "" svg_parts.append( f'' ) svg_parts.append( '' ) filter_id = "pulse-glow" if is_affected else "glow" svg_parts.append( f'' ) svg_parts.append( f'' ) svg_parts.append( f'' ) svg_parts.append( f'{count}' ) for i, entity in enumerate(entities[:6]): angle = (i / 6) * 2 * math.pi - math.pi / 2 dot_x = math.cos(angle) * 46 dot_y = math.sin(angle) * 46 color = ENTITY_TYPE_COLORS.get(entity["type"], "#d4a84b") svg_parts.append( f'' f'' f'' f'{entity["name"]}' ) if count > 6: svg_parts.append( f'+{count - 6}' ) short_name = loc["name"].replace("The ", "") if len(short_name) > 18: short_name = short_name[:16] + "…" svg_parts.append( f'{short_name}' ) svg_parts.append("") svg_parts.append("") return "\n".join(svg_parts)