Snek/src/js/main.js

327 lines
8.3 KiB
JavaScript

(async () => {
// load modules
const assets=require('assets');
const Popup=require('popup');
const SnekGame=require('snek');
const configEditor=require('configEditor');
const input=require('input');
const levels=require('levels');
const config=require('config');
const leaderboards=require('leaderboards');
// get a known state
await new Promise(ok => assets.onReady(ok));
location.hash='menu';
// get our DOM in check
const main=document.querySelector('main');
const nav=main.querySelector('nav');
const canvas=main.querySelector('canvas');
const hud=main.querySelector('#hud');
// load data from server
const levelList=window.levelList=assets.get('levelList');
// detect if we're running with a server
const serverless=window.serverless=await (async() => {
const res=await fetch('api/has-nodejs');
if(!res.ok) return true;
const msg=await res.json();
return msg!='yes';
})();
if(serverless) {
document.body.classList.add('serverless');
} else {
document.body.classList.add('server');
}
// flag the body as loaded
document.body.classList.remove('loading');
// get our global variables
let currentGame=null;
// forward-declare functions
let resizeCanvas, startGame, stopGame, handleWin, handleDeath, menu, help, settings, showLeaderboards, restart, updateHud;
// handle window resize and fullscreen
resizeCanvas=() => {
if(document.fullscreenElement) {
canvas.width=screen.width;
canvas.height=screen.height;
} else {
canvas.width=main.clientWidth;
canvas.height=main.clientHeight;
}
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('keydown', async e => {
if(e.key=='f') {
if(document.fullscreenElement) await document.exitFullscreen();
else await main.requestFullscreen();
resizeCanvas();
}
});
// buid menu from level list
Object.keys(levelList).forEach(category => {
const cat=levelList[category];
const section=nav.appendChild(document.createElement('section'));
const h1=section.appendChild(document.createElement('h1'));
h1.innerText=category[0].toUpperCase()+category.slice(1)+" Mode";
const p=section.appendChild(document.createElement('p'));
p.innerText=cat.desc;
const ul=section.appendChild(document.createElement('ul'));
cat.levels.forEach((level, i) => {
const {displayName, fileName, levelString}=levels.getInfo(category, level);
const li=ul.appendChild(document.createElement('li'));
const a=li.appendChild(document.createElement('a'));
a.href='#'+levelString;
a.innerText=displayName;
if(cat.levelDesc) {
const span=li.appendChild(document.createElement('span'));
span.innerText=cat.levelDesc[i];
}
});
});
// stop a running game
stopGame=() => {
if(currentGame) {
// stop the actual game
currentGame.playing=false;
// setup the DOM
nav.classList.remove('hidden');
canvas.classList.add('hidden');
hud.classList.add('hidden');
}
};
// display the leaderboards
showLeaderboards=() => {
stopGame();
leaderboards.show();
};
// start a new game
startGame=async (category, levelId, filename) => {
// stop any running games and clear popups
stopGame();
Popup.dismiss();
// load rules and level from cache or server
const rules=levelList[category].rules || {};
const level=await levels.get(filename);
// stop any running games and clear popups again
stopGame();
Popup.dismiss();
// create the game and attach the callbacks and config
const snek=currentGame=new SnekGame(level, canvas, rules);
snek.callback=evt => {
if(evt=='tick') {
input.framefn();
snek.handleInputs(input.inputs);
updateHud();
} else if(evt=='win') {
handleWin(snek);
} else if(evt=='die') {
handleDeath(snek);
}
};
// setup the DOM
nav.classList.add('hidden');
canvas.classList.remove('hidden');
hud.classList.remove('hidden');
// push some userdata to the snake
snek.userdata={
category,
levelId,
filename
};
// reset the inputs
input.clear();
// start the actual game
snek.start();
};
// return to the menu
menu=() => {
stopGame();
Popup.dismiss();
};
// show config editor
settings=async () => {
stopGame();
Popup.dismiss();
await configEditor.show();
location.hash='menu';
};
// show help page
help=async () => {
stopGame();
Popup.dismiss();
let iframe=document.createElement('iframe');
iframe.src='help.html';
iframe.style.width='100%';
iframe.style.height='100%';
await new Popup(
"Help",
[iframe],
{ok: "OK"},
true
).display();
location.hash='menu';
};
// display the win popup
handleWin=async snek => {
// hide the HUD
hud.classList.add('hidden');
// fetch userdata from the game
const {category, levelId, filename}=snek.userdata;
// upload scores
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, true, snek);
// create and configure popup
let popup=new Popup("Finished!");
popup.addStrong("You won!");
popup.addContent({
"Time": snek.endPlayTime/1000+'s',
"Score": snek.score,
"Final length": snek.length,
"Final speed": snek.speed+'tps'
});
popup.buttons={
retry: "Retry",
menu: "Main menu"
};
if(levelList[category].nextLevel) {
let nextId=(+levelId)+1;
if(levelList[category].levels.includes(nextId)) popup.buttons.next="Next level";
}
// show the actual popup
let result=await popup.display(main);
// act on it
if(result=='retry') {
startGame(category, levelId, filename);
} else if(result=='menu') {
location.hash='menu';
} else if(result=='next') {
const {category, levelId}=snek.userdata;
let nextId=(+levelId)+1;
let {levelString}=levels.getInfo(category, nextId)
location.hash=levelString;
}
};
// display the death popup
handleDeath=async snek => {
// hide the HUD
hud.classList.add('hidden');
// fetch userdata from the game
const {category, levelId, filename}=snek.userdata;
// upload scores
if(config.getB('player.leaderboards')) leaderboards.upload(category+'/'+levelId, false, snek);
// create and configure popup
let popup=new Popup("Finished!");
popup.addStrong(config.getS('player.name')+' '+snek.death.message);
popup.addEm('('+config.getS('player.name')+' '+snek.death.reason+')');
popup.addContent({
"Time": snek.endPlayTime/1000+'s',
"Score": snek.score,
"Final length": snek.length,
"Final speed": snek.speed+'tps'
});
popup.buttons={
retry: "Retry",
menu: "Main menu"
};
// show the actual popup
let result=await popup.display(main);
// act on it
if(result=='retry') {
startGame(category, levelId, filename);
} else if(result=='menu') {
location.hash='menu';
}
};
// draw status hud
updateHud=() => {
// stay safe
if(!currentGame) return;
// get the actual elements
const speedDisplay=document.querySelector('#hud .speed');
const scoreDisplay=document.querySelector('#hud .score');
const timeDisplay=document.querySelector('#hud .time');
// rpad is useful
const rpad=(n, digits=2, pad=' ') =>
((''+n).length>=digits)?(''+n):(rpad(pad+n, digits, pad));
// actually do the hud
speedDisplay.innerText=rpad(currentGame.speed, 2, '0')+'tps';
scoreDisplay.innerText=currentGame.score;
timeDisplay.innerText=rpad(Math.floor(currentGame.playTime/60000), 2, '0')+
':'+rpad(Math.floor(currentGame.playTime/1000)%60, 2, '0')+
':'+rpad(currentGame.playTime%1000, 3, '0');
};
// quick restart
restart=() => {
if(currentGame && currentGame.playing) {
const {category, levelId, filename}=currentGame.userdata;
startGame(category, levelId, filename);
}
}
window.addEventListener('keydown', e => {
if(e.key=='r') restart();
});
(() => {
let restartbtn=hud.appendChild(document.createElement('span'));
restartbtn.classList.add('restart');
restartbtn.addEventListener('click', restart);
restartbtn.addEventListener('touchend', restart);
})();
// handle page navigation
window.addEventListener('hashchange', () => {
const hash=location.hash.substr(1);
if(hash=='' || hash=='menu') return menu();
else if(hash=='help') return help();
else if(hash=='settings') return settings();
else if(hash=='leaderboards') return showLeaderboards();
const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/);
startGame(category, levelId, filename);
});
// enable input methods overlay
input.init({hud});
})();