diff --git a/party-stage/src/scene/party-guests.js b/party-stage/src/scene/party-guests.js index 264a995..020e26e 100644 --- a/party-stage/src/scene/party-guests.js +++ b/party-stage/src/scene/party-guests.js @@ -26,6 +26,45 @@ export class PartyGuests extends SceneFeature { this.guests = []; this.guestPool = []; // Store all created guests to reuse them sceneFeatureManager.register(this); + + this.organizedMode = null; + this.organizedModeTimer = 0; + this.wasBlackout = false; + this.lastOrganizedLogTime = 0; + this.organizedModeMinDuration = 0; + } + + startOrganizedMode() { + this.organizedModeTimer = 10 + Math.random() * 10; // 10-20 seconds + this.organizedModeMinDuration = 5.0; + + const music = state.music; + this.organizedMode = 'RUNNING'; // Default + + if (music) { + if (music.beatIntensity > 0.7) { + // High intensity: Moshpit or Jumping + this.organizedMode = Math.random() < 0.5 ? 'JUMPING' : 'MOSHPIT'; + } else if (music.loudnessHighs > 0.6) { + this.organizedMode = 'HANDS_UP'; + } + } + + // Random variation + if (Math.random() < 0.4) { + const modes = ['RUNNING', 'JUMPING', 'HANDS_UP', 'CIRCLE_PIT', 'MOSHPIT']; + this.organizedMode = modes[Math.floor(Math.random() * modes.length)]; + } + console.log(`Organized mode started: ${this.organizedMode}`); + + this.guests.forEach(guest => { + let chance = 0.1; + // Higher chance if in the middle or front + if (Math.abs(guest.mesh.position.x) < 5) chance += 0.3; + if (guest.mesh.position.z < -5) chance += 0.3; + + guest.isInOrganizedMode = Math.random() < chance; + }); } init() { @@ -125,7 +164,8 @@ export class PartyGuests extends SceneFeature { shouldRaiseArms: false, handsUpTimer: 0, handsRaisedType: 'BOTH', - randomOffset: Math.random() * 100 + randomOffset: Math.random() * 100, + isInOrganizedMode: false }; } @@ -151,6 +191,46 @@ export class PartyGuests extends SceneFeature { if (this.guests.length === 0 || !state.partyStarted) return; const time = state.clock.getElapsedTime(); + + // --- Organized Mode Logic --- + const isBlackout = state.blackoutMode; + if (!isBlackout && this.wasBlackout) { + // Blackout just ended + if (Math.random() < 0.2) { + this.startOrganizedMode(); + } + } + this.wasBlackout = isBlackout; + + if (this.organizedMode) { + this.organizedModeTimer -= deltaTime; + this.organizedModeMinDuration -= deltaTime; + if (time - this.lastOrganizedLogTime > 1.0) { + const count = this.guests.filter(g => g.isInOrganizedMode).length; + console.log(`Organized Event Active: ${this.organizedMode}. Participants: ${count}`); + this.lastOrganizedLogTime = time; + } + + if (this.organizedModeTimer <= 0 || (isBlackout && this.organizedModeMinDuration <= 0)) { + console.log(`Organized mode ended: ${this.organizedMode}`); + + this.guests.forEach(guest => { + if (guest.isInOrganizedMode) { + guest.state = 'MOVING'; + guest.targetPosition.set( + (Math.random() - 0.5) * movementArea.x, + 0, + movementArea.centerZ + (Math.random() - 0.5) * movementArea.z + ); + guest.moveSpeed = moveSpeed * 1.5; + guest.isInOrganizedMode = false; + } + }); + + this.organizedMode = null; + } + } + const minDistance = 0.8; // Minimum distance to maintain const minDistSq = minDistance * minDistance; @@ -171,6 +251,35 @@ export class PartyGuests extends SceneFeature { this.guests.forEach((guestObj, i) => { const { mesh, leftArm, rightArm } = guestObj; + // Dynamic join/leave logic + if (this.organizedMode) { + if (guestObj.isInOrganizedMode) { + if (Math.random() < 0.001) guestObj.isInOrganizedMode = false; + } else { + if (Math.random() < 0.001) guestObj.isInOrganizedMode = true; + } + } else { + guestObj.isInOrganizedMode = false; + } + + let currentMoveSpeed = moveSpeed; + if (this.organizedMode === 'RUNNING' && guestObj.isInOrganizedMode) { + currentMoveSpeed = moveSpeed * 2.0; + if (guestObj.state === 'WAITING') { + guestObj.waitTime = 0; + } + } else if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) { + currentMoveSpeed = moveSpeed * 2.0; + if (guestObj.state === 'WAITING') { + guestObj.waitTime = 0; + } + } else if (this.organizedMode === 'HANDS_UP' && guestObj.isInOrganizedMode) { + guestObj.handsUpTimer = 0.5; + guestObj.handsRaisedType = 'BOTH'; + } else if (this.organizedMode && !guestObj.isInOrganizedMode) { + currentMoveSpeed = moveSpeed * 0.5; + } + // --- Collision Avoidance --- let separationX = 0; let separationZ = 0; @@ -207,6 +316,56 @@ export class PartyGuests extends SceneFeature { mesh.position.x += separationX * separationStrength * deltaTime; mesh.position.z += separationZ * separationStrength * deltaTime; + if (this.organizedMode === 'CIRCLE_PIT' && guestObj.isInOrganizedMode) { + // --- Circle Pit Logic --- + const centerZ = movementArea.centerZ; + const radius = 3.5; + const speed = 5.0; + + const dx = mesh.position.x; + const dz = mesh.position.z - centerZ; + const dist = Math.sqrt(dx*dx + dz*dz); + + if (dist > radius + 1.0) { + // Run towards circle + const dirX = -dx / dist; + const dirZ = -dz / dist; + + mesh.position.x += dirX * speed * deltaTime; + mesh.position.z += dirZ * speed * deltaTime; + + mesh.rotation.y = Math.atan2(dirX, dirZ); + } else { + // Run in circle + // Tangent vector (counter-clockwise) + let vx = -dz; + let vz = dx; + + // Normalize + if (dist > 0.001) { + vx /= dist; + vz /= dist; + } + + // Move along tangent + mesh.position.x += vx * speed * deltaTime; + mesh.position.z += vz * speed * deltaTime; + + // Radius Correction (Pull towards ideal circle) + const ratio = 1.0 + (radius - dist) * 2.0 * deltaTime; + mesh.position.x *= ratio; // Assumes center X is 0 + mesh.position.z = centerZ + (mesh.position.z - centerZ) * ratio; + + // Face movement direction + mesh.rotation.y = Math.atan2(vx, vz); + } + + // Keep state as MOVING so other logic (like jumping) works, but skip standard pathfinding + guestObj.state = 'MOVING'; + + } else { + // --- Standard Logic (includes MOSHPIT) --- + if (guestObj.state === 'WAITING') { // Face the stage (approx z = -20) const dx = 0 - mesh.position.x; @@ -226,13 +385,45 @@ export class PartyGuests extends SceneFeature { mesh.rotation.z = 0; } + // Moshpit: Don't wait long + if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) { + guestObj.waitTime = 0; + } + if (time > guestObj.waitStartTime + guestObj.waitTime) { + // Gravitate towards stage (negative Z) + const minZ = movementArea.centerZ - movementArea.z / 2; + const maxZ = movementArea.centerZ + movementArea.z / 2; + const zRatio = Math.pow(Math.random(), 1.5); // Bias towards 0 (front) + const newTarget = new THREE.Vector3( (Math.random() - 0.5) * movementArea.x, 0, - movementArea.centerZ + (Math.random() - 0.5) * movementArea.z + minZ + zRatio * (maxZ - minZ) ); guestObj.targetPosition = newTarget; + + // Moshpit: Target center area more often + if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) { + const range = 5.0; + guestObj.targetPosition.set( + (Math.random() - 0.5) * range, + 0, + movementArea.centerZ + (Math.random() - 0.5) * range + ); + } + + // Non-participants: Disperse away from center + if (this.organizedMode && !guestObj.isInOrganizedMode) { + const side = Math.sign(mesh.position.x) || (Math.random() < 0.5 ? 1 : -1); + const xOffset = (movementArea.x / 2) * (0.5 + Math.random() * 0.4); + guestObj.targetPosition.set( + side * xOffset, + 0, + movementArea.centerZ + (Math.random() - 0.5) * movementArea.z + ); + } + guestObj.state = 'MOVING'; } } else if (guestObj.state === 'MOVING') { @@ -240,9 +431,20 @@ export class PartyGuests extends SceneFeature { const targetPosFlat = new THREE.Vector3(guestObj.targetPosition.x, 0, guestObj.targetPosition.z); const distance = currentPosFlat.distanceTo(targetPosFlat); + + // Moshpit: Change direction frequently + if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode && Math.random() < 0.05) { + const range = 5.0; + guestObj.targetPosition.set( + (Math.random() - 0.5) * range, + 0, + movementArea.centerZ + (Math.random() - 0.5) * range + ); + } + if (distance > 0.1) { const direction = targetPosFlat.sub(currentPosFlat).normalize(); - mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); + mesh.position.add(direction.multiplyScalar(currentMoveSpeed * deltaTime)); // If moving away from stage (positive Z), drop hands if (direction.z > 0.1) { @@ -267,6 +469,7 @@ export class PartyGuests extends SceneFeature { guestObj.waitTime = waitTimeBase + Math.random() * waitTimeVariance; } } + } // End else (Standard Logic) // Update hands up timer if (guestObj.handsUpTimer > 0) { @@ -283,7 +486,9 @@ export class PartyGuests extends SceneFeature { } } else { let currentJumpChance = jumpChance * deltaTime; // Base chance over time - if (state.music && state.music.isLoudEnough && state.music.beatIntensity > 0.8) { + if (this.organizedMode === 'JUMPING' && guestObj.isInOrganizedMode) { + currentJumpChance = 5.0 * deltaTime; + } else if (state.music && state.music.isLoudEnough && state.music.beatIntensity > 0.8) { currentJumpChance = 0.1; // High, fixed chance on the beat } @@ -292,7 +497,7 @@ export class PartyGuests extends SceneFeature { guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance; guestObj.jumpStartTime = time; - if (Math.random() < 0.5) { + if (Math.random() < 0.1) { 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';