import * as THREE from 'three'; import { state } from '../state.js'; import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; const guestTextureUrls = [ '/textures/guest1.png', '/textures/guest2.png', '/textures/guest3.png', '/textures/guest4.png', ]; // --- Scene dimensions for positioning --- const stageHeight = 1.5; const stageDepth = 5; const length = 44; // --- Billboard Properties --- const guestHeight = 2.5; const guestWidth = 2.5; export class PartyGuests extends SceneFeature { constructor() { super(); this.guests = []; sceneFeatureManager.register(this); } async init() { const processTexture = (texture) => { const image = texture.image; const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const context = canvas.getContext('2d'); context.drawImage(image, 0, 0); const keyPixelData = context.getImageData(0, 0, 1, 1).data; const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] }; const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const threshold = 20; for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i + 1], b = data[i + 2]; const distance = Math.sqrt(Math.pow(r - keyColor.r, 2) + Math.pow(g - keyColor.g, 2) + Math.pow(b - keyColor.b, 2)); if (distance < threshold) data[i + 3] = 0; } context.putImageData(imageData, 0, 0); return new THREE.CanvasTexture(canvas); }; const materials = await Promise.all(guestTextureUrls.map(async (url) => { const texture = await state.loader.loadAsync(url); const processedTexture = processTexture(texture); return new THREE.MeshStandardMaterial({ map: processedTexture, side: THREE.DoubleSide, alphaTest: 0.5, roughness: 0.7, metalness: 0.1, }); })); const createGuests = () => { const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight); const numGuests = 80; for (let i = 0; i < numGuests; i++) { const material = materials[i % materials.length]; const guest = new THREE.Mesh(geometry, material); const pos = new THREE.Vector3( (Math.random() - 0.5) * 10, guestHeight / 2, (Math.random() * 20) - 6 // Position them in the main hall ); guest.position.copy(pos); state.scene.add(guest); this.guests.push({ mesh: guest, state: 'WAITING', targetPosition: pos.clone(), waitStartTime: 0, waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds isJumping: false, jumpStartTime: 0, }); } }; createGuests(); } update(deltaTime) { if (this.guests.length === 0) return; const cameraPosition = new THREE.Vector3(); state.camera.getWorldPosition(cameraPosition); const time = state.clock.getElapsedTime(); const moveSpeed = 1.0; // Move slower const movementArea = { x: 10, z: 30, y: 0, centerZ: 5 }; const jumpChance = 0.05; // Jump way more const jumpDuration = 0.5; const jumpHeight = 0.1; const jumpVariance = 0.5; this.guests.forEach(guestObj => { const { mesh } = guestObj; mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z); if (guestObj.state === 'WAITING') { if (time > guestObj.waitStartTime + guestObj.waitTime) { const newTarget = new THREE.Vector3( (Math.random() - 0.5) * movementArea.x, movementArea.y + guestHeight / 2, movementArea.centerZ + (Math.random() - 0.5) * movementArea.z ); guestObj.targetPosition = newTarget; guestObj.state = 'MOVING'; } } else if (guestObj.state === 'MOVING') { const distance = mesh.position.distanceTo(guestObj.targetPosition); if (distance > 0.1) { const direction = guestObj.targetPosition.clone().sub(mesh.position).normalize(); mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); } else { guestObj.state = 'WAITING'; guestObj.waitStartTime = time; guestObj.waitTime = 3 + Math.random() * 4; } } if (guestObj.isJumping) { const jumpProgress = (time - guestObj.jumpStartTime) / jumpDuration; if (jumpProgress < 1) { const baseHeight = movementArea.y + guestHeight / 2; mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight; } else { guestObj.isJumping = false; mesh.position.y = movementArea.y + guestHeight / 2; } } else { let currentJumpChance = jumpChance * deltaTime; // Base chance over time if (state.music && state.music.beatIntensity > 0.8) { currentJumpChance = 0.1; // High, fixed chance on the beat } if (Math.random() < currentJumpChance) { guestObj.isJumping = true; guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance; guestObj.jumpStartTime = time; } } }); } } new PartyGuests();