Files
junisaber/src/components/beat-loader.js

342 lines
10 KiB
JavaScript

import {BEAT_WARMUP_OFFSET, BEAT_WARMUP_SPEED, BEAT_WARMUP_TIME} from '../constants/beat';
import utils from '../utils';
/**
* Load beat data (all the beats and such).
*/
AFRAME.registerComponent('beat-loader', {
dependencies: ['stage-colors'],
schema: {
beatAnticipationTime: {default: 2.0},
beatSpeed: {default: 4.0},
beatWarmupTime: {default: BEAT_WARMUP_TIME / 1000},
beatWarmupSpeed: {default: BEAT_WARMUP_SPEED},
challengeId: {type: 'string'}, // If clicked play.
difficulty: {type: 'string'},
isPlaying: {default: false},
menuSelectedChallengeId: {type: 'string'}
},
orientationsHumanized: [
'up',
'down',
'left',
'right',
'upleft',
'upright',
'downleft',
'downright'
],
horizontalPositions: [-0.75, -0.25, 0.25, 0.75],
horizontalPositionsHumanized: {
0: 'left',
1: 'middleleft',
2: 'middleright',
3: 'right'
},
positionHumanized: {
topLeft: {layer: 2, index: 0},
topCenterLeft: {layer: 2, index: 1},
topCenterRight: {layer: 2, index: 2},
topRight: {layer: 2, index: 3},
middleLeft: {layer: 1, index: 0},
middleCenterLeft: {layer: 1, index: 1},
middleCenterRight: {layer: 1, index: 2},
middleRight: {layer: 1, index: 3},
bottomLeft: {layer: 0, index: 0},
bottomCenterLeft: {layer: 0, index: 1},
bottomCenterRight: {layer: 0, index: 2},
bottomRight: {layer: 0, index: 3},
},
verticalPositionsHumanized: {
0: 'bottom',
1: 'middle',
2: 'top'
},
init: function () {
this.audioAnalyserEl = document.getElementById('audioanalyser');
this.beatData = null;
this.beatDataProcessed = false;
this.beatContainer = document.getElementById('beatContainer');
this.beatsTime = undefined;
this.beatsPreloadTime = 0;
this.beatsPreloadTimeTotal =
(this.data.beatAnticipationTime + this.data.beatWarmupTime) * 1000;
this.bpm = undefined;
this.xhr = null;
this.stageColors = this.el.components['stage-colors'];
this.twister = document.getElementById('twister');
this.leftStageLasers = document.getElementById('leftStageLasers');
this.rightStageLasers = document.getElementById('rightStageLasers');
this.el.addEventListener('cleargame', this.clearBeats.bind(this));
},
update: function (oldData) {
const data = this.data;
// Start playing.
if (!oldData.challengeId && data.challengeId && this.beatData) {
this.processBeats();
return;
}
if (!data.menuSelectedChallengeId || !data.difficulty) { return; }
// Prefetch beats.
if (data.menuSelectedChallengeId !== oldData.menuSelectedChallengeId ||
data.difficulty !== oldData.difficulty) {
this.fetchBeats();
}
},
/**
* XHR. Beat data is prefetched when user selects a menu challenge, and stored away
* to be processed later.
*/
fetchBeats: function () {
var el = this.el;
if (this.xhr) { this.xhr.abort(); }
// Load beats.
let url = utils.getS3FileUrl(this.data.menuSelectedChallengeId,
`${this.data.difficulty}.json`);
const xhr = this.xhr = new XMLHttpRequest();
el.emit('beatloaderstart');
console.log(`[beat-loader] Fetching ${url}...`);
xhr.open('GET', url);
xhr.addEventListener('load', () => {
this.beatData = JSON.parse(xhr.responseText);
this.beatDataProcessed = false;
this.xhr = null;
this.el.sceneEl.emit('beatloaderfinish', null, false);
});
xhr.send();
},
/**
* Load the beat data into the game.
*/
processBeats: function () {
// Reset variables used during playback.
// Beats spawn ahead of the song and get to the user in sync with the music.
this.beatsTime = 0;
this.beatsPreloadTime = 0;
this.beatData._events.sort(lessThan);
this.beatData._obstacles.sort(lessThan);
this.beatData._notes.sort(lessThan);
this.bpm = this.beatData._beatsPerMinute;
// Some events have negative time stamp to initialize the stage.
const events = this.beatData._events;
if (events.length && events[0]._time < 0) {
for (let i = 0; events[i]._time < 0; i++) {
this.generateEvent(events[i]);
}
}
this.beatDataProcessed = true;
console.log('[beat-loader] Finished processing beat data.');
},
/**
* Generate beats and stuff according to timestamp.
*/
tick: function (time, delta) {
if (!this.data.isPlaying || !this.data.challengeId || !this.beatData) { return; }
const prevBeatsTime = this.beatsTime;
if (this.beatsPreloadTime === undefined) {
// Get current song time.
const song = this.el.components.song;
if (!song.isPlaying) { return; }
this.beatsTime = (song.getCurrentTime() + this.data.beatAnticipationTime) * 1000;
if (this.beatsTime <= this.beatsPreloadTimeTotal) { return; }
} else {
// Song is not playing and is preloading beats, use maintained beat time.
this.beatsTime = this.beatsPreloadTime;
}
// Load in stuff scheduled between the last timestamp and current timestamp.
// Beats.
const beatsTime = this.beatsTime;
const bpm = this.beatData._beatsPerMinute;
const msPerBeat = 1000 * 60 / this.beatData._beatsPerMinute;
const notes = this.beatData._notes;
for (let i = 0; i < notes.length; ++i) {
let noteTime = notes[i]._time * msPerBeat;
if (noteTime > prevBeatsTime && noteTime <= beatsTime) {
notes[i].time = noteTime;
this.generateBeat(notes[i]);
}
}
// Walls.
const obstacles = this.beatData._obstacles;
for (let i = 0; i < obstacles.length; ++i) {
let noteTime = obstacles[i]._time * msPerBeat;
if (noteTime > prevBeatsTime && noteTime <= beatsTime) {
this.generateWall(obstacles[i]);
}
}
// Stage events.
const events = this.beatData._events;
for (let i = 0; i < events.length; ++i) {
let noteTime = events[i]._time * msPerBeat;
if (noteTime > prevBeatsTime && noteTime <= beatsTime) {
this.generateEvent(events[i]);
}
}
if (this.beatsPreloadTime === undefined) { return; }
if (this.beatsPreloadTime >= this.beatsPreloadTimeTotal) {
// Finished preload.
this.el.sceneEl.emit('beatloaderpreloadfinish', null, false);
this.beatsPreloadTime = undefined;
} else {
// Continue preload.
this.beatsPreloadTime += delta;
}
},
generateBeat: (function () {
const beatObj = {};
// Beats arrive at sword stroke distance synced with the music.
const swordOffset = 1.5;
return function (noteInfo) {
var beatEl;
var color;
const data = this.data;
// if (Math.random() < 0.8) noteInfo._type = 3; // just to DEBUG MINES!
var type = noteInfo._cutDirection === 8 ? 'dot' : 'arrow';
color = noteInfo._type === 0 ? 'red' : 'blue';
if (noteInfo._type === 3) {
type = 'mine';
color = undefined;
}
beatEl = this.requestBeat(type, color);
if (!beatEl) { return; }
// Apply sword offset. Blocks arrive on beat in front of the user.
beatObj.anticipationPosition = -data.beatAnticipationTime * data.beatSpeed - swordOffset;
beatObj.color = color;
beatObj.cutDirection = this.orientationsHumanized[noteInfo._cutDirection];
beatObj.horizontalPosition = this.horizontalPositionsHumanized[noteInfo._lineIndex];
beatObj.speed = this.data.beatSpeed;
beatObj.type = type;
beatObj.verticalPosition = this.verticalPositionsHumanized[noteInfo._lineLayer],
beatObj.warmupPosition = -data.beatWarmupTime * data.beatWarmupSpeed;
beatEl.setAttribute('beat', beatObj);
beatEl.components.beat.updatePosition();
beatEl.play();
beatEl.components.beat.onGenerate();
};
})(),
generateWall: function (wallInfo) {
var el = this.el.sceneEl.components.pool__wall.requestEntity();
const data = this.data;
var speed = this.data.beatSpeed;
if (!el) { return; }
const durationSeconds = 60 * (wallInfo._duration / this.bpm);
el.setAttribute('wall', 'speed', speed);
el.object3D.position.set(
this.horizontalPositions[wallInfo._lineIndex],
1.30,
-(this.data.beatAnticipationTime * speed)
);
el.object3D.scale.set(wallInfo._width * 0.30, 2.5, durationSeconds * speed);
el.play();
},
generateEvent: function (event) {
switch(event._type) {
case 0:
this.stageColors.setColor('fog', event._value);
this.stageColors.setColor('sky', event._value);
this.stageColors.setColor('backglow', event._value);
break;
case 1:
this.stageColors.setColor('tunnelNeon', event._value);
break;
case 2:
this.stageColors.setColor('leftStageLaser0', event._value);
this.stageColors.setColor('leftStageLaser1', event._value);
this.stageColors.setColor('leftStageLaser2', event._value);
break;
case 3:
this.stageColors.setColor('rightStageLaser0', event._value);
this.stageColors.setColor('rightStageLaser1', event._value);
this.stageColors.setColor('rightStageLaser2', event._value);
break;
case 4:
this.stageColors.setColor('floor', event._value);
this.stageColors.setColor('stageNeon', event._value);
break;
case 8:
this.twister.components.twister.pulse(event._value);
break;
case 9:
// zoom was a bit disturbing
this.twister.components.twister.pulse(event._value);
break;
case 12:
this.leftStageLasers.components['stage-lasers'].pulse(event._value);
break;
case 13:
this.rightStageLasers.components['stage-lasers'].pulse(event._value);
break;
}
},
requestBeat: function (type, color) {
var beatPoolName = 'pool__beat-' + type;
var pool;
if (color) { beatPoolName += '-' + color; }
pool = this.el.sceneEl.components[beatPoolName];
if (!pool) {
console.warn('Pool ' + beatPoolName + ' unavailable');
return;
}
return pool.requestEntity();
},
/**
* Restart by returning all beats to pool.
*/
clearBeats: function () {
this.beatsPreloadTime = 0;
this.beatsTime = 0;
for (let i = 0; i < this.beatContainer.children.length; i++) {
let child = this.beatContainer.children[i];
if (child.components.beat) {
child.components.beat.returnToPool(true);
}
if (child.components.wall) {
child.components.wall.returnToPool(true);
}
}
},
});
function lessThan (a, b) { return a._time - b._time; }