window.GDD = window.GDD || {}; const { useState, useEffect, useRef, useCallback, useMemo } = React; /* ═══════════════════════════════════════════════ Seeded PRNG (xoshiro128**) ═══════════════════════════════════════════════ */ function createRng(seed) { let s0 = seed >>> 0 || 1; let s1 = (seed * 1103515245 + 12345) >>> 0; let s2 = (s1 * 1103515245 + 12345) >>> 0; let s3 = (s2 * 1103515245 + 12345) >>> 0; function next() { const t = s0; const r = (t << 9 | t >>> 23); s0 = s1; s1 = s2; s2 = s3; s3 ^= t ^ s3; s0 ^= (s0 << 3 | s0 >>> 29); s1 ^= (s1 << 21 | s1 >>> 11); s2 ^= (s2 << 7 | s2 >>> 25); return ((r + ((s0 ^ s3) >>> 0)) >>> 0) / 4294967296; } return { next, range(a, b) { return a + next() * (b - a); }, int(a, b) { return Math.floor(a + next() * (b - a)); }, pick(arr) { return arr[Math.floor(next() * arr.length)]; }, gaussian(mean, sigma) { const u1 = next() || 0.0001; const u2 = next(); return mean + sigma * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); }, }; } /* ═══════════════════════════════════════════════ Spiral Galaxy Generation Algorithm Inspired by https://vercidium.com/blog/random-galaxy-generation-with-c-and-opengl/ Key ideas: - Systems placed along spiral arms - Arms curve (bend) proportional to distance from center - Gravity pushes density toward center - Height variance via sin wave for 3D depth - Security decreases with distance from center - Each arm is a faction's territory ═══════════════════════════════════════════════ */ function generateGalaxy(seed, overrides = {}) { const rng = createRng(seed); // ── Galaxy shape parameters ── const armCount = overrides.armCount ?? 4; const galaxySize = overrides.galaxySize ?? 300; const armSpread = overrides.armSpread ?? 0.42; const rotationStr = overrides.rotationStrength ?? 2.8; const gravity = overrides.gravity ?? 1.4; const heightMag = overrides.heightMagnitude ?? 15; const heightFreq = overrides.heightFrequency ?? 0.018; const conformChance = 0.72; // ── Faction/arm colors ── const factionDefs = [ { id: 'caldari', name: 'Caldari State', faction: 'Caldari', color: '#38bdf8', armColor: 0x38bdf8 }, { id: 'minmatar', name: 'Minmatar Republic', faction: 'Minmatar', color: '#ef4444', armColor: 0xef4444 }, { id: 'amarr', name: 'Amarr Empire', faction: 'Amarr', color: '#f59e0b', armColor: 0xf0a030 }, { id: 'gallente', name: 'Gallente Federation', faction: 'Gallente', color: '#a855f7', armColor: 0xa855f7 }, ]; const starColors = { O: '#9bb0ff', B: '#aabfff', A: '#cad7ff', F: '#f8f7ff', G: '#fff4ea', K: '#ffd2a1', M: '#ffcc6f' }; const starWeights = { O: 0.01, B: 0.03, A: 0.06, F: 0.1, G: 0.15, K: 0.2, M: 0.45 }; const oreBySec = [ { sec: 0.8, ores: ['Veldspar', 'Scordite'] }, { sec: 0.5, ores: ['Veldspar', 'Scordite', 'Pyroxeres'] }, { sec: 0.1, ores: ['Scordite', 'Pyroxeres', 'Kernite', 'Omber'] }, { sec: -0.1, ores: ['Kernite', 'Omber', 'Jaspet', 'Hemorphite'] }, { sec: -0.5, ores: ['Jaspet', 'Hemorphite', 'Arkonor'] }, ]; function pickStarType() { const r = rng.next(); let acc = 0; for (const [type, w] of Object.entries(starWeights)) { acc += w; if (r < acc) return type; } return 'M'; } function getOresForSec(sec) { for (let i = oreBySec.length - 1; i >= 0; i--) { if (sec >= oreBySec[i].sec) return oreBySec[i].ores; } return oreBySec[oreBySec.length - 1].ores; } // ── Spiral arm position calculator ── function spiralPosition(armIdx, distFactor) { const baseAngle = (2 * Math.PI / armCount) * armIdx; const distance = Math.pow(distFactor, 0.6) * galaxySize; const spreadAngle = armSpread * (Math.PI / armCount) * (0.4 + distFactor * 0.8); const angleOffset = rng.gaussian(0, spreadAngle); const bendAngle = distFactor * rotationStr; const angle = baseAngle + angleOffset + bendAngle; const x = distance * Math.cos(angle); const z = distance * Math.sin(angle); const y = heightMag * Math.sin(distance * heightFreq) * (1 - distFactor * 0.3) + rng.gaussian(0, 2 + distFactor * 4); return { x, y, z, distance, angle: baseAngle + bendAngle }; } // ── Security from distance ── function securityFromDist(distFactor) { // Center (0) → 1.0, Edge (1) → -1.0 const sec = 1.0 - distFactor * 2.0; return Math.round(Math.max(-1, Math.min(1, sec + rng.range(-0.05, 0.05))) * 100) / 100; } // ── Build data structures ── const regions = []; const constellations = []; const systems = []; const stargates = []; const stations = []; const belts = []; const agents = []; // Core region — shared high-sec hub at center const coreRegion = { id: 'core', name: 'Core Worlds', faction: 'Concord', factionColor: '#22d3ee', secMin: 0.8, secMax: 1.0, color: '#22c55e', center: { x: 0, y: 0, z: 0 }, systemIds: [], }; regions.push(coreRegion); // Core systems (5-8 systems near center) const coreConstCount = 2; for (let ci = 0; ci < coreConstCount; ci++) { const constId = `core_c${ci}`; const cx = rng.gaussian(0, 15); const cy = rng.gaussian(0, 3); const cz = rng.gaussian(0, 15); const con = { id: constId, regionId: 'core', x: cx, y: cy, z: cz, systemIds: [] }; constellations.push(con); const sysCount = rng.int(3, 5); for (let si = 0; si < sysCount; si++) { const sx = cx + rng.gaussian(0, 8); const sy = cy + rng.gaussian(0, 2); const sz = cz + rng.gaussian(0, 8); const sec = Math.round((0.8 + rng.next() * 0.2) * 100) / 100; const starType = pickStarType(); const sysId = `${constId}_s${si}`; const sysName = `COR-${rng.int(100, 999)}`; const sys = { id: sysId, name: sysName, regionId: 'core', constellationId: constId, x: sx, y: sy, z: sz, security: sec, starType, starColor: starColors[starType], planetCount: rng.int(2, 7), faction: 'Concord', armIndex: -1, distFactor: 0, }; systems.push(sys); con.systemIds.push(sysId); coreRegion.systemIds.push(sysId); // Core systems get lots of stations and services const stationCount = rng.int(2, 4); for (let sti = 0; sti < stationCount; sti++) { const services = ['Market', 'Refinery', 'Factory', 'Fitting']; if (rng.next() > 0.4) services.push('Insurance'); if (rng.next() > 0.5) services.push('Clone Bay'); stations.push({ id: `${sysId}_stn${sti}`, systemId: sysId, name: `${sysName} Station ${String.fromCharCode(65 + sti)}`, services, }); if (rng.next() > 0.3) { agents.push({ stationId: `${sysId}_stn${sti}`, specialty: rng.pick(['Kill', 'Courier', 'Mining', 'Survey', 'Trade']), faction: 'Concord', }); } } const beltCount = rng.int(1, 3); const ores = getOresForSec(sec); for (let bi = 0; bi < beltCount; bi++) { belts.push({ id: `${sysId}_belt${bi}`, systemId: sysId, oreType: rng.pick(ores) }); } } } // ── Arm regions ── for (let armIdx = 0; armIdx < armCount; armIdx++) { const fDef = factionDefs[armIdx % factionDefs.length]; const region = { id: fDef.id, name: fDef.name, faction: fDef.faction, factionColor: fDef.color, secMin: -1.0, secMax: 0.8, color: fDef.color, center: { x: 0, y: 0, z: 0 }, // computed below systemIds: [], armIndex: armIdx, }; // Each arm has 2-4 constellations at different distances const constCount = rng.int(2, 4); let armCx = 0, armCy = 0, armCz = 0; for (let ci = 0; ci < constCount; ci++) { // Distribute constellations along the arm at increasing distances const distBase = 0.15 + (ci / constCount) * 0.75; const distFactor = distBase + rng.range(-0.05, 0.05); const pos = spiralPosition(armIdx, distFactor); const constId = `${fDef.id}_c${ci}`; const con = { id: constId, regionId: fDef.id, x: pos.x, y: pos.y, z: pos.z, systemIds: [], armIndex: armIdx, distFactor: distFactor, }; constellations.push(con); const sysCount = rng.int(3, 6); for (let si = 0; si < sysCount; si++) { // Systems cluster around the constellation center on the arm const sysDistFactor = distFactor + rng.gaussian(0, 0.04); const sysPos = spiralPosition(armIdx, Math.max(0.08, Math.min(0.98, sysDistFactor))); const sec = securityFromDist(sysDistFactor); const starType = pickStarType(); const sysId = `${constId}_s${si}`; const sysName = `${fDef.faction.substring(0, 3).toUpperCase()}-${rng.int(100, 999)}`; const sys = { id: sysId, name: sysName, regionId: fDef.id, constellationId: constId, x: sysPos.x, y: sysPos.y, z: sysPos.z, security: sec, starType, starColor: starColors[starType], planetCount: rng.int(1, 6), faction: fDef.faction, armIndex: armIdx, distFactor: sysDistFactor, }; systems.push(sys); con.systemIds.push(sysId); region.systemIds.push(sysId); armCx += sysPos.x; armCy += sysPos.y; armCz += sysPos.z; // Stations — more in high-sec, fewer in null let stationCount; if (sec >= 0.8) stationCount = rng.int(2, 4); else if (sec >= 0.5) stationCount = rng.int(1, 3); else if (sec >= 0.1) stationCount = rng.int(1, 2); else if (sec >= 0) stationCount = rng.int(0, 2); else stationCount = rng.int(0, 1); for (let sti = 0; sti < stationCount; sti++) { const services = ['Market']; if (rng.next() > 0.3) services.push('Refinery'); if (rng.next() > 0.4) services.push('Factory'); if (rng.next() > 0.3) services.push('Fitting'); if (rng.next() > 0.5) services.push('Insurance'); stations.push({ id: `${sysId}_stn${sti}`, systemId: sysId, name: `${sysName} Station ${String.fromCharCode(65 + sti)}`, services, }); if (rng.next() > 0.4) { agents.push({ stationId: `${sysId}_stn${sti}`, specialty: rng.pick(['Kill', 'Courier', 'Mining', 'Survey', 'Trade']), faction: fDef.faction, }); } } // Belts const beltCount = sec >= 0.8 ? rng.int(1, 3) : sec >= 0.5 ? rng.int(2, 4) : rng.int(2, 5); const ores = getOresForSec(sec); for (let bi = 0; bi < beltCount; bi++) { belts.push({ id: `${sysId}_belt${bi}`, systemId: sysId, oreType: rng.pick(ores) }); } } } // Compute arm center const armSysCount = region.systemIds.length || 1; region.center = { x: armCx / armSysCount, y: armCy / armSysCount, z: armCz / armSysCount }; regions.push(region); } // ── System lookup map for O(1) access ── const sysMap = new Map(); systems.forEach(s => sysMap.set(s.id, s)); // ── Build stargate graph ── if (systems.length < 2) return { regions, constellations, systems, stargates, stations, belts, agents, seed, sysMap, dustPositions: new Float32Array(0), dustColors: new Float32Array(0), dustCount: 0, params: { armCount, galaxySize, armSpread, rotationStr, gravity, heightMag, heightFreq } }; // 1) MST for connectivity (Prim's — uses sysMap for O(1) lookup) const inMST = new Set([systems[0].id]); const mstEdges = []; const distSq = (a, b) => (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2; while (inMST.size < systems.length) { let bestDist = Infinity, bestA = null, bestB = null; for (const aId of inMST) { const a = sysMap.get(aId); for (const b of systems) { if (inMST.has(b.id)) continue; const d = distSq(a, b); if (d < bestDist) { bestDist = d; bestA = aId; bestB = b.id; } } } if (bestB) { inMST.add(bestB); mstEdges.push([bestA, bestB]); } } mstEdges.forEach(([a, b]) => stargates.push({ from: a, to: b, type: 'mst' })); // 2) Intra-constellation extras constellations.forEach(con => { if (con.systemIds.length < 3) return; const extras = rng.int(1, Math.min(3, con.systemIds.length - 1)); for (let i = 0; i < extras; i++) { const a = rng.pick(con.systemIds); const b = rng.pick(con.systemIds.filter(id => id !== a)); const exists = stargates.some(g => (g.from === a && g.to === b) || (g.from === b && g.to === a)); if (!exists) stargates.push({ from: a, to: b, type: 'intra' }); } }); // Pre-compute sorted arm constellations (avoid repeated filter+sort) const armConstsByArm = new Map(); for (let armIdx = 0; armIdx < armCount; armIdx++) { armConstsByArm.set(armIdx, constellations.filter(c => c.armIndex === armIdx).sort((a, b) => a.distFactor - b.distFactor)); } // 3) Intra-arm connections for (let armIdx = 0; armIdx < armCount; armIdx++) { const armConsts = armConstsByArm.get(armIdx); for (let i = 0; i < armConsts.length - 1; i++) { const a = rng.pick(armConsts[i].systemIds); const b = rng.pick(armConsts[i + 1].systemIds); const exists = stargates.some(g => (g.from === a && g.to === b) || (g.from === b && g.to === a)); if (!exists) stargates.push({ from: a, to: b, type: 'arm-link' }); } } // 4) Core hub const coreSys = coreRegion.systemIds; for (let armIdx = 0; armIdx < armCount; armIdx++) { const armConsts = armConstsByArm.get(armIdx); if (armConsts.length > 0 && coreSys.length > 0) { const borderA = rng.pick(armConsts[0].systemIds); const borderB = rng.pick(coreSys); const exists = stargates.some(g => (g.from === borderA && g.to === borderB) || (g.from === borderB && g.to === borderA)); if (!exists) stargates.push({ from: borderA, to: borderB, type: 'hub' }); } } // 5) Cross-arm choke points for (let armIdx = 0; armIdx < armCount; armIdx++) { const nextArm = (armIdx + 1) % armCount; const armA = regions.find(r => r.armIndex === armIdx); const armB = regions.find(r => r.armIndex === nextArm); if (armA && armB && armA.systemIds.length > 0 && armB.systemIds.length > 0) { const borderA = rng.pick(armA.systemIds); const borderB = rng.pick(armB.systemIds); const exists = stargates.some(g => (g.from === borderA && g.to === borderB) || (g.from === borderB && g.to === borderA)); if (!exists) stargates.push({ from: borderA, to: borderB, type: 'choke' }); } } // 6) High-sec shortcuts const highSecSys = systems.filter(s => s.security >= 0.5); const shortcutCount = Math.floor(highSecSys.length * 0.15); for (let i = 0; i < shortcutCount; i++) { const a = rng.pick(highSecSys); const b = rng.pick(highSecSys.filter(s => s.id !== a.id)); const exists = stargates.some(g => (g.from === a.id && g.to === b.id) || (g.from === b.id && g.to === a.id)); if (!exists) stargates.push({ from: a.id, to: b.id, type: 'shortcut' }); } // ── Connectivity check ── const adj = {}; systems.forEach(s => { adj[s.id] = []; }); stargates.forEach(g => { adj[g.from].push(g.to); adj[g.to].push(g.from); }); const visited = new Set(); const queue = [systems[0].id]; visited.add(systems[0].id); while (queue.length > 0) { const cur = queue.shift(); for (const nb of (adj[cur] || [])) { if (!visited.has(nb)) { visited.add(nb); queue.push(nb); } } } const connected = visited.size === systems.length; const chokeSet = new Set(); stargates.filter(g => g.type === 'choke').forEach(g => { chokeSet.add(g.from); chokeSet.add(g.to); }); const starterSystem = systems.find(s => s.security >= 0.95) || systems.find(s => s.security >= 0.8) || systems[0]; function findPath(startId, endId) { const prev = {}; const vis = new Set([startId]); const q = [startId]; while (q.length > 0) { const cur = q.shift(); if (cur === endId) break; for (const nb of (adj[cur] || [])) { if (!vis.has(nb)) { vis.add(nb); prev[nb] = cur; q.push(nb); } } } const path = []; let c = endId; while (c) { path.unshift(c); c = prev[c]; } return path[0] === startId ? path : []; } // ── Generate background dust as flat typed arrays ── // No intermediate objects — writes directly to Float32Arrays const dustCount = 8000; const armColorDefs = factionDefs.map(f => f.armColor); const armR = armColorDefs.map(c => ((c >> 16) & 0xFF) / 255); const armG = armColorDefs.map(c => ((c >> 8) & 0xFF) / 255); const armB = armColorDefs.map(c => (c & 0xFF) / 255); const dustPositions = new Float32Array(dustCount * 3); const dustColors = new Float32Array(dustCount * 3); const TWO_PI = 2 * Math.PI; const armAngleStep = TWO_PI / armCount; for (let i = 0; i < dustCount; i++) { let distance, angle; const isCore = rng.next() < 0.12; if (isCore) { distance = Math.pow(rng.next(), gravity * 1.5) * galaxySize * 0.25; angle = rng.next() * TWO_PI; const brightness = 0.4 + rng.next() * 0.6; dustPositions[i * 3] = distance * Math.cos(angle); dustPositions[i * 3 + 1] = rng.gaussian(0, 2); dustPositions[i * 3 + 2] = distance * Math.sin(angle); dustColors[i * 3] = brightness; dustColors[i * 3 + 1] = brightness * 0.95; dustColors[i * 3 + 2] = brightness * 1.1; } else { const onArm = rng.next() < conformChance; const armIdx = rng.int(0, armCount); const distFactor = Math.pow(rng.next(), gravity); distance = distFactor * galaxySize; const distRatio = distFactor; const baseAngle = armAngleStep * armIdx; const spreadAngle = armSpread * (Math.PI / armCount) * (0.4 + distRatio * 0.9); const bendAngle = distRatio * rotationStr; if (onArm) { const angleOffset = rng.gaussian(0, spreadAngle); angle = baseAngle + angleOffset + bendAngle; const brightness = 0.3 + (1 - distRatio) * 0.7; const iR = (1 - distRatio) * 0.3; dustPositions[i * 3] = distance * Math.cos(angle); dustPositions[i * 3 + 1] = heightMag * Math.sin(distance * heightFreq) * (1 - distRatio * 0.3) + rng.gaussian(0, 1.5 + distRatio * 3); dustPositions[i * 3 + 2] = distance * Math.sin(angle); dustColors[i * 3] = Math.min(1, armR[armIdx] * brightness + iR); dustColors[i * 3 + 1] = Math.min(1, armG[armIdx] * brightness + iR * 0.83); dustColors[i * 3 + 2] = Math.min(1, armB[armIdx] * brightness + iR * 0.67); } else { angle = rng.next() * TWO_PI; const brightness = 0.08 + (1 - distRatio) * 0.15; dustPositions[i * 3] = distance * Math.cos(angle); dustPositions[i * 3 + 1] = heightMag * Math.sin(distance * heightFreq) * (1 - distRatio * 0.3) + rng.gaussian(0, 2 + distRatio * 4); dustPositions[i * 3 + 2] = distance * Math.sin(angle); dustColors[i * 3] = brightness; dustColors[i * 3 + 1] = brightness * 0.95; dustColors[i * 3 + 2] = brightness * 1.1; } } } return { regions, constellations, systems, stargates, stations, belts, agents, seed, connected, chokeSet, starterSystem, findPath, sysMap, params: { armCount, galaxySize, armSpread, rotationStr, gravity, heightMag, heightFreq }, dustPositions, dustColors, dustCount, }; } /* ═══════════════════════════════════════════════ Helper: Security → Color ═══════════════════════════════════════════════ */ function secColor(sec) { if (sec >= 0.8) return 0x22c55e; if (sec >= 0.5) return 0x86efac; if (sec >= 0.1) return 0xf0a030; if (sec >= 0) return 0xfb923c; if (sec >= -0.4) return 0xef4444; return 0xa855f7; } function secColorCSS(sec) { if (sec >= 0.8) return '#22c55e'; if (sec >= 0.5) return '#86efac'; if (sec >= 0.1) return '#f0a030'; if (sec >= 0) return '#fb923c'; if (sec >= -0.4) return '#ef4444'; return '#a855f7'; } function factionColor(faction) { switch (faction) { case 'Caldari': return 0x38bdf8; case 'Gallente': return 0xa855f7; case 'Minmatar': return 0xef4444; case 'Amarr': return 0xf0a030; case 'Concord': return 0x22d3ee; default: return 0x94a3b8; } } /* ═══════════════════════════════════════════════ Galaxy Demo — Three.js 3D (Spiral Galaxy) ═══════════════════════════════════════════════ */ function GalaxyDemo() { const containerRef = useRef(null); const sceneRef = useRef(null); const [seed, setSeed] = useState(42); const [inputSeed, setInputSeed] = useState('42'); const [galaxy, setGalaxy] = useState(null); const [hoveredSystem, setHoveredSystem] = useState(null); const [selectedSystem, setSelectedSystem] = useState(null); const [pathStart, setPathStart] = useState(null); const [pathEnd, setPathEnd] = useState(null); const [currentPath, setCurrentPath] = useState([]); const [showGates, setShowGates] = useState(true); const [showLabels, setShowLabels] = useState(true); const [showDust, setShowDust] = useState(true); const [viewMode, setViewMode] = useState('security'); const [autoRotate, setAutoRotate] = useState(true); const [galaxyParams, setGalaxyParams] = useState({ armCount: 4, rotationStrength: 2.8, armSpread: 0.42, gravity: 1.4, }); // Refs for Three.js objects we need to update without re-render const galaxyRef = useRef(null); const selectedSystemRef = useRef(null); const hoveredSystemRef = useRef(null); const pathStartRef = useRef(null); const pathEndRef = useRef(null); const currentPathRef = useRef([]); const viewModeRef = useRef('security'); const showGatesRef = useRef(true); const showLabelsRef = useRef(true); const showDustRef = useRef(true); const autoRotateRef = useRef(true); // Keep refs in sync with state useEffect(() => { selectedSystemRef.current = selectedSystem; }, [selectedSystem]); useEffect(() => { hoveredSystemRef.current = hoveredSystem; }, [hoveredSystem]); useEffect(() => { pathStartRef.current = pathStart; }, [pathStart]); useEffect(() => { pathEndRef.current = pathEnd; }, [pathEnd]); useEffect(() => { currentPathRef.current = currentPath; }, [currentPath]); useEffect(() => { viewModeRef.current = viewMode; }, [viewMode]); useEffect(() => { showGatesRef.current = showGates; }, [showGates]); useEffect(() => { showLabelsRef.current = showLabels; }, [showLabels]); useEffect(() => { showDustRef.current = showDust; }, [showDust]); useEffect(() => { autoRotateRef.current = autoRotate; }, [autoRotate]); // Generate galaxy data useEffect(() => { const g = generateGalaxy(seed, galaxyParams); setGalaxy(g); setSelectedSystem(null); setHoveredSystem(null); setPathStart(null); setPathEnd(null); setCurrentPath([]); galaxyRef.current = g; }, [seed, galaxyParams]); // ── Three.js scene setup ── useEffect(() => { const TH = window.GDD.THREE; const T = window.THREE; if (!T || !containerRef.current) return; const container = containerRef.current; // Renderer const renderer = TH.createRenderer(container, { clearColor: 0x020508 }); // Scene const scene = new T.Scene(); scene.fog = new T.FogExp2(0x020508, 0.0004); // Camera — elevated view of the disk const camera = new T.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.5, 8000); camera.position.set(0, 320, 420); camera.lookAt(0, 0, 0); // Orbit controller const orbit = new TH.OrbitController(camera, renderer.domElement, new T.Vector3(0, 0, 0)); orbit.distance = 520; orbit.minDistance = 15; orbit.maxDistance = 2000; orbit.panButton = 2; orbit.rotateSpeed = 0.5; // Lighting TH.setupSpaceLighting(scene); // Background star field — distant const starField = TH.createStarField(5000, 4000); scene.add(starField); // ── Scene groups ── const dustGroup = new T.Group(); scene.add(dustGroup); const coreGlowGroup = new T.Group(); scene.add(coreGlowGroup); const systemGroup = new T.Group(); scene.add(systemGroup); const gateGroup = new T.Group(); scene.add(gateGroup); const labelGroup = new T.Group(); scene.add(labelGroup); const highlightGroup = new T.Group(); scene.add(highlightGroup); // ── Raycasting ── const raycaster = new T.Raycaster(); const mouse = new T.Vector2(); const clickMouse = new T.Vector2(); let systemMeshes = []; // Shared geometries — created once, reused across all systems const sharedCoreGeo = new T.SphereGeometry(2.2, 10, 10); const sharedInnerGeo = new T.SphereGeometry(1.0, 6, 6); // Shared glow texture — one canvas, reused for all system glow sprites const glowCanvas = document.createElement('canvas'); glowCanvas.width = 32; glowCanvas.height = 32; const gCtx = glowCanvas.getContext('2d'); const gGrad = gCtx.createRadialGradient(16, 16, 0, 16, 16, 16); gGrad.addColorStop(0, 'rgba(255,255,255,0.8)'); gGrad.addColorStop(0.2, 'rgba(255,255,255,0.4)'); gGrad.addColorStop(0.5, 'rgba(255,255,255,0.1)'); gGrad.addColorStop(1, 'rgba(255,255,255,0)'); gCtx.fillStyle = gGrad; gCtx.fillRect(0, 0, 32, 32); const sharedGlowTexture = new T.CanvasTexture(glowCanvas); let currentSysMap = null; // cached lookup for O(1) system access // ── Build the 3D scene ── // Set of shared resources that must NOT be disposed during scene rebuild const sharedResources = new Set([sharedCoreGeo, sharedInnerGeo, sharedGlowTexture, sharedRingGeo, sharedHoverRingGeo]); function disposeGroup(group, disposeSharedGeo) { while (group.children.length > 0) { const child = group.children[0]; group.remove(child); // Recursively dispose children (e.g., Group → Mesh/Sprite) if (child.children && child.children.length > 0) { disposeGroup(child, false); } // Only dispose non-shared geometry and materials if (child.geometry && (disposeSharedGeo || !sharedResources.has(child.geometry))) { child.geometry.dispose(); } if (child.material) { // Only dispose textures that aren't shared if (child.material.map && !sharedResources.has(child.material.map)) { child.material.map.dispose(); } child.material.dispose(); } } } function buildScene(galaxy) { // Dispose scene groups — pass true only for groups that own their own geometry disposeGroup(systemGroup, false); // uses shared geometry disposeGroup(gateGroup, true); // owns its geometry disposeGroup(labelGroup, true); // owns its canvas textures disposeGroup(highlightGroup, false); // uses shared geometry/textures disposeGroup(dustGroup, true); // owns its geometry disposeGroup(coreGlowGroup, true); // owns its canvas textures systemMeshes = []; currentSysMap = galaxy.sysMap; if (!galaxy) return; // ── Galaxy dust — use pre-built typed arrays directly (no copy) ── const dustGeo = new T.BufferGeometry(); dustGeo.setAttribute('position', new T.BufferAttribute(galaxy.dustPositions, 3)); dustGeo.setAttribute('color', new T.BufferAttribute(galaxy.dustColors, 3)); const dustMat = new T.PointsMaterial({ size: 1.8, vertexColors: true, transparent: true, opacity: 0.55, sizeAttenuation: true, blending: T.AdditiveBlending, depthWrite: false, }); dustGroup.add(new T.Points(dustGeo, dustMat)); // ── Central core glow ── // Large outer glow const outerGlow = TH.createGlowSprite(0xfff8e0, 120); outerGlow.position.set(0, 0, 0); coreGlowGroup.add(outerGlow); // Medium warm glow const midGlow = TH.createGlowSprite(0xffcc66, 60); midGlow.position.set(0, 0, 0); coreGlowGroup.add(midGlow); // Bright inner core const innerGlow = TH.createGlowSprite(0xffffff, 30); innerGlow.position.set(0, 0, 0); coreGlowGroup.add(innerGlow); // ── Arm nebula sprites ── const armColorDefs = [ { color: 0x38bdf8, r: 0x38, g: 0xb5, b: 0xf8 }, { color: 0xef4444, r: 0xef, g: 0x44, b: 0x44 }, { color: 0xf0a030, r: 0xf0, g: 0xa0, b: 0x30 }, { color: 0xa855f7, r: 0xa8, g: 0x55, b: 0xf7 }, ]; const { armCount, galaxySize, rotationStr } = galaxy.params; for (let ai = 0; ai < armCount; ai++) { const ac = armColorDefs[ai % armColorDefs.length]; // Place nebula sprites at 3 distances along each arm for (let di = 0; di < 3; di++) { const df = 0.25 + di * 0.25; const dist = df * galaxySize; const baseAngle = (2 * Math.PI / armCount) * ai; const bendAngle = df * rotationStr; const angle = baseAngle + bendAngle; const x = dist * Math.cos(angle); const z = dist * Math.sin(angle); const y = galaxy.params.heightMag * Math.sin(dist * galaxy.params.heightFreq) * (1 - df * 0.3); const nebula = TH.createGlowSprite(ac.color, 40 + di * 15); nebula.position.set(x, y, z); nebula.material.opacity = 0.04; coreGlowGroup.add(nebula); } } // ── System spheres (shared geometry + per-system materials) ── // Each system gets its own material so viewMode color changes don't corrupt other systems galaxy.systems.forEach(sys => { const group = new T.Group(); group.position.set(sys.x, sys.y, sys.z); group.userData = { systemId: sys.id }; // Core sphere — clickable, shared geometry but own material const secHex = secColor(sys.security); const coreMat = new T.MeshBasicMaterial({ color: secHex }); const core = new T.Mesh(sharedCoreGeo, coreMat); core.userData = { systemId: sys.id, isSystem: true }; group.add(core); // Glow sprite — reuse shared canvas texture, own material const glowMat = new T.SpriteMaterial({ map: sharedGlowTexture, color: secHex, transparent: true, blending: T.AdditiveBlending, depthWrite: false, }); const glow = new T.Sprite(glowMat); glow.scale.setScalar(10); group.add(glow); // Star color inner dot — shared geometry, own material const innerColor = new T.Color(sys.starColor).getHex(); const innerMat = new T.MeshBasicMaterial({ color: innerColor }); const inner = new T.Mesh(sharedInnerGeo, innerMat); group.add(inner); systemGroup.add(group); systemMeshes.push(core); // Label const label = TH.createLabel(sys.name, secColorCSS(sys.security), 16); label.position.set(sys.x, sys.y + 6, sys.z); label.userData = { systemId: sys.id, isLabel: true }; labelGroup.add(label); }); // ── Stargate lines (use sysMap for O(1) lookup) ── galaxy.stargates.forEach(gate => { const sA = currentSysMap.get(gate.from); const sB = currentSysMap.get(gate.to); if (!sA || !sB) return; let color, opacity; if (gate.type === 'mst') { color = 0x0e1a2a; opacity = 0.4; } else if (gate.type === 'intra') { color = 0x0f1e30; opacity = 0.35; } else if (gate.type === 'arm-link') { color = 0x152535; opacity = 0.3; } else if (gate.type === 'choke') { color = 0xf0a030; opacity = 0.35; } else if (gate.type === 'hub') { color = 0x22c55e; opacity = 0.25; } else { color = 0x101820; opacity = 0.2; } const line = TH.createConnectionLine( { x: sA.x, y: sA.y, z: sA.z }, { x: sB.x, y: sB.y, z: sB.z }, color, opacity ); line.userData = { gateFrom: gate.from, gateTo: gate.to, gateType: gate.type }; gateGroup.add(line); }); } // ── Update highlights (reuses geometry, uses sysMap for O(1)) ── const sharedRingGeo = new T.RingGeometry(5, 6, 24); sharedRingGeo.rotateX(-Math.PI / 2); const sharedHoverRingGeo = new T.RingGeometry(4, 4.8, 24); sharedHoverRingGeo.rotateX(-Math.PI / 2); function updateHighlights() { disposeGroup(highlightGroup, false); // uses shared geometry/textures const g = galaxyRef.current; if (!g) return; const sm = currentSysMap; if (!sm) return; const selId = selectedSystemRef.current?.id; const hovId = hoveredSystemRef.current?.id; const pStart = pathStartRef.current; const pEnd = pathEndRef.current; const path = currentPathRef.current; // Selection ring — shared geometry if (selId) { const sys = sm.get(selId); if (sys) { const ringMat = new T.MeshBasicMaterial({ color: 0x22d3ee, transparent: true, opacity: 0.7, side: T.DoubleSide, depthWrite: false, }); const ring = new T.Mesh(sharedRingGeo, ringMat); ring.position.set(sys.x, sys.y, sys.z); highlightGroup.add(ring); } } // Hover ring if (hovId && hovId !== selId) { const sys = sm.get(hovId); if (sys) { const ringMat = new T.MeshBasicMaterial({ color: 0xf1f5f9, transparent: true, opacity: 0.5, side: T.DoubleSide, depthWrite: false, }); const ring = new T.Mesh(sharedHoverRingGeo, ringMat); ring.position.set(sys.x, sys.y, sys.z); highlightGroup.add(ring); } } // Path markers — reuse shared glow texture if (pStart) { const sys = sm.get(pStart); if (sys) { const mat = new T.SpriteMaterial({ map: sharedGlowTexture, color: 0x22c55e, transparent: true, blending: T.AdditiveBlending, depthWrite: false }); const marker = new T.Sprite(mat); marker.scale.setScalar(20); marker.position.set(sys.x, sys.y + 1, sys.z); highlightGroup.add(marker); } } if (pEnd) { const sys = sm.get(pEnd); if (sys) { const mat = new T.SpriteMaterial({ map: sharedGlowTexture, color: 0xef4444, transparent: true, blending: T.AdditiveBlending, depthWrite: false }); const marker = new T.Sprite(mat); marker.scale.setScalar(20); marker.position.set(sys.x, sys.y + 1, sys.z); highlightGroup.add(marker); } } // Route line if (path.length > 1) { const points = path.map(id => { const sys = sm.get(id); return sys ? new T.Vector3(sys.x, sys.y + 2, sys.z) : new T.Vector3(); }); const routeLine = TH.createRouteLine(points.map(p => ({ x: p.x, y: p.y, z: p.z })), 0x22d3ee); highlightGroup.add(routeLine); } } // ── Update system colors based on viewMode (uses sysMap for O(1)) ── function updateSystemColors() { const g = galaxyRef.current; if (!g) return; const mode = viewModeRef.current; const sm = currentSysMap; systemGroup.children.forEach(group => { const sysId = group.userData.systemId; const sys = sm ? sm.get(sysId) : g.systems.find(s => s.id === sysId); if (!sys) return; const core = group.children.find(c => c.userData.isSystem); if (!core) return; let color; if (mode === 'faction') color = factionColor(sys.faction); else if (mode === 'gates') { const gateCount = g.stargates.filter(gate => gate.from === sys.id || gate.to === sys.id).length; color = gateCount >= 4 ? 0xef4444 : gateCount >= 3 ? 0xf0a030 : gateCount >= 2 ? 0x22d3ee : 0x94a3b8; } else { color = secColor(sys.security); } core.material.color.setHex(color); }); } // ── Mouse interaction ── function getHoveredSystem(event) { const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(systemMeshes, false); if (intersects.length > 0) { const sysId = intersects[0].object.userData.systemId; return currentSysMap ? currentSysMap.get(sysId) : (galaxyRef.current?.systems.find(s => s.id === sysId) || null); } return null; } let mouseDownPos = { x: 0, y: 0 }; const onMouseDown = (e) => { mouseDownPos = { x: e.clientX, y: e.clientY }; }; const onMouseMove = (e) => { const hovered = getHoveredSystem(e); setHoveredSystem(hovered); renderer.domElement.style.cursor = hovered ? 'pointer' : 'default'; }; const onClick = (e) => { if (e.button !== 0) return; // Ignore if it was a drag const dx = e.clientX - mouseDownPos.x; const dy = e.clientY - mouseDownPos.y; if (Math.sqrt(dx * dx + dy * dy) > 5) return; const rect = renderer.domElement.getBoundingClientRect(); clickMouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; clickMouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(clickMouse, camera); const intersects = raycaster.intersectObjects(systemMeshes, false); if (intersects.length > 0) { const sysId = intersects[0].object.userData.systemId; const sys = galaxyRef.current?.systems.find(s => s.id === sysId); if (sys) { setSelectedSystem(sys); orbit.flyTo(sys.x, sys.y, sys.z, 60); } } else { setSelectedSystem(null); } }; const onContextMenu = (e) => { e.preventDefault(); // Ignore if it was a drag const dx = e.clientX - mouseDownPos.x; const dy = e.clientY - mouseDownPos.y; if (Math.sqrt(dx * dx + dy * dy) > 5) return; const hovered = getHoveredSystem(e); if (!hovered || !galaxyRef.current) return; const g = galaxyRef.current; const ps = pathStartRef.current; if (!ps) { setPathStart(hovered.id); setPathEnd(null); setCurrentPath([]); } else if (ps !== hovered.id) { setPathEnd(hovered.id); const path = g.findPath(ps, hovered.id); setCurrentPath(path); } else { setPathStart(null); setPathEnd(null); setCurrentPath([]); } }; renderer.domElement.addEventListener('mousedown', onMouseDown); renderer.domElement.addEventListener('mousemove', onMouseMove); renderer.domElement.addEventListener('click', onClick); renderer.domElement.addEventListener('contextmenu', onContextMenu); // ── Resize handling ── const onResize = () => { TH.handleResize(renderer, camera, container); }; window.addEventListener('resize', onResize); // ── Animation loop ── let animId; let lastTime = performance.now(); let prevHighlightState = ''; function animate() { animId = requestAnimationFrame(animate); const now = performance.now(); const dt = (now - lastTime) / 1000; lastTime = now; // Auto-rotate (slow orbit) if (autoRotateRef.current && !orbit.isDragging && !orbit._flyActive) { orbit.theta += dt * 0.03; } orbit.update(dt); // Only rebuild highlights when state actually changed const stateKey = [ selectedSystemRef.current?.id, hoveredSystemRef.current?.id, pathStartRef.current, pathEndRef.current, currentPathRef.current.join(','), ].join('|'); if (stateKey !== prevHighlightState) { prevHighlightState = stateKey; updateHighlights(); } // Toggle visibility gateGroup.visible = showGatesRef.current; labelGroup.visible = showLabelsRef.current; dustGroup.visible = showDustRef.current; // Subtle dust cloud rotation (very cheap — just rotates the group matrix) if (dustGroup.visible && dustGroup.children.length > 0) { dustGroup.rotation.y += dt * 0.002; } // Core glow pulse const pulse = 1.0 + Math.sin(now * 0.001) * 0.08; coreGlowGroup.children.forEach(sprite => { if (sprite.isSprite && sprite.scale) { const baseScale = sprite.userData.baseScale || sprite.scale.x; sprite.userData.baseScale = baseScale; sprite.scale.setScalar(baseScale * pulse); } }); renderer.render(scene, camera); } sceneRef.current = { buildScene, updateSystemColors, renderer, orbit }; animate(); return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', onResize); renderer.domElement.removeEventListener('mousedown', onMouseDown); renderer.domElement.removeEventListener('mousemove', onMouseMove); renderer.domElement.removeEventListener('click', onClick); renderer.domElement.removeEventListener('contextmenu', onContextMenu); orbit.dispose(); renderer.dispose(); if (container.contains(renderer.domElement)) { container.removeChild(renderer.domElement); } sceneRef.current = null; }; }, []); // Mount once // ── Rebuild 3D scene when galaxy changes ── useEffect(() => { if (sceneRef.current && galaxy) { sceneRef.current.buildScene(galaxy); sceneRef.current.updateSystemColors(); } }, [galaxy]); // ── Update system colors when viewMode changes ── useEffect(() => { if (sceneRef.current) { sceneRef.current.updateSystemColors(); } }, [viewMode]); // ── Seed handlers ── const handleSeedSubmit = useCallback(() => { const parsed = parseInt(inputSeed, 10); if (!isNaN(parsed) && parsed > 0) { setSeed(parsed); } else { let hash = 0; for (let i = 0; i < inputSeed.length; i++) { hash = ((hash << 5) - hash) + inputSeed.charCodeAt(i); hash = hash >>> 0; } setSeed(hash || 1); } }, [inputSeed]); const randomSeed = useCallback(() => { const newSeed = Math.floor(Math.random() * 999999) + 1; setSeed(newSeed); setInputSeed(String(newSeed)); }, []); // ── Selected system details ── const selectedDetails = useMemo(() => { if (!galaxy || !selectedSystem) return null; const sys = selectedSystem; const sysStations = galaxy.stations.filter(s => s.systemId === sys.id); const sysBelts = galaxy.belts.filter(b => b.systemId === sys.id); const sysGates = galaxy.stargates.filter(g => g.from === sys.id || g.to === sys.id); const connectedSystems = sysGates.map(g => { const targetId = g.from === sys.id ? g.to : g.from; return galaxy.sysMap ? galaxy.sysMap.get(targetId) : galaxy.systems.find(s => s.id === targetId); }).filter(Boolean); return { ...sys, sysStations, sysBelts, sysGates, connectedSystems }; }, [galaxy, selectedSystem]); // ── Stats ── const stats = useMemo(() => { if (!galaxy) return null; const totalGates = galaxy.stargates.length; const mstGates = galaxy.stargates.filter(g => g.type === 'mst').length; const chokeGates = galaxy.stargates.filter(g => g.type === 'choke').length; const hubGates = galaxy.stargates.filter(g => g.type === 'hub').length; const avgGatesPerSys = (totalGates * 2 / galaxy.systems.length).toFixed(1); const secDist = { high: 0, low: 0, null: 0, deep: 0 }; galaxy.systems.forEach(s => { if (s.security >= 0.5) secDist.high++; else if (s.security >= 0.1) secDist.low++; else if (s.security >= -0.4) secDist.null++; else secDist.deep++; }); return { systems: galaxy.systems.length, constellations: galaxy.constellations.length, regions: galaxy.regions.length, gates: totalGates, mstGates, chokeGates, hubGates, avgGatesPerSys, stations: galaxy.stations.length, belts: galaxy.belts.length, agents: galaxy.agents.length, connected: galaxy.connected, secDist, }; }, [galaxy]); if (!galaxy) return
Generating galaxy…
; return (
{/* Three.js Container */}
{/* HUD overlay - top left: seed + view mode */}
SEED setInputSeed(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSeedSubmit()} style={{ width: 80, background: '#0a0f18', border: '1px solid #1c2a3f', borderRadius: 3, color: '#f0a030', fontSize: 12, padding: '2px 6px', fontFamily: 'inherit', outline: 'none' }} />
{['security', 'faction', 'gates'].map(mode => ( ))}
{/* Galaxy parameters overlay - below top bar */}
ARMS setGalaxyParams(p => ({ ...p, armCount: parseInt(e.target.value) }))} style={{ width: 50, accentColor: '#f0a030' }} /> {galaxyParams.armCount}
TWIST setGalaxyParams(p => ({ ...p, rotationStrength: parseFloat(e.target.value) }))} style={{ width: 50, accentColor: '#22d3ee' }} /> {galaxyParams.rotationStrength.toFixed(1)}
SPREAD setGalaxyParams(p => ({ ...p, armSpread: parseFloat(e.target.value) }))} style={{ width: 50, accentColor: '#22c55e' }} /> {galaxyParams.armSpread.toFixed(2)}
DENSITY setGalaxyParams(p => ({ ...p, gravity: parseFloat(e.target.value) }))} style={{ width: 50, accentColor: '#a855f7' }} /> {galaxyParams.gravity.toFixed(1)}
{/* Controls overlay - top right */}
{[ { label: 'Gates', val: showGates, set: setShowGates }, { label: 'Labels', val: showLabels, set: setShowLabels }, { label: 'Dust Cloud', val: showDust, set: setShowDust }, { label: 'Auto-Rotate', val: autoRotate, set: setAutoRotate }, ].map(ctrl => ( ))}
{/* Hint bar */}
Click system to inspect Right-click to route Left-drag to orbit Right-drag to pan Scroll to zoom
{/* Sidebar */}
{/* Stats header */}
SPIRAL GALAXY
{stats && [ { label: 'Systems', val: stats.systems, color: '#f0a030' }, { label: 'Regions', val: stats.regions, color: '#38bdf8' }, { label: 'Constellations', val: stats.constellations, color: '#22d3ee' }, { label: 'Stargates', val: stats.gates, color: '#94a3b8' }, { label: 'MST edges', val: stats.mstGates, color: '#22c55e' }, { label: 'Choke gates', val: stats.chokeGates, color: '#ef4444' }, { label: 'Hub gates', val: stats.hubGates, color: '#22c55e' }, { label: 'Stations', val: stats.stations, color: '#22d3ee' }, { label: 'Belts', val: stats.belts, color: '#92400e' }, { label: 'NPC Agents', val: stats.agents, color: '#a855f7' }, { label: 'Avg gates/sys', val: stats.avgGatesPerSys, color: '#94a3b8' }, { label: 'Dust particles', val: galaxy.dustCount || 0, color: '#5a6b82' }, ].map((s, i) => (
{s.label} {s.val}
))}
{stats && (
High: {stats.secDist.high} Low: {stats.secDist.low} Null: {stats.secDist.null} Deep: {stats.secDist.deep}
)} {stats && (
{stats.connected ? '✓ Fully connected' : '✗ Disconnected!'}
)}
{/* Region overview */}
REGIONS
{galaxy.regions.map(r => (
{r.name} {r.systemIds.length} sys
))}
{/* Selected system detail */}
{selectedDetails ? ( <>
{selectedDetails.name}
{[ { label: 'Security', val: selectedDetails.security.toFixed(2), color: secColorCSS(selectedDetails.security) }, { label: 'Star Type', val: selectedDetails.starType, color: selectedDetails.starColor }, { label: 'Faction', val: selectedDetails.faction, color: '#94a3b8' }, { label: 'Planets', val: selectedDetails.planetCount, color: '#94a3b8' }, { label: 'Stations', val: selectedDetails.sysStations.length, color: '#22d3ee' }, { label: 'Belts', val: selectedDetails.sysBelts.length, color: '#92400e' }, { label: 'Gates', val: selectedDetails.sysGates.length, color: '#94a3b8' }, { label: 'Region', val: selectedDetails.regionId, color: '#5a6b82' }, ].map((row, i) => (
{row.label} {row.val}
))}
{selectedDetails.sysStations.length > 0 && (
STATIONS
{selectedDetails.sysStations.map((stn, i) => (
{stn.name} [{stn.services.join(', ')}]
))}
)} {selectedDetails.sysBelts.length > 0 && (
BELTS
{selectedDetails.sysBelts.map((belt, i) => (
Belt {i + 1}: {belt.oreType}
))}
)} {selectedDetails.connectedSystems.length > 0 && (
CONNECTED SYSTEMS
{selectedDetails.connectedSystems.map((cs, i) => (
{ setSelectedSystem(cs); if (sceneRef.current?.orbit) { sceneRef.current.orbit.flyTo(cs.x, cs.y, cs.z, 60); } }}> {cs.security.toFixed(2)} {' '}{cs.name} ({cs.starType})
))}
)} ) : (
Click a system to inspect

Right-click two systems to
calculate shortest route
)}
{/* Route info */} {currentPath.length > 1 && (
ROUTE: {currentPath.length - 1} JUMP{currentPath.length - 1 !== 1 ? 'S' : ''}
{currentPath.map((id, i) => { const sys = galaxy.sysMap ? galaxy.sysMap.get(id) : galaxy.systems.find(s => s.id === id); if (!sys) return null; return ( { setSelectedSystem(sys); if (sceneRef.current?.orbit) { sceneRef.current.orbit.flyTo(sys.x, sys.y, sys.z, 60); } }} > {sys.name} {i < currentPath.length - 1 && } ); })}
)} {/* Gate legend */}
MST Intra Arm-link Choke Hub
); } window.GDD.GalaxyDemo = GalaxyDemo;