diff --git a/node/server/public/app.js b/node/server/public/app.js index 0725872..1c619d8 100644 --- a/node/server/public/app.js +++ b/node/server/public/app.js @@ -97,129 +97,118 @@ draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/'); const gltfLoader = new GLTFLoader(); gltfLoader.setDRACOLoader(draco); -// ---- SpinnerController ---- +// ---- Spinner Controller-Klasse ---- class SpinnerController { - constructor(scene) { - // Grundobjekte - this.scene = scene; - this.spinnerRed = null; - this.spinnerBlue = null; - this.rotatingLights = []; - this.counterRotatingLights = []; - this.LIGHT_RADIUS = 1; - this.baseY = 16.55; - this.connected = false; - this.connectionLostSince = null; - - // Create Spinner, Lights etc. - this.init(); - // Setup WebSocket (mit reconnect) - this.connect(); - } - - async init() { - // Spinner laden - this.spinnerRed = await loadSpinner( - 'assets/models/spinner_red.glb', [0, this.baseY, 0.88], [90, 0, 0], "#ff3333", 0.2 - ); - this.spinnerBlue = await loadSpinner( - 'assets/models/spinner_blue.glb', [0, this.baseY, 0.88], [90, 0, 0], "#3380ff", 0.2 - ); - // 6 Lichter - for (let i = 0; i < 3; i++) { - const L = new THREE.PointLight(0xFFA230, 5, 30); - L.castShadow = true; - this.rotatingLights.push(L); - scene.add(L); - - const L2 = new THREE.PointLight(0xFFA230, 5, 30); - L2.castShadow = true; - this.counterRotatingLights.push(L2); - scene.add(L2); + constructor(scene) { + this.scene = scene; + this.spinnerRed = null; + this.spinnerBlue = null; + this.lights = []; + this.counterLights = []; + this.center = new THREE.Vector3(0, 16.55, 1.5); + this.LIGHT_RADIUS = 1; + this.baseY = 16.55; + this.clock = new THREE.Clock(); + this.ws = null; + this.reconnectDelay = 2000; + this.init(); } - } - connect() { - if (this.ws) this.ws.close(); - const ws = new WebSocket(`ws://${location.host}`); - this.ws = ws; - ws.onopen = () => { - this.connected = true; - this.connectionLostSince = null; - // Spinner wird wieder "schnell und hell" - }; - ws.onclose = () => { - this.connected = false; - this.connectionLostSince = Date.now(); - // Versuche nach 2 Sekunden erneut zu verbinden - setTimeout(() => this.connect(), 2000); - }; - ws.onerror = () => { - ws.close(); - }; - ws.onmessage = async (event) => { - const msg = JSON.parse(event.data); - if (msg.type === 'spirit') { - spawnSpirit(msg.data); // neue Funktion, siehe unten! - } - }; - } + async init() { + // Lade beide Spinner + this.spinnerRed = await this.loadSpinner('assets/models/spinner_red.glb', [0, 16.55, 0.88], [90, 0, 0], "#ff3333", 0.2); + this.spinnerBlue = await this.loadSpinner('assets/models/spinner_blue.glb', [0, 16.55, 0.88], [90, 0, 0], "#3380ff", 0.2); - update(dt, t) { - // Werte je nach Connection - let bobFactor = this.connected ? 0.5 : 0.13; - let spinSpeed = this.connected ? 1.2 : 0.25; - let lightSpeed = this.connected ? 0.8 : 0.18; - let lightInt = this.connected ? 5 : 0.7; - let emissiveInt = this.connected ? 3.0 : 0.15; + // Init rotierende Lichter + for (let i = 0; i < 3; i++) { + const L = new THREE.PointLight(0xFFA230, 5, 30); + L.castShadow = true; + this.lights.push(L); + this.scene.add(L); - // Spinner Bobbing - const bob = Math.sin(t * 1.2) * bobFactor; - const yRed = this.baseY + bob + 0.8; - const yBlue = this.baseY + bob; - - if (this.spinnerRed && this.spinnerBlue) { - this.spinnerRed.position.y = yRed; - this.spinnerBlue.position.y = yBlue; - this.spinnerRed.rotation.y -= spinSpeed * dt; - this.spinnerBlue.rotation.y += spinSpeed * dt; - // Emissive dunkler/heller - this.spinnerRed.traverse(obj => { - if (obj.isMesh && obj.material && obj.material.isMeshStandardMaterial) { - obj.material.emissiveIntensity = emissiveInt; + const L2 = new THREE.PointLight(0xFFA230, 5, 30); + L2.castShadow = true; + this.counterLights.push(L2); + this.scene.add(L2); } - }); - this.spinnerBlue.traverse(obj => { - if (obj.isMesh && obj.material && obj.material.isMeshStandardMaterial) { - obj.material.emissiveIntensity = emissiveInt; - } - }); + + this.connectWebSocket(); } - // Rotierende Lichter - const center = new THREE.Vector3(0, this.baseY, 1.5); - const lightZ = center.z; - for (let i = 0; i < this.rotatingLights.length; i++) { - const ang = t * lightSpeed + i * 2 * Math.PI / 3; - const L = this.rotatingLights[i]; - L.intensity = lightInt; - L.position.set( - center.x + Math.cos(ang) * this.LIGHT_RADIUS, - center.y + Math.sin(ang) * this.LIGHT_RADIUS, - lightZ - ); + async loadSpinner(path, pos, rotDeg, color, opacity) { + const { scene: obj } = await gltfLoader.loadAsync(path); + obj.position.set(...pos); + obj.rotation.set( + THREE.MathUtils.degToRad(rotDeg[0]), + THREE.MathUtils.degToRad(rotDeg[1]), + THREE.MathUtils.degToRad(rotDeg[2]) + ); + obj.traverse(c => { + c.visible = true; + if (c.isMesh && c.material && c.material.isMeshStandardMaterial) { + c.material.transparent = true; + c.material.opacity = opacity; + c.material.emissive = new THREE.Color(color); + c.material.emissiveIntensity = 3.0; + c.material.vertexColors = true; + c.castShadow = true; + } + }); + this.scene.add(obj); + return obj; } - for (let i = 0; i < this.counterRotatingLights.length; i++) { - const ang = -t * lightSpeed + i * 2 * Math.PI / 3; - const L2 = this.counterRotatingLights[i]; - L2.intensity = lightInt; - L2.position.set( - center.x + Math.cos(ang) * this.LIGHT_RADIUS, - center.y + Math.sin(ang) * this.LIGHT_RADIUS, - lightZ - ); + + animate(dt, t) { + // Spinner Animation + const bob = Math.sin(t * 1.2) * 0.5; + const baseY = this.baseY + bob; + if (this.spinnerRed && this.spinnerBlue) { + this.spinnerRed.position.y = baseY + 0.8; + this.spinnerBlue.position.y = baseY; + this.spinnerRed.rotation.y -= 1.2 * dt; + this.spinnerBlue.rotation.y += 1.2 * dt; + } + // Rotierende Lichter + for (let i = 0; i < this.lights.length; i++) { + const ang = t * 0.8 + i * 2 * Math.PI / 3; + this.lights[i].position.set( + this.center.x + Math.cos(ang) * this.LIGHT_RADIUS, + this.center.y + Math.sin(ang) * this.LIGHT_RADIUS, + this.center.z + ); + } + for (let i = 0; i < this.counterLights.length; i++) { + const ang = -t * 0.8 + i * 2 * Math.PI / 3; + this.counterLights[i].position.set( + this.center.x + Math.cos(ang) * this.LIGHT_RADIUS, + this.center.y + Math.sin(ang) * this.LIGHT_RADIUS, + this.center.z + ); + } + } + + connectWebSocket() { + let self = this; + if (self.ws) self.ws.close(); + self.ws = new WebSocket(`ws://${location.host}`); + self.ws.addEventListener('open', () => { + console.log("WebSocket connected!"); + }); + self.ws.addEventListener('message', async (event) => { + const msg = JSON.parse(event.data); + if (msg.type === 'spirit') { + spawnSpirit(msg.data); + } + }); + self.ws.addEventListener('close', () => { + console.warn("WebSocket closed. Reconnecting in " + this.reconnectDelay / 1000 + "s..."); + setTimeout(() => self.connectWebSocket(), self.reconnectDelay); + }); + self.ws.addEventListener('error', (e) => { + console.error("WebSocket error", e); + self.ws.close(); + }); } - } } // ---- Spirit-Klasse ----