diff --git a/app.js b/app.js index 051fe82..4e96efc 100644 --- a/app.js +++ b/app.js @@ -1,27 +1,325 @@ -// ... existing code ... - -// === WEBSOCKET CLIENT === +import * as THREE from 'three'; import { wsClient } from './ws-client.js'; -// Initialize WebSocket client +// ============================================================ +// NEXUS COLOR PALETTE +// ============================================================ +const NEXUS = { + colors: { + bg: 0x000814, + primary: 0x00f5ff, + secondary: 0x7b00ff, + accent: 0xff006e, + text: 0xe0e0e0, + textMuted: 0x888888, + gridLine: 0x001133, + star: 0xffffff, + } +}; + +// ============================================================ +// SCENE SETUP +// ============================================================ +const scene = new THREE.Scene(); +scene.background = new THREE.Color(NEXUS.colors.bg); +scene.fog = new THREE.FogExp2(NEXUS.colors.bg, 0.018); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.toneMapping = THREE.ACESFilmicToneMapping; +renderer.toneMappingExposure = 1.2; +document.body.appendChild(renderer.domElement); + +const camera = new THREE.PerspectiveCamera( + 70, + window.innerWidth / window.innerHeight, + 0.1, + 600 +); +camera.position.set(0, 6, 18); +camera.lookAt(0, 3, 0); + +// Lighting +scene.add(new THREE.AmbientLight(0x112244, 2.5)); +const sun = new THREE.DirectionalLight(0x4466ff, 1.2); +sun.position.set(10, 30, 10); +scene.add(sun); + +// Floor grid +scene.add(new THREE.GridHelper(200, 80, NEXUS.colors.gridLine, NEXUS.colors.gridLine)); + +// Stars +{ + const N = 2500; + const pos = new Float32Array(N * 3); + for (let i = 0; i < N * 3; i++) pos[i] = (Math.random() - 0.5) * 500; + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + scene.add(new THREE.Points(geo, new THREE.PointsMaterial({ color: NEXUS.colors.star, size: 0.25 }))); +} + +// ============================================================ +// HOLOGRAPHIC SIGN — CANVAS TEXTURE +// ============================================================ +function makeSignTexture(portal) { + const W = 512, H = 256; + const cv = document.createElement('canvas'); + cv.width = W; cv.height = H; + const ctx = cv.getContext('2d'); + + // Dark translucent background + ctx.fillStyle = 'rgba(0,4,20,0.88)'; + ctx.fillRect(0, 0, W, H); + + const hex = portal.color; + + // Outer border + ctx.strokeStyle = hex; + ctx.lineWidth = 3; + ctx.shadowColor = hex; + ctx.shadowBlur = 24; + ctx.strokeRect(3, 3, W - 6, H - 6); + + // Inner accent line + ctx.lineWidth = 1; + ctx.strokeStyle = hex + '88'; + ctx.shadowBlur = 0; + ctx.strokeRect(10, 10, W - 20, H - 20); + + // Portal name + ctx.font = 'bold 52px "Courier New", monospace'; + ctx.fillStyle = hex; + ctx.shadowColor = hex; + ctx.shadowBlur = 20; + ctx.textAlign = 'center'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText(portal.name.toUpperCase(), W / 2, 78); + + // Divider line + ctx.shadowBlur = 0; + ctx.strokeStyle = hex + '55'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(30, 92); ctx.lineTo(W - 30, 92); + ctx.stroke(); + + // Description — word-wrap + ctx.font = '20px "Courier New", monospace'; + ctx.fillStyle = '#c8deff'; + ctx.shadowBlur = 4; + ctx.shadowColor = '#4488ff'; + const words = portal.description.split(' '); + let line = '', y = 125; + for (const w of words) { + const test = line + w + ' '; + if (ctx.measureText(test).width > W - 60 && line !== '') { + ctx.fillText(line.trimEnd(), W / 2, y); + line = w + ' '; + y += 28; + } else { + line = test; + } + } + if (line.trim()) ctx.fillText(line.trimEnd(), W / 2, y); + + // Status badge + const isOnline = portal.status === 'online'; + const statusColor = isOnline ? '#00ff88' : '#ff4455'; + ctx.font = 'bold 17px "Courier New", monospace'; + ctx.fillStyle = statusColor; + ctx.shadowColor = statusColor; + ctx.shadowBlur = 12; + ctx.fillText(`● ${portal.status.toUpperCase()}`, W / 2, H - 18); + + const tex = new THREE.CanvasTexture(cv); + return tex; +} + +// Repeating scanline texture (horizontal dark bands) +function makeScanlineTex() { + const cv = document.createElement('canvas'); + cv.width = 4; cv.height = 8; + const ctx = cv.getContext('2d'); + ctx.clearRect(0, 0, 4, 8); + ctx.fillStyle = 'rgba(0,0,0,0.22)'; + ctx.fillRect(0, 0, 4, 3); + const tex = new THREE.CanvasTexture(cv); + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.repeat.set(1, 10); + return tex; +} +const scanlineTex = makeScanlineTex(); + +// ============================================================ +// PORTAL + SIGN CONSTRUCTION +// ============================================================ +const portals = await fetch('./portals.json').then(r => r.json()); + +const animData = []; // holds refs needed each frame + +for (const portal of portals) { + const { x, y, z } = portal.position; + const rotY = portal.rotation?.y ?? 0; + const color = new THREE.Color(portal.color); + + // --- Portal ring --- + const ringGeo = new THREE.TorusGeometry(3, 0.14, 20, 80); + const ringMat = new THREE.MeshStandardMaterial({ + color, + emissive: color, + emissiveIntensity: 1.0, + }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.position.set(x, y + 3.2, z); + ring.rotation.y = rotY; + scene.add(ring); + + // Portal inner glow disc + const discGeo = new THREE.CircleGeometry(2.86, 64); + const discMat = new THREE.MeshBasicMaterial({ + color, + transparent: true, + opacity: 0.12, + side: THREE.DoubleSide, + depthWrite: false, + }); + const disc = new THREE.Mesh(discGeo, discMat); + disc.position.copy(ring.position); + disc.rotation.y = rotY; + scene.add(disc); + + // Portal point light + const pLight = new THREE.PointLight(color, 2.5, 18, 2); + pLight.position.set(x, y + 3.2, z); + scene.add(pLight); + + // --- Holographic sign group --- + const signBaseY = y + 8.0; + const W = 5.2, H = 2.6; + + // Glow backing (slightly larger, blurred by transparency) + const glowGeo = new THREE.PlaneGeometry(W + 0.5, H + 0.3); + const glowMat = new THREE.MeshBasicMaterial({ + color, + transparent: true, + opacity: 0.18, + side: THREE.DoubleSide, + depthWrite: false, + }); + const glowMesh = new THREE.Mesh(glowGeo, glowMat); + glowMesh.position.set(x, signBaseY, z); + glowMesh.rotation.y = rotY; + scene.add(glowMesh); + + // Main sign panel + const signTex = makeSignTexture(portal); + const signGeo = new THREE.PlaneGeometry(W, H); + const signMat = new THREE.MeshBasicMaterial({ + map: signTex, + transparent: true, + opacity: 0.92, + side: THREE.DoubleSide, + depthWrite: false, + }); + const signMesh = new THREE.Mesh(signGeo, signMat); + signMesh.position.set(x, signBaseY, z + 0.02); + signMesh.rotation.y = rotY; + scene.add(signMesh); + + // Scanline overlay + const slGeo = new THREE.PlaneGeometry(W, H); + const slMat = new THREE.MeshBasicMaterial({ + map: scanlineTex, + transparent: true, + opacity: 0.28, + side: THREE.DoubleSide, + depthWrite: false, + }); + const slMesh = new THREE.Mesh(slGeo, slMat); + slMesh.position.set(x, signBaseY, z + 0.04); + slMesh.rotation.y = rotY; + scene.add(slMesh); + + // Vertical connecting beam (sign to ring) + const beamGeo = new THREE.CylinderGeometry(0.03, 0.03, signBaseY - (y + 3.2), 8); + const beamMat = new THREE.MeshBasicMaterial({ + color, + transparent: true, + opacity: 0.35, + }); + const beam = new THREE.Mesh(beamGeo, beamMat); + beam.position.set(x, (signBaseY + y + 3.2) / 2, z); + scene.add(beam); + + animData.push({ + ring, + disc, + glowMesh, + signMesh, + slMesh, + pLight, + baseY: signBaseY, + phaseOffset: Math.random() * Math.PI * 2, + color, + }); +} + +// ============================================================ +// ANIMATION LOOP +// ============================================================ +const clock = new THREE.Clock(); + +function animate() { + requestAnimationFrame(animate); + const t = clock.getElapsedTime(); + + for (const d of animData) { + // Float sign up/down + const floatY = d.baseY + Math.sin(t * 0.7 + d.phaseOffset) * 0.22; + d.signMesh.position.y = floatY; + d.glowMesh.position.y = floatY; + d.slMesh.position.y = floatY; + + // Pulse glow opacity + const pulse = 0.5 + 0.5 * Math.sin(t * 1.4 + d.phaseOffset); + d.glowMesh.material.opacity = 0.08 + pulse * 0.22; + d.signMesh.material.opacity = 0.78 + pulse * 0.18; + + // Pulse portal light + d.pLight.intensity = 1.8 + pulse * 2.0; + + // Slow ring spin + d.ring.rotation.z += 0.004; + + // Disc flicker + d.disc.material.opacity = 0.08 + pulse * 0.12; + + // Scanline drift + d.slMesh.material.map.offset.y = (t * 0.15) % 1; + d.slMesh.material.map.needsUpdate = true; + } + + renderer.render(scene, camera); +} + +animate(); + +// ============================================================ +// RESIZE HANDLER +// ============================================================ +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +// ============================================================ +// WEBSOCKET +// ============================================================ wsClient.connect(); -// Handle WebSocket events -window.addEventListener('player-joined', (event) => { - console.log('Player joined:', event.detail); -}); - -window.addEventListener('player-left', (event) => { - console.log('Player left:', event.detail); -}); - -window.addEventListener('chat-message', (event) => { - console.log('Chat message:', event.detail); -}); - -// Clean up on page unload -window.addEventListener('beforeunload', () => { - wsClient.disconnect(); -}); - -// ... existing code ... +window.addEventListener('player-joined', (e) => console.log('Player joined:', e.detail)); +window.addEventListener('player-left', (e) => console.log('Player left:', e.detail)); +window.addEventListener('chat-message', (e) => console.log('Chat message:', e.detail)); +window.addEventListener('beforeunload', () => wsClient.disconnect()); diff --git a/index.html b/index.html index 34af931..bf7c2df 100644 --- a/index.html +++ b/index.html @@ -14,18 +14,36 @@ + + +
- + + +