Mastodon Politics, Power, and Science: December 2025

Saturday, December 6, 2025

Progress on the campaign manager


You can see that you can build tactical maps automatically from the world map data.  You can place roads, streams, buildings.










The framework is powerful enough to colorize and light an actual real height map of the earth.








 

Friday, December 5, 2025

Campaign Manager Integration Roadmap

 J. Rogers, SE Ohio

We already have most of the individual parts coded and working separately, now we just need to make everything work together. 

Wiring Up Your Existing Components


Current Component Inventory

✅ What You Already Have

Component Status Location
World Generation ✅ Working world_gen.py
Town Generator ✅ Working Hooked to AI
Dungeon Generator ✅ Working Hooked to AI
Map Viewer ✅ Working map_viewer.py
Database ✅ Working db_manager.py
UI Widgets ✅ Working widgets.py
Image Rendering ✅ Working image_strategy.py
Heightmap System ✅ Working 16-bit PNG storage

🔧 What Needs Building

Component Priority Purpose
Campaign Manager 🔴 Critical Orchestrates everything
Display Manager 🔴 Critical Dual window control
Navigation System 🔴 Critical Node traversal
Heightmap Extractor 🟡 High Region extraction
Building Designer 🟢 Medium Interior generation
State Synchronization 🟡 High DM ↔ Player sync

Day 1: Core Framework (Morning Session)

Goal: Get the orchestration layer working

Step 1: Create campaign_manager.py

"""
Central orchestrator for the entire campaign system.
Owns all generators, manages state, routes navigation.
"""

from codex_engine.core.db_manager import DBManager
from codex_engine.generators.world_gen import WorldGenerator
from codex_engine.generators.town_gen import TownGenerator  # Your existing
from codex_engine.generators.dungeon_gen import DungeonGenerator  # Your existing

class CampaignManager:
    """
    The brain of the operation. Manages:
    - Current campaign state
    - Navigation between maps
    - Routing to correct generators
    - Database operations
    """
    
    def __init__(self, db_manager, theme_manager):
        self.db = db_manager
        self.theme = theme_manager
        
        # Initialize all generators
        self.world_gen = WorldGenerator(theme_manager, db_manager)
        self.town_gen = TownGenerator(theme_manager, db_manager)
        self.dungeon_gen = DungeonGenerator(theme_manager, db_manager)
        
        # State
        self.current_campaign_id = None
        self.current_node = None
        self.navigation_stack = []  # For back button
        
    def load_campaign(self, campaign_id):
        """Load a campaign and navigate to world map"""
        self.current_campaign_id = campaign_id
        
        # Get or create world map node
        world_node = self.db.get_node_by_coords(campaign_id, None, 0, 0)
        
        if not world_node:
            print("No world map found, generating...")
            node_id, metadata = self.world_gen.generate_world_node(campaign_id)
            world_node = self.db.get_node_by_coords(campaign_id, None, 0, 0)
        
        self.navigate_to_node(world_node)
        
    def navigate_to_node(self, node):
        """Navigate to any node (world/town/dungeon/room)"""
        if self.current_node:
            self.navigation_stack.append(self.current_node)
        
        self.current_node = node
        print(f"Navigated to: {node['name']} (type: {node['node_type']})")
        
        return node
        
    def navigate_back(self):
        """Go back one level in navigation"""
        if self.navigation_stack:
            self.current_node = self.navigation_stack.pop()
            return self.current_node
        return None
        
    def generate_or_load_child(self, parent_node, marker):
        """
        Universal child location loader.
        Routes to correct generator based on marker type.
        """
        # Check if target already exists
        if marker.get('target_node_id'):
            target = self.db.get_node_by_id(marker['target_node_id'])
            if target:
                return self.navigate_to_node(target)
        
        # Doesn't exist, generate based on type
        marker_type = marker['type']
        
        if marker_type == 'town':
            return self._generate_town(parent_node, marker)
        elif marker_type == 'dungeon':
            return self._generate_dungeon(parent_node, marker)
        elif marker_type == 'building':
            return self._generate_building(parent_node, marker)
        else:
            print(f"Unknown marker type: {marker_type}")
            return None
    
    def _generate_town(self, parent_node, marker):
        """Generate a town using your existing town generator"""
        print(f"Generating town at {marker['coords']}...")
        
        # Extract heightmap region from parent
        # (We'll implement this in Step 3)
        
        # Call your existing town generator
        node_id, metadata = self.town_gen.generate_town_node(
            campaign_id=self.current_campaign_id,
            parent_node_id=parent_node['id'],
            coords_x=marker['local_x'],
            coords_y=marker['local_y'],
            town_name=marker.get('name', 'Unnamed Town')
        )
        
        # Update marker to point to new node
        self.db.update_marker(marker['id'], target_node_id=node_id)
        
        # Load and navigate to it
        new_node = self.db.get_node_by_id(node_id)
        return self.navigate_to_node(new_node)
    
    def _generate_dungeon(self, parent_node, marker):
        """Generate a dungeon using your existing dungeon generator"""
        print(f"Generating dungeon at {marker['coords']}...")
        
        node_id, metadata = self.dungeon_gen.generate_dungeon_node(
            campaign_id=self.current_campaign_id,
            parent_node_id=parent_node['id'],
            coords_x=marker['local_x'],
            coords_y=marker['local_y'],
            dungeon_name=marker.get('name', 'Unnamed Dungeon'),
            theme=marker.get('theme', 'generic')
        )
        
        self.db.update_marker(marker['id'], target_node_id=node_id)
        
        new_node = self.db.get_node_by_id(node_id)
        return self.navigate_to_node(new_node)
    
    def _generate_building(self, parent_node, marker):
        """Generate building interior (to be implemented)"""
        print(f"Building generation not yet implemented")
        return None
    
    def get_current_markers(self):
        """Get all markers for current node"""
        if not self.current_node:
            return []
        
        return self.db.get_markers_for_node(self.current_node['id'])
    
    def create_marker(self, marker_type, local_x, local_y, name, secret=False):
        """Create a new marker on current node"""
        if not self.current_node:
            return None
        
        marker_id = self.db.create_marker(
            node_id=self.current_node['id'],
            marker_type=marker_type,
            name=name,
            local_x=local_x,
            local_y=local_y,
            secret=secret
        )
        
        return self.db.get_marker_by_id(marker_id)

Test it:

# In main.py, test basic navigation
cm = CampaignManager(db, theme)
cm.load_campaign(1)
print(f"Current node: {cm.current_node['name']}")

Day 1: Display Manager (Afternoon Session)

Goal: Get dual window rendering working, save this for stand along project 

