diff --git a/party-stage/src/scene/party-guests.js b/party-stage/src/scene/party-guests.js index 4fc9175..562a902 100644 --- a/party-stage/src/scene/party-guests.js +++ b/party-stage/src/scene/party-guests.js @@ -2,31 +2,24 @@ 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 = 20; +const length = 25; const numGuests = 150; const moveSpeed = 0.8; -const movementArea = { x: 15, z: length, y: 0, centerZ: -4 }; +const movementArea = { x: 20, z: length, y: 0, centerZ: -2 }; const jumpChance = 0.01; const jumpDuration = 0.3; -const jumpHeight = 0.05; -const jumpVariance = 0.5; +const jumpHeight = 0.2; +const jumpVariance = 0.1; const rushIn = false; const waitTimeBase = 10; -const waitTimeVariance = 10; +const waitTimeVariance = 60; -// --- Billboard Properties --- -const guestHeight = 2.5; -const guestWidth = 2.5; +// --- Guest Properties --- +const guestHeight = 1.8; // Approx height of the blob+head export class PartyGuests extends SceneFeature { constructor() { @@ -35,67 +28,96 @@ export class PartyGuests extends SceneFeature { 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, - }); - })); + init() { + // Shared Geometries + // Body: Tall blob (Capsule) + const bodyGeo = new THREE.CapsuleGeometry(0.3, 0.8, 4, 8); + // Head: Sphere + const headGeo = new THREE.SphereGeometry(0.25, 16, 16); + // Arm: Ellipsoid (Scaled Sphere) + const armGeo = new THREE.SphereGeometry(0.12, 16, 16); const createGuests = () => { - const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight); - for (let i = 0; i < numGuests; i++) { - const material = materials[i % materials.length]; - const guest = new THREE.Mesh(geometry, material); + // Random Color + // Dark gray-blue shades + const color = new THREE.Color().setHSL( + 0.6 + (Math.random() * 0.1 - 0.05), // Hue around 0.6 (blue) + 0.1 + Math.random() * 0.1, // Low saturation + 0.01 + Math.random() * 0.05 // Much darker lightness + ); + const material = new THREE.MeshStandardMaterial({ + color: color, + roughness: 0.6, + metalness: 0.1, + }); + + const group = new THREE.Group(); + + // Body + const body = new THREE.Mesh(bodyGeo, material); + body.position.y = 0.7; // Center of capsule (0.8 length + 0.3*2 radius = 1.4 total height. Center at 0.7) + body.castShadow = true; + body.receiveShadow = true; + group.add(body); + + // Head + const head = new THREE.Mesh(headGeo, material); + head.position.y = 1.55; // Top of body + head.castShadow = true; + head.receiveShadow = true; + group.add(head); + + // Arms + const createArm = (isLeft) => { + const pivot = new THREE.Group(); + // Shoulder position + pivot.position.set(isLeft ? -0.35 : 0.35, 1.3, 0); + + const arm = new THREE.Mesh(armGeo, material); + arm.scale.set(1, 3.5, 1); // Ellipsoid + arm.position.y = -0.4; // Hang down from pivot + arm.castShadow = true; + arm.receiveShadow = true; + + pivot.add(arm); + return pivot; + }; + + const leftArm = createArm(true); + const rightArm = createArm(false); + + group.add(leftArm); + group.add(rightArm); + + // Position const pos = new THREE.Vector3( (Math.random() - 0.5) * 10, - guestHeight / 2, + 0, movementArea.centerZ + ( rushIn ? (((Math.random()-0.5) * length * 0.6) - length * 0.3) : ((Math.random()-0.5) * length)) ); - guest.visible = false; // Start invisible - guest.position.copy(pos); - state.scene.add(guest); + + group.position.copy(pos); + group.visible = false; + state.scene.add(group); this.guests.push({ - mesh: guest, + mesh: group, + leftArm, + rightArm, state: 'WAITING', targetPosition: pos.clone(), waitStartTime: 0, waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds - isMirrored: false, - canChangePose: true, isJumping: false, jumpStartTime: 0, + jumpHeight: 0, + shouldRaiseArms: false, + handsUpTimer: 0, + handsRaisedType: 'BOTH', + randomOffset: Math.random() * 100 }); } }; @@ -106,42 +128,65 @@ export class PartyGuests extends SceneFeature { update(deltaTime) { if (this.guests.length === 0 || !state.partyStarted) return; - const cameraPosition = new THREE.Vector3(); - state.camera.getWorldPosition(cameraPosition); - const time = state.clock.getElapsedTime(); this.guests.forEach(guestObj => { - const { mesh } = guestObj; - mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z); - - // --- Mirroring on Beat --- - if (state.music) { - if (state.music.beatIntensity > 0.8 && guestObj.canChangePose) { - guestObj.isMirrored = Math.random() < 0.5; - mesh.material.map.repeat.x = guestObj.isMirrored ? -1 : 1; - mesh.material.map.offset.x = guestObj.isMirrored ? 1 : 0; - guestObj.canChangePose = false; - } else if (state.music.beatIntensity < 0.2) { - guestObj.canChangePose = true; - } - } + const { mesh, leftArm, rightArm } = guestObj; if (guestObj.state === 'WAITING') { + // Face the stage (approx z = -20) + const dx = 0 - mesh.position.x; + const dz = -20 - mesh.position.z; + const targetRotation = Math.atan2(dx, dz); + let rotDiff = targetRotation - mesh.rotation.y; + while (rotDiff > Math.PI) rotDiff -= Math.PI * 2; + while (rotDiff < -Math.PI) rotDiff += Math.PI * 2; + mesh.rotation.y += rotDiff * deltaTime * 2.0; + + // Gentle Bob and Sway + if (!guestObj.isJumping) { + const bobSpeed = 6.0; + mesh.position.y = Math.abs(Math.sin(time * bobSpeed + guestObj.randomOffset)) * 0.05; + mesh.rotation.z = Math.sin(time * (bobSpeed * 0.5) + guestObj.randomOffset) * 0.05; + } else { + mesh.rotation.z = 0; + } + if (time > guestObj.waitStartTime + guestObj.waitTime) { const newTarget = new THREE.Vector3( (Math.random() - 0.5) * movementArea.x, - movementArea.y + guestHeight / 2, + 0, 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); + const currentPosFlat = new THREE.Vector3(mesh.position.x, 0, mesh.position.z); + const targetPosFlat = new THREE.Vector3(guestObj.targetPosition.x, 0, guestObj.targetPosition.z); + + const distance = currentPosFlat.distanceTo(targetPosFlat); if (distance > 0.1) { - const direction = guestObj.targetPosition.clone().sub(mesh.position).normalize(); + const direction = targetPosFlat.sub(currentPosFlat).normalize(); mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); + + // If moving away from stage (positive Z), drop hands + if (direction.z > 0.1) { + guestObj.handsUpTimer = 0; + } + + // Face direction + const targetRotation = Math.atan2(direction.x, direction.z); + let rotDiff = targetRotation - mesh.rotation.y; + while (rotDiff > Math.PI) rotDiff -= Math.PI * 2; + while (rotDiff < -Math.PI) rotDiff += Math.PI * 2; + mesh.rotation.y += rotDiff * deltaTime * 5; + + if (!guestObj.isJumping) { + mesh.position.y = 0; + mesh.rotation.z = 0; + } + } else { guestObj.state = 'WAITING'; guestObj.waitStartTime = time; @@ -149,14 +194,18 @@ export class PartyGuests extends SceneFeature { } } + // Update hands up timer + if (guestObj.handsUpTimer > 0) { + guestObj.handsUpTimer -= deltaTime; + } + 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; + mesh.position.y = Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight; } else { guestObj.isJumping = false; - mesh.position.y = movementArea.y + guestHeight / 2; + mesh.position.y = 0; } } else { let currentJumpChance = jumpChance * deltaTime; // Base chance over time @@ -168,8 +217,35 @@ export class PartyGuests extends SceneFeature { guestObj.isJumping = true; guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance; guestObj.jumpStartTime = time; + + if (Math.random() < 0.5) { + guestObj.handsUpTimer = 2.0 + Math.random() * 3.0; // Keep hands up for 2-5 seconds + const r = Math.random(); + if (r < 0.33) guestObj.handsRaisedType = 'LEFT'; + else if (r < 0.66) guestObj.handsRaisedType = 'RIGHT'; + else guestObj.handsRaisedType = 'BOTH'; + } } } + + // Apply Arm Rotation + let targetLeftAngle = 0; + let targetRightAngle = 0; + + if (guestObj.handsUpTimer > 0) { + const upAngle = -Math.PI; + if (guestObj.handsRaisedType === 'LEFT' || guestObj.handsRaisedType === 'BOTH') { + targetLeftAngle = upAngle; + } + if (guestObj.handsRaisedType === 'RIGHT' || guestObj.handsRaisedType === 'BOTH') { + targetRightAngle = upAngle; + } + } + + leftArm.rotation.x = THREE.MathUtils.lerp(leftArm.rotation.x, targetLeftAngle, deltaTime * 5); + rightArm.rotation.x = THREE.MathUtils.lerp(rightArm.rotation.x, targetRightAngle, deltaTime * 5); + leftArm.rotation.z = 0; + rightArm.rotation.z = 0; }); }