Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Battle City

Спрайт: https://static.codedojo.ru/projects/battlecity/sprite.png
## Installation

Спасибо @vrobik за прозрачный спрайт.
Download a sprite file from https://static.codedojo.ru/projects/battlecity/sprite.png (thanks @vrobik for it) and place it under assets/ subfolder, so you should have something like battlecity/assets/sprite.png

Then make the project's folder be accessible from any web server, like apache/nginx/whatever. For example, you can install a 'Live Server' extention for VS Code, and then do a right click inside the index.html -> Open with Live Server

## Troubleshooting

If you got a CORS policy error in console, you need to use a web server instead of just opening the file in the browser (the path should be like http://whatever, not file://whatever)
5 changes: 3 additions & 2 deletions src/bullet.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class Bullet extends GameObject {
}

get isFromPlayerTank() {
return this.tank?.type === 'playerTank';
return this.tank?.type === 'player1Tank' || this.tank?.type === 'player2Tank';
}

update({ world }) {
Expand Down Expand Up @@ -85,7 +85,8 @@ export default class Bullet extends GameObject {
shouldCollide(object) {
return (
object.type === 'wall' ||
(object.type === 'playerTank' && this.isFromEnemyTank) ||
(object.type === 'player1Tank' && this.isFromEnemyTank) ||
(object.type === 'player2Tank' && this.isFromEnemyTank) ||
(object.type === 'enemyTank' && this.isFromPlayerTank) ||
(object.type === 'bullet' && this.isFromEnemyTank && object.isFromPlayerTank) ||
(object.type === 'bullet' && this.isFromPlayerTank && object.isFromEnemyTank)
Expand Down
92 changes: 66 additions & 26 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ export const UNIT_SIZE = 32;
export const STAGE_SIZE = NUMBER_OF_UNITS * UNIT_SIZE;

export const Key = {
UP: 'ArrowUp',
RIGHT: 'ArrowRight',
DOWN: 'ArrowDown',
LEFT: 'ArrowLeft',
SPACE: 'Space'
PLAYER1_UP: 'ArrowUp',
PLAYER1_LEFT: 'ArrowLeft',
PLAYER1_DOWN: 'ArrowDown',
PLAYER1_RIGHT: 'ArrowRight',
PLAYER1_FIRE: 'Space',
PLAYER2_UP: 'KeyW',
PLAYER2_LEFT: 'KeyA',
PLAYER2_DOWN: 'KeyS',
PLAYER2_RIGHT: 'KeyD',
PLAYER2_FIRE: 'AltLeft'
};

export const Direction = {
Expand All @@ -31,8 +36,8 @@ export const BASE_POSITION = [6 * UNIT_SIZE, 12 * UNIT_SIZE];
export const BASE_WIDTH = UNIT_SIZE;
export const BASE_HEIGHT = UNIT_SIZE;
export const BASE_SPRITES = [
[8 * UNIT_SIZE, 8 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[9 * UNIT_SIZE, 8 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
[11 * UNIT_SIZE, 7.5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[12 * UNIT_SIZE, 7.5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
];

export const BRICK_WALL_SPRITES = [
Expand All @@ -42,18 +47,18 @@ export const BRICK_WALL_SPRITES = [
[11 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // bottom 8/16
[12 * UNIT_SIZE, 4 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // left 8/16
[8 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // 1/4
[13 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 3/4
[13.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 3/4
[13.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 3/4
[13 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 3/4
[14 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top 2/4
[14.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right 2/4
[14.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom 2/4
[14 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // left 2/4
[15 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 1/4
[15.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 1/4
[15.5 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 1/4
[15 * UNIT_SIZE, 4.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 1/4
[8.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 3/4
[9 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 3/4
[9.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 3/4
[10 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 3/4
[12 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top 2/4
[11.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right 2/4
[11 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom 2/4
[10.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // left 2/4
[12.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top left 1/4
[13 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top right 1/4
[13.5 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom right 1/4
[14 * UNIT_SIZE, 4 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom left 1/4
];
export const BRICK_WALL_SPRITE_MAP = {
'0': 5,
Expand Down Expand Up @@ -114,6 +119,18 @@ export const PLAYER1_TANK_SPRITES = [
[7 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
];

export const PLAYER2_TANK_POSITION = [8 * UNIT_SIZE, 12 * UNIT_SIZE];
export const PLAYER2_TANK_SPRITES = [
[8 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[9 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[10 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[11 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[12 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[13 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[14 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE],
[15 * UNIT_SIZE, 0 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE]
];

export const TANK_WIDTH = UNIT_SIZE;
export const TANK_HEIGHT = UNIT_SIZE;
export const TANK_SPEED = 2;
Expand All @@ -131,10 +148,33 @@ export const TANK_EXPLOSION_SPRITES = [
];

export const STEEL_WALL_SPRITES = [
[8 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // 1/4
[8 * UNIT_SIZE, 5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // full
[9 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top
[10 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right
[11 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom
[12 * UNIT_SIZE, 5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE] // left
];
[8 * UNIT_SIZE, 6.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // 1/4
[8 * UNIT_SIZE, 6.5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // full
[9 * UNIT_SIZE, 6.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // top
[10 * UNIT_SIZE, 6.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // right
[11 * UNIT_SIZE, 6.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // bottom
[12 * UNIT_SIZE, 6.5 * UNIT_SIZE, TILE_SIZE, TILE_SIZE] // left
];

export const STAGE_NUMBER_SPRITES = [
[23.5 * UNIT_SIZE, 11.5 * UNIT_SIZE, UNIT_SIZE, UNIT_SIZE], // flag
[19 * UNIT_SIZE, 5.25 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // stage number
];

export const PLAYER1_LIVES_SPRITES = [
[23.5 * UNIT_SIZE, 8.5 * UNIT_SIZE, UNIT_SIZE, TILE_SIZE], // 1P
[23.5 * UNIT_SIZE, 9 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // tank icon
[19.5 * UNIT_SIZE, 5.25 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // number
];

export const PLAYER2_LIVES_SPRITES = [
[23.5 * UNIT_SIZE, 10 * UNIT_SIZE, UNIT_SIZE, TILE_SIZE], // 1P
[23.5 * UNIT_SIZE, 9 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // tank icon
[19.5 * UNIT_SIZE, 5.25 * UNIT_SIZE, TILE_SIZE, TILE_SIZE], // number
];

export const ENEMY_TANK_ICONS_SPRITES = [
[18 * UNIT_SIZE, 5.5 * UNIT_SIZE, TILE_SIZE, UNIT_SIZE],
];

export const ENABLE_SECOND_PLAYER = true;
10 changes: 5 additions & 5 deletions src/game-object.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Direction } from './constants.js';
import { Key, Direction } from './constants.js';
import EventEmitter from './event-emitter.js';

export default class GameObject extends EventEmitter {
static getDirectionForKeys(keys) {
if (keys.has('ArrowUp')) {
if (keys.has(Key.PLAYER1_UP) || keys.has(Key.PLAYER2_UP)) {
return Direction.UP;
} else if (keys.has('ArrowRight')) {
} else if (keys.has(Key.PLAYER1_RIGHT) || keys.has(Key.PLAYER2_RIGHT)) {
return Direction.RIGHT;
} else if (keys.has('ArrowDown')) {
} else if (keys.has(Key.PLAYER1_DOWN) || keys.has(Key.PLAYER2_DOWN)) {
return Direction.DOWN;
} else if (keys.has('ArrowLeft')) {
} else if (keys.has(Key.PLAYER1_LEFT) || keys.has(Key.PLAYER2_LEFT)) {
return Direction.LEFT;
}
}
Expand Down
32 changes: 22 additions & 10 deletions src/input.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Key } from './constants.js';

export default class Input {
constructor() {
this.keys = new Set();
Expand All @@ -7,25 +9,35 @@ export default class Input {
init() {
document.addEventListener('keydown', event => {
switch (event.code) {
case 'ArrowUp':
case 'ArrowRight':
case 'ArrowDown':
case 'ArrowLeft':
case 'Space':
case Key.PLAYER1_UP:
case Key.PLAYER1_LEFT:
case Key.PLAYER1_DOWN:
case Key.PLAYER1_RIGHT:
case Key.PLAYER1_FIRE:
case 'Enter':
case Key.PLAYER2_UP:
case Key.PLAYER2_LEFT:
case Key.PLAYER2_DOWN:
case Key.PLAYER2_RIGHT:
case Key.PLAYER2_FIRE:
event.preventDefault();
this.keys.add(event.code);
}
});

document.addEventListener('keyup', event => {
switch (event.code) {
case 'ArrowUp':
case 'ArrowRight':
case 'ArrowDown':
case 'ArrowLeft':
case 'Space':
case Key.PLAYER1_UP:
case Key.PLAYER1_LEFT:
case Key.PLAYER1_DOWN:
case Key.PLAYER1_RIGHT:
case Key.PLAYER1_FIRE:
case 'Enter':
case Key.PLAYER2_UP:
case Key.PLAYER2_LEFT:
case Key.PLAYER2_DOWN:
case Key.PLAYER2_RIGHT:
case Key.PLAYER2_FIRE:
event.preventDefault();
this.keys.delete(event.code);
}
Expand Down
10 changes: 4 additions & 6 deletions src/player-tank.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ export default class PlayerTank extends Tank {
constructor(args) {
super(args);

this.type = 'playerTank';
this.x = PLAYER1_TANK_POSITION[0];
this.y = PLAYER1_TANK_POSITION[1];
this.direction = Direction.UP;
this.speed = TANK_SPEED;
this.sprites = PLAYER1_TANK_SPRITES;
this.movementKeys = [];
this.fireKeys = [];
}

update({ input, frameDelta, world }) {
if (input.has(Key.UP, Key.RIGHT, Key.DOWN, Key.LEFT)) {
if (input.has(...this.movementKeys)) {
const direction = Tank.getDirectionForKeys(input.keys);
const axis = Tank.getAxisForDirection(direction);
const value = Tank.getValueForDirection(direction);
Expand All @@ -31,7 +29,7 @@ export default class PlayerTank extends Tank {
}
}

if (input.keys.has(Key.SPACE)) {
if (input.has(...this.fireKeys)) {
this.fire();
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/player1-tank.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Key, PLAYER1_TANK_POSITION, PLAYER1_TANK_SPRITES } from './constants.js';
import PlayerTank from './player-tank.js';

export default class Player1Tank extends PlayerTank {
constructor(args) {
super(args);

this.type = 'player1Tank';
this.x = PLAYER1_TANK_POSITION[0];
this.y = PLAYER1_TANK_POSITION[1];
this.sprites = PLAYER1_TANK_SPRITES;
this.movementKeys = [
Key.PLAYER1_UP,
Key.PLAYER1_RIGHT,
Key.PLAYER1_DOWN,
Key.PLAYER1_LEFT,
];
this.fireKeys = [
Key.PLAYER1_FIRE,
];
}
}
22 changes: 22 additions & 0 deletions src/player2-tank.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Key, PLAYER2_TANK_POSITION, PLAYER2_TANK_SPRITES } from './constants.js';
import PlayerTank from './player-tank.js';

export default class Player2Tank extends PlayerTank {
constructor(args) {
super(args);

this.type = 'player2Tank';
this.x = PLAYER2_TANK_POSITION[0];
this.y = PLAYER2_TANK_POSITION[1];
this.sprites = PLAYER2_TANK_SPRITES;
this.movementKeys = [
Key.PLAYER2_UP,
Key.PLAYER2_RIGHT,
Key.PLAYER2_DOWN,
Key.PLAYER2_LEFT
];
this.fireKeys = [
Key.PLAYER2_FIRE,
];
}
}
25 changes: 18 additions & 7 deletions src/stage.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { STAGE_SIZE, TILE_SIZE, TerrainType } from './constants.js';
import { STAGE_SIZE, TILE_SIZE, TerrainType, ENABLE_SECOND_PLAYER } from './constants.js';
import EventEmitter from './event-emitter.js';
import Base from './base.js';
import BrickWall from './brick-wall.js';
import SteelWall from './steel-wall.js';
import PlayerTank from './player-tank.js';
import Player1Tank from './player1-tank.js';
import Player2Tank from './player2-tank.js';
import EnemyTank from './enemy-tank.js';

export default class Stage extends EventEmitter {
Expand Down Expand Up @@ -43,7 +44,8 @@ export default class Stage extends EventEmitter {
super();

this.base = new Base();
this.playerTank = new PlayerTank();
this.player1Tank = new Player1Tank();
this.player2Tank = new Player2Tank();
this.enemyTanks = Stage.createEnemies(data.enemies);
this.terrain = Stage.createTerrain(data.map);
this.enemyTankCount = 0;
Expand All @@ -52,10 +54,14 @@ export default class Stage extends EventEmitter {

this.objects = new Set([
this.base,
this.playerTank,
this.player1Tank,
...this.terrain
]);

if (ENABLE_SECOND_PLAYER) {
this.objects.add(this.player2Tank);
}

this.init();
}

Expand All @@ -64,7 +70,7 @@ export default class Stage extends EventEmitter {
this.emit('gameOver');
});

this.playerTank.on('fire', bullet => {
var playerTankFireFunc = bullet => {
this.objects.add(bullet);

bullet.on('explode', explosion => {
Expand All @@ -78,9 +84,14 @@ export default class Stage extends EventEmitter {
bullet.on('destroyed', () => {
this.objects.delete(bullet);
});
});
}
this.player1Tank.on('fire', playerTankFireFunc);
this.player2Tank.on('fire', playerTankFireFunc);

this.playerTank.on('destroyed', tank => {
this.player1Tank.on('destroyed', tank => {
this.objects.delete(tank);
});
this.player2Tank.on('destroyed', tank => {
this.objects.delete(tank);
});

Expand Down
Loading