Step 2: Create display_manager.py

"""
Manages both DM and player display windows.
Handles rendering, state filtering, synchronization.
"""

import pygame
import os

class DisplayManager:
    """
    Controls both display windows and their views.
    DM window has full access, player window is filtered.
    """
    
    def __init__(self, campaign_manager, theme_manager, dm_screen_size=(1920, 1080)):
        self.campaign = campaign_manager
        self.theme = theme_manager
        
        # Create DM window (your existing main window)
        self.dm_screen = pygame.display.set_mode(dm_screen_size)
        pygame.display.set_caption("The Codex Engine - DM View")
        
        # Create player window on second monitor
        self.player_screen = None
        self.player_enabled = False
        
        # Import your existing MapViewer
        from codex_engine.ui.map_viewer import MapViewer
        
        # DM view (full access)
        self.dm_view = MapViewer(self.dm_screen, theme_manager)
        
        # Player view (will be created when enabled)
        self.player_view = None
        
        # Sync state
        self.auto_sync = True  # Push changes to player automatically
        
    def enable_player_display(self):
        """
        Open player display on second monitor.
        Call this when DM clicks "Enable Player Display" button.
        """
        if self.player_enabled:
            return
        
        # Try to position on second monitor
        # This is OS-dependent, might need tweaking
        os.environ['SDL_VIDEO_WINDOW_POS'] = "1920,0"  # Assumes second monitor is to the right
        
        self.player_screen = pygame.display.set_mode((1920, 1080), pygame.NOFRAME)
        pygame.display.set_caption("Campaign Map - Player View")
        
        from codex_engine.ui.map_viewer import MapViewer
        self.player_view = MapViewer(self.player_screen, self.theme)
        
        # Copy current DM state to player (filtered)
        if self.dm_view.current_node:
            self.sync_to_player()
        
        self.player_enabled = True
        print("Player display enabled on second monitor")
    
    def disable_player_display(self):
        """Close player display"""
        if self.player_screen:
            pygame.display.quit()
            self.player_screen = None
            self.player_view = None
            self.player_enabled = False
    
    def sync_to_player(self):
        """
        Push current DM view to player display (filtered).
        This is where you apply fog of war, hide secret markers, etc.
        """
        if not self.player_enabled or not self.player_view:
            return
        
        # Get current node from DM view
        current_node = self.dm_view.current_node
        
        if not current_node:
            return
        
        # Filter the node data for players
        filtered_node = self._filter_node_for_players(current_node)
        
        # Set filtered node on player view
        self.player_view.set_node(filtered_node)
        
        # Sync camera position (optional - or let player view stay centered)
        if self.auto_sync:
            self.player_view.cam_x = self.dm_view.cam_x
            self.player_view.cam_y = self.dm_view.cam_y
            self.player_view.zoom = self.dm_view.zoom
    
    def _filter_node_for_players(self, node):
        """
        Remove secret information from node data.
        Apply fog of war, hide secret markers, etc.
        """
        # Make a copy so we don't modify original
        filtered = node.copy()
        
        # TODO: Apply fog of war from database
        # fog_state = self.campaign.db.get_fog_of_war(node['id'])
        
        # TODO: Filter metadata to remove DM notes
        if 'metadata' in filtered:
            metadata = filtered['metadata'].copy()
            # Remove any keys that start with 'dm_' or 'secret_'
            filtered['metadata'] = {
                k: v for k, v in metadata.items() 
                if not k.startswith('dm_') and not k.startswith('secret_')
            }
        
        return filtered
    
    def handle_dm_input(self, event):
        """Route input to DM view"""
        self.dm_view.handle_input(event)
        
        # Check if DM navigated to a new node
        # If so, sync to player display
        if self.auto_sync and self.dm_view.current_node != self.campaign.current_node:
            self.sync_to_player()
    
    def update_and_render(self):
        """
        Main render loop - draw both windows.
        Call this every frame.
        """
        # Render DM view
        self.dm_view.draw()
        pygame.display.flip()  # Update DM window
        
        # Render player view if enabled
        if self.player_enabled and self.player_view:
            self.player_view.draw()
            # Need to flip the player display too
            # This is tricky with pygame - might need separate display object

Integration challenge: Pygame's pygame.display.set_mode() only supports one window by default. For true dual window, you need to either:

Option A: Use pygame.display.set_mode() twice with separate pygame.init() instances (hacky)

Option B: Use multi-window library like pyglet or moderngl (better, but requires refactoring)

Option C: Use a web-based player view (Pygame for DM, web browser for players on second screen)

For tomorrow, stick with Option A for proof-of-concept:

# Quick dual-window hack for testing
import subprocess

# DM window: Your main pygame app

# Player window: Launch second pygame instance showing same data
# player_process = subprocess.Popen(['python', 'player_display.py'])

Day 2: Navigation Wiring

Goal: Click markers to navigate between maps

Step 3: Wire up marker clicking in map_viewer.py

# Add to MapViewer class

def __init__(self, screen, theme_manager, campaign_manager=None):
    # ... existing code ...
    self.campaign = campaign_manager  # NEW: Reference to campaign manager
    self.markers = []  # NEW: Cache of current markers
    
def set_node(self, node_data):
    # ... existing code ...
    
    # NEW: Load markers for this node
    if self.campaign:
        self.markers = self.campaign.get_current_markers()

def handle_input(self, event):
    # ... existing code ...
    
    # NEW: Handle mouse clicks on markers
    if event.type == pygame.MOUSEBUTTONDOWN:
        if event.button == 1:  # Left click
            clicked_marker = self._get_marker_at_pos(event.pos)
            if clicked_marker:
                self._navigate_to_marker(clicked_marker)

def _get_marker_at_pos(self, screen_pos):
    """Check if mouse clicked on a marker"""
    # Convert screen coordinates to map coordinates
    map_x, map_y = self._screen_to_map_coords(screen_pos)
    
    # Check each marker
    for marker in self.markers:
        marker_x = marker['local_x']
        marker_y = marker['local_y']
        
        # Simple distance check (can improve with hex math later)
        distance = ((map_x - marker_x)**2 + (map_y - marker_y)**2)**0.5
        
        if distance < 20:  # 20 pixel click radius
            return marker
    
    return None

def _screen_to_map_coords(self, screen_pos):
    """Convert screen pixel to map coordinates"""
    from codex_engine.config import SCREEN_WIDTH, SCREEN_HEIGHT
    
    sx, sy = screen_pos
    center_x = SCREEN_WIDTH // 2
    center_y = SCREEN_HEIGHT // 2
    
    map_x = self.cam_x + (sx - center_x) / self.zoom
    map_y = self.cam_y + (sy - center_y) / self.zoom
    
    return map_x, map_y

