├── .gitignore
├── README.md
├── src
├── player.js
├── steel-wall.js
├── sprite.js
├── animation.js
├── wall.js
├── event-emitter.js
├── bullet-explosion.js
├── base.js
├── tank-explosion.js
├── explosion.js
├── input.js
├── player-tank.js
├── game.js
├── brick-wall.js
├── stages.js
├── enemy-tank.js
├── game-object.js
├── bullet.js
├── tank.js
├── constants.js
├── view.js
└── stage.js
├── index.css
├── index.js
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | /assets
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Battle City
2 |
--------------------------------------------------------------------------------
/src/player.js:
--------------------------------------------------------------------------------
1 | export default class Player {
2 | score = 0;
3 | lives = 2;
4 | }
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | background-color: black;
5 | min-width: 100vw;
6 | min-height: 100vh;
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | }
--------------------------------------------------------------------------------
/src/steel-wall.js:
--------------------------------------------------------------------------------
1 | import { STEEL_WALL_SPRITES } from './constants.js';
2 | import Wall from './wall.js';
3 |
4 | export default class SteelWall extends Wall {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.sprites = STEEL_WALL_SPRITES;
9 | }
10 |
11 | hit(bullet) {
12 | if (this.isDestroyed) return;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/sprite.js:
--------------------------------------------------------------------------------
1 | export default class Sprite {
2 | constructor(src) {
3 | this.src = src;
4 | this.image = new Image();
5 | }
6 |
7 | async load() {
8 | return new Promise((resolve, reject) => {
9 | this.image.src = this.src;
10 | this.image.addEventListener('load', () => resolve(this));
11 | });
12 | }
13 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Input from './src/input.js';
2 | import View from './src/view.js';
3 | import Game from './src/game.js';
4 | import Sprite from './src/sprite.js';
5 |
6 | const canvas = document.querySelector('canvas');
7 | const sprite = new Sprite('./assets/sprite.png');
8 |
9 | const game = new Game({
10 | input: new Input(),
11 | view: new View(canvas, sprite)
12 | });
13 |
14 | game.init().then(() => game.start());
15 |
16 | console.log(game);
--------------------------------------------------------------------------------
/src/animation.js:
--------------------------------------------------------------------------------
1 | export default class Animation {
2 | constructor({ sprites = [], speed = 0 }) {
3 | this.sprites = sprites;
4 | this.speed = speed;
5 | this.frame = 0;
6 | this.frames = 0;
7 | }
8 |
9 | run(frameDelta) {
10 | this.frames += frameDelta;
11 |
12 | if (this.frames > this.speed) {
13 | this.frame = (this.frame + 1) % (this.sprites.length + 1);
14 | this.frames = 0;
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/wall.js:
--------------------------------------------------------------------------------
1 | import { TILE_SIZE } from './constants.js';
2 |
3 | import GameObject from './game-object.js';
4 |
5 | export default class Wall extends GameObject {
6 | constructor({ type, ...rest }) {
7 | super(rest);
8 |
9 | this.type = 'wall';
10 | this.width = TILE_SIZE;
11 | this.height = TILE_SIZE;
12 | this.spriteIndex = 0;
13 | this.damage = 0;
14 | }
15 |
16 | get sprite() {
17 | return this.sprites[this.spriteIndex];
18 | }
19 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Battle City
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/event-emitter.js:
--------------------------------------------------------------------------------
1 | export default class EventManager {
2 | events = new Map();
3 |
4 | on(event, handler) {
5 | if (this.events.has(event)) {
6 | this.events.get(event).add(handler);
7 | } else {
8 | this.events.set(event, new Set([handler]));
9 | }
10 | }
11 |
12 | off(event, handler) {
13 | this.events.get(event)?.delete(handler);
14 | }
15 |
16 | emit(event, arg) {
17 | this.events.get(event)?.forEach(handler => handler(arg));
18 | }
19 | }
--------------------------------------------------------------------------------
/src/bullet-explosion.js:
--------------------------------------------------------------------------------
1 | import { BULLET_EXPLOSION_WIDTH, BULLET_EXPLOSION_HEIGHT, BULLET_EXPLOSION_ANIMATION_SPEED, BULLET_EXPLOSION_SPRITES } from './constants.js';
2 | import Explosion from './explosion.js';
3 |
4 | export default class BulletExplosion extends Explosion {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.width = BULLET_EXPLOSION_WIDTH;
9 | this.height = BULLET_EXPLOSION_HEIGHT;
10 | this.sprites = BULLET_EXPLOSION_SPRITES;
11 | this.animationSpeed = BULLET_EXPLOSION_ANIMATION_SPEED;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/base.js:
--------------------------------------------------------------------------------
1 | import { BASE_POSITION, BASE_WIDTH, BASE_HEIGHT, BASE_SPRITES } from './constants.js';
2 | import GameObject from './game-object.js';
3 |
4 | export default class Base extends GameObject {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.x = BASE_POSITION[0];
9 | this.y = BASE_POSITION[1];
10 | this.width = BASE_WIDTH;
11 | this.height = BASE_HEIGHT;
12 | this.sprites = BASE_SPRITES;
13 | this.destroyed = false;
14 | }
15 |
16 | get sprite() {
17 | return this.sprites[Number(this.destroyed)];
18 | }
19 |
20 | hit() {
21 | this.emit('destroyed', this);
22 | }
23 | }
--------------------------------------------------------------------------------
/src/tank-explosion.js:
--------------------------------------------------------------------------------
1 | import { TANK_EXPLOSION_ANIMATION_SPEED, TANK_EXPLOSION_SPRITES } from './constants.js';
2 | import Explosion from './explosion.js';
3 |
4 | export default class TankExplosion extends Explosion {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.sprites = TANK_EXPLOSION_SPRITES;
9 | this.animationSpeed = TANK_EXPLOSION_ANIMATION_SPEED;
10 | }
11 |
12 | get x() {
13 | return super.x - this.width * 0.5;
14 | }
15 |
16 | get y() {
17 | return super.y - this.height * 0.5;
18 | }
19 |
20 | get width() {
21 | return this.sprite && this.sprite[2];
22 | }
23 |
24 | get height() {
25 | return this.sprite && this.sprite[3];
26 | }
27 | }
--------------------------------------------------------------------------------
/src/explosion.js:
--------------------------------------------------------------------------------
1 | import GameObject from './game-object.js';
2 |
3 | export default class Explosion extends GameObject {
4 | constructor(args) {
5 | super(args);
6 |
7 | this.type = 'explosion';
8 | }
9 |
10 | get sprite() {
11 | return this.sprites[this.animationFrame];
12 | }
13 |
14 | get isExploding() {
15 | return this.animationFrame < this.sprites.length;
16 | }
17 |
18 | update({ frameDelta }) {
19 | if (this.isExploding) {
20 | this.animate(frameDelta);
21 | } else {
22 | this.destroy();
23 | }
24 | }
25 |
26 | animate(frameDelta) {
27 | this.frames += frameDelta;
28 |
29 | if (this.frames > this.animationSpeed) {
30 | this.animationFrame = (this.animationFrame + 1) % (this.sprites.length + 1);
31 | this.frames = 0;
32 | }
33 | }
34 |
35 | hit() {
36 | return;
37 | }
38 |
39 | destroy() {
40 | this.emit('destroyed', this);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/input.js:
--------------------------------------------------------------------------------
1 | export default class Input {
2 | constructor() {
3 | this.keys = new Set();
4 | this.init();
5 | }
6 |
7 | init() {
8 | document.addEventListener('keydown', event => {
9 | switch (event.code) {
10 | case 'ArrowUp':
11 | case 'ArrowRight':
12 | case 'ArrowDown':
13 | case 'ArrowLeft':
14 | case 'Space':
15 | case 'Enter':
16 | event.preventDefault();
17 | this.keys.add(event.code);
18 | }
19 | });
20 |
21 | document.addEventListener('keyup', event => {
22 | switch (event.code) {
23 | case 'ArrowUp':
24 | case 'ArrowRight':
25 | case 'ArrowDown':
26 | case 'ArrowLeft':
27 | case 'Space':
28 | case 'Enter':
29 | event.preventDefault();
30 | this.keys.delete(event.code);
31 | }
32 | });
33 | }
34 |
35 | has(...arg) {
36 | return Array.isArray(arg) ?
37 | arg.some(key => this.keys.has(key)) :
38 | this.keys.has(arg);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/player-tank.js:
--------------------------------------------------------------------------------
1 | import { Key, Direction, PLAYER1_TANK_POSITION, PLAYER1_TANK_SPRITES, TANK_SPEED } from './constants.js';
2 | import Tank from './tank.js';
3 |
4 | export default class PlayerTank extends Tank {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.type = 'playerTank';
9 | this.x = PLAYER1_TANK_POSITION[0];
10 | this.y = PLAYER1_TANK_POSITION[1];
11 | this.direction = Direction.UP;
12 | this.speed = TANK_SPEED;
13 | this.sprites = PLAYER1_TANK_SPRITES;
14 | }
15 |
16 | update({ input, frameDelta, world }) {
17 | if (input.has(Key.UP, Key.RIGHT, Key.DOWN, Key.LEFT)) {
18 | const direction = Tank.getDirectionForKeys(input.keys);
19 | const axis = Tank.getAxisForDirection(direction);
20 | const value = Tank.getValueForDirection(direction);
21 |
22 | this.turn(direction);
23 | this.move(axis, value);
24 | this.animate(frameDelta);
25 |
26 | const isOutOfBounds = world.isOutOfBounds(this);
27 | const hasCollision = world.hasCollision(this);
28 |
29 | if (isOutOfBounds || hasCollision) {
30 | this.move(axis, -value);
31 | }
32 | }
33 |
34 | if (input.keys.has(Key.SPACE)) {
35 | this.fire();
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/game.js:
--------------------------------------------------------------------------------
1 | import stages from './stages.js';
2 | import Stage from './stage.js';
3 |
4 | export default class Game {
5 | constructor({ input, view }) {
6 | this.input = input;
7 | this.view = view;
8 | this.stages = stages;
9 | this.player1 = null;
10 | this.player2 = null;
11 | this.stage = null;
12 | this.stageIndex = 0;
13 | this.frames = 0;
14 | this.lastFrame = 0;
15 |
16 | this.loop = this.loop.bind(this);
17 | this.onGameOver = this.onGameOver.bind(this);
18 | }
19 |
20 | async init() {
21 | await this.view.init();
22 | }
23 |
24 | start() {
25 | this.stage = new Stage(stages[this.stageIndex]);
26 | this.stage.number = this.stageIndex + 1;
27 | this.stage.on('gameOver', this.onGameOver);
28 |
29 | requestAnimationFrame(this.loop);
30 | }
31 |
32 | loop(currentFrame) {
33 | const frameDelta = currentFrame - this.lastFrame;
34 |
35 | this.stage.update(this.input, frameDelta);
36 | this.view.update(this.stage, this.player1, this.player2);
37 | this.frames = 0;
38 |
39 | this.lastFrame = currentFrame;
40 |
41 | requestAnimationFrame(this.loop);
42 | }
43 |
44 | onGameOver() {
45 | // show game over screen
46 | console.log('GAME OVER');
47 | }
48 | }
--------------------------------------------------------------------------------
/src/brick-wall.js:
--------------------------------------------------------------------------------
1 | import { Direction, BRICK_WALL_SPRITES, BRICK_WALL_SPRITE_MAP } from './constants.js';
2 | import Wall from './wall.js';
3 |
4 | export default class BrickWall extends Wall {
5 | constructor(args) {
6 | super(args);
7 |
8 | this.sprites = BRICK_WALL_SPRITES;
9 | this.state = 0b0000;
10 | this.isDestructable = true;
11 | this.isDestroyed = false;
12 | this.lastHitDirection = -1;
13 | }
14 |
15 | get sprite() {
16 | return this.sprites[BRICK_WALL_SPRITE_MAP[this.state]];
17 | }
18 |
19 | update({ world }) {
20 | if (this.isDestroyed) {
21 | world.objects.delete(this);
22 | }
23 | }
24 |
25 | hit(bullet) {
26 | if (this.isDestroyed) return;
27 |
28 | this.damage += 1;
29 |
30 | if (this.damage === 2) {
31 | this.isDestroyed = true;
32 | }
33 |
34 | switch (bullet.direction) {
35 | case Direction.UP:
36 | this.state = this.state | 0b0001;
37 | break;
38 | case Direction.RIGHT:
39 | this.state = this.state | 0b0010;
40 | break;
41 | case Direction.DOWN:
42 | this.state = this.state | 0b0100;
43 | break;
44 | case Direction.LEFT:
45 | this.state = this.state | 0b1000;
46 | break;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/stages.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | map: [
4 | [, , , , , , , , , , , , , , , , , , , , , , , , ,],
5 | [, , , , , , , , , , , , , , , , , , , , , , , , ,],
6 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
7 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
8 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
9 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
10 | [, , 1, 1, , , 1, 1, , , 1, 1, 2, 2, 1, 1, , , 1, 1, , , 1, 1, ,],
11 | [, , 1, 1, , , 1, 1, , , 1, 1, 2, 2, 1, 1, , , 1, 1, , , 1, 1, ,],
12 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
13 | [, , 1, 1, , , 1, 1, , , , , , , , , , , 1, 1, , , 1, 1, ,],
14 | [, , 1, 1, , , 1, 1, , , , , , , , , , , 1, 1, , , 1, 1, ,],
15 | [, , , , , , , , , , 1, 1, , , 1, 1, , , , , , , , , ,],
16 | [, , , , , , , , , , 1, 1, , , 1, 1, , , , , , , , , ,],
17 | [1, 1, , , 1, 1, 1, 1, , , , , , , , , , , 1, 1, 1, 1, , , 1, 1],
18 | [2, 2, , , 1, 1, 1, 1, , , , , , , , , , , 1, 1, 1, 1, , , 2, 2],
19 | [, , , , , , , , , , 1, 1, , , 1, 1, , , , , , , , , ,],
20 | [, , , , , , , , , , 1, 1, 1, 1, 1, 1, , , , , , , , , ,],
21 | [, , 1, 1, , , 1, 1, , , 1, 1, 1, 1, 1, 1, , , 1, 1, , , 1, 1, ,],
22 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
23 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
24 | [, , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, , , 1, 1, ,],
25 | [, , 1, 1, , , 1, 1, , , , , , , , , , , 1, 1, , , 1, 1, ,],
26 | [, , 1, 1, , , 1, 1, , , , , , , , , , , 1, 1, , , 1, 1, ,],
27 | [, , 1, 1, , , 1, 1, , , , 1, 1, 1, 1, , , , 1, 1, , , 1, 1, ,],
28 | [, , , , , , , , , , , 1, , , 1, , , , , , , , , , ,],
29 | [, , , , , , , , , , , 1, , , 1, , , , , , , , , , ,]
30 | ],
31 | enemies: []
32 | //enemies: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
33 | }
34 | ];
--------------------------------------------------------------------------------
/src/enemy-tank.js:
--------------------------------------------------------------------------------
1 | import { Direction, ENEMY_TANK_START_POSITIONS, ENEMY_TANK_SPRITES, ENEMY_TANK_SPEED, ENEMY_TANK_TURN_TIMER_THRESHOLD } from './constants.js';
2 | import Tank from './tank.js';
3 |
4 | export default class EnemyTank extends Tank {
5 | static createRandom() {
6 | const random = Math.floor(Math.random() * 3);
7 | const [x, y] = ENEMY_TANK_START_POSITIONS[random];
8 | const sprites = ENEMY_TANK_SPRITES[0];
9 |
10 | return new EnemyTank({ x, y, sprites });
11 | }
12 |
13 | constructor(args) {
14 | super(args);
15 |
16 | this.type = 'enemyTank';
17 | this.x = 0;
18 | this.y = 0;
19 | this.direction = Direction.DOWN;
20 | this.speed = ENEMY_TANK_SPEED;
21 | this.sprites = ENEMY_TANK_SPRITES[0];
22 |
23 | this.turnTimer = 0;
24 | }
25 |
26 | setPosition(positionIndex) {
27 | this.x = ENEMY_TANK_START_POSITIONS[positionIndex][0];
28 | this.y = ENEMY_TANK_START_POSITIONS[positionIndex][1];
29 | }
30 |
31 | update({ world, frameDelta }) {
32 | if (this.isDestroyed) {
33 | this.explode();
34 | this.destroy();
35 | }
36 |
37 | const direction = this.direction;
38 | const axis = Tank.getAxisForDirection(direction);
39 | const value = Tank.getValueForDirection(direction);
40 |
41 | this.move(axis, value);
42 | this.fire();
43 | this.animate(frameDelta);
44 |
45 | const isOutOfBounds = world.isOutOfBounds(this);
46 | const hasCollision = world.hasCollision(this);
47 |
48 | if (isOutOfBounds || hasCollision) {
49 | this.move(axis, -value);
50 |
51 | if (this.shouldTurn(frameDelta)) {
52 | this.turnRandomly();
53 | }
54 | }
55 | }
56 |
57 | hit(bullet) {
58 | if (bullet.isFromEnemyTank) return;
59 |
60 | super.hit();
61 | }
62 |
63 | shouldTurn(frameDelta) {
64 | this.turnTimer += frameDelta;
65 |
66 | return this.turnTimer > ENEMY_TANK_TURN_TIMER_THRESHOLD;
67 | }
68 |
69 | turnRandomly() {
70 | const randomDirection = Math.floor(Math.random() * 4);
71 |
72 | this.turnTimer = 0;
73 | this.turn(randomDirection);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/game-object.js:
--------------------------------------------------------------------------------
1 | import { Direction } from './constants.js';
2 | import EventEmitter from './event-emitter.js';
3 |
4 | export default class GameObject extends EventEmitter {
5 | static getDirectionForKeys(keys) {
6 | if (keys.has('ArrowUp')) {
7 | return Direction.UP;
8 | } else if (keys.has('ArrowRight')) {
9 | return Direction.RIGHT;
10 | } else if (keys.has('ArrowDown')) {
11 | return Direction.DOWN;
12 | } else if (keys.has('ArrowLeft')) {
13 | return Direction.LEFT;
14 | }
15 | }
16 |
17 | static getAxisForDirection(direction) {
18 | return direction % 2 === 0 ? 'y' : 'x';
19 | }
20 |
21 | static getValueForDirection(direction) {
22 | switch (direction) {
23 | case Direction.UP: return -1;
24 | case Direction.RIGHT: return 1;
25 | case Direction.DOWN: return 1;
26 | case Direction.LEFT: return -1;
27 | }
28 | }
29 |
30 | static getSideForDirection(direction) {
31 | switch (direction) {
32 | case Direction.UP: return 'top';
33 | case Direction.RIGHT: return 'right';
34 | case Direction.DOWN: return 'bottom';
35 | case Direction.LEFT: return 'left';
36 | }
37 | }
38 |
39 | constructor({ x, y, width, height, sprites } = {}) {
40 | super();
41 |
42 | this._x = x;
43 | this._y = y;
44 | this._width = width;
45 | this._height = height;
46 | this.sprites = sprites;
47 | this.animationFrame = 0;
48 | this.animationSpeed = 0;
49 | this.frames = 0;
50 | this.isDestructable = false;
51 | this.isDestroyed = false;
52 | }
53 |
54 | get x() {
55 | return this._x;
56 | }
57 |
58 | set x(value) {
59 | this._x = value;
60 | }
61 |
62 | get y() {
63 | return this._y;
64 | }
65 |
66 | set y(value) {
67 | this._y = value;
68 | }
69 |
70 | get width() {
71 | return this._width;
72 | }
73 |
74 | set width(value) {
75 | this._width = value;
76 | }
77 |
78 | get height() {
79 | return this._height;
80 | }
81 |
82 | set height(value) {
83 | this._height = value;
84 | }
85 |
86 | get top() {
87 | return this.y;
88 | }
89 |
90 | get right() {
91 | return this.x + this.width;
92 | }
93 |
94 | get bottom() {
95 | return this.y + this.height;
96 | }
97 |
98 | get left() {
99 | return this.x;
100 | }
101 |
102 | update() {
103 |
104 | }
105 |
106 | move(axis, value) {
107 | this[axis] += value * this.speed;
108 | }
109 |
110 | stop() {
111 | this.speed = 0;
112 | }
113 | }
--------------------------------------------------------------------------------
/src/bullet.js:
--------------------------------------------------------------------------------
1 | import { BULLET_WIDTH, BULLET_HEIGHT, BULLET_SPRITES, Direction } from './constants.js';
2 | import GameObject from './game-object.js';
3 | import BulletExplosion from './bullet-explosion.js';
4 |
5 | export default class Bullet extends GameObject {
6 | constructor({ tank, direction, speed, ...args }) {
7 | super(args);
8 |
9 | this.type = 'bullet';
10 | this.width = BULLET_WIDTH;
11 | this.height = BULLET_HEIGHT;
12 | this.sprites = BULLET_SPRITES;
13 | this.direction = direction;
14 | this.speed = speed;
15 | this.tank = tank;
16 | this.explosion = null;
17 | }
18 |
19 | get sprite() {
20 | return this.sprites[this.direction];
21 | }
22 |
23 | get isExploding() {
24 | return Boolean(this.explosion);
25 | }
26 |
27 | get isFromEnemyTank() {
28 | return this.tank?.type === 'enemyTank';
29 | }
30 |
31 | get isFromPlayerTank() {
32 | return this.tank?.type === 'playerTank';
33 | }
34 |
35 | update({ world }) {
36 | if (this.isExploding) return;
37 |
38 | const axis = GameObject.getAxisForDirection(this.direction);
39 | const value = GameObject.getValueForDirection(this.direction);
40 |
41 | this.move(axis, value);
42 |
43 | if (world.isOutOfBounds(this)) return this.stop();
44 |
45 | const collision = world.getCollision(this);
46 |
47 | if (collision) {
48 | this.collide(collision.objects);
49 | }
50 | }
51 |
52 | collide(objects) {
53 | let shouldExplode = false;
54 |
55 | for (const object of objects) {
56 | if (!this.shouldCollide(object)) continue;
57 |
58 | object.hit(this);
59 | shouldExplode = this.shouldExplode(object);
60 | }
61 |
62 | if (shouldExplode) {
63 | this.stop();
64 | this.explode();
65 | }
66 | }
67 |
68 | shouldCollide(object) {
69 | return (
70 | object.type === 'wall' ||
71 | (object.type === 'playerTank' && this.isFromEnemyTank) ||
72 | (object.type === 'enemyTank' && this.isFromPlayerTank) ||
73 | (object.type === 'bullet' && this.isFromEnemyTank && object.isFromPlayerTank) ||
74 | (object.type === 'bullet' && this.isFromPlayerTank && object.isFromEnemyTank)
75 | );
76 | }
77 |
78 | shouldExplode(object) {
79 | return object.type !== 'bullet';
80 | }
81 |
82 | hit() {
83 | this.stop();
84 | this.destroy();
85 | }
86 |
87 | explode() {
88 | const [x, y] = this.getExplosionStartingPosition();
89 |
90 | this.explosion = new BulletExplosion({ x, y });
91 | this.explosion.on('destroyed', () => this.destroy());
92 | this.emit('explode', this.explosion);
93 | }
94 |
95 | destroy() {
96 | this.tank = null;
97 | this.explosion = null;
98 | this.emit('destroyed', this);
99 | }
100 |
101 | getExplosionStartingPosition() {
102 | switch (this.direction) {
103 | case Direction.UP: return [this.left - 10, this.top - 12];
104 | case Direction.RIGHT: return [this.right - 16, this.top - 12];
105 | case Direction.DOWN: return [this.left - 10, this.bottom - 16];
106 | case Direction.LEFT: return [this.left - 16, this.top - 12];
107 | }
108 | }
109 | }
--------------------------------------------------------------------------------
/src/tank.js:
--------------------------------------------------------------------------------
1 | import { Direction, TILE_SIZE, TANK_WIDTH, TANK_HEIGHT, TANK_SPEED, TANK_TURN_THRESHOLD } from './constants.js';
2 | import GameObject from './game-object.js';
3 | import Bullet from './bullet.js';
4 | import TankExplosion from './tank-explosion.js';
5 |
6 | export default class Tank extends GameObject {
7 | constructor(args) {
8 | super(args);
9 |
10 | this.width = TANK_WIDTH;
11 | this.height = TANK_HEIGHT;
12 | this.speed = TANK_SPEED;
13 | this.bulletSpeed = 4;
14 | this.bullet = null;
15 | this.explosion = null;
16 | }
17 |
18 | get sprite() {
19 | return this.sprites[this.direction * 2 + this.animationFrame];
20 | }
21 |
22 | get isExploding() {
23 | return Boolean(this.explosion?.isExploding);
24 | }
25 |
26 | turn(direction) {
27 | const prevDirection = this.direction;
28 |
29 | this.direction = direction;
30 |
31 | if (direction === Direction.UP || direction === Direction.DOWN) {
32 | const deltaRight = this.x % TILE_SIZE;
33 | const deltaLeft = TILE_SIZE - deltaRight;
34 |
35 | if (prevDirection === Direction.RIGHT) {
36 | if (deltaRight >= TANK_TURN_THRESHOLD) {
37 | this.x += deltaLeft;
38 | } else {
39 | this.x -= deltaRight;
40 | }
41 | } else if (prevDirection === Direction.LEFT) {
42 | if (deltaLeft >= TANK_TURN_THRESHOLD) {
43 | this.x -= deltaRight;
44 | } else {
45 | this.x += deltaLeft;
46 | }
47 | }
48 | } else {
49 | const deltaBottom = this.y % TILE_SIZE;
50 | const deltaTop = TILE_SIZE - deltaBottom;
51 |
52 | if (prevDirection === Direction.UP) {
53 | if (deltaTop >= TANK_TURN_THRESHOLD) {
54 | this.y -= deltaBottom;
55 | } else {
56 | this.y += deltaTop;
57 | }
58 | } else if (prevDirection === Direction.DOWN) {
59 | if (deltaBottom >= TANK_TURN_THRESHOLD) {
60 | this.y += deltaTop;
61 | } else {
62 | this.y -= deltaBottom;
63 | }
64 | }
65 | }
66 | }
67 |
68 | animate(frameDelta) {
69 | this.frames += frameDelta;
70 |
71 | if (this.frames > 20) {
72 | this.animationFrame ^= 1;
73 | this.frames = 0;
74 | }
75 | }
76 |
77 | fire() {
78 | if (!this.bullet) {
79 | const [x, y] = this.getBulletStartingPosition();
80 |
81 | this.bullet = new Bullet({
82 | x,
83 | y,
84 | tank: this,
85 | direction: this.direction,
86 | speed: this.bulletSpeed
87 | });
88 |
89 | this.bullet.on('destroyed', () => {
90 | this.bullet = null;
91 | });
92 |
93 | this.emit('fire', this.bullet);
94 | }
95 | }
96 |
97 | hit() {
98 | this.explode();
99 | this.destroy();
100 | }
101 |
102 | explode() {
103 | if (this.isExploding) return;
104 |
105 | const [x, y] = this.getExplosionStartingPosition();
106 |
107 | this.explosion = new TankExplosion({
108 | x,
109 | y
110 | });
111 |
112 | this.emit('explode', this.explosion);
113 | }
114 |
115 | destroy() {
116 | this.isDestroyed = true;
117 | this.bullet = null;
118 | this.explosion = null;
119 |
120 | this.emit('destroyed', this);
121 | }
122 |
123 | getBulletStartingPosition() {
124 | switch (this.direction) {
125 | case Direction.UP: return [this.left + 12, this.top - 4];
126 | case Direction.RIGHT: return [this.right - 8, this.top + 12];
127 | case Direction.DOWN: return [this.left + 10, this.bottom - 8];
128 | case Direction.LEFT: return [this.left, this.top + 12];
129 | }
130 | }
131 |
132 | getExplosionStartingPosition() {
133 | return [
134 | this.left + this.width * 0.5,
135 | this.top + this.height * 0.5
136 | ];
137 | }
138 | }
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const NUMBER_OF_UNITS = 13;
2 | export const TILE_SIZE = 16;
3 | export const UNIT_SIZE = 32;
4 | export const STAGE_SIZE = NUMBER_OF_UNITS * UNIT_SIZE;
5 |
6 | export const Key = {
7 | UP: 'ArrowUp',
8 | RIGHT: 'ArrowRight',
9 | DOWN: 'ArrowDown',
10 | LEFT: 'ArrowLeft',
11 | SPACE: 'Space'
12 | };
13 |
14 | export const Direction = {
15 | UP: 0,
16 | RIGHT: 1,
17 | DOWN: 2,
18 | LEFT: 3
19 | };
20 |
21 | export const TerrainType = {
22 | BASE: 0,
23 | BRICK_WALL: 1,
24 | STEEL_WALL: 2,
25 | TREE: 3,
26 | WATER: 4,
27 | ICE: 5
28 | };
29 |
30 | export const BASE_POSITION = [6 * UNIT_SIZE, 12 * UNIT_SIZE];
31 | export const BASE_WIDTH = UNIT_SIZE;
32 | export const BASE_HEIGHT = UNIT_SIZE;
33 | export const BASE_SPRITES = [
34 | [8 * UNIT_SIZE, 8 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
35 | [9 * UNIT_SIZE, 8 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
36 | ];
37 |
38 | export const BRICK_WALL_SPRITES = [
39 | [8 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // full
40 | [9 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // top 8/16
41 | [10 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // right 8/16
42 | [11 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // bottom 8/16
43 | [12 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // left 8/16
44 | [8 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // 1/4
45 | [13 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 3/4
46 | [13.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 3/4
47 | [13.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 3/4
48 | [13 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 3/4
49 | [14 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top 2/4
50 | [14.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right 2/4
51 | [14.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom 2/4
52 | [14 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // left 2/4
53 | [15 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 1/4
54 | [15.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 1/4
55 | [15.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 1/4
56 | [15 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 1/4
57 | ];
58 | export const BRICK_WALL_SPRITE_MAP = {
59 | '0': 5,
60 | '1': 10,
61 | '2': 11,
62 | '4': 12,
63 | '8': 13
64 | };
65 |
66 | export const BULLET_WIDTH = 8;
67 | export const BULLET_HEIGHT = 8;
68 | export const BULLET_SPEED = 4;
69 | export const BULLET_SPRITES = [
70 | [16 * UNIT_SIZE, 0 * UNIT_SIZE, BULLET_WIDTH, BULLET_HEIGHT],
71 | [16.5 * UNIT_SIZE, 0 * UNIT_SIZE, BULLET_WIDTH, BULLET_HEIGHT],
72 | [17 * UNIT_SIZE, 0 * UNIT_SIZE, BULLET_WIDTH, BULLET_HEIGHT],
73 | [17.5 * UNIT_SIZE, 0 * UNIT_SIZE, BULLET_WIDTH, BULLET_HEIGHT]
74 | ];
75 |
76 | export const BULLET_EXPLOSION_WIDTH = UNIT_SIZE;
77 | export const BULLET_EXPLOSION_HEIGHT = UNIT_SIZE;
78 | export const BULLET_EXPLOSION_ANIMATION_SPEED = 50;
79 | export const BULLET_EXPLOSION_SPRITES = [
80 | [16 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
81 | [17 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
82 | [18 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
83 | ];
84 |
85 | export const ENEMY_TANK_SPEED = 1;
86 | export const ENEMY_TANK_TURN_TIMER_THRESHOLD = 200;
87 | export const ENEMY_TANK_START_POSITIONS = [
88 | [6 * UNIT_SIZE, 0],
89 | [0 * UNIT_SIZE, 0],
90 | [12 * UNIT_SIZE, 0],
91 | ];
92 | export const ENEMY_TANK_SPRITES = [
93 | [
94 | [0 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
95 | [1 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
96 | [2 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
97 | [3 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
98 | [4 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
99 | [5 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
100 | [6 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
101 | [7 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
102 | ]
103 | ];
104 |
105 | export const PLAYER1_TANK_POSITION = [4 * UNIT_SIZE, 12 * UNIT_SIZE];
106 | export const PLAYER1_TANK_SPRITES = [
107 | [0 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
108 | [1 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
109 | [2 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
110 | [3 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
111 | [4 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
112 | [5 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
113 | [6 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
114 | [7 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
115 | ];
116 |
117 | export const TANK_WIDTH = UNIT_SIZE;
118 | export const TANK_HEIGHT = UNIT_SIZE;
119 | export const TANK_SPEED = 2;
120 | export const TANK_TURN_THRESHOLD = TILE_SIZE * 0.5;
121 | export const TANK_ANIMATION_FRAME = 20;
122 |
123 | export const TANK_EXPLOSION_ANIMATION_SPEED = 100;
124 | export const TANK_EXPLOSION_SPRITES = [
125 | [16 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
126 | [17 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
127 | [18 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
128 | [19 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE * 2, UNIT_SIZE * 2],
129 | [21 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE * 2, UNIT_SIZE * 2],
130 | [16 * UNIT_SIZE, 2 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
131 | ];
132 |
133 | export const STEEL_WALL_SPRITES = [
134 | [8 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // 1/4
135 | [8 * UNIT_SIZE, 5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // full
136 | [9 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top
137 | [10 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right
138 | [11 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom
139 | [12 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE] // left
140 | ];
--------------------------------------------------------------------------------
/src/view.js:
--------------------------------------------------------------------------------
1 | import { NUMBER_OF_UNITS, UNIT_SIZE, TILE_SIZE } from './constants.js';
2 |
3 | const PLAYFIELD_X = UNIT_SIZE;
4 | const PLAYFIELD_Y = UNIT_SIZE;
5 | const PLAYFIELD_WIDTH = NUMBER_OF_UNITS * UNIT_SIZE;
6 | const PLAYFIELD_HEIGHT = NUMBER_OF_UNITS * UNIT_SIZE;
7 | const PANEL_X = PLAYFIELD_X + PLAYFIELD_WIDTH;
8 | const PANEL_Y = PLAYFIELD_Y;
9 | const PANEL_WIDTH = PANEL_X + UNIT_SIZE * 2;
10 | const PANEL_HEIGHT = PANEL_Y + PLAYFIELD_HEIGHT;
11 |
12 | export default class View {
13 | constructor(canvas, sprite) {
14 | this.canvas = canvas;
15 | this.context = canvas.getContext('2d');
16 | this.context.imageSmoothingEnabled = false;
17 | this.sprite = sprite;
18 | }
19 |
20 | get width() {
21 | return this.canvas.width;
22 | }
23 |
24 | get height() {
25 | return this.canvas.height;
26 | }
27 |
28 | async init() {
29 | await this.sprite.load();
30 | }
31 |
32 | update(stage, player1, player2) {
33 | this.clearScreen();
34 | this.renderStage(stage);
35 | this.renderPanel(stage, player1, player2);
36 | this.renderGrid();
37 | }
38 |
39 | clearScreen() {
40 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
41 | }
42 |
43 | renderStage(stage) {
44 | this.context.fillStyle = '#636363';
45 | this.context.fillRect(0, 0, this.width, this.height);
46 |
47 | this.context.fillStyle = '#000000';
48 | this.context.fillRect(PLAYFIELD_X, PLAYFIELD_Y, PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT);
49 |
50 | for (const object of stage.objects) {
51 | const { x, y, width, height, sprite } = object;
52 |
53 | if (!sprite) return;
54 |
55 | this.context.drawImage(
56 | this.sprite.image,
57 | ...sprite,
58 | PLAYFIELD_X + x,
59 | PLAYFIELD_Y + y,
60 | width,
61 | height
62 | );
63 |
64 | if (object.debug) {
65 | this.context.strokeStyle = '#ff0000';
66 | this.context.lineWidth = 2;
67 | this.context.strokeRect(x + 1, y + 1, width - 2, height - 2);
68 | object.debug = false;
69 | }
70 | }
71 | }
72 |
73 | renderPanel(stage, player1, player2) {
74 | this.renderEnemyTankIcons(stage.enemyTanks);
75 | this.renderPlayer1Lives(player1);
76 | this.renderStageNumber(stage);
77 | }
78 |
79 | renderGrid() {
80 | for (let y = 0; y < NUMBER_OF_UNITS; y++) {
81 | for (let x = 0; x < NUMBER_OF_UNITS; x++) {
82 | this.context.strokeStyle = '#ffffff';
83 | this.context.lineWidth = .2;
84 | this.context.strokeRect(
85 | PLAYFIELD_X + (x * UNIT_SIZE + 1),
86 | PLAYFIELD_Y + (y * UNIT_SIZE + 1),
87 | UNIT_SIZE - 2,
88 | UNIT_SIZE - 2
89 | );
90 | }
91 | }
92 |
93 | for (let y = 0; y < NUMBER_OF_UNITS * 2; y++) {
94 | for (let x = 0; x < NUMBER_OF_UNITS * 2; x++) {
95 | this.context.strokeStyle = '#ffffff';
96 | this.context.lineWidth = .1;
97 | this.context.strokeRect(
98 | PLAYFIELD_X + (x * TILE_SIZE + 1),
99 | PLAYFIELD_Y + (y * TILE_SIZE + 1),
100 | TILE_SIZE - 2,
101 | TILE_SIZE - 2
102 | );
103 | }
104 | }
105 | }
106 |
107 | renderEnemyTankIcons(enemyTanks) {
108 | this.context.fillStyle = '#000000';
109 |
110 | for (let i = 0, x = 0, y = 0; i < enemyTanks.length; i++) {
111 | this.context.drawImage(
112 | this.sprite.image,
113 | UNIT_SIZE * 18,
114 | UNIT_SIZE * 5.5,
115 | TILE_SIZE,
116 | TILE_SIZE,
117 | PANEL_X + x * TILE_SIZE + 16,
118 | PANEL_Y + y * TILE_SIZE + 16,
119 | TILE_SIZE,
120 | TILE_SIZE
121 | );
122 |
123 | if (x === 1) {
124 | x = 0;
125 | y++;
126 | } else {
127 | x++;
128 | }
129 | }
130 | }
131 |
132 | renderPlayer1Lives(player1) {
133 | this.context.drawImage(
134 | this.sprite.image,
135 | UNIT_SIZE * 16,
136 | UNIT_SIZE * 6,
137 | UNIT_SIZE,
138 | TILE_SIZE,
139 | PANEL_X + TILE_SIZE,
140 | PANEL_Y + PANEL_HEIGHT * 0.5,
141 | UNIT_SIZE,
142 | TILE_SIZE
143 | );
144 |
145 | this.context.drawImage(
146 | this.sprite.image,
147 | UNIT_SIZE * 16,
148 | UNIT_SIZE * 6.5,
149 | TILE_SIZE,
150 | TILE_SIZE,
151 | PANEL_X + TILE_SIZE,
152 | PANEL_Y + PANEL_HEIGHT * 0.5 + TILE_SIZE,
153 | TILE_SIZE,
154 | TILE_SIZE
155 | );
156 |
157 | this.context.drawImage(
158 | this.sprite.image,
159 | UNIT_SIZE * 19.5,
160 | UNIT_SIZE * 5,
161 | TILE_SIZE,
162 | TILE_SIZE,
163 | PANEL_X + TILE_SIZE * 2,
164 | PANEL_Y + PANEL_HEIGHT * 0.5 + TILE_SIZE,
165 | TILE_SIZE,
166 | TILE_SIZE
167 | );
168 | }
169 |
170 | renderStageNumber(stage) {
171 | this.context.drawImage(
172 | this.sprite.image,
173 | UNIT_SIZE * 18,
174 | UNIT_SIZE * 6,
175 | UNIT_SIZE,
176 | UNIT_SIZE,
177 | PANEL_X + TILE_SIZE,
178 | PANEL_Y + PANEL_HEIGHT * 0.75,
179 | UNIT_SIZE,
180 | UNIT_SIZE
181 | );
182 |
183 | this.context.drawImage(
184 | this.sprite.image,
185 | UNIT_SIZE * 19,
186 | UNIT_SIZE * 5,
187 | TILE_SIZE,
188 | TILE_SIZE,
189 | PANEL_X + TILE_SIZE * 2,
190 | PANEL_Y + PANEL_HEIGHT * 0.75 + UNIT_SIZE,
191 | TILE_SIZE,
192 | TILE_SIZE
193 | );
194 | }
195 | }
--------------------------------------------------------------------------------
/src/stage.js:
--------------------------------------------------------------------------------
1 | import { STAGE_SIZE, TILE_SIZE, TerrainType } from './constants.js';
2 | import EventEmitter from './event-emitter.js';
3 | import Base from './base.js';
4 | import BrickWall from './brick-wall.js';
5 | import SteelWall from './steel-wall.js';
6 | import PlayerTank from './player-tank.js';
7 | import EnemyTank from './enemy-tank.js';
8 |
9 | export default class Stage extends EventEmitter {
10 | static createObject(type, args) {
11 | switch (type) {
12 | case TerrainType.BRICK_WALL: return new BrickWall(args);
13 | case TerrainType.STEEL_WALL: return new SteelWall(args);
14 | }
15 | }
16 |
17 | static createTerrain(map) {
18 | const objects = [];
19 |
20 | for (let i = 0; i < map.length; i++) {
21 | for (let j = 0; j < map.length; j++) {
22 | const value = map[j][i];
23 |
24 | if (value) {
25 | const object = Stage.createObject(value, {
26 | x: i * TILE_SIZE,
27 | y: j * TILE_SIZE
28 | });
29 |
30 | objects.push(object);
31 | }
32 | }
33 | }
34 |
35 | return objects;
36 | }
37 |
38 | static createEnemies(types) {
39 | return types.map(type => new EnemyTank({ type }));
40 | }
41 |
42 | get width() {
43 | return STAGE_SIZE;
44 | }
45 |
46 | get height() {
47 | return STAGE_SIZE;
48 | }
49 |
50 | get top() {
51 | return 0;
52 | }
53 |
54 | get right() {
55 | return this.width;
56 | }
57 |
58 | get bottom() {
59 | return this.height;
60 | }
61 |
62 | get left() {
63 | return 0;
64 | }
65 |
66 | constructor(data) {
67 | super();
68 |
69 | this.base = new Base();
70 | this.playerTank = new PlayerTank();
71 | this.enemyTanks = Stage.createEnemies(data.enemies);
72 | this.terrain = Stage.createTerrain(data.map);
73 | this.enemyTankCount = 0;
74 | this.enemyTankTimer = 0;
75 | this.enemyTankPositionIndex = 0;
76 |
77 | this.objects = new Set([
78 | this.base,
79 | this.playerTank,
80 | ...this.terrain
81 | ]);
82 |
83 | this.init();
84 | }
85 |
86 | init() {
87 | this.base.on('destroyed', () => {
88 | this.emit('gameOver');
89 | });
90 |
91 | this.playerTank.on('fire', bullet => {
92 | this.objects.add(bullet);
93 |
94 | bullet.on('explode', explosion => {
95 | this.objects.add(explosion);
96 |
97 | explosion.on('destroyed', () => {
98 | this.objects.delete(explosion);
99 | });
100 | });
101 |
102 | bullet.on('destroyed', () => {
103 | this.objects.delete(bullet);
104 | });
105 | });
106 |
107 | this.playerTank.on('destroyed', tank => {
108 | this.objects.delete(tank);
109 | });
110 |
111 | this.enemyTanks.map(enemyTank => {
112 | enemyTank.on('fire', bullet => {
113 | this.objects.add(bullet);
114 |
115 | bullet.on('explode', explosion => {
116 | this.objects.add(explosion);
117 |
118 | explosion.on('destroyed', () => {
119 | this.objects.delete(explosion);
120 | });
121 | });
122 |
123 | bullet.on('destroyed', () => {
124 | this.objects.delete(bullet);
125 | });
126 | });
127 |
128 | enemyTank.on('explode', explosion => {
129 | this.objects.add(explosion);
130 |
131 | explosion.on('destroyed', () => {
132 | this.objects.delete(explosion);
133 | });
134 | });
135 |
136 | enemyTank.on('destroyed', () => this.removeEnemyTank(enemyTank));
137 | });
138 | }
139 |
140 | update(input, frameDelta) {
141 | const state = {
142 | input,
143 | frameDelta,
144 | world: this
145 | };
146 |
147 | if (this.shouldAddEnemyTank(frameDelta)) {
148 | this.addEnemyTank();
149 | }
150 |
151 | this.objects.forEach(object => object.update(state));
152 | }
153 |
154 | isOutOfBounds(object) {
155 | return (
156 | object.top < this.top ||
157 | object.right > this.right ||
158 | object.bottom > this.bottom ||
159 | object.left < this.left
160 | );
161 | }
162 |
163 | hasCollision(object) {
164 | const collision = this.getCollision(object);
165 |
166 | return Boolean(collision);
167 | }
168 |
169 | getCollision(object) {
170 | const collisionObjects = this.getCollisionObjects(object);
171 |
172 | if (collisionObjects.size > 0) {
173 | return { objects: collisionObjects };
174 | }
175 | }
176 |
177 | getCollisionObjects(object) {
178 | const objects = new Set();
179 |
180 | for (const other of this.objects) {
181 | if (other !== object && this.haveCollision(object, other)) {
182 | objects.add(other);
183 | }
184 | }
185 |
186 | return objects;
187 | }
188 |
189 | haveCollision(a, b) {
190 | return (
191 | a.left < b.right &&
192 | a.right > b.left &&
193 | a.top < b.bottom &&
194 | a.bottom > b.top
195 | );
196 | }
197 |
198 | shouldAddEnemyTank(frameDelta) {
199 | this.enemyTankTimer += frameDelta;
200 |
201 | return this.enemyTankTimer > 1000 && this.enemyTankCount < 4;
202 | }
203 |
204 | removeWall() { }
205 |
206 | addEnemyTank() {
207 | const tank = this.enemyTanks.shift();
208 |
209 | if (tank) {
210 | tank.setPosition(this.enemyTankPositionIndex);
211 |
212 | this.enemyTankCount += 1;
213 | this.enemyTankTimer = 0;
214 | this.enemyTankPositionIndex = (this.enemyTankPositionIndex + 1) % 3;
215 |
216 | this.objects.add(tank);
217 | }
218 | }
219 |
220 | removeEnemyTank(enemyTank) {
221 | this.objects.delete(enemyTank);
222 | this.enemyTankCount -= 1;
223 | }
224 | }
--------------------------------------------------------------------------------