const assets=require('assets'); const config=require('config'); const {tiles: T}=require('tiles'); // declare our tiles let snake, wall, hole, fire, flammable, fruit, superFruit, decayFruit, portalA, portalB, portalC, portalD, key, door, switchTile, spikes; // load our tiles assets.onReady(() => { wall=assets.get('wall'); hole=assets.get('hole'); fire=assets.get('fire'); flammable=assets.get('flammable'); superFruit=assets.get('superFruit'); decayFruit=assets.get('decayFruit'); portalA=assets.get('portalA'); portalB=assets.get('portalB'); portalC=assets.get('portalC'); portalD=assets.get('portalD'); key=assets.get('key'); door=assets.get('door'); switchTile=assets.get('switch'); spikes=assets.get('spikes'); snake=assets.get('snake'); fruit=assets.get('fruit'); }); const draw=(game, canvas=game.canvas, ctx=canvas.getContext('2d')) => { // clear the canvas, because it's easier than having to deal with everything ctx.clearRect(0, 0, canvas.width, canvas.height); // get the cell size and offset const cellSize=Math.min( canvas.width/game.dimensions[0], canvas.height/game.dimensions[1] ); const offsetX=(canvas.width-cellSize*game.dimensions[0])/2; const offsetY=(canvas.height-cellSize*game.dimensions[1])/2; // tile draw functions const putTile=(x, y, tile) => ctx.drawImage( tile, offsetX+cellSize*x, offsetY+cellSize*y, cellSize, cellSize ); const putTileAnim=(x, y, tile) => putTile(x, y, tile[ Math.floor(Date.now()/1000*60+x+y)%tile.length ]); const putTileAnimPercent=(x, y, tile, percent) => putTile(x, y, tile[ Math.min(Math.round(percent*tile.length), tile.length-1) ]); // adjascence check const checkAdj=(x, y) => { let adj={}; adj.u=game.world[x][y-1]; adj.d=game.world[x][y+1]; adj.l=(game.world[x-1] || [])[y]; adj.r=(game.world[x+1] || [])[y]; adj.ul=(game.world[x-1] || [])[y-1]; adj.ur=(game.world[x+1] || [])[y-1]; adj.dl=(game.world[x-1] || [])[y+1]; adj.dr=(game.world[x+1] || [])[y+1]; return adj; }; // draw a grid/checkerboard if requested if(config.getS('appearance.grid')=='grid') { ctx.strokeStyle='rgba(0, 0, 0, 50%)'; ctx.lineCap='square'; ctx.lineWidth=1; ctx.beginPath(); for(let x=1; x adj[k]==T.HOLE || adj[k]==T.HOLE_S) .forEach(k => putTile(x, y, hole[k])); } break; case T.FLAMMABLE: case T.FLAMMABLE_S: putTile(x, y, flammable); break; case T.SUPER_FOOD: putTileAnim(x, y, superFruit); break; case T.PORTAL_A: case T.PORTAL_A_S: putTileAnim(x, y, portalA); break; case T.PORTAL_B: case T.PORTAL_B_S: putTileAnim(x, y, portalB); break; case T.PORTAL_C: case T.PORTAL_C_S: putTileAnim(x, y, portalC); break; case T.PORTAL_D: case T.PORTAL_D_S: putTileAnim(x, y, portalD); break; case T.KEY: putTile(x, y, key); break; case T.DOOR: putTile(x, y, door); break; case T.SWITCH_ON: case T.SWITCH_ON_S: putTile(x, y, switchTile.on); break; case T.SWITCH_OFF: case T.SWITCH_OFF_S: putTile(x, y, switchTile.off); break; case T.SPIKES_ON: putTile(x, y, spikes.on); break; case T.SPIKES_OFF: case T.SPIKES_OFF_S: putTile(x, y, spikes.off); break; } } } // draw our decaying fruits (they have more information than just XY, so they need to be drawn here game.decayFood.forEach(([x, y, birth]) => putTileAnimPercent(x, y, decayFruit, (game.playTime-birth)/2000) ); // draw the lines between portals if(Object.keys(game.portals).length) { ctx.strokeStyle='rgba(128, 128, 128, 20%)'; ctx.lineCap='round'; ctx.lineWidth=cellSize*.15; const drawTunnel=([xa, ya], [xb, yb]) => { const angle=(Math.floor(Date.now()/10)%360)*(Math.PI/180); for(let i=0; i<=1; i++) { const dx=cellSize/3*Math.cos(angle+i*Math.PI); const dy=cellSize/3*Math.sin(angle+i*Math.PI); ctx.beginPath(); ctx.moveTo( offsetX+cellSize*(xa+1/2)+dx, offsetY+cellSize*(ya+1/2)+dy ); ctx.lineTo( offsetX+cellSize*(xb+1/2)+dx, offsetY+cellSize*(yb+1/2)+dy ); ctx.stroke(); } }; if(game.portals.a && game.portals.b) drawTunnel(game.portals.a, game.portals.b); if(game.portals.c && game.portals.d) drawTunnel(game.portals.c, game.portals.d); } // draw our snake (it gets drawn completely differently, so here it goes) { ctx.fillStyle=snake.color; ctx.strokeStyle=snake.color; ctx.lineCap=snake.cap; ctx.lineJoin=snake.join; ctx.lineWidth=cellSize*snake.tailSize; ctx.beginPath(); ctx.ellipse( offsetX+cellSize*(game.snake[0][0]+1/2), offsetY+cellSize*(game.snake[0][1]+1/2), cellSize/2*snake.headSize, cellSize/2*snake.headSize, 0, 0, Math.PI*2 ); ctx.fill(); ctx.beginPath(); game.snake.forEach(([x, y], i, a) => { ctx.lineTo( offsetX+cellSize*(x+1/2), offsetY+cellSize*(y+1/2) ); if(i!=0 && Math.hypot(x-a[i-1][0], y-a[i-1][1])>1) { ctx.lineWidth=cellSize*snake.tailWrapSize; } else { ctx.lineWidth=cellSize*snake.tailSize; } ctx.stroke(); ctx.beginPath() ctx.moveTo( offsetX+cellSize*(x+1/2), offsetY+cellSize*(y+1/2) ); }); ctx.stroke(); } // our fruit has a nice animation to it between .8 and 1.2 scale { const ms=Date.now(); const fruitScale=Math.sin(ms/400*Math.PI)*.2+1 game.fruits.forEach(([x, y]) => { ctx.drawImage( fruit, offsetX+cellSize*x+(1-fruitScale)*cellSize/2, offsetY+cellSize*y+(1-fruitScale)*cellSize/2, cellSize*fruitScale, cellSize*fruitScale ); }); } // show the timer if(game.rules.winCondition=='time') { if(config.getS('appearance.timer')=='border' || config.getS('appearance.timer')=='both') { let remaining=(game.rules.gameDuration-game.playTime)/game.rules.gameDuration; const w=game.dimensions[0]*cellSize; const h=game.dimensions[1]*cellSize; const p=w*2+h*2; const wp=w/p; const hp=h/p; const pdst=(st, ed, frac) => (ed-st)*frac+st; ctx.strokeStyle='#930a16'; ctx.lineJoin='miter'; ctx.lineCap='round'; ctx.lineWidth=5; ctx.beginPath(); ctx.moveTo(canvas.width/2, offsetY+2); let sp=Math.min(wp/2, remaining); remaining-=sp; ctx.lineTo(pdst(canvas.width/2, w+offsetX-2, sp/wp*2), offsetY+2); if(remaining) { sp=Math.min(hp, remaining); remaining-=sp; ctx.lineTo(w+offsetX-2, pdst(offsetY+2, offsetY+h-2, sp/hp)); } if(remaining) { sp=Math.min(wp, remaining); remaining-=sp; ctx.lineTo(pdst(w+offsetX-2, offsetX+2, sp/wp), offsetY+h-2); } if(remaining) { sp=Math.min(hp, remaining); remaining-=sp; ctx.lineTo(offsetX+2, pdst(offsetY+h-2, offsetY+2, sp/hp)); } if(remaining) { ctx.lineTo(pdst(offsetX+2, canvas.width/2, remaining/wp*2), offsetY+2); } ctx.stroke(); } if(config.getS('appearance.timer')=='number' || config.getS('appearance.timer')=='both') { let remaining=''+Math.ceil((game.rules.gameDuration-game.playTime)/1000); while(remaining.length<(''+game.rules.gameDuration/1000).length) remaining='0'+remaining; ctx.fillStyle='#930a16'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.font='4rem "Fira Code"'; ctx.fillText(remaining, canvas.width/2, canvas.height/2); } } // draw the border around our game area { ctx.fillStyle='black'; ctx.fillRect(0, 0, canvas.width, offsetY); ctx.fillRect(0, 0, offsetX, canvas.height); ctx.fillRect(offsetX+cellSize*game.dimensions[0], 0, offsetX, canvas.height); ctx.fillRect(0, offsetY+cellSize*game.dimensions[1], canvas.width, offsetY); } }; return module.exports={ draw };