|
<!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> |
Also, avoid using
UIEvent.which
in favor ofKeyboardEvent.code
(more specific, based on keyboard layout) orKeyboardEvent.key
(general, not based on keyboard layout).The main difference between the two is that
code
will report the physical key on the keyboard, whereaskey
will only report the key type. For example, if you press the "Right Shift" key, the value ofcode
will beShiftRight
, but the value ofkey
will be simplyShift
. If you need to treat the left and rightAlt
,Ctrl
, orShift
keys as different actions then you should usecode
. If you do not care, just stick with the saferkey
.For the alpha/number/arrow keys the value will typically be the same.
I noticed that Steven's later games i.e. Helicopter and Puzzle Bobble use
code
instead ofwhich
.Here is a simple website for checking keyboard event properties: https://www.toptal.com/developers/keycode