diff --git a/src/components/beat-loader.js b/src/components/beat-loader.js index 3abc7e6..70bad0b 100644 --- a/src/components/beat-loader.js +++ b/src/components/beat-loader.js @@ -10,16 +10,23 @@ AFRAME.registerComponent('beat-loader', { }, update: function () { - var challengeId = this.data.challengeId; + if (!this.data.challengeId || !this.data.difficulty) { return; } + this.loadBeats(this.data.challengeId, this.data.difficulty); + }, + + /** + * XHR. + */ + loadBeats: function (id, difficulty) { var el = this.el; var xhr; - if (!challengeId || !diffjculty) { return; } - // Load beats. + let url = utils.getS3FileUrl(this.data.challengeId, `${this.data.difficulty}.json`); xhr = new XMLHttpRequest(); el.emit('beatloaderstart'); - xhr.open('GET', utils.getS3FileUrl(challengeId, `${this.data.difficulty}.json`)); + console.log(`Fetching ${url}...`); + xhr.open('GET', url); xhr.addEventListener('load', () => { this.handleBeats(JSON.parse(xhr.responseText)); }); @@ -30,16 +37,7 @@ AFRAME.registerComponent('beat-loader', { * TODO: Load the beat data into the game. */ handleBeats: function (beatData) { - var el = this.el; - - history.pushState( - '', - challenge.songName, - updateQueryParam(window.location.href, 'challenge', this.data.challengeId) - ); - - document.title = `Super Saber - ${challenge.songName}`; - el.emit('beatloaderfinish'); + this.el.sceneEl.emit('beatloaderfinish', beatData, false); console.log('Finished loading challenge data.'); }, }); diff --git a/src/components/console-shortcuts.js b/src/components/console-shortcuts.js index eda2745..75f1ef5 100644 --- a/src/components/console-shortcuts.js +++ b/src/components/console-shortcuts.js @@ -1,6 +1,10 @@ AFRAME.registerComponent('console-shortcuts', { - play: function() { + play: function () { + window.$ = val => document.querySelector(val); + window.$$ = val => document.querySelectorAll(val); + window.$$$ = val => document.querySelector(`[${val}]`).getAttribute(val); + window.$$$$ = val => document.querySelector(`[${val}]`).components[val]; window.scene = this.el; window.state = this.el.systems.state.state; - }, + } }); diff --git a/src/components/discolight.js b/src/components/discolight.js deleted file mode 100644 index 4c3b355..0000000 --- a/src/components/discolight.js +++ /dev/null @@ -1,16 +0,0 @@ -AFRAME.registerComponent('discolight', { - schema: { - color: { type: 'color' }, - speed: { default: 1.0 }, - }, - init: function() { - this.color = new THREE.Color(this.data.color); - this.hsl = this.color.getHSL(); - }, - tick: function(time, delta) { - this.hsl.h += delta * 0.0001 * this.data.speed; - if (this.hsl.l > 1.0) this.hsl.l = 0.0; - this.color.setHSL(this.hsl.h, this.hsl.s, this.hsl.l); - this.el.setAttribute('light', { color: this.color.getHex() }); - }, -}); diff --git a/src/components/discotube.js b/src/components/discotube.js deleted file mode 100644 index 2acdd3a..0000000 --- a/src/components/discotube.js +++ /dev/null @@ -1,14 +0,0 @@ -AFRAME.registerComponent('discotube', { - schema: { - speedX: { default: 1.0 }, - speedY: { default: 0.1 }, - }, - init: function() { - this.material = this.el.object3D.children[0].material; - }, - tick: function(time, delta) { - if (this.material == null) return; - this.material.map.offset.x -= delta * 0.0001 * this.data.speedX; - this.material.map.offset.y -= delta * 0.0001 * this.data.speedY; - }, -}); diff --git a/src/components/history.js b/src/components/history.js new file mode 100644 index 0000000..8fbb3c1 --- /dev/null +++ b/src/components/history.js @@ -0,0 +1,20 @@ +/** + * Update window title and history. + */ +AFRAME.registerComponent('history', { + schema: { + challengeId: {type: 'string'}, + songName: {type: 'string'}, + songSubName: {type: 'string'} + }, + + update: function () { + const data = this.data; + history.pushState( + '', + data.songName, + updateQueryParam(window.location.href, 'challenge', data.challengeId) + ); + document.title = `Super Saber - ${data.songName}`; + } +}); diff --git a/src/components/play-audio.js b/src/components/play-audio.js deleted file mode 100644 index 103281c..0000000 --- a/src/components/play-audio.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Play audio element on event. - */ -AFRAME.registerComponent('play-audio', { - schema: { - audio: { type: 'string' }, - event: { type: 'string' }, - volume: { type: 'number', default: 1 }, - }, - - multiple: true, - - init: function() { - var audio; - audio = document.querySelector(this.data.audio); - audio.volume = this.data.volume; - - this.el.addEventListener(this.data.event, evt => { - if (!audio.paused) { - audio.pause(); - audio.currentTime = 0; - } - audio.play(); - }); - }, -}); diff --git a/src/components/play-button.js b/src/components/play-button.js deleted file mode 100644 index c5721db..0000000 --- a/src/components/play-button.js +++ /dev/null @@ -1,13 +0,0 @@ -AFRAME.registerComponent('play-button', { - init: function() { - var el = this.el; - - el.addEventListener('click', () => { - el.sceneEl.emit('playbuttonclick'); - }); - - el.sceneEl.addEventListener('youtubefinished', evt => { - el.object3D.visible = false; - }); - }, -}); diff --git a/src/components/play-sound.js b/src/components/play-sound.js new file mode 100644 index 0000000..a3ad8bc --- /dev/null +++ b/src/components/play-sound.js @@ -0,0 +1,43 @@ +var SoundPool = require('../lib/soundpool'); + +AFRAME.registerSystem('play-sound', { + init: function () { + this.lastSoundPlayed = ''; + this.lastSoundPlayedTime = 0; + this.pools = {}; + }, + + createPool: function (sound, volume) { + if (this.pools[sound]) { return; } + this.pools[sound] = new SoundPool(sound, volume); + }, + + playSound: function (sound, volume) { + this.createPool(sound, volume); + this.pools[sound].play(); + + this.lastSoundPlayed = sound; + this.lastSoundTime = this.el.time; + } +}); + +/** + * Play sound on event. + */ +AFRAME.registerComponent('play-sound', { + schema: { + enabled: {default: true}, + event: {type: 'string'}, + sound: {type: 'string'}, + volume: {type: 'number', default: 1} + }, + + multiple: true, + + init: function () { + this.el.addEventListener(this.data.event, evt => { + if (!this.data.enabled) { return; } + this.system.playSound(this.data.sound, this.data.volume); + }); + } +}); diff --git a/src/components/preview-song.js b/src/components/preview-song.js index 29414db..026a893 100644 --- a/src/components/preview-song.js +++ b/src/components/preview-song.js @@ -31,6 +31,7 @@ AFRAME.registerComponent('preview-song', { }, update: function (oldData) { + // Stop. if (oldData.challengeId && !this.data.challengeId) { if (this.animation) { this.animation.pause(); } this.audio.pause(); diff --git a/src/components/recenter.js b/src/components/recenter.js index 723d6f1..20660c2 100644 --- a/src/components/recenter.js +++ b/src/components/recenter.js @@ -14,9 +14,7 @@ AFRAME.registerComponent('recenter', { this.checkInViewAfterRecenter = this.checkInViewAfterRecenter.bind(this); // Delay to make sure we have a valid pose. sceneEl.addEventListener('enter-vr', () => { - setTimeout(() => { - this.recenter(); - }, 100); + setTimeout(() => { this.recenter(); }, 100); }); // User can also recenter the menu manually. sceneEl.addEventListener('menudown', () => { @@ -49,7 +47,7 @@ AFRAME.registerComponent('recenter', { checkInViewAfterRecenter: function() { var camera = this.el.sceneEl.camera; var frustum = this.frustum; - var menu = document.querySelector('#menu'); + var menu = this.el; var menuPosition = this.menuPosition; camera.updateMatrix(); camera.updateMatrixWorld(); diff --git a/src/components/song.js b/src/components/song.js new file mode 100644 index 0000000..56420a3 --- /dev/null +++ b/src/components/song.js @@ -0,0 +1,39 @@ +const utils = require('../utils'); + +/** + * Active challenge song / audio. + */ +AFRAME.registerComponent('song', { + schema: { + challengeId: {default: ''}, + isPlaying: {default: false} + }, + + init: function () { + // Use audio element for audioanalyser. + this.audio = document.createElement('audio'); + this.audio.setAttribute('id', 'song'); + this.el.sceneEl.appendChild(this.audio); + }, + + update: function (oldData) { + var el = this.el; + var data = this.data; + + // Changed challenge. + if (data.challengeId !== oldData.challengeId) { + let songUrl = utils.getS3FileUrl(data.challengeId, 'song.ogg'); + this.audio.currentTime = 0; + this.audio.src = data.challengeId ? songUrl : ''; + console.log(`Playing ${songUrl}...`); + } + + // Keep playback state up to date. + if ((data.isPlaying && data.challengeId) && this.audio.paused) { + this.audio.play(); + return; + } else if ((!data.isPlaying || !data.challengeId) && !this.audio.paused) { + this.audio.pause(); + } + } +}); diff --git a/src/index.html b/src/index.html index d2bbdc7..f15cd89 100644 --- a/src/index.html +++ b/src/index.html @@ -5,29 +5,25 @@ - - - - - + + - - + + - - - + {% include './templates/environment.html' %} {% include './templates/gameUi.html' %} {% include './templates/menu.html' %} @@ -39,7 +35,8 @@ - + diff --git a/src/lib/soundpool.js b/src/lib/soundpool.js index 4a96c26..3fc7e3e 100644 --- a/src/lib/soundpool.js +++ b/src/lib/soundpool.js @@ -1,22 +1,27 @@ -module.exports = function SoundPool (src, volume, size) { +module.exports = function SoundPool (src, volume) { var currSound = 0; var i; var pool = []; var sound; - for (i = 0; i < size; i++) { - sound = new Audio(src); - sound.volume = volume; - sound.load(); - pool.push(sound); - } + sound = new Audio(src); + sound.volume = volume; + pool.push(sound); return { play: function () { + // Dynamic size pool. + if (pool[currSound].currentTime !== 0 || !pool[currSound].ended) { + sound = new Audio(src); + sound.volume = volume; + pool.push(sound); + currSound++; + } + if (pool[currSound].currentTime === 0 || pool[currSound].ended) { pool[currSound].play(); } - currSound = (currSound + 1) % size; + currSound = (currSound + 1) % pool.length; } }; }; diff --git a/src/state/index.js b/src/state/index.js index bac4366..efae627 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -9,79 +9,95 @@ AFRAME.registerState({ challenge: { author: '', difficulty: '', - downloads: '', - downloadsText: '', id: AFRAME.utils.getUrlParameter('challenge'), + image: '', isLoading: false, songName: '', - songSubName: '', + songSubName: '' }, inVR: false, - maxStreak: 0, menu: { - active: true + active: true, + playButtonText: 'Play' }, menuDifficulties: [], menuSelectedChallenge: { + author: '', + difficulty: '', + downloads: '', + downloadsText: '', id: '', - image: '' + image: '', + songName: '', + songSubName: '' + }, + score: { + maxStreak: 0, + score: 0, + streak: 0 }, - playButtonText: 'Play', - score: 0, - scoreText: '', // screen: keep track of layers or depth. Like breadcrumbs. screen: hasInitialChallenge ? 'challenge' : 'home', screenHistory: [], - searchResults: [], - streak: 0 + searchResults: [] }, handlers: { - beatloaderfinish: function (state, payload) { + beatloaderfinish: (state) => { state.challenge.isLoading = false; }, - beatloaderstart: function (state, payload) { + beatloaderstart: (state) => { state.challenge.isLoading = true; }, - challengeset: function (state, payload) { - state.challenge.id = payload.challengeId; - state.score = 0; - state.streak = 0; - state.maxStreak = 0; - state.menu.active = false; - state.menuSelectedChallenge.id = ''; - setScreen(state, 'challenge'); - }, - /** * Song clicked from menu. */ - menuchallengeselect: function (state, id) { + menuchallengeselect: (state, id) => { + // Copy from challenge store populated from search results. let challengeData = challengeDataStore[id]; Object.assign(state.menuSelectedChallenge, challengeData); - state.menuSelectedChallenge.id = id; + state.menuDifficulties.length = 0; for (let i = 0; i < challengeData.difficulties.length; i++) { state.menuDifficulties.push(challengeData.difficulties[i]); } + state.menuSelectedChallenge.image = utils.getS3FileUrl(id, 'image.jpg'); state.menuSelectedChallenge.downloadsText = `${challengeData.downloads} Plays`; + + // Choose first difficulty. + // TODO: Default and order by easiest to hardest. + state.menuSelectedChallenge.difficulty = state.menuDifficulties[0]; }, - menudifficultyselect: function (state, difficulty) { + menudifficultyselect: (state, difficulty) => { state.menuSelectedChallenge.difficulty = difficulty; }, - playbuttonclick: function (state) { + /** + * Start challenge. + * Transfer staged challenge to the active challenge. + */ + playbuttonclick: (state) => { + // Reset score. + state.score.maxStreak = 0; + state.score.score = 0; + state.score.streak = 0; + + // Set challenge. `beat-loader` is listening. + Object.assign(state.challenge, state.menuSelectedChallenge); + + // Reset menu. state.menu.active = false; + state.menuSelectedChallenge.id = ''; }, /** * Update search results. Will automatically render using `bind-for` (menu.html). */ - searchresults: function (state, payload) { + searchresults: (state, payload) => { var i; state.searchResults.length = 0; for (i = 0; i < 6; i++) { @@ -93,21 +109,23 @@ AFRAME.registerState({ state.searchResults.__dirty = true; }, - togglemenu: function (state) { + togglemenu: (state) => { state.menu.active = !state.menu.active; }, - 'enter-vr': function (state, payload) { + 'enter-vr': (state) => { state.inVR = true; }, - 'exit-vr': function (state, payload) { + 'exit-vr': (state) => { state.inVR = false; } }, - computeState: function (state) { - state.scoreText = `Streak: ${state.streak} / Max Streak: ${state.maxStreak} / Score: ${state.score}`; + /** + * Post-process the state after each action. + */ + computeState: (state) => { } }); diff --git a/src/templates/environment.html b/src/templates/environment.html index efbe8d3..0169131 100644 --- a/src/templates/environment.html +++ b/src/templates/environment.html @@ -1,4 +1,4 @@ - + + + diff --git a/src/templates/gameUi.html b/src/templates/gameUi.html index e76380b..32b6f19 100644 --- a/src/templates/gameUi.html +++ b/src/templates/gameUi.html @@ -1,36 +1,30 @@ + text="align: right; width: 1.25; value: SCORE; color: #d6d955; anchor: right; letterSpacing: -2" + position="-0.089 0.183 -1.01"> + text="width: 0.9; value: STREAK; color: #f95895; letterSpacing: -2; anchor: left" + position="0.097 0.183 -1.01"> + text="width: 0.7; value: MAX; color: #f95895; anchor: left; letterSpacing: -2" + position="0.101 0.1 -1.01"> + text="align: right; width: 2; color: #feffc1; anchor: right; letterSpacing: -2" + position="-0.08 0.132 -1"> + text="width: 1.25; color: #ffbdd6; anchor: left; letterSpacing: -2" + position="0.098 0.144 -1"> + text="width: 0.8; color: #ffbdd6; anchor: left; letterSpacing: -2" + position="0.1 0.074 -1"> diff --git a/src/templates/menu.html b/src/templates/menu.html index f6601a9..8f65f05 100644 --- a/src/templates/menu.html +++ b/src/templates/menu.html @@ -12,7 +12,7 @@ geometry="primitive: plane; width: 0.8; height: 0.1" material="shader: flat; color: #111" position="0 -0.13 -0.01" - play-audio="event: mouseenter; audio: #hoverSound; volume: 0.03" + play-sound="event: mouseenter; sound: #hoverSound; volume: 0.03" animation__mouseenter="property: material.color; from: #111; to: #666; startEvents: mouseenter; pauseEvents: mouseleave; dur: 150" animation__mouseleave="property: material.color; from: #666; to: #111; startEvents: mouseleave; pauseEvents: mouseenter; dur: 150" raycastable> @@ -50,7 +50,7 @@ geometry="primitive: plane; width: 0.3; height: 0.1" material="shader: flat; color: #111" position="-0.4 -0.005 0" - play-audio="event: mouseenter; audio: #hoverSound; volume: 0.03" + play-sound="event: mouseenter; sound: #hoverSound; volume: 0.03" animation__mouseenter="property: material.color; from: #111; to: #666; startEvents: mouseenter; pauseEvents: mouseleave; dur: 150" animation__mouseleave="property: material.color; from: #666; to: #111; startEvents: mouseleave; pauseEvents: mouseenter; dur: 150" bind-toggle__raycastable="menu.active && !!menuSelectedChallenge.id"> @@ -75,10 +75,11 @@ -