330 lines
8.5 KiB
JavaScript
330 lines
8.5 KiB
JavaScript
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<game.dimensions[0]; x++) {
|
|
ctx.moveTo(offsetX+x*cellSize, offsetY);
|
|
ctx.lineTo(offsetX+x*cellSize, canvas.height-offsetY);
|
|
}
|
|
for(let y=1; y<game.dimensions[1]; y++) {
|
|
ctx.moveTo(offsetX, offsetY+y*cellSize);
|
|
ctx.lineTo(canvas.width-offsetX, offsetY+y*cellSize);
|
|
}
|
|
ctx.stroke();
|
|
} else if(config.getS('appearance.grid')=='checkerboard') {
|
|
ctx.fillStyle='rgba(0, 0, 0, 10%)';
|
|
for(let x=0; x<game.dimensions[0]; x++) {
|
|
for(let y=(x+1)%2; y<game.dimensions[1]; y+=2) {
|
|
ctx.fillRect(offsetX+x*cellSize, offsetY+y*cellSize, cellSize, cellSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw our tiles
|
|
for(let x=0; x<game.dimensions[0]; x++) {
|
|
for(let y=0; y<game.dimensions[1]; y++) {
|
|
switch(game.world[x][y]) {
|
|
case T.WALL:
|
|
putTile(x, y, wall);
|
|
break;
|
|
|
|
case T.FIRE:
|
|
putTileAnim(x, y, fire);
|
|
break;
|
|
|
|
case T.HOLE:
|
|
case T.HOLE_S: {
|
|
putTile(x, y, hole.base);
|
|
let adj=checkAdj(x, y);
|
|
Object
|
|
.keys(adj)
|
|
.filter(k => 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
|
|
};
|