diff --git a/animations-baker.js b/animations-baker.js index de2675fb55..f12f63f2cc 100644 --- a/animations-baker.js +++ b/animations-baker.js @@ -540,104 +540,128 @@ const {CharsetEncoder} = require('three/examples/js/libs/mmdparser.js'); 'sword_topdown_slash.fbx', 'sword_topdown_slash_step.fbx', ]; + const animationComboIndices = comboAnimationNames.map(comboAnimationName => { const animation = animations.find(a => a.name === comboAnimationName); const {tracks, object} = animation; // console.log('got interpolants', object, animation.name); - const bones = []; - object.traverse(o => { - if (o.isBone) { - const bone = o; - bone.initialPosition = bone.position.clone(); - bone.initialQuaternion = bone.quaternion.clone(); - bones.push(bone); + + const validateAndRemoveScaleTracks = () => { + const epsilon = 0.2; + const allOnesEpsilon = arr => arr.every(v => Math.abs(1 - v) < epsilon); + const tracksToRemove = []; + + for (const track of tracks) { + if (/\.scale$/.test(track.name)) { + if (allOnesEpsilon(track.values)) { + const index = tracks.indexOf(track); + tracksToRemove.push(index); + } else { + throw new Error(`This track has invalid values. All scale transforms must be set to 1. Aborting.\n + Animation: ${animation.name}, Track: ${track.name}, values: \n + ${track.values}`); + } + } } - }); - // console.log('got bones', bones.map(b => b.name)); - const rootBone = object; // not really a bone - const leftHandBone = bones.find(b => b.name === 'mixamorigLeftHand'); - const rightHandBone = bones.find(b => b.name === 'mixamorigRightHand'); - const epsilon = 0.2; - const allOnesEpsilon = arr => arr.every(v => Math.abs(1 - v) < epsilon); - - const bonePositionInterpolants = {}; - const boneQuaternionInterpolants = {}; - const tracksToRemove = []; - for (const track of tracks) { - if (/\.position$/.test(track.name)) { - const boneName = track.name.replace(/\.position$/, ''); - // const bone = bones.find(b => b.name === boneName); - const boneInterpolant = new THREE.LinearInterpolant(track.times, track.values, track.getValueSize()); - bonePositionInterpolants[boneName] = boneInterpolant; - } else if (/\.quaternion$/.test(track.name)) { - const boneName = track.name.replace(/\.quaternion$/, ''); - // const bone = bones.find(b => b.name === boneName); - const boneInterpolant = new THREE.QuaternionLinearInterpolant(track.times, track.values, track.getValueSize()); - boneQuaternionInterpolants[boneName] = boneInterpolant; - } else if (/\.scale$/.test(track.name)) { - if (allOnesEpsilon(track.values)) { - const index = tracks.indexOf(track); - tracksToRemove.push(index); + // remove scale transform tracks as they won't be used; + let i = tracksToRemove.length; + while (i--) { + tracks.splice(tracksToRemove[i], 1); + } + }; + validateAndRemoveScaleTracks(); + + const getBoneInterpolants = () => { + const bonePositionInterpolants = {}; + const boneQuaternionInterpolants = {}; + + for (const track of tracks) { + if (/\.position$/.test(track.name)) { + const boneName = track.name.replace(/\.position$/, ''); + // const bone = bones.find(b => b.name === boneName); + const boneInterpolant = new THREE.LinearInterpolant(track.times, track.values, track.getValueSize()); + bonePositionInterpolants[boneName] = boneInterpolant; + } else if (/\.quaternion$/.test(track.name)) { + const boneName = track.name.replace(/\.quaternion$/, ''); + // const bone = bones.find(b => b.name === boneName); + const boneInterpolant = new THREE.QuaternionLinearInterpolant(track.times, track.values, track.getValueSize()); + boneQuaternionInterpolants[boneName] = boneInterpolant; } else { - throw new Error(`This track has invalid values. All scale transforms must be set to 1. Aborting.\n Animation: ${animation.name}, Track: ${track.name}, values: \n ${track.values}`); + console.warn('unknown track name', animation.name, track); } - } else { - console.warn('unknown track name', animation.name, track); } - } - // remove scale transform tracks as they won't be used; - let i = tracksToRemove.length; - while (i--) { - tracks.splice(tracksToRemove[i], 1); - } - - const walkBufferSize = 256; - const leftHandDeltas = new Float32Array(walkBufferSize); - const rightHandDeltas = new Float32Array(walkBufferSize); + return {bonePositionInterpolants, boneQuaternionInterpolants}; + }; - let lastRightHandPos = new THREE.Vector3(); - let lastLeftHandPos = new THREE.Vector3(); + const extractBones = root => { + const bones = []; + root.traverse(o => { + if (o.isBone) { + const bone = o; + bone.initialPosition = bone.position.clone(); + bone.initialQuaternion = bone.quaternion.clone(); + bones.push(bone); + } + }); + return bones; + }; + const walkBufferSize = 256; let maxRightDeltaIndex = 0; - let maxLeftDeltaIndex = 0; + let maxLeftDeltaIndex = 0; - for (let i = 0; i < walkBufferSize; i++) { - const f = i / (walkBufferSize - 1); - for (const bone of bones) { - const positionInterpolant = bonePositionInterpolants[bone.name]; - const quaternionInterpolant = boneQuaternionInterpolants[bone.name]; - if (positionInterpolant) { - const pv = positionInterpolant.evaluate(f * animation.duration); - bone.position.fromArray(pv); - } else { - bone.position.copy(bone.initialPosition); - } - if (quaternionInterpolant) { - const qv = quaternionInterpolant.evaluate(f * animation.duration); - bone.quaternion.fromArray(qv); - } else { - bone.quaternion.copy(bone.initialQuaternion); + const leftHandDeltas = new Float32Array(walkBufferSize); + const rightHandDeltas = new Float32Array(walkBufferSize); + + const bake = () => { + const rootBone = object; // not really a bone + const bones = extractBones(rootBone); + const leftHandBone = bones.find(b => b.name === 'mixamorigLeftHand'); + const rightHandBone = bones.find(b => b.name === 'mixamorigRightHand'); + + const {bonePositionInterpolants, boneQuaternionInterpolants} = getBoneInterpolants(); + + let lastRightHandPos = new THREE.Vector3(); + let lastLeftHandPos = new THREE.Vector3(); + + for (let i = 0; i < walkBufferSize; i++) { + const f = i / (walkBufferSize - 1); + for (const bone of bones) { + const positionInterpolant = bonePositionInterpolants[bone.name]; + const quaternionInterpolant = boneQuaternionInterpolants[bone.name]; + if (positionInterpolant) { + const pv = positionInterpolant.evaluate(f * animation.duration); + bone.position.fromArray(pv); + } else { + bone.position.copy(bone.initialPosition); + } + if (quaternionInterpolant) { + const qv = quaternionInterpolant.evaluate(f * animation.duration); + bone.quaternion.fromArray(qv); + } else { + bone.quaternion.copy(bone.initialQuaternion); + } } + rootBone.updateMatrixWorld(true); + + const fbxScale = 100; + const leftHand = new THREE.Vector3().setFromMatrixPosition(leftHandBone.matrixWorld).divideScalar(fbxScale); + const rightHand = new THREE.Vector3().setFromMatrixPosition(rightHandBone.matrixWorld).divideScalar(fbxScale); + + const leftHandDelta = i === 0 ? 0 : lastLeftHandPos.distanceTo(leftHand); + const rightHandDelta = i === 0 ? 0 : lastRightHandPos.distanceTo(rightHand); + + leftHandDeltas[i] = leftHandDelta * 1000; + rightHandDeltas[i] = rightHandDelta * 1000; + + maxRightDeltaIndex = rightHandDeltas[i] > rightHandDeltas[maxRightDeltaIndex] ? i : maxRightDeltaIndex; + maxLeftDeltaIndex = leftHandDeltas[i] > leftHandDeltas[maxRightDeltaIndex] ? i : maxLeftDeltaIndex; + + lastLeftHandPos.set(leftHand.x, leftHand.y, leftHand.z); + lastRightHandPos.set(rightHand.x, rightHand.y, rightHand.z); } - rootBone.updateMatrixWorld(true); - const fbxScale = 100; - const leftHand = new THREE.Vector3().setFromMatrixPosition(leftHandBone.matrixWorld).divideScalar(fbxScale); - const rightHand = new THREE.Vector3().setFromMatrixPosition(rightHandBone.matrixWorld).divideScalar(fbxScale); - - const leftHandDelta = i === 0 ? 0 : lastLeftHandPos.distanceTo(leftHand); - const rightHandDelta = i === 0 ? 0 : lastRightHandPos.distanceTo(rightHand); - - leftHandDeltas[i] = leftHandDelta * 1000; - rightHandDeltas[i] = rightHandDelta * 1000; - - maxRightDeltaIndex = rightHandDeltas[i] > rightHandDeltas[maxRightDeltaIndex] ? i : maxRightDeltaIndex; - maxLeftDeltaIndex = leftHandDeltas[i] > leftHandDeltas[maxRightDeltaIndex] ? i : maxLeftDeltaIndex; - - - lastLeftHandPos.set(leftHand.x, leftHand.y, leftHand.z); - lastRightHandPos.set(rightHand.x, rightHand.y, rightHand.z); - } - + }; + bake(); // console.log('got', leftHandDeltas, rightHandDeltas, maxLeftDeltaIndex, maxRightDeltaIndex); diff --git a/avatars/constants.js b/avatars/constants.js index f90b237474..9c89eb40e3 100644 --- a/avatars/constants.js +++ b/avatars/constants.js @@ -1,4 +1,6 @@ export const idleFactorSpeed = 0; export const walkFactorSpeed = 0.25; export const runFactorSpeed = 0.7; -export const narutoRunTimeFactor = 2; \ No newline at end of file +export const narutoRunTimeFactor = 2; +export const eatFrameIndices = [500, 800, 1100]; +export const drinkFrameIndices = [400, 700, 1000]; \ No newline at end of file diff --git a/character-controller.js b/character-controller.js index 2f1b7b0048..88afedbf15 100644 --- a/character-controller.js +++ b/character-controller.js @@ -256,6 +256,7 @@ class PlayerBase extends THREE.Object3D { indexUrl, }); this.updateVoicer(); + this.dispatchEvent({type: 'voicepackloaded'}); } setVoiceEndpoint(voiceId) { if (!voiceId) throw new Error('voice Id is null') diff --git a/character-sfx.js b/character-sfx.js index 20301bfc6a..4b4fefa8d9 100644 --- a/character-sfx.js +++ b/character-sfx.js @@ -8,11 +8,11 @@ import { walkFactorSpeed, runFactorSpeed, narutoRunTimeFactor, + eatFrameIndices, + drinkFrameIndices, } from './avatars/constants.js'; import { crouchMaxTime, - eatFrameIndices, - drinkFrameIndices, } from './constants.js'; import { mod, @@ -72,331 +72,453 @@ const _getActionFrameIndex = (f, frameTimes) => { return i; }; -class CharacterSfx extends EventTarget{ +class Sfx extends EventTarget { constructor(player) { super(); this.player = player; + this.oldGrunt = null; + this.voiceFiles = null; + this.soundFiles = sounds.getSoundFiles(); + this.player.addEventListener('voicepackloaded', this.setVoiceFiles.bind(this)); + } + cleanup() { + this.player.removeEventListener('voicepackloaded', this.setVoiceFiles.bind(this)); + } + setVoiceFiles() { + const actionVoices = this.player.voicePack.actionVoices; + const emoteVoices = this.player.voicePack.emoteVoices; + this.voiceFiles = { + actionVoices: { + hurt: actionVoices.filter(f => /hurt/i.test(f.name)), + scream: actionVoices.filter(f => /scream/i.test(f.name)), + attack: actionVoices.filter(f => /attack/i.test(f.name)), + angry: actionVoices.filter(f => /angry/i.test(f.name)), + gasp: actionVoices.filter(f => /gasp/i.test(f.name)), + jump: actionVoices.filter(f => /jump/i.test(f.name)), + narutoRun: actionVoices.filter(f => /nr/i.test(f.name)) + }, + emoteVoices: { + alertSoft: emoteVoices.filter(f => /alert/i.test(f.name)), + alert: emoteVoices.filter(f => /alert/i.test(f.name)), + angrySoft: emoteVoices.filter(f => /angry/i.test(f.name)), + angry: emoteVoices.filter(f => /angry/i.test(f.name)), + embarrassedSoft: emoteVoices.filter(f => /emba/i.test(f.name)), + embarrassed: emoteVoices.filter(f => /emba/i.test(f.name)), + headNodSoft: emoteVoices.filter(f => /nod/i.test(f.name)), + headNod: emoteVoices.filter(f => /nod/i.test(f.name)), + headShakeSoft: emoteVoices.filter(f => /shake/i.test(f.name)), + headShake: emoteVoices.filter(f => /shake/i.test(f.name)), + sadSoft: emoteVoices.filter(f => /sad/i.test(f.name)), + sad: emoteVoices.filter(f => /sad/i.test(f.name)), + surpriseSoft: emoteVoices.filter(f => /surprise/i.test(f.name)), + surprise: emoteVoices.filter(f => /surprise/i.test(f.name)), + victorySoft: emoteVoices.filter(f => /victory/i.test(f.name)), + victory: emoteVoices.filter(f => /victory/i.test(f.name)) + } + }; + } + playGrunt(type, index) { + if (this.voiceFiles) { + const voiceFiles = this.voiceFiles.actionVoices[type]; + let offset, duration; + + if (index === undefined) { + let voice = selectVoice(voiceFiles); + duration = voice.duration; + offset = voice.offset; + } + else { + duration = voiceFiles[index].duration; + offset = voiceFiles[index].offset; + } + + const audioContext = audioManager.getAudioContext(); + const audioBufferSourceNode = audioContext.createBufferSource(); + audioBufferSourceNode.buffer = this.player.voicePack.audioBuffer; - this.lastJumpState = false; - this.lastStepped = [false, false]; - this.lastWalkTime = 0; + // control mouth movement with audio volume + if (!this.player.avatar.isAudioEnabled()) { + this.player.avatar.setAudioEnabled(true); + } + audioBufferSourceNode.connect(this.player.avatar.getAudioInput()); - - - this.lastSwordComboName = null; - this.swordComboStartTime = 0; - - this.lastEatFrameIndex = -1; - this.lastDrinkFrameIndex = -1; + // if the oldGrunt are still playing + if (this.oldGrunt) { + this.oldGrunt.stop(); + this.oldGrunt = null; + } - this.narutoRunStartTime = 0; - this.narutoRunFinishTime = 0; - this.narutoRunTrailSoundStartTime = 0; - this.narutoRunTurnSoundStartTime = 0; - this.currentQ = new THREE.Quaternion(); - this.preQ = new THREE.Quaternion(); - this.arr = [0, 0, 0, 0]; + this.oldGrunt=audioBufferSourceNode; + // clean the oldGrunt if voice end + audioBufferSourceNode.addEventListener('ended', () => { + if (this.oldGrunt === audioBufferSourceNode) { + this.oldGrunt = null; + } + }); - this.startRunningTime = 0; - this.willGasp = false; + audioBufferSourceNode.start(0, offset, duration); + } + } + playEmote(type, index) { + if (this.voiceFiles) { // ensure voice pack loaded + const voiceFiles = this.voiceFiles.emoteVoices[type]; + let offset, duration; + + if (index === undefined) { + let voice = selectVoice(voiceFiles); + duration = voice.duration; + offset = voice.offset; + } + else { + duration = voiceFiles[index].duration; + offset = voiceFiles[index].offset; + } + + const audioContext = audioManager.getAudioContext(); + const audioBufferSourceNode = audioContext.createBufferSource(); + audioBufferSourceNode.buffer = this.player.voicePack.audioBuffer; - this.oldNarutoRunSound = null; - this.lastEmote = null; + // control mouth movement with audio volume + if (!this.player.avatar.isAudioEnabled()) { + this.player.avatar.setAudioEnabled(true); + } + audioBufferSourceNode.connect(this.player.avatar.getAudioInput()); - this.currentStep = null; - this.currentSwimmingHand = null; - this.setSwimmingHand = true; + // if the oldGrunt are still playing + if (this.oldGrunt) { + this.oldGrunt.stop(); + this.oldGrunt = null; + } - this.lastLandState = false; + this.oldGrunt=audioBufferSourceNode; + // clean the oldGrunt if voice end + audioBufferSourceNode.addEventListener('ended', () => { + if (this.oldGrunt === audioBufferSourceNode) { + this.oldGrunt = null; + } + }); + + audioBufferSourceNode.start(0, offset, duration); + } + } +} +class JumpSfx extends Sfx { + constructor(player) { + super(player); + this.lastLandState = false; + this.lastJumpState = false; } update(timestamp, timeDiffS) { - if (!this.player.avatar) { - return; + if (this.player.avatar.jumpState && !this.lastJumpState) { + sounds.playSoundName('jump'); + + const jumpAction = this.player.getAction('jump'); + if(jumpAction?.trigger === 'jump') { + this.playGrunt('jump'); + } } - + if(this.player.avatar.landState && !this.lastLandState){ + sounds.playSoundName('land'); + } + this.lastLandState = this.player.avatar.landState; + this.lastJumpState = this.player.avatar.jumpState; + } +} + +class StepSfx extends Sfx { + constructor(player) { + super(player); + this.lastWalkTime = 0; + this.lastStepped = [false, false]; + this.narutoRunFinishTime = 0; + this.currentStep = null; + } + update(timestamp, timeDiffS) { const timeSeconds = timestamp/1000; const currentSpeed = localVector.set(this.player.avatar.velocity.x, 0, this.player.avatar.velocity.z).length(); - const idleWalkFactor = Math.min(Math.max((currentSpeed - idleFactorSpeed) / (walkFactorSpeed - idleFactorSpeed), 0), 1); const walkRunFactor = Math.min(Math.max((currentSpeed - walkFactorSpeed) / (runFactorSpeed - walkFactorSpeed), 0), 1); const crouchFactor = Math.min(Math.max(1 - (this.player.avatar.crouchTime / crouchMaxTime), 0), 1); - const soundFiles = sounds.getSoundFiles(); - // const soundFileAudioBuffer = sounds.getSoundFileAudioBuffer(); - - // jump - const _handleJump = () => { - if (this.player.avatar.jumpState && !this.lastJumpState) { - sounds.playSoundName('jump'); - - // play jump grunt - if(this.player.hasAction('jump') && this.player.getAction('jump').trigger === 'jump'){ - this.playGrunt('jump'); - } - } /*else if (this.lastJumpState && !this.player.avatar.jumpState) { - sounds.playSoundName('land'); - }*/ - if(this.player.avatar.landState && !this.lastLandState){ - sounds.playSoundName('land'); - } - this.lastLandState = this.player.avatar.landState; - this.lastJumpState = this.player.avatar.jumpState; - }; - _handleJump(); - - // step - const _handleStep = () => { - if (idleWalkFactor > 0.7 && !this.player.avatar.jumpState && !this.player.avatar.fallLoopState && !this.player.avatar.flyState && !this.player.hasAction('swim')) { - const isRunning = walkRunFactor > 0.5; - const isCrouching = crouchFactor > 0.5; - const isNarutoRun = this.player.avatar.narutoRunState; - const walkRunAnimationName = (() => { - if (isNarutoRun) { - return 'naruto run.fbx'; - } else { - const animationAngles = isCrouching ? - Avatar.getClosest2AnimationAngles('crouch', this.player.avatar.getAngle()) - : - (isRunning ? - Avatar.getClosest2AnimationAngles('run', this.player.avatar.getAngle()) - : - Avatar.getClosest2AnimationAngles('walk', this.player.avatar.getAngle()) + if ( + idleWalkFactor > 0.7 && + !this.player.avatar.jumpState && + !this.player.avatar.fallLoopState && + !this.player.avatar.flyState && + !this.player.hasAction('swim') && + !this.player.hasAction('sit') + ) { + const isRunning = walkRunFactor > 0.5; + const isCrouching = crouchFactor > 0.5; + const isNarutoRun = this.player.avatar.narutoRunState; + const walkRunAnimationName = (() => { + if (isNarutoRun) { + return 'naruto run.fbx'; + } else { + const animationAngles = isCrouching + ? Avatar.getClosest2AnimationAngles( + 'crouch', + this.player.avatar.getAngle() + ) + : isRunning + ? Avatar.getClosest2AnimationAngles( + 'run', + this.player.avatar.getAngle() + ) + : Avatar.getClosest2AnimationAngles( + 'walk', + this.player.avatar.getAngle() ); - return animationAngles[0].name; - } - })(); - const localSoundFiles = (() => { - if (isNarutoRun) { - return soundFiles.narutoRun; - } else if (isCrouching) { - return soundFiles.walk; - } else if (isRunning) { - return soundFiles.run; - } else { - return soundFiles.walk; - } - })(); - const animations = Avatar.getAnimations(); - const animation = animations.find(a => a.name === walkRunAnimationName); - const animationStepIndices = Avatar.getAnimationStepIndices(); - const animationIndices = animationStepIndices.find(i => i.name === walkRunAnimationName); - const {leftStepIndices, rightStepIndices} = animationIndices; - - const offset = offsets[walkRunAnimationName] ?? 0; // ?? window.lol; - const _getStepIndex = timeSeconds => { - const timeMultiplier = walkRunAnimationName === 'naruto run.fbx' ? narutoRunTimeFactor : 1; - const walkTime = (timeSeconds * timeMultiplier + offset) % animation.duration; - const walkFactor = walkTime / animation.duration; - const stepIndex = Math.floor(mod(walkFactor, 1) * leftStepIndices.length); - return stepIndex; - }; - - const startIndex = _getStepIndex(this.lastWalkTime); - const endIndex = _getStepIndex(timeSeconds); - for (let i = startIndex;; i++) { - i = i % leftStepIndices.length; - if (i !== endIndex) { - if (leftStepIndices[i] && !this.lastStepped[0] && !this.player.avatar.narutoRunState && timeSeconds-this.narutoRunFinishTime>0.5) { - const candidateAudios = localSoundFiles//.filter(a => a.paused); - if (candidateAudios.length > 0) { - /* for (const a of candidateAudios) { - !a.paused && a.pause(); - } */ - this.currentStep = 'left'; - const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; - sounds.playSound(audioSpec); - } + return animationAngles[0].name; + } + })(); + const localSoundFiles = (() => { + if (isNarutoRun) { + return this.soundFiles.narutoRun; + } else if (isCrouching) { + return this.soundFiles.walk; + } else if (isRunning) { + return this.soundFiles.run; + } else { + return this.soundFiles.walk; + } + })(); + const animations = Avatar.getAnimations(); + const animation = animations.find((a) => a.name === walkRunAnimationName); + const animationStepIndices = Avatar.getAnimationStepIndices(); + const animationIndices = animationStepIndices.find( + (i) => i.name === walkRunAnimationName + ); + const { leftStepIndices, rightStepIndices } = animationIndices; + + const offset = offsets[walkRunAnimationName] ?? 0; // ?? window.lol; // check + const _getStepIndex = (timeSeconds) => { + const timeMultiplier = + walkRunAnimationName === 'naruto run.fbx' ? narutoRunTimeFactor : 1; + const walkTime = + (timeSeconds * timeMultiplier + offset) % animation.duration; + const walkFactor = walkTime / animation.duration; + const stepIndex = Math.floor( + mod(walkFactor, 1) * leftStepIndices.length + ); + return stepIndex; + }; + + const startIndex = _getStepIndex(this.lastWalkTime); + const endIndex = _getStepIndex(timeSeconds); + for (let i = startIndex; ; i++) { + i = i % leftStepIndices.length; + if (i !== endIndex) { + if ( + leftStepIndices[i] && + !this.lastStepped[0] && + !this.player.avatar.narutoRunState && + timeSeconds - this.narutoRunFinishTime > 0.5 + ) { + const candidateAudios = localSoundFiles; //.filter(a => a.paused); + if (candidateAudios.length > 0) { + this.currentStep = 'left'; + const audioSpec = + candidateAudios[ + Math.floor(Math.random() * candidateAudios.length) + ]; + sounds.playSound(audioSpec); } - this.lastStepped[0] = leftStepIndices[i]; - - if (rightStepIndices[i] && !this.lastStepped[1] && !this.player.avatar.narutoRunState && timeSeconds-this.narutoRunFinishTime>0.5) { - const candidateAudios = localSoundFiles// .filter(a => a.paused); - if (candidateAudios.length > 0) { - /* for (const a of candidateAudios) { - !a.paused && a.pause(); - } */ - this.currentStep = 'right'; - const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; - sounds.playSound(audioSpec); - } + } + this.lastStepped[0] = leftStepIndices[i]; + + if ( + rightStepIndices[i] && + !this.lastStepped[1] && + !this.player.avatar.narutoRunState && + timeSeconds - this.narutoRunFinishTime > 0.5 + ) { + const candidateAudios = localSoundFiles; // .filter(a => a.paused); + if (candidateAudios.length > 0) { + this.currentStep = 'right'; + const audioSpec = + candidateAudios[ + Math.floor(Math.random() * candidateAudios.length) + ]; + sounds.playSound(audioSpec); } - this.lastStepped[1] = rightStepIndices[i]; - } else { - break; } + this.lastStepped[1] = rightStepIndices[i]; + } else { + break; } - - this.lastWalkTime = timeSeconds; } - - }; - - if (!this.player.hasAction('sit')) { - _handleStep(); + this.lastWalkTime = timeSeconds; } + } +} - const _handleSwim = () => { - if(this.player.hasAction('swim')){ - // const candidateAudios = soundFiles.water; - // console.log(candidateAudios); - if(this.player.getAction('swim').animationType === 'breaststroke'){ - if(this.setSwimmingHand && this.player.actionInterpolants.movements.get() % breaststrokeDuration <= breaststrokeOffset){ - this.setSwimmingHand = false; - this.currentSwimmingHand = null; - } - else if(!this.setSwimmingHand && this.player.actionInterpolants.movements.get() % breaststrokeDuration > breaststrokeOffset){ - let regex = new RegExp('^water/swim[0-9]*.wav$'); - const candidateAudios = soundFiles.water.filter(f => regex.test(f.name)); - const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; - if(this.player.getAction('swim').onSurface) - sounds.playSound(audioSpec); - - this.setSwimmingHand = true; - this.currentSwimmingHand = 'right'; - } - - } - else if(this.player.getAction('swim').animationType === 'freestyle'){ - let regex = new RegExp('^water/swim_fast[0-9]*.wav$'); - const candidateAudios = soundFiles.water.filter(f => regex.test(f.name)); - const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; - - if(this.setSwimmingHand && this.player.actionInterpolants.movements.get() % freestyleDuration <= freestyleOffset){ - // console.log('left hand') - if(this.player.getAction('swim').onSurface) - sounds.playSound(audioSpec); - this.currentSwimmingHand = 'left'; - this.setSwimmingHand = false; - } - else if(!this.setSwimmingHand && this.player.actionInterpolants.movements.get() % freestyleDuration > freestyleOffset){ - // console.log('right hand') - if(this.player.getAction('swim').onSurface) - sounds.playSound(audioSpec); - this.currentSwimmingHand = 'right'; - this.setSwimmingHand = true; - } - } +class SwimSfx extends Sfx { + constructor(player) { + super(player); + this.currentSwimmingHand = null; + this.setSwimmingHand = true; + } + update(timestamp, timeDiffS) { + const swimAction = this.player.getAction('swim'); + if(swimAction?.animationType === 'breaststroke') { + if(this.setSwimmingHand && this.player.actionInterpolants.movements.get() % breaststrokeDuration <= breaststrokeOffset){ // check + this.setSwimmingHand = false; + this.currentSwimmingHand = null; + } + else if(!this.setSwimmingHand && this.player.actionInterpolants.movements.get() % breaststrokeDuration > breaststrokeOffset){ + let regex = new RegExp('^water/swim[0-9]*.wav$'); + const candidateAudios = this.soundFiles.water.filter(f => regex.test(f.name)); + const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; + if(swimAction.onSurface) + sounds.playSound(audioSpec); + + this.setSwimmingHand = true; + this.currentSwimmingHand = 'right'; + } + } else if(swimAction?.animationType === 'freestyle') { + let regex = new RegExp('^water/swim_fast[0-9]*.wav$'); + const candidateAudios = this.soundFiles.water.filter(f => regex.test(f.name)); + const audioSpec = candidateAudios[Math.floor(Math.random() * candidateAudios.length)]; + + if(this.setSwimmingHand && this.player.actionInterpolants.movements.get() % freestyleDuration <= freestyleOffset){ + if(swimAction.onSurface) sounds.playSound(audioSpec); + this.currentSwimmingHand = 'left'; + this.setSwimmingHand = false; + } + else if(!this.setSwimmingHand && this.player.actionInterpolants.movements.get() % freestyleDuration > freestyleOffset){ + if(swimAction.onSurface) sounds.playSound(audioSpec); + this.currentSwimmingHand = 'right'; + this.setSwimmingHand = true; } } + } +} - _handleSwim(); - - - const _handleNarutoRun = () => { - - this.currentQ.copy(this.player.quaternion); - - let temp=this.currentQ.angleTo(this.preQ); - for(let i=0;i<4;i++){ - let temp2=this.arr[i]; - this.arr[i]=temp; - temp=temp2; +class NarutoRunSfx extends Sfx { + constructor(player) { + super(player); + this.narutoRunStartTime = 0; + this.narutoRunFinishTime = 0; + this.narutoRunTrailSoundStartTime = 0; + this.narutoRunTurnSoundStartTime = 0; + this.oldNarutoRunSound = null; + this.currentQ = new THREE.Quaternion(); + this.preQ = new THREE.Quaternion(); + this.arr = [0, 0, 0, 0]; + this.willGasp = false; + } + update(timestamp, timeDiffS) { + const timeSeconds = timestamp/1000; + this.currentQ.copy(this.player.quaternion); + let temp = this.currentQ.angleTo(this.preQ); + for(let i = 0; i < 4; i++) { + let temp2 = this.arr[i]; + this.arr[i] = temp; + temp = temp2; + } + if(this.player.avatar.narutoRunState) { + if(this.narutoRunStartTime===0) { + this.narutoRunStartTime=timeSeconds; + sounds.playSound(this.soundFiles.sonicBoom[0]); + this.playGrunt('narutoRun'); // check shared } - - - - if(this.player.avatar.narutoRunState){ - if(this.narutoRunStartTime===0){ - this.narutoRunStartTime=timeSeconds; - sounds.playSound(soundFiles.sonicBoom[0]); - this.playGrunt('narutoRun'); - } - else { - if(this.arr.reduce((a,b)=>a+b) >= Math.PI/3){ - - this.arr.fill(0) - if(timeSeconds - this.narutoRunTurnSoundStartTime>soundFiles.sonicBoom[3].duration-0.9 || this.narutoRunTurnSoundStartTime==0){ - sounds.playSound(soundFiles.sonicBoom[3]); - this.narutoRunTurnSoundStartTime = timeSeconds; - } - - } - - if(timeSeconds - this.narutoRunTrailSoundStartTime>soundFiles.sonicBoom[2].duration-0.2 || this.narutoRunTrailSoundStartTime==0){ - - const localSound = sounds.playSound(soundFiles.sonicBoom[2]); - this.oldNarutoRunSound = localSound; - localSound.addEventListener('ended', () => { - if (this.oldNarutoRunSound === localSound) { - this.oldNarutoRunSound = null; - } - }); - - this.narutoRunTrailSoundStartTime = timeSeconds; + else { + if(this.arr.reduce((a, b) => a + b) >= Math.PI / 3) { + this.arr.fill(0) + if(timeSeconds - this.narutoRunTurnSoundStartTime > this.soundFiles.sonicBoom[3].duration - 0.9 || this.narutoRunTurnSoundStartTime == 0) { + sounds.playSound(this.soundFiles.sonicBoom[3]); + this.narutoRunTurnSoundStartTime = timeSeconds; } } - - // if naruto run play more than 2 sec, set willGasp - if(timeSeconds - this.narutoRunStartTime > 2){ - this.willGasp = true; + if(timeSeconds - this.narutoRunTrailSoundStartTime > this.soundFiles.sonicBoom[2].duration - 0.2 || this.narutoRunTrailSoundStartTime==0) { + const localSound = sounds.playSound(this.soundFiles.sonicBoom[2]); + this.oldNarutoRunSound = localSound; + localSound.addEventListener('ended', () => { + if (this.oldNarutoRunSound === localSound) { + this.oldNarutoRunSound = null; + } + }); + this.narutoRunTrailSoundStartTime = timeSeconds; } - } - if(!this.player.avatar.narutoRunState && this.narutoRunStartTime!=0 ){ - this.narutoRunStartTime=0; - this.narutoRunFinishTime=timeSeconds; - this.narutoRunTrailSoundStartTime=0; - this.narutoRunTurnSoundStartTime=0; - sounds.playSound(soundFiles.sonicBoom[1]); - if (this.oldNarutoRunSound) { - !this.oldNarutoRunSound.paused && this.oldNarutoRunSound.stop(); - this.oldNarutoRunSound = null; - } + // if naruto run play more than 2 sec, set willGasp + if(timeSeconds - this.narutoRunStartTime > 2){ + this.willGasp = true; } - this.preQ.x=this.currentQ.x; - this.preQ.y=this.currentQ.y; - this.preQ.z=this.currentQ.z; - this.preQ.w=this.currentQ.w; - - }; - _handleNarutoRun(); - - // combo - const dispatchComboSoundEvent = (soundIndex) =>{ - this.dispatchEvent(new MessageEvent('meleewhoosh', { - data: { - index: soundIndex - }, - })); } - const _handleCombo = () => { - if (this.player.hasAction('use') && this.player.getAction('use').behavior === 'sword') { - const comboAnimationName = this.player.getAction('use').animationCombo ? - aimAnimations[this.player.getAction('use').animationCombo[this.player.avatar.useAnimationIndex]] - : null; - if (comboAnimationName) { - if (comboAnimationName !== this.lastSwordComboName) { - this.swordComboStartTime = timeSeconds; - this.alreadyPlayComboSound = false; - } - const animations = Avatar.getAnimations(); - const animation = animations.find(a => a.name === comboAnimationName); - const animationComboIndices = Avatar.getanimationComboIndices(); - const animationIndices = animationComboIndices.find(i => i.name === comboAnimationName); - const handDeltas = this.player.getAction('use').boneAttachment === 'leftHand' ? animationIndices.rightHandDeltas : animationIndices.leftHandDeltas; - const maxDeltaIndex = this.player.getAction('use').boneAttachment === 'leftHand' ? animationIndices.maxRightDeltaIndex : animationIndices.maxLeftDeltaIndex; - - const ratio = (timeSeconds - this.swordComboStartTime) / animation.duration; - if (ratio <= 1 && !this.alreadyPlayComboSound) { - const index = Math.floor(ratio * handDeltas.length); - if (index > maxDeltaIndex) { - this.alreadyPlayComboSound = true; - this.playGrunt('attack'); - const soundIndex = this.player.avatar.useAnimationIndex; - dispatchComboSoundEvent(soundIndex); - } + if(!this.player.avatar.narutoRunState && this.narutoRunStartTime!=0 ) { + this.narutoRunStartTime = 0; + this.narutoRunFinishTime = timeSeconds; + this.narutoRunTrailSoundStartTime = 0; + this.narutoRunTurnSoundStartTime = 0; + sounds.playSound(this.soundFiles.sonicBoom[1]); + if (this.oldNarutoRunSound) { + !this.oldNarutoRunSound.paused && this.oldNarutoRunSound.stop(); + this.oldNarutoRunSound = null; + } + } + this.preQ.x = this.currentQ.x; + this.preQ.y = this.currentQ.y; + this.preQ.z = this.currentQ.z; + this.preQ.w = this.currentQ.w; // check q.copy? + } +} + +class ComboSfx extends Sfx { + constructor(player) { + super(player); + this.lastSwordComboName = null; + this.swordComboStartTime = 0; + this.alreadyPlayComboSound = false; + } + update(timestamp, timeDiffS) { + const timeSeconds = timestamp/1000; + const useAction = this.player.getAction('use'); + if (useAction?.behavior === 'sword' && useAction?.animationCombo) { + const comboAnimationName = aimAnimations[useAction.animationCombo[useAction.index]]; + if (comboAnimationName) { + if (comboAnimationName !== this.lastSwordComboName) { + this.swordComboStartTime = timeSeconds; + this.alreadyPlayComboSound = false; + } + const animations = Avatar.getAnimations(); + const animation = animations.find(a => a.name === comboAnimationName); + const animationComboIndices = Avatar.getanimationComboIndices(); + const animationIndices = animationComboIndices.find(i => i.name === comboAnimationName); + const handDeltas = useAction.boneAttachment === 'leftHand' ? animationIndices.rightHandDeltas : animationIndices.leftHandDeltas; + const maxDeltaIndex = useAction.boneAttachment === 'leftHand' ? animationIndices.maxRightDeltaIndex : animationIndices.maxLeftDeltaIndex; + + const ratio = (timeSeconds - this.swordComboStartTime) / animation.duration; + if (ratio <= 1 && !this.alreadyPlayComboSound) { + const index = Math.floor(ratio * handDeltas.length); + if (index > maxDeltaIndex) { + this.alreadyPlayComboSound = true; + this.playGrunt('attack'); + const index = useAction.index; + const soundRegex = new RegExp(`^combat/sword_slash${index}-[0-9]*.wav$`); + const soundCandidate = this.soundFiles.combat.filter(f => soundRegex.test(f.name)); + const audioSpec = soundCandidate[Math.floor(Math.random() * soundCandidate.length)]; + sounds.playSound(audioSpec); } } - this.lastSwordComboName = comboAnimationName; } - - }; + this.lastSwordComboName = comboAnimationName; + } + } +} - _handleCombo(); - - const _handleGasp = () =>{ - const isRunning = currentSpeed > 0.5; +class GaspSfx extends Sfx { + constructor(player) { + super(player); + this.startRunningTime = 0; + this.willGasp = false; + } + update(timestamp, timeDiffS) { + const timeSeconds = timestamp/1000; + const currentSpeed = localVector.set(this.player.avatar.velocity.x, 0, this.player.avatar.velocity.z).length(); + const isRunning = currentSpeed > 0.5; if(isRunning){ if(this.startRunningTime === 0) this.startRunningTime = timeSeconds; @@ -412,44 +534,36 @@ class CharacterSfx extends EventTarget{ if(timeSeconds - this.startRunningTime > 5 && this.startRunningTime !== 0){ this.willGasp = true; } - - - } - _handleGasp(); + } +} - const _handleFood = () => { - const useAction = this.player.getAction('use'); +class FoodSfx extends Sfx { + constructor(player) { + super(player); + this.lastEatFrameIndex = -1; + this.lastDrinkFrameIndex = -1; + } + update(timestamp, timeDiffS) { + const useAction = this.player.getAction('use'); if (useAction) { const _handleEat = () => { const v = this.player.actionInterpolants.use.get(); const eatFrameIndex = _getActionFrameIndex(v, eatFrameIndices); - - // console.log('chomp', v, eatFrameIndex, this.lastEatFrameIndex); if (eatFrameIndex !== 0 && eatFrameIndex !== this.lastEatFrameIndex) { sounds.playSoundName('chomp'); - // control mouth movement - this.player.characterBehavior.setMouthMoving(0.04,0.04,0.1,0.02); + this.player.characterBehavior.setMouthMoving(0.04, 0.04, 0.1, 0.02); } - this.lastEatFrameIndex = eatFrameIndex; }; const _handleDrink = () => { - // console.log('drink action', useAction); - const v = this.player.actionInterpolants.use.get(); const drinkFrameIndex = _getActionFrameIndex(v, drinkFrameIndices); - - // console.log('gulp', v, drinkFrameIndex, this.lastDrinkFrameIndex); if (drinkFrameIndex !== 0 && drinkFrameIndex !== this.lastDrinkFrameIndex) { sounds.playSoundName('gulp'); - // control mouth movement this.player.characterBehavior.setMouthMoving(0.1,0.1,0.1,0.1); } - this.lastDrinkFrameIndex = drinkFrameIndex; }; - - // console.log('got use action', useAction); switch (useAction.behavior) { case 'eat': { _handleEat(); @@ -464,176 +578,49 @@ class CharacterSfx extends EventTarget{ } } } - }; - _handleFood(); - - // emote - const _handleEmote = () => { - if(this.player.avatar.emoteAnimation && this.lastEmote !== this.player.avatar.emoteAnimation){ - this.playEmote(this.player.avatar.emoteAnimation); - } - this.lastEmote = this.player.avatar.emoteAnimation; - }; - _handleEmote(); } - playGrunt(type, index){ - if (this.player.voicePack) { // ensure voice pack loaded - let voiceFiles, offset, duration; - switch (type) { - case 'hurt': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /hurt/i.test(f.name)); - break; - } - case 'scream': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /scream/i.test(f.name)); - break; - } - case 'attack': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /attack/i.test(f.name)); - break; - } - case 'angry': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /angry/i.test(f.name)); - break; - } - case 'gasp': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /gasp/i.test(f.name)); - break; - } - case 'jump': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /jump/i.test(f.name)); - break; - } - case 'narutoRun': { - voiceFiles = this.player.voicePack.actionVoices.filter(f => /nr/i.test(f.name)); - break; - } - } - - if (index === undefined) { - let voice = selectVoice(voiceFiles); - duration = voice.duration; - offset = voice.offset; - } - else { - duration = voiceFiles[index].duration; - offset = voiceFiles[index].offset; - } - - const audioContext = audioManager.getAudioContext(); - const audioBufferSourceNode = audioContext.createBufferSource(); - audioBufferSourceNode.buffer = this.player.voicePack.audioBuffer; - - // control mouth movement with audio volume - if (!this.player.avatar.isAudioEnabled()) { - this.player.avatar.setAudioEnabled(true); - } - audioBufferSourceNode.connect(this.player.avatar.getAudioInput()); - - // if the oldGrunt are still playing - if (this.oldGrunt) { - this.oldGrunt.stop(); - this.oldGrunt = null; - } - - this.oldGrunt=audioBufferSourceNode; - // clean the oldGrunt if voice end - audioBufferSourceNode.addEventListener('ended', () => { - if (this.oldGrunt === audioBufferSourceNode) { - this.oldGrunt = null; - } - }); +} - audioBufferSourceNode.start(0, offset, duration); +class EmoteSfx extends Sfx { + constructor(player) { + super(player); + this.lastEmote = null; + } + update(timestamp, timeDiffS) { + if(this.player.avatar.emoteAnimation && this.lastEmote !== this.player.avatar.emoteAnimation){ + this.playEmote(this.player.avatar.emoteAnimation); } + this.lastEmote = this.player.avatar.emoteAnimation; } - playEmote(type, index){ - if (this.player.voicePack) { // ensure voice pack loaded - let voiceFiles, offset, duration; - switch (type) { - case 'alertSoft': - case 'alert': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /alert/i.test(f.name)); - break; - } - case 'angrySoft': - case 'angry': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /angry/i.test(f.name)); - break; - } - case 'embarrassedSoft': - case 'embarrassed': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /emba/i.test(f.name)); - break; - } - case 'headNodSoft': - case 'headNod': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /nod/i.test(f.name)); - break; - } - case 'headShakeSoft': - case 'headShake': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /shake/i.test(f.name)); - break; - } - case 'sadSoft': - case 'sad': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /sad/i.test(f.name)); - break; - } - case 'surpriseSoft': - case 'surprise': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /surprise/i.test(f.name)); - break; - } - case 'victorySoft': - case 'victory': { - voiceFiles = this.player.voicePack.emoteVoices.filter(f => /victory/i.test(f.name)); - break; - } - default: { - voiceFiles = this.player.voicePack.emoteVoices; - break; - } - } - - if (index === undefined) { - let voice = selectVoice(voiceFiles); - duration = voice.duration; - offset = voice.offset; - } - else { - duration = voiceFiles[index].duration; - offset = voiceFiles[index].offset; - } - - const audioContext = audioManager.getAudioContext(); - const audioBufferSourceNode = audioContext.createBufferSource(); - audioBufferSourceNode.buffer = this.player.voicePack.audioBuffer; - - // control mouth movement with audio volume - if (!this.player.avatar.isAudioEnabled()) { - this.player.avatar.setAudioEnabled(true); - } - audioBufferSourceNode.connect(this.player.avatar.getAudioInput()); - - // if the oldGrunt are still playing - if (this.oldGrunt) { - this.oldGrunt.stop(); - this.oldGrunt = null; - } - - this.oldGrunt=audioBufferSourceNode; - // clean the oldGrunt if voice end - audioBufferSourceNode.addEventListener('ended', () => { - if (this.oldGrunt === audioBufferSourceNode) { - this.oldGrunt = null; - } - }); +} - audioBufferSourceNode.start(0, offset, duration); +class CharacterSfx extends Sfx { + constructor(player) { + super(player); + + this.jumpSfx = new JumpSfx(this.player); + this.stepSfx = new StepSfx(this.player); + this.swimSfx = new SwimSfx(this.player); + this.narutoRunSfx = new NarutoRunSfx(this.player); + this.comboSfx = new ComboSfx(this.player); + this.gaspSfx = new GaspSfx(this.player); + this.foodSfx = new FoodSfx(this.player); + this.emoteSfx = new EmoteSfx(this.player); + } + update(timestamp, timeDiffS) { + if (!this.player.avatar) { + return; } + this.jumpSfx.update(timestamp, timeDiffS); + this.stepSfx.update(timestamp, timeDiffS); + this.swimSfx.update(timestamp, timeDiffS); + this.narutoRunSfx.update(timestamp, timeDiffS); + this.comboSfx.update(timestamp, timeDiffS); + this.gaspSfx.update(timestamp, timeDiffS); + this.foodSfx.update(timestamp, timeDiffS); + this.emoteSfx.update(timestamp, timeDiffS); } + destroy() { this.cleanup && this.cleanup(); } diff --git a/constants.js b/constants.js index 6a2e0c86b7..9ebb82d9e9 100644 --- a/constants.js +++ b/constants.js @@ -133,9 +133,6 @@ export const avatarInterpolationFrameRate = 60; export const avatarInterpolationTimeDelay = 1000/(avatarInterpolationFrameRate * 0.5); export const avatarInterpolationNumFrames = 4; -export const eatFrameIndices = [500, 800, 1100]; -export const drinkFrameIndices = [400, 700, 1000]; - export const defaultMaxId = 8192; export const defaultMusicVolume = 0.35; diff --git a/metaverse-components.js b/metaverse-components.js index 4c5f054ad8..ebe17a985b 100644 --- a/metaverse-components.js +++ b/metaverse-components.js @@ -3,7 +3,7 @@ import wear from './metaverse_components/wear.js'; import pet from './metaverse_components/pet.js'; import drop from './metaverse_components/drop.js'; // import mob from './metaverse_components/mob.js'; -import use from './metaverse_components/use.js'; +// import use from './metaverse_components/use.js'; const componentTemplates = { wear, @@ -11,7 +11,7 @@ const componentTemplates = { pet, drop, // mob, - use, + // use, }; export { componentTemplates, diff --git a/metaverse_components/use.js b/metaverse_components/use.js deleted file mode 100644 index e0e46f39c7..0000000000 --- a/metaverse_components/use.js +++ /dev/null @@ -1,45 +0,0 @@ -import * as THREE from 'three'; -import metaversefile from 'metaversefile'; - - - -export default (app, component) => { - const {useUse} = metaversefile; - const sounds = metaversefile.useSound(); - const soundFiles = sounds.getSoundFiles(); - - let using = false; - let player = null; - - - - useUse((e) => { - using = e.use; - }); - const meleewhoosh = (e) =>{ - if(using){ - const index = e.data.index; - const soundRegex = new RegExp(`^combat/sword_slash${index}-[0-9]*.wav$`); - const soundCandidate = soundFiles.combat.filter(f => soundRegex.test(f.name)); - const audioSpec = soundCandidate[Math.floor(Math.random() * soundCandidate.length)]; - sounds.playSound(audioSpec); - } - } - const _unwear = () => { - if (component.behavior === 'sword') { - player.characterSfx.removeEventListener('meleewhoosh', meleewhoosh); - } - player = null; - }; - app.addEventListener('wearupdate', e => { - if (e.wear) { - player = e.player; - if (component.behavior === 'sword') { - player.characterSfx.addEventListener('meleewhoosh', meleewhoosh); - } - } else { - _unwear(); - } - }); - return app; -}; \ No newline at end of file