def _navigate_to_marker(self, marker):
    """Tell campaign manager to navigate to this marker"""
    if not self.campaign:
        return
    
    print(f"Clicked marker: {marker['name']}")
    
    # This triggers generation if needed
    new_node = self.campaign.generate_or_load_child(
        parent_node=self.current_node,
        marker=marker
    )
    
    if new_node:
        # Load the new node
        self.set_node(new_node)

def draw(self):
    # ... existing map rendering code ...
    
    # NEW: Draw markers on top of map
    self._draw_markers()

def _draw_markers(self):
    """Render marker icons on the map"""
    from codex_engine.config import SCREEN_WIDTH, SCREEN_HEIGHT
    
    for marker in self.markers:
        # Skip secret markers if this is player view
        if marker.get('secret') and not self.show_secrets:
            continue
        
        # Convert marker map coords to screen coords
        map_x = marker['local_x']
        map_y = marker['local_y']
        
        screen_x = (SCREEN_WIDTH // 2) + (map_x - self.cam_x) * self.zoom
        screen_y = (SCREEN_HEIGHT // 2) + (map_y - self.cam_y) * self.zoom
        
        # Check if on screen
        if 0 <= screen_x <= SCREEN_WIDTH and 0 <= screen_y <= SCREEN_HEIGHT:
            # Draw marker icon (simple circle for now)
            color = self._get_marker_color(marker['type'])
            pygame.draw.circle(self.screen, color, (int(screen_x), int(screen_y)), 8)
            
            # Draw label
            if self.zoom > 0.5:  # Only show labels when zoomed in
                label = self.font_small.render(marker['name'], True, (255, 255, 255))
                self.screen.blit(label, (screen_x + 12, screen_y - 10))

def _get_marker_color(self, marker_type):
    """Color code markers by type"""
    colors = {
        'town': (100, 200, 100),      # Green
        'dungeon': (200, 100, 100),   # Red
        'building': (200, 200, 100),  # Yellow
        'npc': (100, 100, 200),       # Blue
        'poi': (200, 100, 200),       # Purple
    }
    return colors.get(marker_type, (150, 150, 150))  # Gray default

Day 2: Heightmap Extraction (Afternoon)

Goal: Extract parent terrain for child maps

Step 4: Create terrain_extractor.py

"""
Extracts regions from parent heightmaps and upscales them.
Maintains terrain consistency across zoom levels.
"""

import numpy as np
from PIL import Image
from codex_engine.config import MAPS_DIR

class TerrainExtractor:
    """
    Handles extraction of map regions from parent heightmaps.
    Used when generating towns/dungeons that need to sit on real terrain.
    """
    
    @staticmethod
    def extract_region(parent_map_path, center_x, center_y, radius, target_size):
        """
        Extract a square region from parent heightmap and upscale.
        
        Args:
            parent_map_path: Path to parent heightmap PNG
            center_x, center_y: Center coordinates in parent map
            radius: Half-width of extraction in parent pixels
            target_size: Output resolution (e.g., 1000x1000)
        
        Returns:
            Upscaled heightmap as numpy array (0-1 normalized)
        """
        # Load parent heightmap
        parent_img = Image.open(parent_map_path)
        parent_heightmap = np.array(parent_img, dtype=np.float32) / 65535.0
        
        parent_height, parent_width = parent_heightmap.shape
        
        # Calculate extraction bounds with wrapping
        x_start = int(center_x - radius)
        x_end = int(center_x + radius)
        y_start = int(center_y - radius)
        y_end = int(center_y + radius)
        
        # Handle wrapping (toroidal world)
        if x_start < 0 or x_end >= parent_width or y_start < 0 or y_end >= parent_height:
            # Need to wrap - extract with modulo
            extract_width = x_end - x_start
            extract_height = y_end - y_start
            region = np.zeros((extract_height, extract_width), dtype=np.float32)
            
            for dy in range(extract_height):
                for dx in range(extract_width):
                    src_y = (y_start + dy) % parent_height
                    src_x = (x_start + dx) % parent_width
                    region[dy, dx] = parent_heightmap[src_y, src_x]
        else:
            # Simple slice
            region = parent_heightmap[y_start:y_end, x_start:x_end]
        
        # Upscale using bilinear interpolation
        region_img = Image.fromarray((region * 65535).astype(np.uint16), mode='I;16')
        upscaled_img = region_img.resize((target_size, target_size), Image.BILINEAR)
        upscaled = np.array(upscaled_img, dtype=np.float32) / 65535.0
        
        return upscaled
    
    @staticmethod
    def save_heightmap(heightmap, filename):
        """Save heightmap as 16-bit PNG"""
        uint16_data = (heightmap * 65535).astype(np.uint16)
        img = Image.fromarray(uint16_data, mode='I;16')
        img.save(MAPS_DIR / filename)
        return filename
    
    @staticmethod
    def get_terrain_type(heightmap, sea_level_norm):
        """
        Analyze heightmap to determine terrain type.
        Returns: 'ocean', 'coast', 'plains', 'hills', 'mountains'
        """
        avg_height = np.mean(heightmap)
        
        if avg_height < sea_level_norm - 0.1:
            return 'ocean'
        elif avg_height < sea_level_norm + 0.05:
            return 'coast'
        elif avg_height < 0.4:
            return 'plains'
        elif avg_height < 0.7:
            return 'hills'
        else:
            return 'mountains'

Wire it into your generators:

# In town_gen.py
def generate_town_node(self, campaign_id, parent_node_id, coords_x, coords_y, town_name):
    # Get parent heightmap
    parent_node = self.db.get_node_by_id(parent_node_id)
    parent_map_path = MAPS_DIR / parent_node['metadata']['file_path']
    
    # Extract region
    from codex_engine.terrain.terrain_extractor import TerrainExtractor
    town_heightmap = TerrainExtractor.extract_region(
        parent_map_path,
        center_x=coords_x,
        center_y=coords_y,
        radius=100,  # 200x200 extraction
        target_size=1000  # Upscale to 1000x1000
    )
    
    # Determine terrain type for AI context
    sea_level_norm = (0 - parent_node['metadata']['real_min']) / \
                     (parent_node['metadata']['real_max'] - parent_node['metadata']['real_min'])
    
    terrain_type = TerrainExtractor.get_terrain_type(town_heightmap, sea_level_norm)
    
    # Generate town via AI (your existing code)
    # Pass terrain_type in prompt for context
    town_data = self._generate_via_ai(town_name, terrain_type)
    
    # Save heightmap
    import uuid
    map_filename = f"{uuid.uuid4()}.png"
    TerrainExtractor.save_heightmap(town_heightmap, map_filename)
    
    # Create node
    # ... rest of your existing code

Day 3: Building Designer System

Goal: Generate building interiors themed to campaign setting

Step 5: Create building_designer.py

"""
Procedural building interior generator.
Supports multiple themes: fantasy (D&D), sci-fi (Traveller), modern, etc.
"""

import json
from dataclasses import dataclass
from typing import List, Dict

@dataclass
class RoomTemplate:
    """Template for a room type"""
    name: str
    min_size: tuple  # (width, height) in grid squares
    max_size: tuple
    furniture: List[str]
    required_exits: int
    theme_specific: Dict  # Theme-specific properties

class BuildingDesigner:
    """
    Generates building interiors using themed room templates.
    """
    
    def __init__(self, theme='fantasy'):
        self.theme = theme
        self.room_templates = self._load_room_templates(theme)
    
    def _load_room_templates(self, theme):
        """Load room templates for the given theme"""
        
        if theme == 'fantasy':
            return {
                'tavern_common_room': RoomTemplate(
                    name='Common Room',
                    min_size=(8, 8),
                    max_size=(15, 15),
                    furniture=['tables', 'chairs', 'fireplace', 'bar'],
                    required_exits=2,
                    theme_specific={
                        'lighting': 'firelight',
                        'floor': 'wooden planks',
                        'atmosphere': 'smoky'
                    }
                ),
                'bedroom': RoomTemplate(
                    name='Bedroom',
                    min_size=(3, 3),
                    max_size=(6, 6),
                    furniture=['bed', 'chest', 'nightstand'],
                    required_exits=1,
                    theme_specific={
                        'privacy': True
                    }
                ),
                'kitchen': RoomTemplate(
                    name='Kitchen',
                    min_size=(4, 4),
                    max_size=(8, 8),
                    furniture=['stove', 'counter', 'pantry', 'table'],
                    required_exits=2,
                    theme_specific={
                        'fire_hazard': True
                    }
                ),
                'storage': RoomTemplate(
                    name='Storage Room',
                    min_size=(3, 3),
                    max_size=(6, 6),
                    furniture=['shelves', 'barrels', 'crates'],
                    required_exits=1,
                    theme_specific={}
                ),
            }
        
        elif theme == 'scifi':
            return {
                'bridge': RoomTemplate(
                    name='Bridge',
                    min_size=(10, 8),
                    max_size=(20, 15),
                    furniture=['command_chair', 'consoles', 'viewscreen', 'stations'],
                    required_exits=2,
                    theme_specific={
                        'lighting': 'led panels',
                        'floor': 'metal grating',
                        'tech_level': 'high'
                    }
                ),
                'quarters': RoomTemplate(
                    name='Crew Quarters',
                    min_size=(3, 3),
                    max_size=(5, 5),
                    furniture=['bunk', 'locker', 'terminal'],
                    required_exits=1,
                    theme_specific={
                        'life_support': True
                    }
                ),
                'engine_room': RoomTemplate(
                    name='Engine Room',
                    min_size=(8, 8),
                    max_size=(15, 12),
                    furniture=['reactor', 'control_panels', 'cooling_tanks'],
                    required_exits=2,
                    theme_specific={
                        'radiation': True,
                        'noise_level': 'high'
                    }
                ),
            }
        
        else:
            # Default generic templates
            return {}
    
    def generate_building(self, building_type, size='medium'):
        """
        Generate a complete building interior.
        
        Args:
            building_type: 'inn', 'shop', 'starship', 'warehouse', etc.
            size: 'small', 'medium', 'large'
        
        Returns:
            Building data structure with rooms, connections, furniture
        """
        
        # Define building type → room type mapping
        building_compositions = {
            'fantasy': {
                'inn': ['tavern_common_room', 'bedroom', 'bedroom', 'bedroom', 'kitchen', 'storage'],
                'shop': ['shop_floor', 'storage', 'workshop'],
                'mansion': ['foyer', 'dining_room', 'bedroom', 'bedroom', 'kitchen', 'library', 'study'],
                'smithy': ['forge', 'workshop', 'storage', 'display'],
            },
            'scifi': {
                'starship': ['bridge', 'quarters', 'quarters', 'engine_room', 'cargo_bay', 'medbay'],
                'station': ['command', 'habitation', 'docking', 'maintenance', 'recreation'],
            }
        }
        
        # Get room list for this building type
        if self.theme not in building_compositions:
            print(f"Theme {self.theme} not supported")
            return None
        
        if building_type not in building_compositions[self.theme]:
            print(f"Building type {building_type} not found in theme {self.theme}")
            return None
        
        room_types = building_compositions[self.theme][building_type]
        
        # Generate building structure
        building = {
            'type': building_type,
            'theme': self.theme,
            'size': size,
            'rooms': [],
            'connections': []
        }
        
        # Generate each room
        for i, room_type in enumerate(room_types):
            if room_type in self.room_templates:
                template = self.room_templates[room_type]
                room = self._generate_room(template, room_id=i)
                building['rooms'].append(room)
        
        # Connect rooms (simple linear for now)
        for i in range(len(building['rooms']) - 1):
            building['connections'].append({
                'from_room': i,
                'to_room': i + 1,
                'door_type': 'normal'
            })
        
        return building
    
    def _generate_room(self, template, room_id):
        """Generate a single room from template"""
        import random
        
        # Random size within template bounds
        width = random.randint(template.min_size[0], template.max_size[0])
        height = random.randint(template.min_size[1], template.max_size[1])
        
        room = {
            'id': room_id,
            'name': template.name,

Thursday, December 4, 2025

Dual Monitor Campaign Manager - Design Specification

 J. Rogers, SE Ohio

Executive Summary

A comprehensive tabletop RPG campaign management system utilizing procedurally generated fractal world maps, hierarchical location generation, and dual-monitor display architecture. The system separates DM control/private information from player-visible tactical displays, enabling seamless in-person or hybrid gameplay with dynamic map generation, fog of war, and persistent campaign state.


System Architecture

Core Components

1. World State Manager (Central Authority)

  • Maintains single source of truth for all campaign data
  • Manages hierarchical map structure (World → Region → Town → Dungeon → Room)
  • Handles database persistence and state synchronization
  • Broadcasts state changes to both display windows
  • Manages session state, undo/redo history

2. DM Control Window (Primary Display)

  • Full-featured interface for campaign management
  • Access to all hidden information, notes, and controls
  • World generation and editing tools
  • NPC/encounter management
  • Campaign metadata and session logs

3. Player Display Window (Secondary/Projector)

  • Clean, UI-minimal tactical view
  • Shows only revealed information
  • Optimized for readability at distance
  • No chrome, buttons, or DM tools visible
  • Scales to arbitrary display sizes

4. Heightmap Extraction Engine

  • Extracts regions from world-scale fractal heightmaps
  • Upscales to local detail resolution
  • Applies consistent water levels across scales
  • Caches regions for performance

5. Content Generation Service

  • LLM-based procedural generation of locations
  • Returns structured JSON for parsing
  • Context-aware (uses terrain, nearby locations, campaign themes)
  • Maintains consistency through coordinate-based seeding

Display Architecture

DM Control Window Layout

┌─────────────────────────────────────────────────────────────┐
│ [Campaign: The Sundered Realms]    Session: 12    [⚙ Menu] │
├──────────────────┬──────────────────────────────────────────┤
│                  │                                          │
│  SIDEBAR         │         PRIMARY MAP VIEW                 │
│  (250px)         │         (Resizable)                      │
│                  │                                          │
│ [World Controls] │  • Fractal world map with lighting      │
│  - Sea Level     │  • ALL markers visible (public+secret)  │
│  - Light Dir     │  • Hex/square grid overlay              │
│  - Light Height  │  • Camera controls (pan/zoom)           │
│  - Light Power   │  • Right-click context menus            │
│  - Grid Size     │  • Selection tools                      │
│                  │  • Measurement tools                    │
│ [Generation]     │                                          │
│  - New Map       │  [Minimap overlay in corner]            │
│  - Add Marker    │                                          │
│  - Gen Location  │                                          │
│                  │                                          │
│ [Fog of War]     │                                          │
│  - Reveal Mode   │                                          │
│  - Hide Mode     │                                          │
│  - Clear All     │                                          │
│  - Save State    │                                          │
│                  │                                          │
│ [Layer Toggles]  │                                          │
│  ☑ Terrain       │                                          │
│  ☑ Grid          │                                          │
│  ☑ Markers       │                                          │
│  ☑ Tokens        │                                          │
│  ☐ Secret Doors  │                                          │
│  ☐ Traps         │                                          │
│                  │                                          │
│ [Quick Notes]    │                                          │
│  [Text area]     │                                          │
│                  │                                          │
└──────────────────┴──────────────────────────────────────────┘
│ STATUS: World Map | Zoom: 1.5x | Coords: (2048, 1536)      │
│ [< Prev Map] [Next Map >] [Push to Display] [Center View]  │
└─────────────────────────────────────────────────────────────┘

Player Display Window Layout

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│                                                             │
│                                                             │
│                                                             │
│                  FULL-SCREEN MAP DISPLAY                    │
│                  (No UI elements)                           │
│                                                             │
│              • Current tactical map only                    │
│              • Fog of war applied                           │
│              • Player tokens visible                        │
│              • Revealed markers only                        │
│              • Grid overlay (optional)                      │
│              • Synchronized with DM view                    │
│                                                             │
│                                                             │
│                                                             │
│                                                             │
│                                                             │
│               [Optional: Scale bar in corner]               │
└─────────────────────────────────────────────────────────────┘

Hierarchical Map System

Map Level Structure

Level 0: WORLD MAP (4097x4097 fractal heightmap)
  ├─ Coordinates: (0-4096, 0-4096)
  ├─ 1 pixel = ~1 km
  ├─ Markers for: Cities, Dungeons, Landmarks, Portals
  ├─ Terrain: Fractal-generated with erosion
  └─ Water Level: Global sea level slider
  
Level 1: REGION MAP (Extracted 200x200 → upscaled 2000x2000)
  ├─ Extracted from parent World Map coordinates
  ├─ 1 pixel = ~100 meters
  ├─ Markers for: Districts, Buildings, Dungeon Entrances
  ├─ Terrain: Upscaled parent heightmap + local detail
  └─ Water: Inherited from parent sea level
  
Level 2: LOCAL MAP (Extracted 100x100 → upscaled 1000x1000)
  ├─ Extracted from parent Region Map
  ├─ 1 pixel = ~10 meters
  ├─ Markers for: Rooms, NPCs, Items, Events
  ├─ Terrain: Upscaled with architectural overlays
  └─ Hex/Square grid for tactical play
  
Level 3: ROOM MAP (Procedurally generated or hand-drawn)
  ├─ Grid-based tactical combat map
  ├─ 5ft per square standard D&D scale
  ├─ Markers for: Furniture, Traps, Secret Doors
  ├─ Tokens for: Characters, Monsters
  └─ Fully detailed battle map

Map Transition Flow

User clicks marker on World Map
  ↓
System checks if location exists in database
  ↓
  ├─ EXISTS: Load from database
  │   ├─ Load heightmap file
  │   ├─ Load marker data
  │   ├─ Load fog of war state
  │   └─ Display map
  │
  └─ NOT EXISTS: Generate new location
      ├─ Extract heightmap region from parent
      ├─ Upscale to target resolution
      ├─ Send generation request to LLM
      │   ├─ Prompt: "Generate [type] at coords [x,y]"
      │   ├─ Context: Terrain type, nearby locations, theme
      │   └─ Return: Structured JSON
      ├─ Parse JSON response
      ├─ Create markers from JSON locations
      ├─ Save to database
      └─ Display new map

Heightmap Extraction System

Region Extraction Algorithm

def extract_region(parent_heightmap, center_x, center_y, radius, target_size):
    """
    Extract a square region from parent map and upscale to target resolution.
    
    Args:
        parent_heightmap: Full-resolution parent heightmap (numpy array)
        center_x, center_y: Center coordinates in parent map
        radius: Half-width of extraction in parent pixels
        target_size: Output resolution (e.g., 1000x1000)
    
    Returns:
        Upscaled heightmap at target resolution
    """
    # Extract square region with bounds checking
    x_start = max(0, center_x - radius)
    x_end = min(parent_heightmap.shape[1], center_x + radius)
    y_start = max(0, center_y - radius)
    y_end = min(parent_heightmap.shape[0], center_y + radius)
    
    region = parent_heightmap[y_start:y_end, x_start:x_end]
    
    # Upscale using bilinear interpolation (good enough!)
    from PIL import Image
    img = Image.fromarray((region * 65535).astype(np.uint16), mode='I;16')
    upscaled = img.resize((target_size, target_size), Image.BILINEAR)
    
    return np.array(upscaled, dtype=np.float32) / 65535.0

Terrain Consistency Rules

  1. Water Level Inheritance: Child maps use same normalized sea level as parent
  2. River Continuity: Rivers on parent map become rivers on child map at same relative positions
  3. Elevation Consistency: Mountain on world map = mountainous terrain on region map
  4. Biome Matching: Forest on world map = forested region on local map

Fog of War System

Data Structure

fog_of_war_state = {
    "map_id": "world_map_uuid",
    "revealed_hexes": set([
        (x1, y1, z1),  # Hex coordinates
        (x2, y2, z2),
        ...
    ]),
    "partially_visible": {
        (x3, y3, z3): 0.5,  # 50% revealed
    },
    "exploration_history": [
        {"timestamp": "2024-01-15T14:30", "hexes": [(x1,y1,z1), ...]},
    ]
}

Fog of War Modes

Mode DM View Player View Use Case
Full Reveal All terrain visible All terrain visible Exploration, travel montage
Exploration All visible Only revealed hexes Active dungeon crawling
Hidden All visible Nothing visible DM prep, secret reveals
Line of Sight All visible Only in token LOS Tactical combat
Partial All visible Dimmed unrevealed Show terrain, hide details

Reveal Mechanisms

  1. Manual Brush: DM clicks/drags to reveal hexes
  2. Token Vision: Auto-reveal based on token sight radius
  3. Area Reveal: Reveal all hexes in rectangular/circular region
  4. Room Reveal: Reveal all hexes in a marked room boundary
  5. Conditional: Reveal when trigger condition met (lever pulled, door opened)

Content Generation System

LLM Generation Pipeline

1. User places marker on map
   ↓
2. System determines context:
   - Marker type (town/dungeon/wilderness)
   - Coordinates on parent map
   - Terrain type under marker (from heightmap)
   - Nearby existing locations (query database)
   - Campaign theme/setting
   ↓
3. Construct LLM prompt:
   "Generate a [type] at coordinates [x,y] on a fantasy world.
    Terrain: [forest/mountain/coast/etc]
    Nearby: [list of nearby locations]
    Theme: [campaign theme]
    Return JSON with: name, description, npcs, locations, rumors, connections"
   ↓
4. Send to Claude API with JSON schema enforcement
   ↓
5. Receive structured JSON response
   ↓
6. Parse and validate JSON
   ↓
7. Create map node in database
   ↓
8. Create child markers from JSON locations
   ↓
9. Extract heightmap region from parent
   ↓
10. Save to database with metadata
   ↓
11. Display generated location

JSON Schema Structure

{
  "location_name": "Forest Clearing",
  "type": "town",
  "theme": "Frontier lumber village",
  "coordinates": {
    "parent_map_id": "world_map_uuid",
    "center_x": 2450,
    "center_y": 1830,
    "radius": 100
  },
  "terrain_info": {
    "primary": "forest",
    "secondary": "river",
    "elevation": "low"
  },
  "description": "A secluded lumber village...",
  "atmosphere": "Woodsmoke and damp earth...",
  "population": 150,
  "government": "Elder council",
  "locations": [
    {
      "name": "The Old Griffin Inn",
      "type": "inn",
      "local_coords": {"x": 45, "y": 67},
      "description": "Rustic, smoky...",
      "services": ["lodging", "food", "drink"],
      "prices": {"room": "5 cp", "meal": "2 cp"}
    },
    {
      "name": "Margaret's Smithy",
      "type": "smithy",
      "local_coords": {"x": 78, "y": 23},
      "description": "A loud workshop...",
      "services": ["repair", "craft"],
      "npc_id": "margaret_smith"
    }
  ],
  "npcs": [
    {
      "id": "margaret_smith",
      "name": "Margaret Smith",
      "role": "Blacksmith",
      "personality": "Gruff, honest, practical",
      "quirk": "Refuses to forge on new moon",
      "location_id": "margarets_smithy",
      "plot_hooks": [
        "Needs rare ore from deep forest for mill repair"
      ]
    }
  ],
  "rumors": [
    {
      "title": "The Whispering Timber",
      "type": "mystery",
      "description": "The Old Griffin's timbers still weep...",
      "truth_level": 0.7
    }
  ],
  "connections": [
    {
      "direction": "north",
      "type": "road",
      "leads_to": {
        "description": "Deep forest",
        "generates": "wilderness_encounter"
      }
    },
    {
      "direction": "south",
      "type": "river",
      "leads_to": {
        "description": "Downstream settlement",
        "coordinates": {"world_x": 2450, "world_y": 1750}
      }
    }
  ],
  "map_generation": {
    "hex_grid": true,
    "grid_size": 64,
    "special_hexes": [
      {"coords": [12, 15], "type": "river", "color": "blue"},
      {"coords": [45, 67], "type": "building", "name": "The Old Griffin"}
    ]
  }
}

Token & Combat System

Token Data Structure

token = {
    "id": "uuid",
    "name": "Thorin Ironforge",
    "type": "player_character",  # or npc, monster
    "owner": "player_1",  # or "dm"
    "map_id": "current_map_uuid",
    "position": {"x": 12, "y": 15, "z": 0},  # Hex or grid coords
    "stats": {
        "hp_current": 45,
        "hp_max": 58,
        "ac": 18,
        "speed": 30,
        "conditions": ["blessed", "hasted"]
    },
    "visibility": {
        "visible_to_players": true,
        "sight_radius": 60,  # feet or hexes
        "darkvision": 60
    },
    "image": "path/to/token.png",
    "size": "medium",  # Affects grid space
    "initiative": 17
}

Combat Mode Features

DM Control Window:

  • Initiative tracker (drag to reorder)
  • HP/status tracking for all tokens
  • Hidden monster stats
  • Dice roller
  • Condition assignment
  • AoE template tools

Player Display Window:

  • Player tokens with visible HP bars
  • Monster tokens (only if revealed)
  • Grid for movement
  • AoE effect visualization
  • Turn indicator
  • Measurement ruler

State Synchronization

Event Broadcasting System

class StateManager:
    def __init__(self):
        self.dm_window = None
        self.player_window = None
        self.current_state = {}
    
    def broadcast_event(self, event_type, data):
        """Send state change to both windows"""
        event = {
            "type": event_type,
            "data": data,
            "timestamp": time.time()
        }
        
        # Update DM window (full data)
        if self.dm_window:
            self.dm_window.handle_event(event)
        
        # Update player window (filtered data)
        if self.player_window:
            filtered_event = self.filter_for_players(event)
            self.player_window.handle_event(filtered_event)
    
    def filter_for_players(self, event):
        """Remove hidden information from event"""
        if event["type"] == "token_move":
            # Only show visible tokens
            if event["data"]["token"]["visibility"]["visible_to_players"]:
                return event
            return None
        
        elif event["type"] == "fog_reveal":
            # Always show fog reveals
            return event
        
        elif event["type"] == "marker_add":
            # Only show public markers
            if not event["data"]["marker"]["secret"]:
                return event
            return None
        
        return event

Synchronized Events

  • Map Navigation: When DM changes maps, player display updates
  • Fog of War: Revealed hexes appear on player display in real-time
  • Token Movement: Player can move own tokens, DM sees all movement
  • Marker Placement: Public markers appear on both displays
  • Zoom/Pan: DM can "push" their view to player display
  • Combat Actions: Initiative, HP changes, conditions broadcast

Database Schema

Core Tables

-- Campaigns
CREATE TABLE campaigns (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    theme TEXT,
    created_at TIMESTAMP,
    last_played TIMESTAMP
);

-- Map Nodes (Hierarchical)
CREATE TABLE nodes (
    id TEXT PRIMARY KEY,
    campaign_id TEXT REFERENCES campaigns(id),
    parent_node_id TEXT REFERENCES nodes(id),  -- NULL for world map
    node_type TEXT,  -- world/region/town/dungeon/room
    name TEXT,
    coords_x INTEGER,
    coords_y INTEGER,
    coords_z INTEGER,  -- For dungeon levels
    heightmap_file TEXT,  -- Path to PNG file
    metadata JSON,  -- Flexible storage for generation data
    created_at TIMESTAMP
);

-- Markers
CREATE TABLE markers (
    id TEXT PRIMARY KEY,
    node_id TEXT REFERENCES nodes(id),
    marker_type TEXT,  -- town/dungeon/npc/item/encounter
    name TEXT,
    local_x INTEGER,
    local_y INTEGER,
    target_node_id TEXT REFERENCES nodes(id),  -- Where this leads
    secret BOOLEAN DEFAULT false,
    metadata JSON,
    created_at TIMESTAMP
);

-- Tokens
CREATE TABLE tokens (
    id TEXT PRIMARY KEY,
    campaign_id TEXT REFERENCES campaigns(id),
    current_node_id TEXT REFERENCES nodes(id),
    name TEXT,
    token_type TEXT,  -- pc/npc/monster
    owner TEXT,  -- player name or "dm"
    position_x INTEGER,
    position_y INTEGER,
    stats JSON,
    visibility JSON,
    image_path TEXT,
    created_at TIMESTAMP
);

-- Fog of War State
CREATE TABLE fog_of_war (
    node_id TEXT REFERENCES nodes(id),
    hex_x INTEGER,
    hex_y INTEGER,
    hex_z INTEGER,
    revealed BOOLEAN DEFAULT false,
    visibility REAL DEFAULT 0.0,  -- 0.0 to 1.0
    revealed_at TIMESTAMP,
    PRIMARY KEY (node_id, hex_x, hex_y, hex_z)
);

-- Session Log
CREATE TABLE session_log (
    id TEXT PRIMARY KEY,
    campaign_id TEXT REFERENCES campaigns(id),
    timestamp TIMESTAMP,
    event_type TEXT,
    event_data JSON
);

Performance Optimization

Caching Strategy

  1. Heightmap Cache: Keep last 5 accessed heightmaps in RAM
  2. Render Cache: Cache rendered map tiles at current zoom level
  3. Token Sprites: Pre-load and cache all token images
  4. Fog State: Cache revealed hex sets for fast lookup
  5. Database: Index on coordinates and parent relationships

Rendering Pipeline

Frame Update (60 FPS target):
  1. Check if camera moved or zoom changed
     ├─ Yes: Mark visible tiles dirty
     └─ No: Skip terrain rendering
  
  2. Render visible terrain tiles only
     ├─ Use cached tiles if available
     └─ Render new tiles if needed
  
  3. Apply fog of war mask
     ├─ Use cached fog state
     └─ Only recalculate if state changed
  
  4. Render tokens
     ├─ Only tokens in viewport
     └─ Use sprite cache
  
  5. Render UI overlay
     ├─ Grid lines
     └─ Selection highlights
  
  6. Blit to screen

Network Optimization (Future: Remote Players)

  • Delta compression for state updates
  • Tile-based map streaming
  • Token position interpolation
  • Lazy loading of non-visible data

User Workflows

Starting a New Campaign

  1. DM clicks "New Campaign"
  2. System prompts for campaign name and theme
  3. System generates world map (4097x4097 fractal)
  4. Applies initial erosion and detail
  5. Sets default sea level
  6. Saves world to database
  7. Opens DM control window at world level

Placing a New Town

  1. DM right-clicks on world map
  2. Selects "Add Marker → Town"
  3. System extracts terrain under cursor
  4. System prompts LLM with context
  5. LLM returns town JSON
  6. System creates town node in database
  7. System extracts and upscales heightmap region
  8. Town marker appears on world map
  9. DM clicks marker to zoom into town

Running Combat

  1. DM navigates to combat location (room/clearing)
  2. DM clicks "Start Combat"
  3. System switches to grid mode
  4. DM places monster tokens
  5. System rolls initiative
  6. DM clicks "Push to Display"
  7. Player display shows combat map
  8. Players see their tokens and revealed enemies
  9. DM tracks HP, conditions, turn order
  10. Player display updates in real-time

Exploring a Dungeon

  1. Party enters dungeon entrance marker
  2. System loads dungeon map
  3. Player display shows only entrance room (fog)
  4. DM uses reveal brush as party explores
  5. Hexes reveal on player display
  6. Players discover markers (secret doors, treasure)
  7. Clicking exit leads to next level
  8. System generates if not exists

Implementation Phases

Phase 1: Core Dual Window System

  • [ ] Implement window manager with two Pygame windows
  • [ ] Create state synchronization system
  • [ ] Build event broadcasting
  • [ ] Basic camera controls in both windows
  • [ ] Display same map in both windows

Phase 2: Heightmap Extraction

  • [ ] Implement region extraction algorithm
  • [ ] Build upscaling system (PIL bilinear)
  • [ ] Test extraction at multiple zoom levels
  • [ ] Cache extracted regions
  • [ ] Verify water level consistency

Phase 3: Fog of War

  • [ ] Implement fog state storage
  • [ ] Build reveal/hide tools for DM
  • [ ] Render fog overlay on player display
  • [ ] Add manual brush reveal
  • [ ] Implement save/load fog state

Phase 4: LLM Generation

  • [ ] Design JSON schema for locations
  • [ ] Build prompt templates
  • [ ] Implement Claude API integration
  • [ ] Parse and validate JSON responses
  • [ ] Create markers from generated data
  • [ ] Handle generation failures gracefully

Phase 5: Token System

  • [ ] Design token data structure
  • [ ] Implement token placement/movement
  • [ ] Build drag-and-drop interface
  • [ ] Add HP/status tracking
  • [ ] Sync tokens between displays
  • [ ] Filter hidden tokens from player view

Phase 6: Combat Mode

  • [ ] Build initiative tracker
  • [ ] Add grid snapping for tokens
  • [ ] Implement AoE templates
  • [ ] Create measurement tools
  • [ ] Add turn indicator
  • [ ] Build condition assignment UI

Phase 7: Polish & Optimization

  • [ ] Implement tile-based rendering
  • [ ] Add render caching
  • [ ] Optimize fog of war rendering
  • [ ] Add keyboard shortcuts
  • [ ] Build session logging
  • [ ] Create backup/restore system

Technical Requirements

Hardware

  • Minimum: Dual-core CPU, 8GB RAM, integrated graphics
  • Recommended: Quad-core CPU, 16GB RAM, dedicated GPU
  • Display: Two monitors (any resolution, player display scales)

Software Dependencies

  • Python 3.10+
  • Pygame 2.5+
  • NumPy 1.24+
  • Pillow (PIL) 10.0+
  • Anthropic API (Claude)
  • SQLite 3

File Storage

  • World heightmaps: ~32MB per 4097x4097 map (16-bit PNG)
  • Region heightmaps: ~2MB per 1000x1000 map
  • Database: ~100MB per campaign (typical)
  • Token images: Variable
  • Total: ~1GB per active campaign

Future Enhancements

Multiplayer Support

  • WebSocket server for remote players
  • Browser-based player clients
  • Shared token control
  • Chat/dice rolling integration

Advanced Generation

  • Weather systems
  • Time of day lighting
  • Seasonal changes
  • Dynamic events (fires, floods)

Content Library

  • Pre-generated town templates
  • Monster stat blocks database
  • Magic item library
  • Spell effect visuals

DM Tools

  • Note-taking system
  • NPC relationship graphs
  • Quest tracking
  • Loot tables
  • Random encounter generator

Audio Integration

  • Ambient sound based on terrain
  • Music zones
  • Sound effects for actions
  • Voice chat integration

Conclusion

This dual-monitor campaign manager transforms procedurally generated fractal worlds into a comprehensive virtual tabletop system. By separating DM control from player display and leveraging hierarchical heightmap extraction, the system provides seamless zoom from world-scale to tactical grid while maintaining terrain consistency.

The LLM-driven content generation ensures infinite, contextually appropriate locations, while the fog of war and token systems enable classic tabletop gameplay enhanced by digital tools. The result is a system that feels like having an infinitely detailed, pre-made campaign world that generates itself on-demand as players explore.

Core Philosophy: Usable beats perfect. Every feature prioritizes functionality and game flow over technical perfection. The system should enhance gameplay, not interrupt it.

Forging the World Engine

J. Rogers, SE Ohio

The code is here: https://github.com/BuckRogers1965/Mega-Mappers/tree/main/CodexProject

Today, we transformed the Codex Engine's map generator from a proof-of-concept into a robust, physics-driven simulation. The initial state was unacceptable—a static, spiky mess that failed to model a real world. We didn't just patch it; we rebuilt it from the ground up, following a strict architectural specification.

Today's Progress: A Multi-Layered Simulation

The core principle of today's work was moving from simple noise filters to a multi-stage geological simulation. A world isn't just random bumps; it's a history of formation and erosion.

  1. Fractal Foundation: We threw out the old noise logic entirely. The new generator now uses a periodic sine-based fractal noise that naturally wraps seamlessly. It uses coordinates from 0 to 2Ï€. This creates the large-scale, low-frequency structures that form the basis of continents and ocean basins. It's no longer a random field of static; it's a coherent landmass.  There are still some lines between edges, so the world is not yet overlapping correctly.

  2. Simulated Weathering: A raw fractal is still too perfect. We implemented two critical physics passes as per the specification:

    • Hydraulic Erosion: By simulating thousands of virtual "raindrops," the system now carves realistic river valleys and deposits sediment to create smooth coastal plains.

    • Thermal Erosion: This pass simulates gravity, collapsing slopes that are too steep into natural, weathered angles. This crucial step eliminated the "bed of nails" look and produced believable mountain ranges.

    • We had to reduce these two techniques to a less abrasive method to keep detail in the world.

  3. Real-Time Physics in the Viewer: The map is no longer just a pretty picture; it's an interactive model.

    • Dynamic Water Level: The sea level is now a plane that rises and falls over the fixed geometry of the heightmap. The normalization process ensures the slider is intuitive: 0% is a dry world, 100% is a water world.

    • Dynamic Lighting: We implemented a real-time hillshading engine. The GM now has full control over the sun's direction, height, and intensity, allowing for the creation of dramatic shadows that reveal the topography.  This is ok, but we can improve the simulation to make the design better.

  4. The GM's Toolkit: A simulation is useless without tools for strategy.

    • The Annotation Layer: GMs can now Shift-Click to place persistent markers on the map. These markers can be moved by dragging, edited, and deleted. This is the first step in turning the map from a piece of art into a campaign dashboard.

    • The Viewport Grid: The Hex/Square grid is now a true viewport overlay. It remains a fixed size on the screen, acting as a tactical reference that is independent of the map's zoom level. The scale bar in the corner provides instant context for travel distance.

    • We want to add more overlays, political boundaries, god maps, lay lines so we can keep tract of the world. 

The Plan for Tomorrow: Drilling Down

Today, we built the macro-scale world. Tomorrow, we connect it to the micro.

The markers we implemented are currently just notes. The next logical and most critical step is to turn them into entry points.

When a GM clicks a marker labeled "Dungeon" or "Town," the system will "drill down," generating a new map for that specific location. This is the core of the project's "fractal" design philosophy, where detail is generated on demand, and then persisted to database and disk.

The technical implementation will be:

  1. Node Hierarchy: Clicking a marker will query the database for a child node linked to that location.

  2. On-Demand Generation: If no child node exists, the system will call the appropriate generator plugin—the Tactical Dungeon Generator for a dungeon marker, or a Settlement Generator for a town. This generation will be saved to a new .png heightmap and linked in the database.

  3. Seamless Transition: The MapViewer will load this new node, automatically switching its RenderStrategy from the world-scale ImageMapStrategy to a new GridMapStrategy designed for square-grid tactical maps.

Tomorrow, we stop looking at the world from orbit. We land.

The code is here: https://github.com/BuckRogers1965/Mega-Mappers/tree/main/CodexProject

Progress on the campaign manager

You can see that you can build tactical maps automatically from the world map data.  You can place roads, streams, buildings. The framework ...