(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}); })();