diff --git a/README.md b/README.md index 6e41b6e..ffeb279 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/bullet.js b/src/bullet.js index 487a484..1722670 100644 --- a/src/bullet.js +++ b/src/bullet.js @@ -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 }) { @@ -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) diff --git a/src/constants.js b/src/constants.js index 61c8e03..5712b68 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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 = { @@ -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 = [ @@ -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, @@ -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; @@ -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 -]; \ No newline at end of file + [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; diff --git a/src/game-object.js b/src/game-object.js index 8994088..e97e5e7 100644 --- a/src/game-object.js +++ b/src/game-object.js @@ -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; } } diff --git a/src/input.js b/src/input.js index d39a67d..12f27f7 100644 --- a/src/input.js +++ b/src/input.js @@ -1,3 +1,5 @@ +import { Key } from './constants.js'; + export default class Input { constructor() { this.keys = new Set(); @@ -7,12 +9,17 @@ 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); } @@ -20,12 +27,17 @@ export default class Input { 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); } diff --git a/src/player-tank.js b/src/player-tank.js index 19f8652..730036e 100644 --- a/src/player-tank.js +++ b/src/player-tank.js @@ -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); @@ -31,7 +29,7 @@ export default class PlayerTank extends Tank { } } - if (input.keys.has(Key.SPACE)) { + if (input.has(...this.fireKeys)) { this.fire(); } } diff --git a/src/player1-tank.js b/src/player1-tank.js new file mode 100644 index 0000000..ff29f56 --- /dev/null +++ b/src/player1-tank.js @@ -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, + ]; + } +} \ No newline at end of file diff --git a/src/player2-tank.js b/src/player2-tank.js new file mode 100644 index 0000000..14b68e3 --- /dev/null +++ b/src/player2-tank.js @@ -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, + ]; + } +} \ No newline at end of file diff --git a/src/stage.js b/src/stage.js index d025279..5c0843f 100644 --- a/src/stage.js +++ b/src/stage.js @@ -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 { @@ -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; @@ -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(); } @@ -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 => { @@ -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); }); diff --git a/src/view.js b/src/view.js index 32a34f6..12c1bcb 100644 --- a/src/view.js +++ b/src/view.js @@ -1,4 +1,12 @@ -import { NUMBER_OF_UNITS, UNIT_SIZE, TILE_SIZE } from './constants.js'; +import { + NUMBER_OF_UNITS, + UNIT_SIZE, + TILE_SIZE, + STAGE_NUMBER_SPRITES, + ENEMY_TANK_ICONS_SPRITES, + PLAYER1_LIVES_SPRITES, + PLAYER2_LIVES_SPRITES, +} from './constants.js'; const PLAYFIELD_X = UNIT_SIZE; const PLAYFIELD_Y = UNIT_SIZE; @@ -73,6 +81,7 @@ export default class View { renderPanel(stage, player1, player2) { this.renderEnemyTankIcons(stage.enemyTanks); this.renderPlayer1Lives(player1); + this.renderPlayer2Lives(player2); this.renderStageNumber(stage); } @@ -110,14 +119,11 @@ export default class View { for (let i = 0, x = 0, y = 0; i < enemyTanks.length; i++) { this.context.drawImage( this.sprite.image, - UNIT_SIZE * 18, - UNIT_SIZE * 5.5, - TILE_SIZE, - TILE_SIZE, + ...ENEMY_TANK_ICONS_SPRITES[0], PANEL_X + x * TILE_SIZE + 16, PANEL_Y + y * TILE_SIZE + 16, TILE_SIZE, - TILE_SIZE + UNIT_SIZE ); if (x === 1) { @@ -130,38 +136,64 @@ export default class View { } renderPlayer1Lives(player1) { + let top_x_position = PANEL_X + TILE_SIZE; + let top_y_position = PANEL_Y + PANEL_HEIGHT * 0.5; + this.context.drawImage( this.sprite.image, - UNIT_SIZE * 16, - UNIT_SIZE * 6, - UNIT_SIZE, - TILE_SIZE, - PANEL_X + TILE_SIZE, - PANEL_Y + PANEL_HEIGHT * 0.5, + ...PLAYER1_LIVES_SPRITES[0], + top_x_position, + top_y_position, UNIT_SIZE, TILE_SIZE ); this.context.drawImage( this.sprite.image, - UNIT_SIZE * 16, - UNIT_SIZE * 6.5, - TILE_SIZE, - TILE_SIZE, - PANEL_X + TILE_SIZE, - PANEL_Y + PANEL_HEIGHT * 0.5 + TILE_SIZE, + ...PLAYER1_LIVES_SPRITES[1], + top_x_position, + top_y_position + TILE_SIZE, TILE_SIZE, TILE_SIZE ); this.context.drawImage( this.sprite.image, - UNIT_SIZE * 19.5, - UNIT_SIZE * 5, + ...PLAYER1_LIVES_SPRITES[2], + top_x_position + TILE_SIZE, + top_y_position + TILE_SIZE, TILE_SIZE, + TILE_SIZE + ); + } + + renderPlayer2Lives(player2) { + let top_x_position = PANEL_X + TILE_SIZE; + let top_y_position = PANEL_Y + PANEL_HEIGHT * 0.5 + UNIT_SIZE + TILE_SIZE; + + this.context.drawImage( + this.sprite.image, + ...PLAYER2_LIVES_SPRITES[0], + top_x_position, + top_y_position, + UNIT_SIZE, + TILE_SIZE + ); + + this.context.drawImage( + this.sprite.image, + ...PLAYER2_LIVES_SPRITES[1], + top_x_position, + top_y_position + TILE_SIZE, TILE_SIZE, - PANEL_X + TILE_SIZE * 2, - PANEL_Y + PANEL_HEIGHT * 0.5 + TILE_SIZE, + TILE_SIZE + ); + + this.context.drawImage( + this.sprite.image, + ...PLAYER2_LIVES_SPRITES[2], + top_x_position + TILE_SIZE, + top_y_position + TILE_SIZE, TILE_SIZE, TILE_SIZE ); @@ -170,10 +202,7 @@ export default class View { renderStageNumber(stage) { this.context.drawImage( this.sprite.image, - UNIT_SIZE * 18, - UNIT_SIZE * 6, - UNIT_SIZE, - UNIT_SIZE, + ...STAGE_NUMBER_SPRITES[0], PANEL_X + TILE_SIZE, PANEL_Y + PANEL_HEIGHT * 0.75, UNIT_SIZE, @@ -182,10 +211,7 @@ export default class View { this.context.drawImage( this.sprite.image, - UNIT_SIZE * 19, - UNIT_SIZE * 5, - TILE_SIZE, - TILE_SIZE, + ...STAGE_NUMBER_SPRITES[1], PANEL_X + TILE_SIZE * 2, PANEL_Y + PANEL_HEIGHT * 0.75 + UNIT_SIZE, TILE_SIZE,