This guide provides technical information for developers working on the Luminari Wilderness Editor project.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend API │ │ LuminariMUD │
│ (React/TS) │◄──►│ (FastAPI) │◄──►│ MySQL │
│ │ │ (Python 3.8+) │ │ │
│ - Map Interface │ │ - Authentication│ │ - Game Tables │
│ - Drawing Tools │ │ - CRUD Ops │ │ - Spatial Data │
│ - State Mgmt │ │ - Game Integration │ - Live Game DB │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- React 18.3+: Component-based UI framework
- TypeScript 5.5+: Static typing and enhanced developer experience
- Vite: Fast build tool and development server
- Tailwind CSS: Utility-first CSS framework
- Lucide React: Icon library
- Supabase Client: Authentication and real-time features
- ESLint: Code linting and style enforcement
- TypeScript Compiler: Type checking and compilation
- PostCSS: CSS processing with Tailwind
- Vite Dev Server: Hot module replacement and fast builds
wildeditor/
├── apps/
│ ├── frontend/ # React TypeScript frontend
│ │ ├── src/
│ │ │ ├── components/ # React components
│ │ │ │ ├── AuthForm.tsx
│ │ │ │ ├── MapCanvas.tsx
│ │ │ │ ├── ToolPalette.tsx
│ │ │ │ ├── PropertiesPanel.tsx
│ │ │ │ └── StatusBar.tsx
│ │ │ ├── hooks/ # Custom React hooks
│ │ │ │ ├── useAuth.ts # Authentication logic
│ │ │ │ └── useEditor.ts # Editor state management
│ │ │ ├── services/ # API client and external services
│ │ │ │ └── api.ts # API client functions
│ │ │ ├── lib/ # Utility libraries
│ │ │ │ └── supabase.ts # Supabase client
│ │ │ ├── types/ # Type imports from shared
│ │ │ │ └── index.ts # Re-exports from @wildeditor/shared
│ │ │ ├── App.tsx # Main application component
│ │ │ └── main.tsx # Application entry point
│ │ └── package.json # Frontend dependencies
│ └── backend/ # Express TypeScript API (TEMPORARY)
│ ├── src/
│ │ ├── controllers/ # Request handlers
│ │ ├── routes/ # API route definitions
│ │ ├── middleware/ # Auth & validation middleware
│ │ ├── models/ # Database models
│ │ ├── config/ # Configuration files
│ │ └── index.ts # Express server entry point
│ └── package.json # Backend dependencies
├── packages/
│ └── shared/ # Shared TypeScript types
│ ├── src/
│ │ └── types/
│ │ └── index.ts # Shared interfaces
│ └── package.json # Shared package config
├── docs/ # Documentation
├── package.json # Root workspace configuration
├── turbo.json # Turborepo configuration
└── database-setup.sql # Supabase schema (development)
- Node.js 18+ and npm
- Git for version control
- VS Code (recommended) with extensions:
- TypeScript and JavaScript Language Features
- ES7+ React/Redux/React-Native snippets
- Tailwind CSS IntelliSense
- ESLint
-
Clone and setup
git clone https://github.com/moshehbenavraham/wildeditor.git cd wildeditor npm install -
Environment configuration
cp .env.example .env # Edit .env with your settings -
Start development servers (both frontend and backend)
npm run dev # Starts both frontend (:5173) and backend (:8000) -
Open in browser Navigate to
http://localhost:5173
# Start both frontend and backend
npm run dev
# Start individual services
npm run dev:frontend # Frontend only (:5173)
npm run dev:backend # Backend only (:8000)
# Build all packages
npm run build
# Build individual packages
npm run build:frontend
npm run build:backend
# Type checking across all packages
npm run type-check
# Linting across all packages
npm run lint
npm run lint:fix
# Clean all build artifacts
npm run cleanCurrent Implementation (Development):
- Express.js backend with TypeScript (TEMPORARY)
- Supabase PostgreSQL for data persistence
- Shared types package for consistency
- JWT authentication via Supabase
Future Implementation (Production):
- Python FastAPI backend
- Direct MySQL integration with LuminariMUD
- Same API contract and shared types
- Enhanced spatial operations
# Development build with hot reload
npm run dev
# Production build
npm run build
# Preview production build
npm run preview
# Lint code
npm run lintThe central map component handles wilderness display and interaction:
interface MapProps {
regions: Region[];
paths: Path[];
selectedFeature?: Feature;
onFeatureSelect: (feature: Feature) => void;
onCoordinateClick: (coords: WildernessCoordinates) => void;
}
export const Map: React.FC<MapProps> = ({
regions,
paths,
selectedFeature,
onFeatureSelect,
onCoordinateClick
}) => {
// Map implementation
};Key Features:
- Canvas-based rendering for performance
- Zoom and pan functionality
- Layer management (regions, paths, grid)
- Click-to-coordinate conversion
- Feature selection and highlighting
Each drawing tool is implemented as a separate component:
interface DrawingToolProps {
isActive: boolean;
onFeatureCreate: (feature: Feature) => void;
onCancel: () => void;
}
export const PolygonTool: React.FC<DrawingToolProps> = ({
isActive,
onFeatureCreate,
onCancel
}) => {
// Polygon drawing logic
};Tool Types:
- SelectTool: Feature selection and editing with precision algorithms
- PointTool: Single-point landmark creation
- PolygonTool: Multi-point region creation with validation
- LinestringTool: Linear path creation with validation
The drawing system has been significantly optimized for performance:
// Memoized coordinate transformations
const transformedRegions = useMemo(() => {
return regions.map(region => ({
...region,
canvasCoords: region.coordinates.map(gameToCanvas)
}));
}, [regions, gameToCanvas]);
// Optimized drawing functions
const drawRegionOptimized = useCallback((ctx, region) => {
// Uses pre-computed canvas coordinates
// Avoids coordinate transformation during render
}, [state.selectedItem]);Key Optimizations:
- Pre-computed coordinate transformations with memoization
- Selective re-rendering based on state changes
- Canvas cleanup in useEffect hooks
- Reduced computational overhead for complex drawings
const isPointInPolygon = useCallback((point: Coordinate, polygon: Coordinate[]): boolean => {
if (polygon.length < 3) return false;
let isInside = false;
const x = point.x, y = point.y;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[j].y;
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
isInside = !isInside;
}
}
return isInside;
}, []);const distanceToLineSegment = useCallback((point: Coordinate, lineStart: Coordinate, lineEnd: Coordinate): number => {
const A = point.x - lineStart.x;
const B = point.y - lineStart.y;
const C = lineEnd.x - lineStart.x;
const D = lineEnd.y - lineStart.y;
const dot = A * C + B * D;
const lenSq = C * C + D * D;
if (lenSq === 0) return Math.sqrt(A * A + B * B);
let param = Math.max(0, Math.min(1, dot / lenSq));
const xx = lineStart.x + param * C;
const yy = lineStart.y + param * D;
return Math.sqrt((point.x - xx) ** 2 + (point.y - yy) ** 2);
}, []);The coordinate system has been completely rewritten for accuracy:
const canvasToGame = useCallback((clientX: number, clientY: number): Coordinate => {
const rect = canvasRef.current?.getBoundingClientRect();
const canvas = canvasRef.current;
if (!rect || !canvas) return { x: 0, y: 0 };
// Account for zoom scaling
const scale = state.zoom / 100;
const canvasX = clientX - rect.left;
const canvasY = clientY - rect.top;
// Convert to actual canvas coordinates with zoom consideration
const actualCanvasWidth = canvas.width / scale;
const actualCanvasHeight = canvas.height / scale;
const normalizedX = Math.max(0, Math.min(1, canvasX / actualCanvasWidth));
const normalizedY = Math.max(0, Math.min(1, canvasY / actualCanvasHeight));
return {
x: Math.round((normalizedX * 2048) - 1024),
y: Math.round(1024 - (normalizedY * 2048))
};
}, [state.zoom]);Key Fixes:
- Proper zoom-aware coordinate conversion
- Canvas scaling consideration
- Bounds checking and clamping
- Accurate mouse position tracking at all zoom levels
The application uses React hooks for state management:
// Map state hook
export const useMap = () => {
const [zoom, setZoom] = useState(1);
const [center, setCenter] = useState({ x: 0, y: 0 });
const [layers, setLayers] = useState({
regions: true,
paths: true,
grid: false
});
return {
zoom,
center,
layers,
setZoom,
setCenter,
toggleLayer: (layer: string) => {
setLayers(prev => ({
...prev,
[layer]: !prev[layer]
}));
}
};
};// tailwind.config.js
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
wilderness: {
forest: '#228B22',
mountain: '#8B7355',
water: '#4682B4',
desert: '#F4A460'
}
},
spacing: {
'18': '4.5rem',
'88': '22rem'
}
}
},
plugins: []
};// Use consistent class patterns
const buttonClasses = {
base: 'px-4 py-2 rounded-md font-medium transition-colors',
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700'
};
export const Button: React.FC<ButtonProps> = ({ variant = 'primary', children, ...props }) => (
<button
className={`${buttonClasses.base} ${buttonClasses[variant]}`}
{...props}
>
{children}
</button>
);// lib/api.ts
class ApiClient {
private baseUrl: string;
private token?: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
setToken(token: string) {
this.token = token;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...(this.token && { Authorization: `Bearer ${this.token}` }),
...options.headers
};
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
// Region methods
async getRegions(params?: RegionQueryParams): Promise<Region[]> {
const query = new URLSearchParams(params as any).toString();
return this.request<Region[]>(`/regions?${query}`);
}
async createRegion(region: CreateRegionRequest): Promise<Region> {
return this.request<Region>('/regions', {
method: 'POST',
body: JSON.stringify(region)
});
}
// Path methods
async getPaths(params?: PathQueryParams): Promise<Path[]> {
return this.request<Path[]>('/paths');
}
// Session methods
async saveSession(changes: SessionChanges): Promise<void> {
return this.request<void>('/session/save', {
method: 'POST',
body: JSON.stringify(changes)
});
}
}
export const apiClient = new ApiClient(process.env.VITE_API_URL || 'http://localhost:3000/api');// hooks/useRegions.ts
export const useRegions = (params?: RegionQueryParams) => {
const [regions, setRegions] = useState<Region[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchRegions = async () => {
try {
setLoading(true);
const data = await apiClient.getRegions(params);
setRegions(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchRegions();
}, [params]);
return { regions, loading, error, refetch: fetchRegions };
};// lib/coordinates.ts
export interface WildernessCoordinates {
x: number; // -1024 to 1024
y: number; // -1024 to 1024
}
export interface ScreenCoordinates {
x: number; // Screen pixels
y: number; // Screen pixels
}
export class CoordinateSystem {
constructor(
private mapWidth: number,
private mapHeight: number,
private wildernessSize: number = 2048
) {}
screenToWilderness(screen: ScreenCoordinates, zoom: number): WildernessCoordinates {
const scaledX = screen.x / zoom;
const scaledY = screen.y / zoom;
const x = Math.round((scaledX / this.mapWidth) * this.wildernessSize - this.wildernessSize / 2);
const y = Math.round(this.wildernessSize / 2 - (scaledY / this.mapHeight) * this.wildernessSize);
return { x, y };
}
wildernessToScreen(wilderness: WildernessCoordinates, zoom: number): ScreenCoordinates {
const normalizedX = (wilderness.x + this.wildernessSize / 2) / this.wildernessSize;
const normalizedY = (this.wildernessSize / 2 - wilderness.y) / this.wildernessSize;
const x = normalizedX * this.mapWidth * zoom;
const y = normalizedY * this.mapHeight * zoom;
return { x, y };
}
validateCoordinates(coords: WildernessCoordinates): boolean {
const max = this.wildernessSize / 2;
return coords.x >= -max && coords.x <= max && coords.y >= -max && coords.y <= max;
}
}// components/tools/PolygonTool.tsx
export const PolygonTool: React.FC<DrawingToolProps> = ({ isActive, onFeatureCreate }) => {
const [points, setPoints] = useState<WildernessCoordinates[]>([]);
const [isDrawing, setIsDrawing] = useState(false);
const handleMapClick = useCallback((coords: WildernessCoordinates) => {
if (!isActive) return;
if (!isDrawing) {
// Start new polygon
setPoints([coords]);
setIsDrawing(true);
} else {
// Add point to current polygon
setPoints(prev => [...prev, coords]);
}
}, [isActive, isDrawing]);
const handleDoubleClick = useCallback(() => {
if (points.length >= 3) {
// Complete polygon
const region: Region = {
coordinates: points,
type: RegionType.GEOGRAPHIC,
// ... other properties
};
onFeatureCreate(region);
setPoints([]);
setIsDrawing(false);
}
}, [points, onFeatureCreate]);
const handleKeyPress = useCallback((event: KeyboardEvent) => {
if (event.key === 'Escape') {
// Cancel drawing
setPoints([]);
setIsDrawing(false);
} else if (event.key === 'Enter' && points.length >= 3) {
// Complete polygon
handleDoubleClick();
}
}, [points, handleDoubleClick]);
useEffect(() => {
if (isActive) {
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}
}, [isActive, handleKeyPress]);
return (
<div className="polygon-tool">
{isDrawing && (
<div className="drawing-instructions">
<p>Click to add points. Double-click or press Enter to finish.</p>
<p>Press Escape to cancel.</p>
</div>
)}
</div>
);
};// __tests__/coordinates.test.ts
import { CoordinateSystem } from '../lib/coordinates';
describe('CoordinateSystem', () => {
const coordSystem = new CoordinateSystem(1024, 1024, 2048);
test('converts screen to wilderness coordinates correctly', () => {
const screen = { x: 512, y: 512 };
const wilderness = coordSystem.screenToWilderness(screen, 1);
expect(wilderness).toEqual({ x: 0, y: 0 });
});
test('validates coordinates within range', () => {
expect(coordSystem.validateCoordinates({ x: 0, y: 0 })).toBe(true);
expect(coordSystem.validateCoordinates({ x: 1025, y: 0 })).toBe(false);
expect(coordSystem.validateCoordinates({ x: 0, y: -1025 })).toBe(false);
});
});// __tests__/api.test.ts
import { apiClient } from '../lib/api';
describe('API Client', () => {
test('fetches regions successfully', async () => {
const regions = await apiClient.getRegions();
expect(Array.isArray(regions)).toBe(true);
expect(regions.length).toBeGreaterThan(0);
expect(regions[0]).toHaveProperty('vnum');
expect(regions[0]).toHaveProperty('coordinates');
});
test('creates region with valid data', async () => {
const newRegion = {
name: 'Test Region',
zone_vnum: 10000,
region_type: 1,
coordinates: [
{ x: 100, y: 100 },
{ x: 200, y: 100 },
{ x: 200, y: 200 },
{ x: 100, y: 200 }
]
};
const created = await apiClient.createRegion(newRegion);
expect(created).toHaveProperty('vnum');
expect(created.name).toBe(newRegion.name);
});
});// components/map/MapCanvas.tsx
export const MapCanvas: React.FC<MapCanvasProps> = ({ regions, paths, zoom }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationFrameRef = useRef<number>();
const render = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Render regions
regions.forEach(region => {
ctx.beginPath();
region.coordinates.forEach((coord, index) => {
const screen = coordSystem.wildernessToScreen(coord, zoom);
if (index === 0) {
ctx.moveTo(screen.x, screen.y);
} else {
ctx.lineTo(screen.x, screen.y);
}
});
ctx.closePath();
ctx.fillStyle = getRegionColor(region.type);
ctx.fill();
ctx.strokeStyle = '#000';
ctx.stroke();
});
// Render paths
paths.forEach(path => {
ctx.beginPath();
path.coordinates.forEach((coord, index) => {
const screen = coordSystem.wildernessToScreen(coord, zoom);
if (index === 0) {
ctx.moveTo(screen.x, screen.y);
} else {
ctx.lineTo(screen.x, screen.y);
}
});
ctx.strokeStyle = getPathColor(path.type);
ctx.lineWidth = getPathWidth(path.type);
ctx.stroke();
});
}, [regions, paths, zoom]);
useEffect(() => {
const animate = () => {
render();
animationFrameRef.current = requestAnimationFrame(animate);
};
animate();
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [render]);
return <canvas ref={canvasRef} className="map-canvas" />;
};// hooks/useMemoryOptimization.ts
export const useMemoryOptimization = () => {
const [visibleFeatures, setVisibleFeatures] = useState<Feature[]>([]);
const updateVisibleFeatures = useCallback((
allFeatures: Feature[],
viewport: Viewport,
zoom: number
) => {
// Only render features within viewport
const visible = allFeatures.filter(feature =>
isFeatureInViewport(feature, viewport, zoom)
);
setVisibleFeatures(visible);
}, []);
return { visibleFeatures, updateVisibleFeatures };
};// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['./src/lib/coordinates', './src/lib/geometry']
}
}
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
});// lib/errorHandler.ts
export class ErrorHandler {
static handle(error: Error, context?: string) {
console.error(`Error in ${context}:`, error);
// Send to monitoring service
if (process.env.NODE_ENV === 'production') {
// Send to error tracking service
}
// Show user-friendly message
return this.getUserMessage(error);
}
private static getUserMessage(error: Error): string {
if (error.message.includes('Network')) {
return 'Connection error. Please check your internet connection.';
}
if (error.message.includes('Unauthorized')) {
return 'Please log in to continue.';
}
return 'An unexpected error occurred. Please try again.';
}
}// lib/performance.ts
export class PerformanceMonitor {
static measureRender(componentName: string, renderFn: () => void) {
const start = performance.now();
renderFn();
const end = performance.now();
console.log(`${componentName} render time: ${end - start}ms`);
if (end - start > 16) { // > 60fps
console.warn(`Slow render detected in ${componentName}`);
}
}
static measureApiCall(endpoint: string, apiFn: () => Promise<any>) {
const start = performance.now();
return apiFn().finally(() => {
const end = performance.now();
console.log(`API call to ${endpoint}: ${end - start}ms`);
});
}
}This developer guide provides the foundation for understanding and contributing to the Luminari Wilderness Editor codebase. For more specific implementation details, refer to the inline code documentation and the API documentation.