(async () => { // load modules const assets=require('assets'); const Popup=require('popup'); const SnekGame=require('snek'); const input=require('input'); const levels=require('levels'); // 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'); // load config const config=assets.get('config'); //TODO use an actual config module // load data from server const levelList=assets.get('levelList'); // get our global variables let currentGame=null; // forward-declare functions let resizeCanvas, getLevel, startGame, handleWin, handleDeath, menu, help; // 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(); } }); // get a level for a category and an id getLevel=(category, id) => { const cat=levelList[category]; id=''+id; const displayName=cat.levelDisplay .replace(//g, id) .replace(//g, id.toLowerCase()); const fileName=cat.levelFilename .replace(//g, id) .replace(//g, id.toLowerCase()); const levelString=category+'/'+id+'/'+fileName; return { displayName, fileName, levelString }; }; // 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}=getLevel(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]; } }); }); // start a new game startGame=async (category, levelId, filename) => { // stop any running games if(currentGame) currentGame.playing=false; // load rules and level from cache or server const rules=levelList[category].rules || {}; const level=await levels.get(filename); // 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); } else if(evt=='win') { handleWin(snek); } else if(evt=='die') { handleDeath(snek); } }; snek.config=config; // setup the DOM nav.classList.add('hidden'); canvas.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=() => { // stop any running games if(currentGame) currentGame.playing=false; // setup the DOM nav.classList.remove('hidden'); canvas.classList.add('hidden'); }; // display the win popup handleWin=async snek => { // get userdata back const {category, levelId, filename}=snek.userdata; // create and configure popup let popup=new Popup("Finished!"); popup.addStrong("You won!"); popup.addContent({ "Time": snek.playTime/1000+'s', "Score": snek.score, "Final length": snek.snake.length }); 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') { let nextId=(+levelId)+1; let {levelString}=getLevel(category, nextId) location.hash=levelString; } }; // display the death popup handleDeath=async snek => { // get userdata back const {category, levelId, filename}=snek.userdata; // create and configure popup let popup=new Popup("Finished!"); popup.addStrong("You died..."); popup.addContent({ "Time": snek.playTime/1000+'s', "Score": snek.score, "Final length": snek.snake.length }); 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'; } }; // handle page navigation window.addEventListener('hashchange', () => { const hash=location.hash.substr(1); if(hash=='' || hash=='menu') return menu(); else if(hash=='help') return help(); const [_, category, levelId, filename]=location.hash.match(/([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_-]+?)\/(.+)/); startGame(category, levelId, filename); }); // enable input methods according to config if(config.keyboard.enabled) { input.enableHandler(input.availableHandlers.keyboard); } if(config.gamepad.enabled) { input.enableHandler(input.availableHandlers.gamepad); } if(config.touchscreen.enabled) { if(config.touchscreen.mode=='crosspad') { input.enableHandler(input.availableHandlers.touchscreenCrosspad); } else if(config.touchscreen.mode=='joystick') { input.enableHandler(input.availableHandlers.touchscreenJoystick); } else if(config.touchscreen.mode=='swipe') { input.enableHandler(input.availableHandlers.touchscreenSwipe); } } input.updateConfig(config); })();