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); } } }); } }