|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Basic Bomberman HTML Game</title> |
|
<meta charset="UTF-8"> |
|
<style> |
|
html, body { |
|
height: 100%; |
|
margin: 0; |
|
} |
|
|
|
body { |
|
background: black; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
canvas { |
|
background: forestgreen; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<canvas width="960" height="832" id="game"></canvas> |
|
<script> |
|
const canvas = document.getElementById('game'); |
|
const context = canvas.getContext('2d'); |
|
const grid = 64; |
|
const numRows = 13; |
|
const numCols = 15; |
|
|
|
// create a new canvas and draw the soft wall image. then we can use this |
|
// canvas to draw the images later on |
|
const softWallCanvas = document.createElement('canvas'); |
|
const softWallCtx = softWallCanvas.getContext('2d'); |
|
softWallCanvas.width = softWallCanvas.height = grid; |
|
|
|
softWallCtx.fillStyle = 'black'; |
|
softWallCtx.fillRect(0, 0, grid, grid); |
|
softWallCtx.fillStyle = '#a9a9a9'; |
|
|
|
// 1st row brick |
|
softWallCtx.fillRect(1, 1, grid - 2, 20); |
|
|
|
// 2nd row bricks |
|
softWallCtx.fillRect(0, 23, 20, 18); |
|
softWallCtx.fillRect(22, 23, 42, 18); |
|
|
|
// 3rd row bricks |
|
softWallCtx.fillRect(0, 43, 42, 20); |
|
softWallCtx.fillRect(44, 43, 20, 20); |
|
|
|
// create a new canvas and draw the soft wall image. then we can use this |
|
// canvas to draw the images later on |
|
const wallCanvas = document.createElement('canvas'); |
|
const wallCtx = wallCanvas.getContext('2d'); |
|
wallCanvas.width = wallCanvas.height = grid; |
|
|
|
wallCtx.fillStyle = 'black'; |
|
wallCtx.fillRect(0, 0, grid, grid); |
|
wallCtx.fillStyle = 'white'; |
|
wallCtx.fillRect(0, 0, grid - 2, grid - 2); |
|
wallCtx.fillStyle = '#a9a9a9'; |
|
wallCtx.fillRect(2, 2, grid - 4, grid - 4); |
|
|
|
// create a mapping of object types |
|
const types = { |
|
wall: '▉', |
|
softWall: 1, |
|
bomb: 2 |
|
}; |
|
|
|
// keep track of all entities |
|
let entities = []; |
|
|
|
// keep track of what is in every cell of the game using a 2d array. the |
|
// template is used to note where walls are and where soft walls cannot spawn. |
|
// '▉' represents a wall |
|
// 'x' represents a cell that cannot have a soft wall (player start zone) |
|
let cells = []; |
|
const template = [ |
|
['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉'], |
|
['▉','x','x', , , , , , , , , ,'x','x','▉'], |
|
['▉','x','▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉','x','▉'], |
|
['▉','x', , , , , , , , , , , ,'x','▉'], |
|
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'], |
|
['▉', , , , , , , , , , , , , ,'▉'], |
|
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'], |
|
['▉', , , , , , , , , , , , , ,'▉'], |
|
['▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉'], |
|
['▉','x', , , , , , , , , , , ,'x','▉'], |
|
['▉','x','▉', ,'▉', ,'▉', ,'▉', ,'▉', ,'▉','x','▉'], |
|
['▉','x','x', , , , , , , , , ,'x','x','▉'], |
|
['▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉','▉'] |
|
]; |
|
|
|
// populate the level with walls and soft walls |
|
function generateLevel() { |
|
cells = []; |
|
|
|
for (let row = 0; row < numRows; row++) { |
|
cells[row] = []; |
|
|
|
for (let col = 0; col < numCols; col++) { |
|
|
|
// 90% chance cells will contain a soft wall |
|
if (!template[row][col] && Math.random() < 0.90) { |
|
cells[row][col] = types.softWall; |
|
} |
|
else if (template[row][col] === types.wall) { |
|
cells[row][col] = types.wall; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// blow up a bomb and its surrounding tiles |
|
function blowUpBomb(bomb) { |
|
|
|
// bomb has already exploded so don't blow up again |
|
if (!bomb.alive) return; |
|
|
|
bomb.alive = false; |
|
|
|
// remove bomb from grid |
|
cells[bomb.row][bomb.col] = null; |
|
|
|
// explode bomb outward by size |
|
const dirs = [{ |
|
// up |
|
row: -1, |
|
col: 0 |
|
}, { |
|
// down |
|
row: 1, |
|
col: 0 |
|
}, { |
|
// left |
|
row: 0, |
|
col: -1 |
|
}, { |
|
// right |
|
row: 0, |
|
col: 1 |
|
}]; |
|
dirs.forEach((dir) => { |
|
for (let i = 0; i < bomb.size; i++) { |
|
const row = bomb.row + dir.row * i; |
|
const col = bomb.col + dir.col * i; |
|
const cell = cells[row][col]; |
|
|
|
// stop the explosion if it hit a wall |
|
if (cell === types.wall) { |
|
return; |
|
} |
|
|
|
// center of the explosion is the first iteration of the loop |
|
entities.push(new Explosion(row, col, dir, i === 0 ? true : false)); |
|
cells[row][col] = null; |
|
|
|
// bomb hit another bomb so blow that one up too |
|
if (cell === types.bomb) { |
|
|
|
// find the bomb that was hit by comparing positions |
|
const nextBomb = entities.find((entity) => { |
|
return ( |
|
entity.type === types.bomb && |
|
entity.row === row && entity.col === col |
|
); |
|
}); |
|
blowUpBomb(nextBomb); |
|
} |
|
|
|
// stop the explosion if hit anything |
|
if (cell) { |
|
return; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
// bomb constructor function |
|
function Bomb(row, col, size, owner) { |
|
this.row = row; |
|
this.col = col; |
|
this.radius = grid * 0.4; |
|
this.size = size; // the size of the explosion |
|
this.owner = owner; // which player placed this bomb |
|
this.alive = true; |
|
this.type = types.bomb; |
|
|
|
// bomb blows up after 3 seconds |
|
this.timer = 3000; |
|
|
|
// update the bomb each frame |
|
this.update = function(dt) { |
|
this.timer -= dt; |
|
|
|
// blow up bomb if timer is done |
|
if (this.timer <= 0) { |
|
return blowUpBomb(this); |
|
} |
|
|
|
// change the size of the bomb every half second. we can determine the size |
|
// by dividing by 500 (half a second) and taking the ceiling of the result. |
|
// then we can check if the result is even or odd and change the size |
|
const interval = Math.ceil(this.timer / 500); |
|
if (interval % 2 === 0) { |
|
this.radius = grid * 0.4; |
|
} |
|
else { |
|
this.radius = grid * 0.5; |
|
} |
|
}; |
|
|
|
// render the bomb each frame |
|
this.render = function() { |
|
const x = (this.col + 0.5) * grid; |
|
const y = (this.row + 0.5) * grid; |
|
|
|
// draw bomb |
|
context.fillStyle = 'black'; |
|
context.beginPath(); |
|
context.arc(x, y, this.radius, 0, 2 * Math.PI); |
|
context.fill(); |
|
|
|
// draw bomb fuse moving up and down with the bomb size |
|
const fuseY = (this.radius === grid * 0.5 ? grid * 0.15 : 0); |
|
context.strokeStyle = 'white'; |
|
context.lineWidth = 5; |
|
context.beginPath(); |
|
context.arc( |
|
(this.col + 0.75) * grid, |
|
(this.row + 0.25) * grid - fuseY, |
|
10, Math.PI, -Math.PI / 2 |
|
); |
|
context.stroke(); |
|
}; |
|
} |
|
|
|
// explosion constructor function |
|
function Explosion(row, col, dir, center) { |
|
this.row = row; |
|
this.col = col; |
|
this.dir = dir; |
|
this.alive = true; |
|
|
|
// show explosion for 0.3 seconds |
|
this.timer = 300; |
|
|
|
// update the explosion each frame |
|
this.update = function(dt) { |
|
this.timer -= dt; |
|
|
|
if (this.timer <=0) { |
|
this.alive = false; |
|
} |
|
}; |
|
|
|
// render the explosion each frame |
|
this.render = function() { |
|
const x = this.col * grid; |
|
const y = this.row * grid; |
|
const horizontal = this.dir.col; |
|
const vertical = this.dir.row; |
|
|
|
// create a fire effect by stacking red, orange, and yellow on top of |
|
// each other using progressively smaller rectangles |
|
context.fillStyle = '#D72B16'; // red |
|
context.fillRect(x, y, grid, grid); |
|
|
|
context.fillStyle = '#F39642'; // orange |
|
|
|
// determine how to draw based on if it's vertical or horizontal |
|
// center draws both ways |
|
if (center || horizontal) { |
|
context.fillRect(x, y + 6, grid, grid - 12); |
|
} |
|
if (center || vertical) { |
|
context.fillRect(x + 6, y, grid - 12, grid); |
|
} |
|
|
|
context.fillStyle = '#FFE5A8'; // yellow |
|
|
|
if (center || horizontal) { |
|
context.fillRect(x, y + 12, grid, grid - 24); |
|
} |
|
if (center || vertical) { |
|
context.fillRect(x + 12, y, grid - 24, grid); |
|
} |
|
}; |
|
} |
|
|
|
// player character (just a simple circle) |
|
const player = { |
|
row: 1, |
|
col: 1, |
|
numBombs: 1, |
|
bombSize: 3, |
|
radius: grid * 0.35, |
|
render() { |
|
const x = (this.col + 0.5) * grid; |
|
const y = (this.row + 0.5) * grid; |
|
|
|
context.save(); |
|
context.fillStyle = 'white'; |
|
context.beginPath(); |
|
context.arc(x, y, this.radius, 0, 2 * Math.PI); |
|
context.fill(); |
|
} |
|
} |
|
|
|
// game loop |
|
let last; |
|
let dt; |
|
function loop(timestamp) { |
|
requestAnimationFrame(loop); |
|
context.clearRect(0,0,canvas.width,canvas.height); |
|
|
|
// calculate the time difference since the last update. requestAnimationFrame |
|
// passes the current timestamp as a parameter to the loop |
|
if (!last) { |
|
last = timestamp; |
|
} |
|
dt = timestamp - last; |
|
last = timestamp; |
|
|
|
// update and render everything in the grid |
|
for (let row = 0; row < numRows; row++) { |
|
for (let col = 0; col < numCols; col++) { |
|
switch(cells[row][col]) { |
|
case types.wall: |
|
context.drawImage(wallCanvas, col * grid, row * grid); |
|
break; |
|
case types.softWall: |
|
context.drawImage(softWallCanvas, col * grid, row * grid); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// update and render all entities |
|
entities.forEach((entity) => { |
|
entity.update(dt); |
|
entity.render(); |
|
}); |
|
|
|
// remove dead entities |
|
entities = entities.filter((entity) => entity.alive); |
|
|
|
player.render(); |
|
} |
|
|
|
// listen to keyboard events to move the snake |
|
document.addEventListener('keydown', function(e) { |
|
let row = player.row; |
|
let col = player.col; |
|
|
|
// left arrow key |
|
if (e.which === 37) { |
|
col--; |
|
} |
|
// up arrow key |
|
else if (e.which === 38) { |
|
row--; |
|
} |
|
// right arrow key |
|
else if (e.which === 39) { |
|
col++; |
|
} |
|
// down arrow key |
|
else if (e.which === 40) { |
|
row++; |
|
} |
|
// space key (bomb) |
|
else if ( |
|
e.which === 32 && !cells[row][col] && |
|
// count the number of bombs the player has placed |
|
entities.filter((entity) => { |
|
return entity.type === types.bomb && entity.owner === player |
|
}).length < player.numBombs |
|
) { |
|
// place bomb |
|
const bomb = new Bomb(row, col, player.bombSize, player); |
|
entities.push(bomb); |
|
cells[row][col] = types.bomb; |
|
} |
|
|
|
// don't move the player if something is already at that position |
|
if (!cells[row][col]) { |
|
player.row = row; |
|
player.col = col; |
|
} |
|
}); |
|
|
|
// start the game |
|
generateLevel(); |
|
requestAnimationFrame(loop); |
|
</script> |
|
</body> |
|
</html> |
@pusheen13 it's a bit outside the scope of this project, though I'm sure someone has to have done it already.