diff --git a/app.js b/app.js index 051fe82..174c45b 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,240 @@ -// ... existing code ... - -// === WEBSOCKET CLIENT === +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { wsClient } from './ws-client.js'; -// Initialize WebSocket client +// === COLOR PALETTE === +const NEXUS = { + colors: { + bg: 0x0a0a1a, + platform: 0x12122a, + platformEdge: 0x4444aa, + runeGlow: 0x00ffcc, + runeAlt: 0xff44ff, + ambient: 0x111133, + star: 0xffffff, + } +}; + +// === SCENE SETUP === +const canvas = document.getElementById('nexus-canvas'); +const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +renderer.shadowMap.enabled = true; + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(NEXUS.colors.bg); +scene.fog = new THREE.FogExp2(NEXUS.colors.bg, 0.012); + +const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); +camera.position.set(0, 9, 20); +camera.lookAt(0, 1, 0); + +// === ORBIT CONTROLS === +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; +controls.dampingFactor = 0.05; +controls.minDistance = 6; +controls.maxDistance = 60; +controls.maxPolarAngle = Math.PI * 0.52; +controls.target.set(0, 1, 0); + +// === LIGHTING === +const ambientLight = new THREE.AmbientLight(NEXUS.colors.ambient, 1.2); +scene.add(ambientLight); + +const coreLight = new THREE.PointLight(NEXUS.colors.runeGlow, 3, 40); +coreLight.position.set(0, 4, 0); +scene.add(coreLight); + +const fillLight = new THREE.PointLight(NEXUS.colors.runeAlt, 1, 25); +fillLight.position.set(0, 2, 0); +scene.add(fillLight); + +// === CENTER PLATFORM === +const platformGeo = new THREE.CylinderGeometry(5, 5.5, 0.5, 48); +const platformMat = new THREE.MeshStandardMaterial({ + color: NEXUS.colors.platform, + metalness: 0.7, + roughness: 0.35, + envMapIntensity: 1.0, +}); +const platform = new THREE.Mesh(platformGeo, platformMat); +platform.receiveShadow = true; +scene.add(platform); + +// Platform top face glow disc +const glowDiscGeo = new THREE.CircleGeometry(4.8, 48); +const glowDiscMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.platformEdge, + transparent: true, + opacity: 0.08, +}); +const glowDisc = new THREE.Mesh(glowDiscGeo, glowDiscMat); +glowDisc.rotation.x = -Math.PI / 2; +glowDisc.position.y = 0.26; +scene.add(glowDisc); + +// Platform edge ring +const edgeRingGeo = new THREE.TorusGeometry(5.05, 0.06, 8, 64); +const edgeRingMat = new THREE.MeshBasicMaterial({ color: NEXUS.colors.platformEdge }); +const edgeRing = new THREE.Mesh(edgeRingGeo, edgeRingMat); +edgeRing.rotation.x = Math.PI / 2; +edgeRing.position.y = 0.25; +scene.add(edgeRing); + +// === FLOATING RUNES RING === +const ELDER_FUTHARK = ['ᚠ', 'ᚢ', 'ᚦ', 'ᚨ', 'ᚱ', 'ᚲ', 'ᚷ', 'ᚹ', 'ᚺ', 'ᚾ', 'ᛁ', 'ᛃ']; +const RUNE_RING_RADIUS = 7.5; +const RUNE_BASE_Y = 2.2; + +function createRuneTexture(symbol, glowColor, textColor) { + const size = 128; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, size, size); + + // Outer glow halo + ctx.shadowColor = glowColor; + ctx.shadowBlur = 28; + + // Draw symbol twice for intensity + ctx.fillStyle = textColor; + ctx.font = 'bold 70px serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(symbol, size / 2, size / 2); + ctx.fillText(symbol, size / 2, size / 2); + + return new THREE.CanvasTexture(canvas); +} + +const runeGroup = new THREE.Group(); +scene.add(runeGroup); + +const runeObjects = []; +ELDER_FUTHARK.forEach((symbol, i) => { + const isPrimary = i % 2 === 0; + const glowColor = isPrimary ? '#00ffcc' : '#ff44ff'; + const textColor = isPrimary ? '#aaffee' : '#ffaaff'; + + const texture = createRuneTexture(symbol, glowColor, textColor); + const geo = new THREE.PlaneGeometry(1.1, 1.1); + const mat = new THREE.MeshBasicMaterial({ + map: texture, + transparent: true, + side: THREE.DoubleSide, + depthWrite: false, + opacity: 0.92, + }); + + const rune = new THREE.Mesh(geo, mat); + const baseAngle = (i / ELDER_FUTHARK.length) * Math.PI * 2; + rune.userData.baseAngle = baseAngle; + rune.userData.phaseOffset = (i / ELDER_FUTHARK.length) * Math.PI * 2; + + rune.position.set( + Math.cos(baseAngle) * RUNE_RING_RADIUS, + RUNE_BASE_Y, + Math.sin(baseAngle) * RUNE_RING_RADIUS + ); + + runeGroup.add(rune); + runeObjects.push(rune); +}); + +// Rune ring orbit indicator (faint torus) +const orbitRingGeo = new THREE.TorusGeometry(RUNE_RING_RADIUS, 0.025, 6, 80); +const orbitRingMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.runeGlow, + transparent: true, + opacity: 0.12, +}); +const orbitRing = new THREE.Mesh(orbitRingGeo, orbitRingMat); +orbitRing.rotation.x = Math.PI / 2; +orbitRing.position.y = RUNE_BASE_Y; +scene.add(orbitRing); + +// === STARFIELD === +const starGeo = new THREE.BufferGeometry(); +const STAR_COUNT = 2500; +const starPositions = new Float32Array(STAR_COUNT * 3); +for (let i = 0; i < STAR_COUNT; i++) { + starPositions[i * 3 + 0] = (Math.random() - 0.5) * 500; + starPositions[i * 3 + 1] = (Math.random() - 0.5) * 500; + starPositions[i * 3 + 2] = (Math.random() - 0.5) * 500; +} +starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3)); +const starMat = new THREE.PointsMaterial({ + color: NEXUS.colors.star, + size: 0.18, + sizeAttenuation: true, + transparent: true, + opacity: 0.8, +}); +scene.add(new THREE.Points(starGeo, starMat)); + +// === ANIMATION LOOP === +const clock = new THREE.Clock(); + +function animate() { + requestAnimationFrame(animate); + const t = clock.getElapsedTime(); + + // Slowly orbit runes around the platform + runeObjects.forEach((rune) => { + const angle = rune.userData.baseAngle + t * 0.18; + rune.position.x = Math.cos(angle) * RUNE_RING_RADIUS; + rune.position.z = Math.sin(angle) * RUNE_RING_RADIUS; + // Gentle vertical float, each rune out of phase + rune.position.y = RUNE_BASE_Y + Math.sin(t * 0.9 + rune.userData.phaseOffset) * 0.35; + // Always face the camera (billboard) + rune.lookAt(camera.position); + }); + + // Pulse the core light + coreLight.intensity = 2.5 + Math.sin(t * 1.8) * 0.6; + fillLight.intensity = 0.8 + Math.sin(t * 2.3 + 1.2) * 0.3; + + // Slowly rotate the glowing disc + glowDisc.rotation.z = t * 0.05; + + controls.update(); + renderer.render(scene, camera); +} + +animate(); + +// === RESIZE HANDLER === +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +// === AUDIO TOGGLE === +const audioToggle = document.getElementById('audio-toggle'); +const ambientSound = document.getElementById('ambient-sound'); +if (audioToggle && ambientSound) { + audioToggle.addEventListener('click', () => { + if (ambientSound.paused) { + ambientSound.play().catch(() => {}); + audioToggle.classList.remove('muted'); + audioToggle.textContent = '🔊'; + } else { + ambientSound.pause(); + audioToggle.classList.add('muted'); + audioToggle.textContent = '🔇'; + } + }); +} + +// === WEBSOCKET CLIENT === wsClient.connect(); -// Handle WebSocket events window.addEventListener('player-joined', (event) => { console.log('Player joined:', event.detail); }); @@ -19,9 +247,6 @@ window.addEventListener('chat-message', (event) => { console.log('Chat message:', event.detail); }); -// Clean up on page unload window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); - -// ... existing code ... diff --git a/index.html b/index.html index 34af931..bc1706d 100644 --- a/index.html +++ b/index.html @@ -14,18 +14,31 @@ + + - + + +
+
THE NEXUS
+
-
-
- + diff --git a/style.css b/style.css index a2aab15..03c75fc 100644 --- a/style.css +++ b/style.css @@ -1,18 +1,74 @@ +:root { + --color-bg: #0a0a1a; + --color-primary: #00ffcc; + --color-secondary: #ff44ff; + --color-text: #e0e0ff; + --color-text-muted: #666699; + --font-body: 'Courier New', monospace; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-body); + overflow: hidden; + width: 100vw; + height: 100vh; +} + +#nexus-canvas { + display: block; + width: 100vw; + height: 100vh; +} + +#hud { + position: absolute; + top: 12px; + left: 50%; + transform: translateX(-50%); + pointer-events: none; +} + +#hud-title { + font-size: 13px; + letter-spacing: 6px; + color: var(--color-primary); + text-shadow: 0 0 10px var(--color-primary); + opacity: 0.7; +} + /* === AUDIO TOGGLE === */ +#audio-control { + position: absolute; + top: 8px; + right: 8px; +} + #audio-toggle { font-size: 14px; - background-color: var(--color-primary-primary); - color: var(--color-bg); + background-color: rgba(0, 255, 204, 0.15); + color: var(--color-primary); + border: 1px solid var(--color-primary); padding: 4px 8px; border-radius: 4px; font-family: var(--font-body); transition: background-color 0.3s ease; + cursor: pointer; } #audio-toggle:hover { - background-color: var(--color-secondary); + background-color: rgba(0, 255, 204, 0.3); } #audio-toggle.muted { - background-color: var(--color-text-muted); + color: var(--color-text-muted); + border-color: var(--color-text-muted); + background-color: transparent; }