327 lines
8.4 KiB
JavaScript
327 lines
8.4 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.target.tagName.toLowerCase()=='input') return;
|
|
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});
|
|
})();
|