diff --git a/tv-player/src/effects/EffectsManager.js b/tv-player/src/effects/EffectsManager.js index a118f7e..4ef0786 100644 --- a/tv-player/src/effects/EffectsManager.js +++ b/tv-player/src/effects/EffectsManager.js @@ -1,5 +1,6 @@ import { DustEffect } from './dust.js'; import { FliesEffect } from './flies.js'; +import { SpiderEffect } from './spider.js'; export class EffectsManager { constructor(scene) { @@ -12,6 +13,7 @@ export class EffectsManager { // This is now the single place to manage which effects are active. this.addEffect(new DustEffect(scene)); this.addEffect(new FliesEffect(scene)); + this.addEffect(new SpiderEffect(scene)); } addEffect(effect) { diff --git a/tv-player/src/effects/spider.js b/tv-player/src/effects/spider.js new file mode 100644 index 0000000..738890b --- /dev/null +++ b/tv-player/src/effects/spider.js @@ -0,0 +1,131 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; + +const SPIDER_COUNT = 5; +const SPIDER_SPEED = 0.0001; +const SPIDER_TURN_SPEED = 0.02; +const SPIDER_WAIT_MIN = 200; // frames +const SPIDER_WAIT_MAX = 500; // frames + +export class SpiderEffect { + constructor(scene) { + this.spiders = []; + this._setupSpiders(scene); + } + + _getRandomPointOnWall(wall) { + const position = new THREE.Vector3(); + const width = wall.geometry.parameters.width; + const height = wall.geometry.parameters.height; + + position.x = (Math.random() - 0.5) * width; + position.y = (Math.random() - 0.5) * height; + position.z = 0; // Local z is 0 for a plane + + // Convert local position to world position + return wall.localToWorld(position); + } + + _createSpiderMesh() { + const spiderGroup = new THREE.Group(); + const spiderMaterial = new THREE.MeshPhongMaterial({ color: 0x919191, shininess: 50 }); + + // Body + const bodyGeometry = new THREE.SphereGeometry(0.01, 6, 5); + const body = new THREE.Mesh(bodyGeometry, spiderMaterial); + body.scale.z = 0.6; // Flatten the sphere + body.castShadow = true; + spiderGroup.add(body); + + // Head + const headGeometry = new THREE.SphereGeometry(0.005, 5, 4); + const head = new THREE.Mesh(headGeometry, spiderMaterial); + head.position.y = 0.015; + head.castShadow = true; + spiderGroup.add(head); + + spiderGroup.userData = { + state: 'crawling', // 'crawling', 'waiting' + waitTimer: 0, + t: 0, + curve: null, + currentWall: null, + }; + + return spiderGroup; + } + + _findNewTarget(spider) { + if (!spider.userData.currentWall) { + // First time, pick a random wall + const walls = state.crawlSurfaces; + if (walls.length === 0) return; + spider.userData.currentWall = walls[Math.floor(Math.random() * walls.length)]; + spider.position.copy(this._getRandomPointOnWall(spider.userData.currentWall)); + } + + const startPoint = spider.position.clone(); + const endPoint = this._getRandomPointOnWall(spider.userData.currentWall); + + // Create a curved path on the wall + const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5); + const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize(); + const wallNormal = spider.userData.currentWall.getWorldDirection(new THREE.Vector3()).negate(); + + // Get a perpendicular vector on the plane of the wall + const perpendicular = new THREE.Vector3().crossVectors(direction, wallNormal).normalize(); + const offsetMagnitude = startPoint.distanceTo(endPoint) * (Math.random() * 0.4 - 0.2); // Random offset left or right + + const controlPoint = midPoint.clone().add(perpendicular.multiplyScalar(offsetMagnitude)); + + spider.userData.curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint); + spider.userData.t = 0; + spider.userData.state = 'crawling'; + } + + _setupSpiders(scene) { + for (let i = 0; i < SPIDER_COUNT; i++) { + const spider = this._createSpiderMesh(); + scene.add(spider); + this.spiders.push(spider); + this._findNewTarget(spider); // Initial placement + } + } + + update() { + this.spiders.forEach(spider => { + const data = spider.userData; + + if (data.state === 'crawling') { + if (!data.curve) { + this._findNewTarget(spider); + return; + } + + data.t += SPIDER_SPEED; + if (data.t >= 1) { + spider.position.copy(data.curve.v2); + data.state = 'waiting'; + data.waitTimer = SPIDER_WAIT_MIN + Math.random() * (SPIDER_WAIT_MAX - SPIDER_WAIT_MIN); + } else { + spider.position.copy(data.curve.getPoint(data.t)); + + // Smoothly turn the spider towards the tangent of the curve + const tangent = data.curve.getTangent(data.t); + const up = data.currentWall.getWorldDirection(new THREE.Vector3()).negate(); + + const targetQuaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent).multiply( + new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), up) + ); + + spider.quaternion.slerp(targetQuaternion, SPIDER_TURN_SPEED); + } + } else if (data.state === 'waiting') { + data.waitTimer--; + if (data.waitTimer <= 0) { + this._findNewTarget(spider); + } + } + }); + } +} \ No newline at end of file diff --git a/tv-player/src/scene/room-walls.js b/tv-player/src/scene/room-walls.js index 7ac1e75..27e5006 100644 --- a/tv-player/src/scene/room-walls.js +++ b/tv-player/src/scene/room-walls.js @@ -18,24 +18,28 @@ export function createRoomWalls() { const backWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial); backWall.position.set(0, state.roomHeight / 2, -state.roomSize / 2); backWall.receiveShadow = true; + backWall.name = 'backWall'; state.scene.add(backWall); // 2. Front Wall (behind the camera) const frontWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial); frontWall.position.set(0, state.roomHeight / 2, state.roomSize / 2); frontWall.rotation.y = Math.PI; + frontWall.name = 'frontWall'; frontWall.receiveShadow = true; state.scene.add(frontWall); // 3. Left Wall const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial); leftWall.rotation.y = Math.PI / 2; + leftWall.name = 'leftWall'; leftWall.position.set(-state.roomSize / 2, state.roomHeight / 2, 0); leftWall.receiveShadow = true; state.scene.add(leftWall); // 4. Right Wall const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial); + rightWall.name = 'rightWall'; rightWall.rotation.y = -Math.PI / 2; rightWall.position.set(state.roomSize / 2, state.roomHeight / 2, 0); rightWall.receiveShadow = true; @@ -58,6 +62,8 @@ export function createRoomWalls() { ceiling.position.set(0, state.roomHeight, 0); ceiling.receiveShadow = true; state.scene.add(ceiling); + + state.crawlSurfaces.push(backWall, frontWall, leftWall, rightWall); // --- 6. Add a Window to the Back Wall --- const windowWidth = 1.5; diff --git a/tv-player/src/state.js b/tv-player/src/state.js index a750a86..8dd6357 100644 --- a/tv-player/src/state.js +++ b/tv-player/src/state.js @@ -44,6 +44,7 @@ export function initState() { // Utilities loader: new THREE.TextureLoader(), landingSurfaces: [], + crawlSurfaces: [], // Surfaces for spiders to crawl on bookLevitation: { state: 'resting', // 'resting', 'levitating', 'returning' timer: 0,