From 4726b419f421ccf10eef93c7444c3cbe5f27687c Mon Sep 17 00:00:00 2001 From: Dejvino Date: Fri, 21 Nov 2025 21:35:10 +0100 Subject: [PATCH] Feature: musicians dancing and jumping from stage to floor --- party-cathedral/src/core/animate.js | 11 +- .../src/scene/medieval-musicians.js | 147 ++++++++++++++++-- 2 files changed, 143 insertions(+), 15 deletions(-) diff --git a/party-cathedral/src/core/animate.js b/party-cathedral/src/core/animate.js index d322e0d..7322458 100644 --- a/party-cathedral/src/core/animate.js +++ b/party-cathedral/src/core/animate.js @@ -67,10 +67,19 @@ function updateVideo() { // --- Animation Loop --- +let lastTime = -1; export function animate() { requestAnimationFrame(animate); - const deltaTime = 1; + let deltaTime = 0; + if (lastTime !== -1) { + const newTime = state.clock.getElapsedTime(); + deltaTime = newTime - lastTime; + lastTime = newTime; + } else { + lastTime = state.clock.getElapsedTime(); + } + sceneFeatureManager.update(deltaTime); state.effectsManager.update(); updateCamera(); diff --git a/party-cathedral/src/scene/medieval-musicians.js b/party-cathedral/src/scene/medieval-musicians.js index d377751..50a8706 100644 --- a/party-cathedral/src/scene/medieval-musicians.js +++ b/party-cathedral/src/scene/medieval-musicians.js @@ -4,6 +4,15 @@ import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; import musiciansTextureUrl from '/textures/musician1.png'; +// --- Stage dimensions for positioning --- + const stageHeight = 1.5; + const stageDepth = 5; + const length = 40; + + // --- Billboard Properties --- + const musicianHeight = 2.5; + const musicianWidth = 2.5; + export class MedievalMusicians extends SceneFeature { constructor() { super(); @@ -12,15 +21,6 @@ export class MedievalMusicians extends SceneFeature { } init() { - // --- Stage dimensions for positioning --- - const stageHeight = 1.5; - const stageDepth = 5; - const length = 40; - - // --- Billboard Properties --- - const musicianHeight = 2.5; - const musicianWidth = 2.5; - // Load the texture and create the material inside the callback state.loader.load(musiciansTextureUrl, (texture) => { // 1. Draw texture to canvas to process it @@ -60,7 +60,7 @@ export class MedievalMusicians extends SceneFeature { const material = new THREE.MeshStandardMaterial({ map: processedTexture, side: THREE.DoubleSide, - transparent: true, + alphaTest: 0.5, // Treat pixels with alpha < 0.5 as fully transparent roughness: 0.7, metalness: 0.1, }); @@ -77,9 +77,25 @@ export class MedievalMusicians extends SceneFeature { musicianPositions.forEach(pos => { const musician = new THREE.Mesh(geometry, material); musician.position.copy(pos); - state.scene.add(musician); - this.musicians.push(musician); + + // Store musician object with state for animation + this.musicians.push({ + mesh: musician, + // --- State for complex movement --- + currentPlane: 'stage', // 'stage' or 'floor' + state: 'WAITING', + targetPosition: pos.clone(), + waitStartTime: 0, + waitTime: 1 + Math.random() * 2, // Wait 1-3 seconds + jumpStartPos: null, + jumpEndPos: null, + jumpProgress: 0, + + // --- State for jumping in place --- + isJumping: false, + jumpStartTime: 0, + }); }); }); } @@ -90,9 +106,112 @@ export class MedievalMusicians extends SceneFeature { const cameraPosition = new THREE.Vector3(); state.camera.getWorldPosition(cameraPosition); - this.musicians.forEach(musician => { + const time = state.clock.getElapsedTime(); + const moveSpeed = 2.0; + const stageArea = { x: 10, z: 4, y: stageHeight, centerZ: -length / 2 + stageDepth / 2 }; + const floorArea = { x: 10, z: 4, y: 0, centerZ: -length / 2 + stageDepth + 2 }; + const planeEdgeZ = -length / 2 + stageDepth; + const planeJumpChance = 0.1; + const jumpChance = 0.005; + const jumpDuration = 0.5; + const jumpHeight = 2.0; + + this.musicians.forEach(musicianObj => { + const { mesh } = musicianObj; + // We only want to rotate on the Y axis to keep them upright - musician.lookAt(cameraPosition.x, musician.position.y, cameraPosition.z); + mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z); + + // --- Main State Machine --- + const area = musicianObj.currentPlane === 'stage' ? stageArea : floorArea; + const otherArea = musicianObj.currentPlane === 'stage' ? floorArea : stageArea; + if (musicianObj.state === 'WAITING') { + if (time > musicianObj.waitStartTime + musicianObj.waitTime) { + if (Math.random() < planeJumpChance) { + // --- Decide to jump to the other plane --- + musicianObj.state = 'PREPARING_JUMP'; + const targetX = (Math.random() - 0.5) * area.x; + musicianObj.targetPosition = new THREE.Vector3(targetX, mesh.position.y, planeEdgeZ); + } else { + // --- Decide to move to a new spot on the current plane --- + const newTarget = new THREE.Vector3( + (Math.random() - 0.5) * area.x, + area.y + musicianHeight/2, + area.centerZ + (Math.random() - 0.5) * area.z + ); + musicianObj.targetPosition = newTarget; + musicianObj.state = 'MOVING'; + } + } + } else if (musicianObj.state === 'MOVING') { + const distance = mesh.position.distanceTo(musicianObj.targetPosition); + if (distance > 0.1) { + const direction = musicianObj.targetPosition.clone().sub(mesh.position).normalize(); + mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); + } else { + musicianObj.state = 'WAITING'; + musicianObj.waitStartTime = time; + musicianObj.waitTime = 1 + Math.random() * 2; + } + } else if (musicianObj.state === 'PREPARING_JUMP') { + const distance = mesh.position.distanceTo(musicianObj.targetPosition); + if (distance > 0.1) { + const direction = musicianObj.targetPosition.clone().sub(mesh.position).normalize(); + mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); + } else { + // --- Arrived at edge, start the plane jump --- + musicianObj.state = 'JUMPING_PLANE'; + musicianObj.jumpStartPos = mesh.position.clone(); + const targetPlane = musicianObj.currentPlane === 'stage' ? 'floor' : 'stage'; + const targetArea = targetPlane === 'stage' ? stageArea : floorArea; + musicianObj.jumpEndPos = new THREE.Vector3( + mesh.position.x, + targetArea.y + musicianHeight/2, + planeEdgeZ + (targetPlane === 'stage' ? -1 : 1) + ); + musicianObj.targetPosition = musicianObj.jumpEndPos.clone(); + musicianObj.currentPlane = targetPlane; + musicianObj.jumpProgress = 0; + } + } else if (musicianObj.state === 'JUMPING_PLANE') { + musicianObj.jumpProgress += deltaTime / jumpDuration; + if (musicianObj.jumpProgress < 1) { + // Determine base height based on which half of the jump we're in + const baseHeight = musicianObj.jumpProgress < 0.5 ? musicianObj.jumpStartPos.y : musicianObj.jumpEndPos.y; + const arcHeight = Math.sin(musicianObj.jumpProgress * Math.PI) * jumpHeight; + + // Interpolate horizontal position + const horizontalProgress = musicianObj.jumpProgress; + mesh.position.x = THREE.MathUtils.lerp(musicianObj.jumpStartPos.x, musicianObj.jumpEndPos.x, horizontalProgress); + mesh.position.z = THREE.MathUtils.lerp(musicianObj.jumpStartPos.z, musicianObj.jumpEndPos.z, horizontalProgress); + + // Apply vertical arc + mesh.position.y = baseHeight + arcHeight; + } else { + // Landed + mesh.position.copy(musicianObj.jumpEndPos); + musicianObj.state = 'WAITING'; + musicianObj.waitStartTime = time; + musicianObj.waitTime = 1 + Math.random() * 2; + } + } + + // --- Jumping in place (can happen in any state except during a plane jump) --- + if (musicianObj.isJumping) { + const jumpProgress = (time - musicianObj.jumpStartTime) / jumpDuration; + if (jumpProgress < 1) { + const baseHeight = area.y + musicianHeight/2; + mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * jumpHeight; + } else { + musicianObj.isJumping = false; + mesh.position.y = area.y + musicianHeight / 2; + } + } else { + if (Math.random() < jumpChance && musicianObj.state !== 'JUMPING_PLANE' && musicianObj.state !== 'PREPARING_JUMP') { + musicianObj.isJumping = true; + musicianObj.jumpStartTime = time; + } + } }); } }