Files
junisaber/src/state/index.js
2018-10-16 02:57:07 -07:00

395 lines
11 KiB
JavaScript

/* global localStorage */
var utils = require('../utils');
const challengeDataStore = {};
const SEARCH_PER_PAGE = 6;
const SONG_NAME_TRUNCATE = 24;
const SONG_SUB_NAME_TRUNCATE = 32;
const DAMAGE_DECAY = 0.25;
const DAMAGE_MAX = 10;
/**
* State handler.
*
* 1. `handlers` is an object of events that when emitted to the scene will run the handler.
*
* 2. The handler function modifies the state.
*
* 3. Entities and components that are `bind`ed automatically update:
* `bind__<componentName>="<propertyName>: some.item.in.state"`
*/
AFRAME.registerState({
initialState: {
activeHand: localStorage.getItem('hand') || 'right',
challenge: {
author: '',
difficulty: '',
id: AFRAME.utils.getUrlParameter('challenge'),
image: '',
isLoading: false,
isBeatsPreloaded: false,
songName: '',
songSubName: ''
},
damage: 0,
inVR: false,
isGameOver: false, // Game over screen.
isPaused: false, // Playing, but paused. Not active during menu.
isPlaying: false, // Actively playing (slicing beats).
isSongFetching: false, // Fetching stage.
isSongLoading: false, // Either fetching or decoding.
isVictory: false, // Victory screen.
keyboardActive: false, // Whether search is open.
menuActive: true,
menuDifficulties: [],
menuSelectedChallenge: {
author: '',
difficulty: '',
downloads: '',
downloadsText: '',
id: '',
index: -1,
image: '',
songName: '',
songSubName: ''
},
multiplierText: '1x',
score: {
accuracy: '',
beatsHit: 0,
beatsMissed: 0,
combo: 0,
maxCombo: 0,
multiplier: 1,
rank: '',
score: 0
},
search: {
active: true,
page: 0,
hasNext: false,
hasPrev: false,
results: [],
songNameTexts: '',
songSubNameTexts: ''
},
searchResultsPage: []
},
handlers: {
/**
* Swap left-handed or right-handed mode.
*/
activehandswap: state => {
state.activeHand = state.activeHand === 'right' ? 'left' : 'right';
localStorage.setItem('activeHand', state.activeHand);
},
beathit: state => {
if (state.damage > DAMAGE_DECAY) {
state.damage -= DAMAGE_DECAY;
}
state.score.beatsHit++;
state.score.score++;
state.score.combo++;
if (state.score.combo > state.score.maxCombo) {
state.score.maxCombo = state.score.combo;
}
state.score.multiplier = state.score.combo >= 8
? 8
: 2 * Math.floor(Math.log2(state.score.combo));
},
beatmiss: state => {
state.score.beatsMissed++;
takeDamage(state);
},
beatwrong: state => {
state.score.beatsMissed++;
takeDamage(state);
},
beatloaderfinish: (state) => {
state.challenge.isLoading = false;
},
beatloaderpreloadfinish: (state) => {
state.challenge.isBeatsPreloaded = true;
},
beatloaderstart: (state) => {
state.challenge.isBeatsPreloaded = false;
state.challenge.isLoading = true;
},
gamemenuresume: (state) => {
state.isPaused = false;
},
gamemenurestart: (state) => {
resetScore(state);
state.isBeatsPreloaded = false;
state.isGameOver = false;
state.isPaused = false;
},
gamemenuexit: (state) => {
resetScore(state);
state.isBeatsPreloaded = false;
state.isGameOver = false;
state.isPaused = false;
state.isVictory = false;
state.menuActive = true;
state.challenge.id = '';
},
keyboardclose: (state) => {
state.keyboardActive = false;
},
keyboardopen: (state) => {
state.keyboardActive = true;
},
/**
* Song clicked from menu.
*/
menuchallengeselect: (state, id) => {
// Copy from challenge store populated from search results.
let challengeData = challengeDataStore[id];
Object.assign(state.menuSelectedChallenge, challengeData);
// Populate difficulty options.
state.menuDifficulties.length = 0;
for (let i = 0; i < challengeData.difficulties.length; i++) {
state.menuDifficulties.unshift(challengeData.difficulties[i]);
}
state.menuDifficulties.sort(difficultyComparator);
// Default to easiest difficulty.
state.menuSelectedChallenge.difficulty = state.menuDifficulties[0];
state.menuSelectedChallenge.image = utils.getS3FileUrl(id, 'image.jpg');
state.menuSelectedChallenge.downloadsText = `${challengeData.downloads} Plays`;
computeMenuSelectedChallengeIndex(state);
state.keyboardActive = false;
},
menuchallengeunselect: state => {
state.menuSelectedChallenge.id = '';
},
menudifficultyselect: (state, difficulty) => {
state.menuSelectedChallenge.difficulty = difficulty;
},
pausegame: (state) => {
if (!state.isPlaying) { return; }
state.isPaused = true;
},
/**
* Start challenge.
* Transfer staged challenge to the active challenge.
*/
playbuttonclick: (state) => {
resetScore(state);
// Set challenge. `beat-loader` is listening.
Object.assign(state.challenge, state.menuSelectedChallenge);
// Reset menu.
state.menuActive = false;
state.menuSelectedChallenge.id = '';
state.keyboardActive = false;
state.challenge.isLoading = true;
state.isSongLoading = true;
},
searchprevpage: function (state) {
if (state.search.page === 0) { return; }
state.search.page--;
computeSearchPagination(state);
},
searchnextpage: function (state) {
if (state.search.page > Math.floor(state.search.results.length / SEARCH_PER_PAGE)) {
return;
}
state.search.page++;
computeSearchPagination(state);
},
/**
* Update search results. Will automatically render using `bind-for` (menu.html).
*/
searchresults: (state, payload) => {
var i;
state.search.page = 0;
state.search.results = payload.results;
for (i = 0; i < payload.results.length; i++) {
let result = payload.results[i];
result.songSubName = result.songSubName || 'Unknown Artist';
result.shortSongName = truncate(result.songName, SONG_NAME_TRUNCATE).toUpperCase();
result.shortSongSubName = truncate(result.songSubName, SONG_SUB_NAME_TRUNCATE);
challengeDataStore[result.id] = result;
}
computeSearchPagination(state);
computeMenuSelectedChallengeIndex(state);
},
songfetchfinish: (state) => {
state.isSongFetching = false;
},
songloadfinish: (state) => {
state.isSongFetching = false;
state.isSongLoading = false;
},
songloadstart: (state) => {
state.isSongFetching = true;
state.isSongLoading = true;
},
'enter-vr': (state) => {
state.inVR = true;
},
'exit-vr': (state) => {
state.inVR = false;
},
victory: function (state) {
state.isVictory = true;
const accuracy = state.beatsHit / (state.beatsMissed + state.beatsHit);
state.score.accuracy = `${(accuracy * 100).toFixed()}%`;
if (accuracy === 1) {
state.rank = 'S';
} else if (accuracy >= .90) {
state.rank = 'A';
} else if (accuracy >= .80) {
state.rank = 'B';
} else if (accuracy >= .70) {
state.rank = 'C';
} else if (accuracy >= .60) {
state.rank = 'D';
} else {
state.rank = 'F';
}
},
wallhitstart: function (state) {
takeDamage(state);
}
},
/**
* Post-process the state after each action.
*/
computeState: (state) => {
state.isPlaying =
!state.menuActive && !state.isPaused && !state.isVictory && !state.isGameOver &&
!state.challenge.isLoading && !state.isSongLoading;
const anyMenuOpen = state.menuActive || state.isPaused || state.isVictory || state.isGameOver;
state.leftRaycasterActive = anyMenuOpen && state.activeHand === 'left' && state.inVR;
state.rightRaycasterActive = anyMenuOpen && state.activeHand === 'right' && state.inVR;
// Song is decoding if it is loading, but not fetching.
if (state.isSongLoading) {
state.loadingText = state.isSongFetching ? 'Downloading song...' : 'Processing song...';
} else {
state.loadingText = '';
}
state.multiplierText = `${state.score.multiplier}x`;
}
});
function computeSearchPagination (state) {
let numPages = Math.ceil(state.search.results.length / SEARCH_PER_PAGE);
state.search.hasPrev = state.search.page > 0;
state.search.hasNext = state.search.page < numPages - 1;
state.search.songNameTexts = '';
state.search.songSubNameTexts = '';
state.searchResultsPage.length = 0;
state.searchResultsPage.__dirty = true;
for (let i = state.search.page * SEARCH_PER_PAGE;
i < state.search.page * SEARCH_PER_PAGE + SEARCH_PER_PAGE; i++) {
if (!state.search.results[i]) { break; }
state.searchResultsPage.push(state.search.results[i]);
state.search.songNameTexts +=
truncate(state.search.results[i].songName, SONG_NAME_TRUNCATE).toUpperCase() + '\n';
state.search.songSubNameTexts +=
truncate(state.search.results[i].songSubName, SONG_SUB_NAME_TRUNCATE) + '\n';
}
for (let i = 0; i < state.searchResultsPage.length; i++) {
state.searchResultsPage[i].index = i;
}
computeMenuSelectedChallengeIndex(state);
}
function truncate (str, length) {
if (!str) { return ''; }
if (str.length >= length) {
return str.substring(0, length - 3) + '...';
}
return str;
}
const DIFFICULTIES = ['Easy', 'Normal', 'Hard', 'Expert', 'ExpertPlus'];
function difficultyComparator (a, b) {
const aIndex = DIFFICULTIES.indexOf(a);
const bIndex = DIFFICULTIES.indexOf(b);
if (aIndex < bIndex) { return -1; }
if (aIndex > bIndex) { return 1; }
return 0;
}
function takeDamage (state) {
if (AFRAME.utils.getUrlParameter('godmode')) { return; }
if (!state.isPlaying) { return; }
state.damage++;
state.score.combo = 0;
state.score.multiplier = 1;
checkGameOver(state);
}
function checkGameOver (state) {
if (state.damage >= DAMAGE_MAX) {
state.damage = 0;
state.isGameOver = true;
}
}
function resetScore (state) {
state.damage = 0;
state.score.beatsHit = 0;
state.score.beatsMissed = 0;
state.score.combo = 0;
state.score.maxCombo = 0;
state.score.score = 0;
state.score.multiplier = 1;
}
function computeMenuSelectedChallengeIndex (state) {
state.menuSelectedChallenge.index = -1;
for (let i = 0; i < state.searchResultsPage.length; i++) {
if (state.searchResultsPage[i].id === state.menuSelectedChallenge.id) {
state.menuSelectedChallenge.index = i;
break;
}
}
}