diff --git a/node/server/public/app.js b/node/server/public/app.js
index 2586446..02d9b50 100644
--- a/node/server/public/app.js
+++ b/node/server/public/app.js
@@ -10,14 +10,14 @@ import { VignetteShader } from "https://cdn.jsdelivr.net/npm/three@0.155.0/examp
// ---- Basis Three.js Szene ----
const scene = new THREE.Scene();
-const ASPECT = 3/2, SCALE = 15;
-const hw = SCALE/2, hh = (SCALE/ASPECT)/2;
+const ASPECT = 3 / 2, SCALE = 15;
+const hw = SCALE / 2, hh = (SCALE / ASPECT) / 2;
const camera = new THREE.OrthographicCamera(-hw, hw, hh, -hh, 0.1, 1000);
camera.position.set(0, -14.424, 20);
camera.rotation.set(THREE.MathUtils.degToRad(55), 0, 0);
const container = document.getElementById('viewer');
-const renderer = new THREE.WebGLRenderer({ antialias:true });
+const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
@@ -27,36 +27,16 @@ container.appendChild(renderer.domElement);
const foliageTexture = new THREE.TextureLoader().load('assets/sprites/foliage.png');
foliageTexture.colorSpace = THREE.SRGBColorSpace;
const FoliageOverlayShader = {
- uniforms: {
- 'tDiffuse': { value: null },
- 'tFoliage': { value: foliageTexture },
- 'opacity': { value: 1.0 }
- },
- vertexShader: `
- varying vec2 vUv;
- void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }
- `,
- fragmentShader: `
- uniform sampler2D tDiffuse;
- uniform sampler2D tFoliage;
- uniform float opacity;
- varying vec2 vUv;
- void main() {
- vec4 base = texture2D(tDiffuse, vUv);
- vec4 foliage = texture2D(tFoliage, vUv);
- gl_FragColor = mix(base, vec4(foliage.rgb, base.a), foliage.a * opacity);
- }
- `
+ uniforms: { 'tDiffuse': { value: null }, 'tFoliage': { value: foliageTexture }, 'opacity': { value: 1.0 } },
+ vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
+ fragmentShader: `uniform sampler2D tDiffuse; uniform sampler2D tFoliage; uniform float opacity; varying vec2 vUv;
+ void main() { vec4 base = texture2D(tDiffuse, vUv); vec4 foliage = texture2D(tFoliage, vUv); gl_FragColor = mix(base, vec4(foliage.rgb, base.a), foliage.a * opacity); }`
};
const foliageOverlayPass = new ShaderPass(FoliageOverlayShader);
-
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(foliageOverlayPass);
-composer.addPass(new UnrealBloomPass(
- new THREE.Vector2(container.clientWidth, container.clientHeight),
- 0.8, 0.2, 0.4
-));
+composer.addPass(new UnrealBloomPass(new THREE.Vector2(container.clientWidth, container.clientHeight), 0.8, 0.2, 0.4));
const vignettePass = new ShaderPass(VignetteShader);
vignettePass.uniforms['offset'].value = 0.3;
vignettePass.uniforms['darkness'].value = 1.35;
@@ -64,341 +44,336 @@ composer.addPass(vignettePass);
composer.addPass(new ShaderPass(GammaCorrectionShader));
// ---- Resize Handler ----
-function onResize(){
- const W=container.clientWidth, H=container.clientHeight, winA=W/H;
- let vw,vh,vx,vy;
- if(winA>ASPECT){
- vh=H; vw=H*ASPECT; vx=(W-vw)/2; vy=0;
- } else {
- vw=W; vh=W/ASPECT; vx=0; vy=(H-vh)/2;
- }
- renderer.setSize(W,H);
- renderer.setViewport(vx,vy,vw,vh);
- renderer.setScissor(vx,vy,vw,vh);
- renderer.setScissorTest(true);
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
- renderer.toneMappingExposure = 1.2;
- renderer.outputColorSpace = THREE.SRGBColorSpace;
- composer.setSize(W, H);
- const dpr = window.devicePixelRatio || 1;
- renderer.setPixelRatio(dpr);
- composer.setPixelRatio(dpr);
- camera.updateProjectionMatrix();
+function onResize() {
+ const W = container.clientWidth, H = container.clientHeight, winA = W / H;
+ let vw, vh, vx, vy;
+ if (winA > ASPECT) { vh = H; vw = H * ASPECT; vx = (W - vw) / 2; vy = 0; }
+ else { vw = W; vh = W / ASPECT; vx = 0; vy = (H - vh) / 2; }
+ renderer.setSize(W, H);
+ renderer.setViewport(vx, vy, vw, vh);
+ renderer.setScissor(vx, vy, vw, vh);
+ renderer.setScissorTest(true);
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
+ renderer.toneMappingExposure = 1.2;
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
+ composer.setSize(W, H);
+ const dpr = window.devicePixelRatio || 1;
+ renderer.setPixelRatio(dpr);
+ composer.setPixelRatio(dpr);
+ camera.updateProjectionMatrix();
}
-window.addEventListener('resize',onResize);
+window.addEventListener('resize', onResize);
onResize();
-// ---- Environment Map ----
+// ---- Environment ----
const texLoader = new THREE.TextureLoader();
-const pmremGen = new THREE.PMREMGenerator(renderer);
+const pmremGen = new THREE.PMREMGenerator(renderer);
texLoader.load('assets/hdri/environment.jpg', tex => {
- const envRT = pmremGen.fromEquirectangular(tex).texture;
- scene.environment = envRT;
- scene.background = envRT;
- tex.dispose();
- pmremGen.dispose();
+ const envRT = pmremGen.fromEquirectangular(tex).texture;
+ scene.environment = envRT;
+ scene.background = envRT;
+ tex.dispose();
+ pmremGen.dispose();
});
-// ---- Directional Sun ----
+// ---- Licht, Shadow-Only-Material, Loader ----
const sun = new THREE.DirectionalLight(0xFFA230, 2);
sun.position.set(21, -25, 30);
sun.castShadow = true;
-sun.shadow.mapSize.width = 2048;
+sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
-sun.shadow.camera.near = 1;
-sun.shadow.camera.far = 100;
-sun.shadow.camera.left = -30;
-sun.shadow.camera.right = 30;
-sun.shadow.camera.top = 30;
-sun.shadow.camera.bottom = -30;
+sun.shadow.camera.near = 1;
+sun.shadow.camera.far = 100;
+sun.shadow.camera.left = -30;
+sun.shadow.camera.right = 30;
+sun.shadow.camera.top = 30;
+sun.shadow.camera.bottom = -30;
scene.add(sun);
-// ---- GLTF/DRACO Loader ----
-const draco = new DRACOLoader();
+const shadowOnlyMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.01, transparent: true, depthWrite: false });
+
+const draco = new DRACOLoader();
draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(draco);
-// ---- Utility: GLBs laden ----
-async function loadGLB(path, pos, rotDeg, {receiveShadow=false, castShadow=false, emissive=null, visible=true, shadowOnly=false} = {}) {
- const { scene: obj } = await gltfLoader.loadAsync(path);
- obj.position.set(pos[0], pos[1], pos[2]);
- obj.rotation.set(
- THREE.MathUtils.degToRad(rotDeg[0]),
- THREE.MathUtils.degToRad(rotDeg[1]),
- THREE.MathUtils.degToRad(rotDeg[2])
- );
- obj.traverse(c => {
- c.visible = visible;
- if (c.isMesh) {
- c.castShadow = castShadow;
- c.receiveShadow = receiveShadow;
- if (shadowOnly) c.material = new THREE.MeshBasicMaterial({ color: 0x000000, opacity: 0.01, transparent: true, depthWrite: false });
- if (emissive && c.material && c.material.isMeshStandardMaterial) {
- c.material.emissive = new THREE.Color(emissive);
- c.material.emissiveIntensity = 1.0;
- }
+// ---- Spinner Controller-Klasse ----
+class SpinnerController {
+ 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();
}
- });
- scene.add(obj);
- return obj;
-}
-async function 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;
+ 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);
+
+ // 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);
+
+ const L2 = new THREE.PointLight(0xFFA230, 5, 30);
+ L2.castShadow = true;
+ this.counterLights.push(L2);
+ this.scene.add(L2);
+ }
+
+ this.connectWebSocket();
+ }
+
+ 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;
+ }
+
+ 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();
+ });
}
- });
- scene.add(obj);
- return obj;
}
-// ---- Grundszene initialisieren
-let spinnerRed, spinnerBlue, torigate, landscape, shadowTree;
-const rotatingLights = [], counterRotatingLights = [];
-const LIGHT_RADIUS = 1;
-const clock = new THREE.Clock();
-
-(async()=>{
- landscape = await loadGLB(
- 'assets/models/landscape.glb',
- [0,0,0], [90,0,0],
- {receiveShadow:true, castShadow:true}
- );
- torigate = await loadGLB(
- 'assets/models/tori.glb',
- [0,6.59,0.375], [90,0,0],
- {receiveShadow:true, castShadow:true}
- );
- spinnerRed = await loadSpinner(
- 'assets/models/spinner_red.glb', [0,16.55,0.88], [90,0,0], "#ff3333", 0.2
- );
- spinnerBlue = await loadSpinner(
- 'assets/models/spinner_blue.glb', [0,16.55,0.88], [90,0,0], "#3380ff", 0.2
- );
- shadowTree = await loadGLB(
- 'assets/models/tree_low.glb',
- [0.0,0.0,0.0], [90,0,0],
- {receiveShadow:false, castShadow:true, shadowOnly: true}
- );
- for(let i=0;i<3;i++){
- const L = new THREE.PointLight(0xFFA230, 5, 30);
- L.castShadow = true;
- rotatingLights.push(L);
- scene.add(L);
-
- const L2 = new THREE.PointLight(0xFFA230, 5, 30);
- L2.castShadow = true;
- counterRotatingLights.push(L2);
- scene.add(L2);
- }
- animate();
-})();
-
-// ---- Spirit-Instanzen-Array (mehrere gleichzeitig!)
-const activeSpirits = [];
-
// ---- Spirit-Klasse ----
-const MOVE_SPEED = 1;
class Spirit {
- constructor(obj3d, info) {
- this.clock = new THREE.Clock();
- this.grp = new THREE.Group();
- this.spiritMeshes = [];
- this.isFading = true;
- this.info = info || {};
- this.grp.add(obj3d);
+ constructor(scene, gltfScene, info, spawnPosition) {
+ this.scene = scene;
+ this.grp = new THREE.Group();
+ this.gltf = gltfScene;
+ this.info = info || {};
+ this.spawnY = spawnPosition.y;
+ this.clock = new THREE.Clock();
+ this.isFading = true;
+ this.lifeTime = 20; // Sekunden
+ this.spiritMeshes = [];
+ this.grp.add(this.gltf);
+ // exakt wie im Original: rotate, platzieren, leicht nach hinten
+ this.gltf.rotation.x = -Math.PI;
+ this.grp.position.set(spawnPosition.x, spawnPosition.y, spawnPosition.z - 0.6);
- // Rotation & Position wie im Original
- obj3d.rotation.x = -Math.PI;
- let y = spinnerRed ? spinnerRed.position.y - 1.5 : 15;
- this.grp.position.set(0, y, 0.88 - 0.6);
+ this.gltf.traverse((mesh) => {
+ if (mesh.isMesh) {
+ mesh.castShadow = true;
+ mesh.receiveShadow = true;
+ mesh.userData.originalMaterial = mesh.material.clone();
+ mesh.material = mesh.material.clone();
+ mesh.material.color.set(0xffffcc);
+ mesh.material.opacity = 1.0;
+ mesh.material.transparent = true;
+ mesh.material.emissive?.set(0xffffcc);
+ mesh.material.emissiveIntensity = 2.0;
+ this.spiritMeshes.push(mesh);
+ }
+ });
+ this.scene.add(this.grp);
+ }
- obj3d.traverse(mesh => {
- if (mesh.isMesh) {
- mesh.castShadow = true;
- mesh.receiveShadow = true;
- mesh.userData.originalMaterial = mesh.material.clone();
- mesh.material = mesh.material.clone();
- mesh.material.color.set(0xffffcc);
- mesh.material.opacity = 0.0;
- mesh.material.transparent = true;
- mesh.material.emissive?.set(0xffffcc);
- mesh.material.emissiveIntensity = 2.0;
- this.spiritMeshes.push(mesh);
- }
- });
- scene.add(this.grp);
- }
-
- update(dt) {
- const t = this.clock.getElapsedTime();
- if(this.spiritMeshes && this.isFading){
- for(const mesh of this.spiritMeshes){
- if(t < 0.5){
- mesh.material.opacity = 1;
- mesh.material.color.lerp(mesh.userData.originalMaterial.color, t/0.5);
- if(mesh.material.emissive)
- mesh.material.emissive.lerp(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000), t/0.5);
- mesh.material.emissiveIntensity = 2.0 * (1-t/0.5) + (mesh.userData.originalMaterial.emissiveIntensity||1.0)*(t/0.5);
- } else {
- mesh.material.opacity = mesh.userData.originalMaterial.opacity ?? 1.0;
- mesh.material.color.copy(mesh.userData.originalMaterial.color);
- if(mesh.material.emissive)
- mesh.material.emissive.copy(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000));
- mesh.material.emissiveIntensity = mesh.userData.originalMaterial.emissiveIntensity ?? 1.0;
- this.isFading = false;
+ update(dt) {
+ const t = this.clock.getElapsedTime();
+ // Fading
+ if (this.spiritMeshes && this.isFading) {
+ for (const mesh of this.spiritMeshes) {
+ if (t < 0.5) {
+ mesh.material.opacity = 1;
+ mesh.material.color.lerp(mesh.userData.originalMaterial.color, t / 0.5);
+ if (mesh.material.emissive)
+ mesh.material.emissive.lerp(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000), t / 0.5);
+ mesh.material.emissiveIntensity =
+ 2.0 * (1 - t / 0.5) +
+ (mesh.userData.originalMaterial.emissiveIntensity || 1.0) * (t / 0.5);
+ } else {
+ mesh.material.opacity = mesh.userData.originalMaterial.opacity ?? 1.0;
+ mesh.material.color.copy(mesh.userData.originalMaterial.color);
+ if (mesh.material.emissive)
+ mesh.material.emissive.copy(mesh.userData.originalMaterial.emissive || new THREE.Color(0x000000));
+ mesh.material.emissiveIntensity =
+ mesh.userData.originalMaterial.emissiveIntensity ?? 1.0;
+ this.isFading = false;
+ }
+ }
}
- }
+ // Vertikales Despawn-Movement
+ this.grp.position.y -= 1 * dt;
+ // Nach Lebenszeit entfernen
+ if (t > this.lifeTime) {
+ this.dispose();
+ return false;
+ }
+ return true;
}
- // Bewegung nach unten
- this.grp.position.y -= MOVE_SPEED * dt;
- // LEBENSDAUER 20 Sekunden
- if (t > 20) {
- this.dispose();
- return false;
- }
- return true;
- }
- dispose() {
- scene.remove(this.grp);
- this.grp.traverse((mesh) => {
- if (mesh.isMesh) {
- mesh.geometry.dispose();
- if (Array.isArray(mesh.material)) mesh.material.forEach((m) => m.dispose());
- else mesh.material.dispose();
- }
+ dispose() {
+ this.scene.remove(this.grp);
+ this.gltf.traverse((mesh) => {
+ if (mesh.isMesh) {
+ mesh.geometry.dispose();
+ if (Array.isArray(mesh.material)) {
+ mesh.material.forEach((m) => m.dispose());
+ } else {
+ mesh.material.dispose();
+ }
+ }
+ });
+ }
+}
+
+// ---- Szene initialisieren ----
+let spinnerController;
+let landscape, torigate, shadowTree;
+const activeSpirits = [];
+const clock = new THREE.Clock();
+
+(async () => {
+ // Lade statische Models
+ landscape = await loadGLB('assets/models/landscape.glb', [0, 0, 0], [90, 0, 0], { receiveShadow: true, castShadow: true });
+ torigate = await loadGLB('assets/models/tori.glb', [0, 6.59, 0.375], [90, 0, 0], { receiveShadow: true, castShadow: true });
+ shadowTree = await loadGLB('assets/models/tree_low.glb', [0, 0, 0], [90, 0, 0], { receiveShadow: false, castShadow: true, shadowOnly: true });
+
+ spinnerController = new SpinnerController(scene);
+
+ animate();
+})();
+
+// ---- Utility: GLBs laden ----
+async function loadGLB(path, pos, rotDeg, { receiveShadow = false, castShadow = false, emissive = null, visible = true, shadowOnly = false } = {}) {
+ const { scene: obj } = await gltfLoader.loadAsync(path);
+ obj.position.set(pos[0], pos[1], pos[2]);
+ obj.rotation.set(
+ THREE.MathUtils.degToRad(rotDeg[0]),
+ THREE.MathUtils.degToRad(rotDeg[1]),
+ THREE.MathUtils.degToRad(rotDeg[2])
+ );
+ obj.traverse(c => {
+ c.visible = visible;
+ if (c.isMesh) {
+ c.castShadow = castShadow;
+ c.receiveShadow = receiveShadow;
+ if (shadowOnly) c.material = shadowOnlyMaterial;
+ if (emissive && c.material && c.material.isMeshStandardMaterial) {
+ c.material.emissive = new THREE.Color(emissive);
+ c.material.emissiveIntensity = 1.0;
+ }
+ }
});
- }
+ scene.add(obj);
+ return obj;
+}
+
+// ---- Spirit spawnen ----
+async function spawnSpirit(spiritData) {
+ // Exakte Platzierung wie im Original
+ let spawnPos = { x: 0, y: spinnerController ? (spinnerController.spinnerRed ? spinnerController.spinnerRed.position.y - 1.5 : 15) : 15, z: 0.88 };
+ const { scene: gltfScene } = await gltfLoader.loadAsync(spiritData.modelUrl);
+ const spirit = new Spirit(scene, gltfScene, spiritData, spawnPos);
+ activeSpirits.push(spirit);
+ updateSpiritOverlay(spiritData);
+}
+
+// ---- Overlay für Spirit-Infos ----
+function updateSpiritOverlay(spirit) {
+ let el = document.getElementById('spirit-info');
+ if (!el) {
+ el = document.createElement('div');
+ el.id = 'spirit-info';
+ el.style = `
+ position:absolute; left:20px; top:20px; color:white;
+ background:rgba(0,0,0,0.6); padding:10px 18px; border-radius:10px;
+ font-family: sans-serif; z-index:10; max-width: 320px;
+ `;
+ document.body.appendChild(el);
+ }
+ el.innerHTML = `${spirit.name || 'Spirit'}
${spirit.desc || ""}`;
}
// ---- Render-Loop ----
-function animate(){
- const dt = clock.getDelta(), t = clock.getElapsedTime();
+function animate() {
+ const dt = clock.getDelta(), t = clock.getElapsedTime();
+ // Spinner-Animation & Netzwerk
+ if (spinnerController) spinnerController.animate(dt, t);
- // Spinner Animation
- const bob = Math.sin(t*1.2)*0.5;
- const baseY = 16.55 + bob;
- if (spinnerRed && spinnerBlue) {
- spinnerRed.position.y = baseY + 0.8;
- spinnerBlue.position.y = baseY;
- spinnerRed.rotation.y -= 1.2 * dt;
- spinnerBlue.rotation.y += 1.2 * dt;
- }
- // Rotierende Lichter
- const center = new THREE.Vector3(0, 16.55, 1.5);
- const lightZ = center.z;
- for(let i=0; i= 0; i--) {
- if (!activeSpirits[i].update(dt)) {
- activeSpirits.splice(i, 1);
+ // Update & remove expired spirits:
+ for (let i = activeSpirits.length - 1; i >= 0; i--) {
+ if (!activeSpirits[i].update(dt)) {
+ activeSpirits.splice(i, 1);
+ }
}
- }
- composer.render(scene, camera);
- requestAnimationFrame(animate);
-}
-// --- WebSocket-Client mit Auto-Reconnect ---
-let ws;
-let reconnectTimeout = 1000; // Start: 1s
-
-function connectWebSocket() {
- ws = new WebSocket(`ws://${location.host}`);
-
- ws.addEventListener('open', () => {
- console.log('[WebSocket] Verbunden!');
- reconnectTimeout = 1000; // Reset
- });
-
- ws.addEventListener('message', async (event) => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type === 'spirit') {
- await spawnSpirit(msg.data);
- }
- } catch (e) {
- console.warn('WebSocket Message-Fehler:', e);
- }
- });
-
- ws.addEventListener('close', () => {
- console.warn('[WebSocket] Verbindung geschlossen! Versuche erneut...');
- retryConnect();
- });
-
- ws.addEventListener('error', (e) => {
- ws.close(); // Fehler → schließen, dann reconnect
- });
-}
-
-function retryConnect() {
- setTimeout(() => {
- reconnectTimeout = Math.min(reconnectTimeout * 2, 12000); // Bis 12s erhöhen
- connectWebSocket();
- }, reconnectTimeout + Math.random() * 300);
-}
-
-// Initial verbinden
-connectWebSocket();
-
-
-async function spawnSpirit(spirit) {
- console.log("Lade Spirit", spirit.modelUrl);
-
- // Modell laden
- const { scene: spiritObj } = await gltfLoader.loadAsync(spirit.modelUrl);
-
- // Spirit-Objekt mit Animation/Fading/Motion erzeugen!
- const s = new Spirit(spiritObj, spirit);
- activeSpirits.push(s);
-
- // Overlay zeigt immer den aktuellsten
- updateSpiritOverlay(spirit);
-}
-
-// Overlay für Spirit-Infos
-function updateSpiritOverlay(spirit) {
- let el = document.getElementById('spirit-info');
- if (!el) {
- el = document.createElement('div');
- el.id = 'spirit-info';
- el.style = `
- position:absolute; left:20px; top:20px; color:white;
- background:rgba(0,0,0,0.6); padding:10px 18px; border-radius:10px;
- font-family: sans-serif; z-index:10; max-width: 320px;
- `;
- document.body.appendChild(el);
- }
- el.innerHTML = `${spirit.name || 'Spirit'}
${spirit.desc || ""}`;
-}
\ No newline at end of file
+ composer.render(scene, camera);
+ requestAnimation
\ No newline at end of file