From 6b292b32ad2ec3004559cd0ddb5391573db0bc76 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 4 Jan 2026 06:28:28 +0000 Subject: [PATCH] Feature: configurable toggle for music console and count of guests --- party-stage/src/scene/config-ui.js | 37 +++++ party-stage/src/scene/dj.js | 9 +- party-stage/src/scene/music-console.js | 2 + party-stage/src/scene/party-guests.js | 185 ++++++++++++++----------- party-stage/src/state.js | 2 + 5 files changed, 148 insertions(+), 87 deletions(-) diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index 613e043..6c74f74 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -114,9 +114,43 @@ export class ConfigUI extends SceneFeature { // Side Screens Toggle createToggle('Side Screens', 'sideScreensEnabled'); + // Music Console Toggle + createToggle('Music Console', 'consoleEnabled', (enabled) => { + const consoleFeature = sceneFeatureManager.features.find(f => f.constructor.name === 'MusicConsole'); + if (consoleFeature && consoleFeature.group) consoleFeature.group.visible = enabled; + }); + // Console RGB Toggle createToggle('Console RGB Panel', 'consoleRGBEnabled'); + // Guest Count Input + const guestRow = document.createElement('div'); + guestRow.style.display = 'flex'; + guestRow.style.alignItems = 'center'; + guestRow.style.justifyContent = 'space-between'; + + const guestLabel = document.createElement('label'); + guestLabel.innerText = 'Guest Count'; + + const guestInput = document.createElement('input'); + guestInput.type = 'number'; + guestInput.min = '0'; + guestInput.max = '500'; + guestInput.value = state.config.guestCount; + guestInput.style.width = '60px'; + guestInput.onchange = (e) => { + const val = parseInt(e.target.value, 10); + state.config.guestCount = val; + saveConfig(); + const guestsFeature = sceneFeatureManager.features.find(f => f.constructor.name === 'PartyGuests'); + if (guestsFeature) guestsFeature.setGuestCount(val); + }; + this.guestInput = guestInput; + + guestRow.appendChild(guestLabel); + guestRow.appendChild(guestInput); + rightContainer.appendChild(guestRow); + // DJ Hat Selector const hatRow = document.createElement('div'); hatRow.style.display = 'flex'; @@ -323,6 +357,8 @@ export class ConfigUI extends SceneFeature { lasersEnabled: true, sideScreensEnabled: true, consoleRGBEnabled: true, + consoleEnabled: true, + guestCount: 150, djHat: 'None' }; for (const key in defaults) { @@ -332,6 +368,7 @@ export class ConfigUI extends SceneFeature { if (this.toggles[key].callback) this.toggles[key].callback(defaults[key]); } } + if (this.guestInput) this.guestInput.value = defaults.guestCount; if (this.hatSelect) this.hatSelect.value = defaults.djHat; this.updateStatus(); }; diff --git a/party-stage/src/scene/dj.js b/party-stage/src/scene/dj.js index 1212603..954162f 100644 --- a/party-stage/src/scene/dj.js +++ b/party-stage/src/scene/dj.js @@ -164,7 +164,7 @@ export class DJ extends SceneFeature { if (this.armTimer <= 0) { this.armState = Math.floor(Math.random() * 5); this.armTimer = 2 + Math.random() * 4; - if (Math.random() < 0.3) this.armState = 4; // Twiddling some more + if (Math.random() < 0.3 && state.config.consoleEnabled) this.armState = 4; // Twiddling some more } const upAngle = Math.PI * 0.85; @@ -176,7 +176,7 @@ export class DJ extends SceneFeature { let targetLeftX = 0; let targetRightX = 0; - if (this.armState === 4) { + if (this.armState === 4 && state.config.consoleEnabled) { // Twiddling targetLeftZ = 0.2; targetRightZ = 0.2; @@ -192,7 +192,7 @@ export class DJ extends SceneFeature { this.currentLeftAngleX = THREE.MathUtils.lerp(this.currentLeftAngleX, targetLeftX, deltaTime * 5); this.currentRightAngleX = THREE.MathUtils.lerp(this.currentRightAngleX, targetRightX, deltaTime * 5); - if (this.armState === 4) { + if (this.armState === 4 && state.config.consoleEnabled) { const t = time * 15; this.leftArm.rotation.z = -this.currentLeftAngle + Math.cos(t) * 0.05; this.rightArm.rotation.z = this.currentRightAngle + Math.sin(t) * 0.05; @@ -215,7 +215,8 @@ export class DJ extends SceneFeature { this.moveTimer -= deltaTime; if (this.moveTimer <= 0) { this.state = 'MOVING'; - this.targetX = (Math.random() - 0.5) * 2.5; + const range = state.config.consoleEnabled ? 2.5 : 10.0; + this.targetX = (Math.random() - 0.5) * range; } } else if (this.state === 'MOVING') { const speed = 1.5; diff --git a/party-stage/src/scene/music-console.js b/party-stage/src/scene/music-console.js index 0b8c2e7..207f2f9 100644 --- a/party-stage/src/scene/music-console.js +++ b/party-stage/src/scene/music-console.js @@ -21,6 +21,8 @@ export class MusicConsole extends SceneFeature { // Position on stage, centered group.position.set(0, stageY, -16.5); state.scene.add(group); + this.group = group; + this.group.visible = state.config.consoleEnabled; // 1. The Stand/Table Body const standGeo = new THREE.BoxGeometry(consoleWidth, consoleHeight, consoleDepth); diff --git a/party-stage/src/scene/party-guests.js b/party-stage/src/scene/party-guests.js index 10e39d6..04d69cf 100644 --- a/party-stage/src/scene/party-guests.js +++ b/party-stage/src/scene/party-guests.js @@ -7,7 +7,6 @@ import sceneFeatureManager from './SceneFeatureManager.js'; const stageHeight = 1.5; const stageDepth = 5; const length = 25; -const numGuests = 150; const moveSpeed = 0.8; const movementArea = { x: 15, z: length, y: 0, centerZ: -2 }; const jumpChance = 0.01; @@ -25,6 +24,7 @@ export class PartyGuests extends SceneFeature { constructor() { super(); this.guests = []; + this.guestPool = []; // Store all created guests to reuse them sceneFeatureManager.register(this); } @@ -36,96 +36,115 @@ export class PartyGuests extends SceneFeature { const headGeo = new THREE.SphereGeometry(0.25, 16, 16); // Arm: Ellipsoid (Scaled Sphere) const armGeo = new THREE.SphereGeometry(0.12, 16, 16); + + this.geometries = { bodyGeo, headGeo, armGeo }; - const createGuests = () => { - for (let i = 0; i < numGuests; i++) { - // 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, - }); + // Initialize with config count + this.setGuestCount(state.config.guestCount); + } - const group = new THREE.Group(); + createGuest() { + // 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 scale = 0.85 + Math.random() * 0.3; - group.scale.setScalar(scale); + 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); + const scale = 0.85 + Math.random() * 0.3; + group.scale.setScalar(scale); - // 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); + // Body + const body = new THREE.Mesh(this.geometries.bodyGeo, material); + body.position.y = 0.7; + body.castShadow = true; + body.receiveShadow = true; + group.add(body); - // 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; - }; + // Head + const head = new THREE.Mesh(this.geometries.headGeo, material); + head.position.y = 1.55; + head.castShadow = true; + head.receiveShadow = true; + group.add(head); - const leftArm = createArm(true); - const rightArm = createArm(false); - - group.add(leftArm); - group.add(rightArm); - - // Position - const pos = new THREE.Vector3( - (Math.random() - 0.5) * movementArea.x, - 0, - movementArea.centerZ + ( rushIn - ? (((Math.random()-0.5) * length * 0.6) - length * 0.3) - : ((Math.random()-0.5) * length)) - ); - - group.position.copy(pos); - group.visible = false; - state.scene.add(group); - - this.guests.push({ - mesh: group, - leftArm, - rightArm, - state: 'WAITING', - targetPosition: pos.clone(), - waitStartTime: 0, - waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds - isJumping: false, - jumpStartTime: 0, - jumpHeight: 0, - shouldRaiseArms: false, - handsUpTimer: 0, - handsRaisedType: 'BOTH', - randomOffset: Math.random() * 100 - }); - } + // 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(this.geometries.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; }; - createGuests(); + const leftArm = createArm(true); + const rightArm = createArm(false); + + group.add(leftArm); + group.add(rightArm); + + // Position + const pos = new THREE.Vector3( + (Math.random() - 0.5) * movementArea.x, + 0, + movementArea.centerZ + ( rushIn + ? (((Math.random()-0.5) * length * 0.6) - length * 0.3) + : ((Math.random()-0.5) * length)) + ); + + group.position.copy(pos); + group.visible = false; + state.scene.add(group); + + return { + mesh: group, + leftArm, + rightArm, + state: 'WAITING', + targetPosition: pos.clone(), + waitStartTime: 0, + waitTime: 3 + Math.random() * 4, + isJumping: false, + jumpStartTime: 0, + jumpHeight: 0, + shouldRaiseArms: false, + handsUpTimer: 0, + handsRaisedType: 'BOTH', + randomOffset: Math.random() * 100 + }; + } + + setGuestCount(count) { + // Ensure we have enough guests in the pool + while (this.guestPool.length < count) { + this.guestPool.push(this.createGuest()); + } + + // Update visibility and active list + this.guests = []; + this.guestPool.forEach((guest, index) => { + if (index < count) { + guest.mesh.visible = state.partyStarted; // Only visible if party started + this.guests.push(guest); + } else { + guest.mesh.visible = false; + } + }); } update(deltaTime) { @@ -280,7 +299,7 @@ export class PartyGuests extends SceneFeature { onPartyStart() { const stageFrontZ = -40 / 2 + 5 + 5; // In front of the stage this.guests.forEach((guestObj, index) => { - guestObj.mesh.visible = true; + guestObj.mesh.visible = true; // Make sure active guests are visible // Rush to the stage guestObj.state = 'MOVING'; if (index % 2 === 0) { diff --git a/party-stage/src/state.js b/party-stage/src/state.js index 70c42ec..77fa8d0 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -8,6 +8,8 @@ export function initState() { lasersEnabled: true, sideScreensEnabled: true, consoleRGBEnabled: true, + consoleEnabled: true, + guestCount: 150, djHat: 'None' // 'None', 'Santa', 'Top Hat' }; try {