Initial commit
3
.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/env"]
|
||||
}
|
||||
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
8
Dockerfile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FROM node:10-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --production=false --ignore-engines
|
||||
COPY . .
|
||||
RUN npx webpack
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server/app.js"]
|
||||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dmytro Vasin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
53
README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
## A Bomberman-style game with multiplayer option.
|
||||
|
||||
A Bomberman-style game with multiplayer option made with [Phaser.js](https://phaser.io/), [Node.js](https://nodejs.org/uk/), [Express.js](http://expressjs.com/), [Socket.io](https://socket.io/).
|
||||
|
||||
### Game description:
|
||||
|
||||
The game is designed for up to three players.
|
||||
|
||||
Games can be played on one of two maps.
|
||||
|
||||

|
||||
|
||||
Player models user will receive randomly when he will enter the game.
|
||||
|
||||
The winning player is the last one standing.
|
||||
|
||||
Within the game, players can upgrade skills like:
|
||||
( Change to drop - 50% when player break the block )
|
||||
|
||||
*  Speed: can increase to 3
|
||||
*  Bomb setting time: can be reduced to 0.5 seconds
|
||||
*  Power: no limit
|
||||
|
||||
## Demo:
|
||||
You can find a tutorial on how to make Bomberman-style games here: [Tutorial (need work)](https://github.com/DmytroVasin/bomber/blob/master/tutorial.md)
|
||||
|
||||
A demo of this game can be found on Heroku: [Bomberman with multiplayer - Demo](https://bomb-attack.herokuapp.com/)
|
||||
|
||||
Note: To play the game, you should open the browser in two separate windows. The game pauses when You open a new tab in the same window. Open game in different windows.
|
||||
|
||||
## Game: *Click to play*:
|
||||
[](https://player.vimeo.com/video/246595375?autoplay=1)
|
||||
|
||||
## Menu: *Click to play*:
|
||||
[](https://player.vimeo.com/video/247095838?autoplay=1)
|
||||
|
||||
## Setup:
|
||||
The game requires Node and Yarn (npm) package manager. Make sure that you already have both installed on your system before trying to launch it.
|
||||
|
||||
Steps:
|
||||
1. Clone the repository.
|
||||
2. Run `yarn install` inside a newly created directory.
|
||||
3. Start the server with the command `yarn run server` ( defined in the `package.json` file ). This will launch `webpack` in your development environment and then start the `node` server.
|
||||
4. Check out the game at [http://localhost:3000](http://localhost:3000)
|
||||
5. Enjoy!
|
||||
|
||||
## Notes:
|
||||
You can use my code as a boilerplate if you want, but I would suggest you change the tile sizes. I've picked tiles that are 35x35 pixels, but tiles that are 32x32 would be more ideal. All free templates are based on this tile size, and it is also handily divisible by 2.
|
||||
|
||||
## To Debug Node process:
|
||||
1. Open: chrome://inspect/#devices
|
||||
2. Click 'Open dedicated DevTools for Node'
|
||||
3. "server": "webpack --mode development && node --inspect server/app.js",
|
||||
BIN
_readme/intro.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
_readme/maps.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
_readme/menu.png
Normal file
|
After Width: | Height: | Size: 943 KiB |
BIN
_readme/power.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
_readme/speed.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
_readme/time.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
2390
client/bundle.js
Normal file
1
client/bundle.js.map
Normal file
18
client/css/base.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
background: #DEDEDE;
|
||||
}
|
||||
|
||||
html, body, #game-wrapper, #game-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#game-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
BIN
client/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/images/game/bombs.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/images/game/bone_tileset.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
client/images/game/chars/0-face.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
client/images/game/chars/1-face.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
client/images/game/chars/1-preview.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
client/images/game/chars/10-face.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
client/images/game/chars/10-preview.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
client/images/game/chars/11-face.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
client/images/game/chars/11-preview.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
client/images/game/chars/2-face.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
client/images/game/chars/2-preview.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
client/images/game/chars/3-face.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
client/images/game/chars/3-preview.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
client/images/game/chars/4-face.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
client/images/game/chars/4-preview.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
client/images/game/chars/5-face.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
client/images/game/chars/5-preview.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
client/images/game/chars/6-face.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
client/images/game/chars/6-preview.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
client/images/game/chars/7-face.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
client/images/game/chars/7-preview.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
client/images/game/chars/8-face.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
client/images/game/chars/8-preview.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
client/images/game/chars/9-face.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
client/images/game/chars/9-preview.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
client/images/game/delay_up_bonus.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
client/images/game/delay_up_no_bonus.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
client/images/game/explosion_center.png
Executable file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/images/game/explosion_down.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/explosion_horizontal.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/explosion_left.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/explosion_right.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/explosion_up.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/explosion_vertical.png
Executable file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/images/game/placeholder_power.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
client/images/game/placeholder_speed.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
client/images/game/placeholder_time.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
client/images/game/power_up_bonus.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
client/images/game/speed_up_bonus.png
Normal file
|
After Width: | Height: | Size: 9 KiB |
BIN
client/images/game/speed_up_no_bonus.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
client/images/game/spoil_tileset.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
client/images/menu/accepts.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
client/images/menu/buttons.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
client/images/menu/cold_map_preview.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
client/images/menu/game_enter.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
client/images/menu/hot_map_preview.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
client/images/menu/left_arrow.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
client/images/menu/main_menu.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
client/images/menu/right_arrow.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
client/images/menu/slot_backdrop.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
24
client/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>Bomb Attack</title>
|
||||
<link rel='stylesheet' href='css/base.css'/>
|
||||
|
||||
<script src='lib/phase-slide.js'></script>
|
||||
<script src='lib/phaser.min.js'></script>
|
||||
<script src='/socket.io/socket.io.js'></script>
|
||||
|
||||
<script src='bundle.js'></script>
|
||||
|
||||
<script>
|
||||
window.clientSocket = io.connect();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='game-wrapper'>
|
||||
<div id='game-container'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
28
client/js/app.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import Boot from './states/boot';
|
||||
import Preload from './states/preload';
|
||||
import Menu from './states/menu';
|
||||
import SelectMap from './states/select_map';
|
||||
import PendingGame from './states/pending_game';
|
||||
import Play from './states/play';
|
||||
import Win from './states/win';
|
||||
|
||||
class Game extends Phaser.Game {
|
||||
constructor() {
|
||||
super(980, 630, Phaser.AUTO, 'game-container');
|
||||
|
||||
// Tell Phaser to use setTimeOut even if RAF(request animation frame) is available.
|
||||
this.config['forceSetTimeOut'] = true
|
||||
|
||||
this.state.add('Boot', Boot);
|
||||
this.state.add('Preload', Preload);
|
||||
this.state.add('Menu', Menu);
|
||||
this.state.add('SelectMap', SelectMap);
|
||||
this.state.add('PendingGame', PendingGame);
|
||||
this.state.add('Play', Play);
|
||||
this.state.add('Win', Win);
|
||||
|
||||
this.state.start('Boot');
|
||||
}
|
||||
}
|
||||
|
||||
new Game();
|
||||
30
client/js/entities/bomb.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { TILE_SIZE, EXPLOSION_TIME } from '../utils/constants';
|
||||
|
||||
export default class Bomb extends Phaser.Sprite {
|
||||
|
||||
constructor(game, id, col, row) {
|
||||
let centerCol = (col * TILE_SIZE) + TILE_SIZE / 2
|
||||
let centerRow = (row * TILE_SIZE) + TILE_SIZE / 2
|
||||
|
||||
super(game, centerCol, centerRow, 'bomb_tileset');
|
||||
this.scale.setTo(0.7);
|
||||
this.anchor.setTo(0.5);
|
||||
|
||||
this.game = game
|
||||
this.id = id;
|
||||
|
||||
this.game.physics.arcade.enable(this);
|
||||
|
||||
this.game.add.tween(this.scale).to({ x: 1.2, y: 1.2 }, EXPLOSION_TIME, Phaser.Easing.Linear.None, true);
|
||||
|
||||
this.body.immovable = true;
|
||||
// TODO: https://phaser.io/docs/2.4.4/Phaser.AnimationManager.html#add
|
||||
this.animations.add('bomb', [0,1,2,3,4,5,6,7,8,9,10,11,12,13], 6, true);
|
||||
this.animations.play('bomb');
|
||||
}
|
||||
|
||||
update() {
|
||||
// this.game.debug.body(this);
|
||||
}
|
||||
|
||||
}
|
||||
9
client/js/entities/bone.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { TILE_SIZE } from '../utils/constants';
|
||||
|
||||
export default class Bone extends Phaser.Sprite {
|
||||
|
||||
constructor(game, col, row) {
|
||||
super(game, (col * TILE_SIZE), (row * TILE_SIZE), 'bone_tileset');
|
||||
}
|
||||
|
||||
}
|
||||
74
client/js/entities/enemy_player.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { TILE_SIZE, PING } from '../utils/constants';
|
||||
import { Text } from '../helpers/elements';
|
||||
|
||||
export default class EnemyPlayer extends Phaser.Sprite {
|
||||
|
||||
constructor({ game, id, spawn, skin }) {
|
||||
super(game, spawn.x, spawn.y, 'bomberman_' + skin);
|
||||
|
||||
this.game = game
|
||||
this.id = id;
|
||||
|
||||
this.currentPosition = spawn;
|
||||
this.lastMoveAt = 0;
|
||||
|
||||
this.game.physics.arcade.enable(this);
|
||||
this.body.setSize(20, 20, 6, 6);
|
||||
this.body.immovable = true;
|
||||
|
||||
this.animations.add('up', [9, 10, 11], 15, true);
|
||||
this.animations.add('down', [0, 1, 2], 15, true);
|
||||
this.animations.add('right', [6, 7, 8], 15, true);
|
||||
this.animations.add('left', [3, 4, 5], 15, true);
|
||||
|
||||
this.defineSelf(skin)
|
||||
}
|
||||
|
||||
update () {
|
||||
// this.game.debug.body(this);
|
||||
}
|
||||
|
||||
goTo(newPosition) {
|
||||
this.lastMoveAt = this.game.time.now;
|
||||
|
||||
this.animateFace(newPosition);
|
||||
|
||||
this.game.add.tween(this).to(newPosition, PING, Phaser.Easing.Linear.None, true);
|
||||
}
|
||||
|
||||
animateFace(newPosition) {
|
||||
let face = 'down';
|
||||
let diffX = newPosition.x - this.currentPosition.x;
|
||||
let diffY = newPosition.y - this.currentPosition.y;
|
||||
|
||||
if (diffX < 0) {
|
||||
face = 'left'
|
||||
} else if (diffX > 0) {
|
||||
face = 'right'
|
||||
} else if (diffY < 0) {
|
||||
face = 'up'
|
||||
} else if (diffY > 0) {
|
||||
face = 'down'
|
||||
}
|
||||
|
||||
this.animations.play(face)
|
||||
this.currentPosition = newPosition;
|
||||
}
|
||||
|
||||
defineSelf(name) {
|
||||
let playerText = new Text({
|
||||
game: this.game,
|
||||
x: TILE_SIZE / 2,
|
||||
y: -10,
|
||||
text: name,
|
||||
style: {
|
||||
font: '14px Areal',
|
||||
fill: '#FFFFFF',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 3
|
||||
}
|
||||
})
|
||||
|
||||
this.addChild(playerText);
|
||||
}
|
||||
}
|
||||
18
client/js/entities/fire_blast.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { TILE_SIZE } from '../utils/constants';
|
||||
|
||||
export default class FireBlast extends Phaser.Sprite {
|
||||
|
||||
constructor(game, cell) {
|
||||
super(game, (cell.col * TILE_SIZE), (cell.row * TILE_SIZE), cell.type, 0);
|
||||
|
||||
this.game = game
|
||||
|
||||
this.animations.add('blast', [0, 1, 2, 3, 4]);
|
||||
|
||||
// 15 - framerate, loop, kill_on_complete
|
||||
this.play('blast', 15, false, true);
|
||||
|
||||
this.game.physics.arcade.enable(this);
|
||||
}
|
||||
|
||||
}
|
||||
51
client/js/entities/info.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export default class Info {
|
||||
|
||||
constructor({ game, player }) {
|
||||
this.game = game;
|
||||
this.player = player;
|
||||
|
||||
this.style = { font: '14px Arial', fill: '#ffffff', align: 'left' }
|
||||
this.redStyle = { font: '30px Arial', fill: '#ff0044', align: 'center' };
|
||||
|
||||
let bootsIcon = new Phaser.Image(this.game, 5, 2, 'placeholder_speed');
|
||||
this.speedText = new Phaser.Text(this.game, 35, 7, this.speedLabel(), this.style);
|
||||
bootsIcon.addChild(this.speedText)
|
||||
this.game.add.existing(bootsIcon);
|
||||
|
||||
let powerIcon = new Phaser.Image(this.game, 110, 2, 'placeholder_power');
|
||||
this.powerText = new Phaser.Text(this.game, 35, 7, this.powerLabel(), this.style);
|
||||
powerIcon.addChild(this.powerText)
|
||||
this.game.add.existing(powerIcon);
|
||||
|
||||
let delayIcon = new Phaser.Image(this.game, 215, 2, 'placeholder_time');
|
||||
this.delayText = new Phaser.Text(this.game, 35, 7, this.delayLabel(), this.style);
|
||||
delayIcon.addChild(this.delayText)
|
||||
this.game.add.existing(delayIcon);
|
||||
|
||||
this.deadText = this.game.add.text(this.game.world.centerX, this.game.world.height - 30, 'You died :(', this.redStyle);
|
||||
this.deadText.anchor.set(0.5);
|
||||
this.deadText.visible = false
|
||||
}
|
||||
|
||||
refreshStatistic() {
|
||||
this.speedText.text = this.speedLabel();
|
||||
this.powerText.text = this.powerLabel();
|
||||
this.delayText.text = this.delayLabel();
|
||||
}
|
||||
|
||||
showDeadInfo() {
|
||||
this.deadText.visible = true
|
||||
}
|
||||
|
||||
speedLabel() {
|
||||
return this.player.speed
|
||||
}
|
||||
|
||||
powerLabel() {
|
||||
return `x ${this.player.power}`
|
||||
}
|
||||
|
||||
delayLabel() {
|
||||
return `${this.player.delay / 1000} sec.`
|
||||
}
|
||||
}
|
||||
177
client/js/entities/player.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import {
|
||||
PING, TILE_SIZE, MAX_SPEED, STEP_SPEED, INITIAL_SPEED, SPEED, POWER, DELAY,
|
||||
MIN_DELAY, STEP_DELAY, INITIAL_DELAY, INITIAL_POWER, STEP_POWER
|
||||
} from '../utils/constants';
|
||||
|
||||
import Info from './info';
|
||||
import { SpoilNotification, Text } from '../helpers/elements';
|
||||
|
||||
export default class Player extends Phaser.Sprite {
|
||||
|
||||
constructor({ game, id, spawn, skin }) {
|
||||
super(game, spawn.x, spawn.y, 'bomberman_' + skin);
|
||||
|
||||
this.game = game;
|
||||
this.id = id;
|
||||
|
||||
this.prevPosition = { x: spawn.x, y: spawn.y };
|
||||
|
||||
this.delay = INITIAL_DELAY;
|
||||
this.power = INITIAL_POWER;
|
||||
this.speed = INITIAL_SPEED;
|
||||
this._lastBombTime = 0;
|
||||
|
||||
this.game.add.existing(this);
|
||||
this.game.physics.arcade.enable(this);
|
||||
this.body.setSize(20, 20, 6, 6);
|
||||
|
||||
game.time.events.loop(PING , this.positionUpdaterLoop.bind(this));
|
||||
|
||||
this.animations.add('up', [9, 10, 11], 15, true);
|
||||
this.animations.add('down', [0, 1, 2], 15, true);
|
||||
this.animations.add('right', [6, 7, 8], 15, true);
|
||||
this.animations.add('left', [3, 4, 5], 15, true);
|
||||
|
||||
this.info = new Info({ game: this.game, player: this });
|
||||
|
||||
this.defineKeyboard()
|
||||
this.defineSelf(skin)
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.alive) {
|
||||
this.handleMoves()
|
||||
this.handleBombs()
|
||||
}
|
||||
|
||||
// this.game.debug.body(this);
|
||||
// this.game.debug.spriteInfo(this, 32, 32);
|
||||
}
|
||||
|
||||
defineKeyboard() {
|
||||
this.upKey = this.game.input.keyboard.addKey(Phaser.Keyboard.UP)
|
||||
this.downKey = this.game.input.keyboard.addKey(Phaser.Keyboard.DOWN)
|
||||
this.leftKey = this.game.input.keyboard.addKey(Phaser.Keyboard.LEFT)
|
||||
this.rightKey = this.game.input.keyboard.addKey(Phaser.Keyboard.RIGHT)
|
||||
this.spaceKey = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
|
||||
}
|
||||
|
||||
handleMoves() {
|
||||
this.body.velocity.set(0);
|
||||
let animationsArray = []
|
||||
|
||||
if (this.leftKey.isDown){
|
||||
this.body.velocity.x = -this.speed;
|
||||
animationsArray.push('left')
|
||||
} else if (this.rightKey.isDown) {
|
||||
this.body.velocity.x = this.speed;
|
||||
animationsArray.push('right')
|
||||
}
|
||||
|
||||
if (this.upKey.isDown) {
|
||||
this.body.velocity.y = -this.speed;
|
||||
animationsArray.push('up')
|
||||
} else if (this.downKey.isDown) {
|
||||
this.body.velocity.y = this.speed;
|
||||
animationsArray.push('down')
|
||||
}
|
||||
|
||||
let currentAnimation = animationsArray[0]
|
||||
if (currentAnimation){
|
||||
this.animations.play(currentAnimation)
|
||||
return
|
||||
}
|
||||
|
||||
this.animations.stop();
|
||||
}
|
||||
|
||||
handleBombs() {
|
||||
if (this.game.input.keyboard.isDown(Phaser.Keyboard.SPACEBAR)) {
|
||||
let now = this.game.time.now;
|
||||
|
||||
if (now > this._lastBombTime) {
|
||||
this._lastBombTime = now + this.delay;
|
||||
|
||||
clientSocket.emit('create bomb', { col: this.currentCol(), row: this.currentRow() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCol() {
|
||||
return Math.floor(this.body.position.x / TILE_SIZE)
|
||||
}
|
||||
|
||||
currentRow() {
|
||||
return Math.floor(this.body.position.y / TILE_SIZE)
|
||||
}
|
||||
|
||||
positionUpdaterLoop() {
|
||||
let newPosition = { x: this.position.x, y: this.position.y }
|
||||
|
||||
if (this.prevPosition.x !== newPosition.x || this.prevPosition.y !== newPosition.y) {
|
||||
clientSocket.emit('update player position', newPosition);
|
||||
this.prevPosition = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
becomesDead() {
|
||||
this.info.showDeadInfo()
|
||||
this.kill();
|
||||
}
|
||||
|
||||
pickSpoil( spoil_type ){
|
||||
if ( spoil_type === SPEED ){ this.increaseSpeed() }
|
||||
if ( spoil_type === POWER ){ this.increasePower() }
|
||||
if ( spoil_type === DELAY ){ this.increaseDelay() }
|
||||
}
|
||||
|
||||
increaseSpeed(){
|
||||
let asset = 'speed_up_no_bonus'
|
||||
|
||||
if (this.speed < MAX_SPEED) {
|
||||
this.speed = this.speed + STEP_SPEED;
|
||||
this.info.refreshStatistic();
|
||||
asset = 'speed_up_bonus'
|
||||
}
|
||||
|
||||
new SpoilNotification({ game: this.game, asset: asset, x: this.position.x, y: this.position.y })
|
||||
}
|
||||
|
||||
increaseDelay(){
|
||||
let asset = 'delay_up_no_bonus'
|
||||
|
||||
if (this.delay > MIN_DELAY){
|
||||
this.delay -= STEP_DELAY;
|
||||
this.info.refreshStatistic();
|
||||
asset = 'delay_up_bonus'
|
||||
}
|
||||
|
||||
new SpoilNotification({ game: this.game, asset: asset, x: this.position.x, y: this.position.y })
|
||||
}
|
||||
|
||||
increasePower(){
|
||||
let asset = 'power_up_bonus'
|
||||
|
||||
this.power += STEP_POWER;
|
||||
this.info.refreshStatistic();
|
||||
|
||||
new SpoilNotification({ game: this.game, asset: asset, x: this.position.x, y: this.position.y })
|
||||
}
|
||||
|
||||
defineSelf(name) {
|
||||
let playerText = new Text({
|
||||
game: this.game,
|
||||
x: TILE_SIZE / 2,
|
||||
y: -10,
|
||||
text: `\u272E ${name} \u272E`,
|
||||
style: {
|
||||
font: '15px Areal',
|
||||
fill: '#FFFFFF',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 3
|
||||
}
|
||||
})
|
||||
|
||||
this.addChild(playerText);
|
||||
}
|
||||
}
|
||||
25
client/js/entities/spoil.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { SPEED, POWER, DELAY, TILE_SIZE } from '../utils/constants';
|
||||
|
||||
export default class Spoil extends Phaser.Sprite {
|
||||
|
||||
constructor(game, spoil) {
|
||||
|
||||
let spoil_type;
|
||||
if (spoil.spoil_type === DELAY) {
|
||||
spoil_type = 0
|
||||
}
|
||||
if (spoil.spoil_type === POWER) {
|
||||
spoil_type = 1
|
||||
}
|
||||
if (spoil.spoil_type === SPEED) {
|
||||
spoil_type = 2
|
||||
}
|
||||
|
||||
super(game, (spoil.col * TILE_SIZE), (spoil.row * TILE_SIZE), 'spoil_tileset', spoil_type);
|
||||
|
||||
this.id = spoil.id
|
||||
|
||||
this.game.physics.arcade.enable(this);
|
||||
}
|
||||
|
||||
}
|
||||
132
client/js/helpers/elements.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
export class Text extends Phaser.Text {
|
||||
|
||||
constructor({ game, x, y, text, style }) {
|
||||
super(game, x, y, text, style);
|
||||
this.anchor.setTo(0.5);
|
||||
|
||||
this.game.add.existing(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Button extends Phaser.Button {
|
||||
|
||||
constructor({ game, x, y, asset, callback, callbackContext, overFrame, outFrame, downFrame, upFrame }) {
|
||||
super(game, x, y, asset, callback, callbackContext, overFrame, outFrame, downFrame, upFrame);
|
||||
this.anchor.setTo(0.5);
|
||||
|
||||
this.game.add.existing(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class TextButton extends Phaser.Button {
|
||||
|
||||
constructor({ game, x, y, asset, callback, callbackContext, overFrame, outFrame, downFrame, upFrame, label, style }) {
|
||||
super(game, x, y, asset, callback, callbackContext, overFrame, outFrame, downFrame, upFrame);
|
||||
this.anchor.setTo(0.5);
|
||||
|
||||
this.text = new Phaser.Text(this.game, 0, 0, label, style);
|
||||
this.text.anchor.setTo(0.5);
|
||||
|
||||
this.addChild(this.text);
|
||||
|
||||
this.game.add.existing(this);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.setFrames(3, 3);
|
||||
this.inputEnabled = false;
|
||||
this.input.useHandCursor = false;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.setFrames(1, 0, 2);
|
||||
this.inputEnabled = true;
|
||||
this.input.useHandCursor = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class GameSlots extends Phaser.Group {
|
||||
|
||||
constructor({ game, availableGames, callback, callbackContext, x, y, style }) {
|
||||
super(game);
|
||||
|
||||
let game_slot_asset = 'slot_backdrop'
|
||||
let game_enter_asset = 'list_icon'
|
||||
|
||||
let yOffset = y;
|
||||
|
||||
for (let availableGame of availableGames) {
|
||||
let gameBox = new Phaser.Image(this.game, x, yOffset, game_slot_asset)
|
||||
let button = new Phaser.Button(this.game, gameBox.width - 100, 12, game_enter_asset, callback.bind(callbackContext, { game_id: availableGame.id }), null, 1, 0, 2, 1);
|
||||
let text = new Phaser.Text(this.game, 30, 25, `Join Game: ${availableGame.name}`, style);
|
||||
|
||||
gameBox.addChild(button);
|
||||
gameBox.addChild(text);
|
||||
|
||||
this.add(gameBox);
|
||||
|
||||
yOffset += 105;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.callAll('kill') // destroy
|
||||
}
|
||||
}
|
||||
|
||||
export class PlayerSlots extends Phaser.Group {
|
||||
|
||||
constructor({ game, max_players, players, x, y, asset_empty, asset_player, style }) {
|
||||
super(game);
|
||||
|
||||
let xOffset = x;
|
||||
|
||||
for (let i = 0; i < max_players; i++) {
|
||||
let slotBox
|
||||
let slotName
|
||||
let _player = players[i]
|
||||
|
||||
if (_player) {
|
||||
slotBox = new Phaser.Image(this.game, xOffset, y, asset_player+_player.skin)
|
||||
slotName = new Phaser.Text(this.game, slotBox.width/2, slotBox.height + 15, _player.skin, style);
|
||||
slotName.anchor.setTo(0.5);
|
||||
slotBox.addChild(slotName);
|
||||
} else {
|
||||
slotBox = new Phaser.Image(this.game, xOffset, y, asset_empty)
|
||||
}
|
||||
|
||||
this.add(slotBox);
|
||||
xOffset += 170;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.callAll('kill')
|
||||
}
|
||||
}
|
||||
|
||||
export class SpoilNotification extends Phaser.Group {
|
||||
|
||||
constructor({ game, asset, x, y }) {
|
||||
super(game)
|
||||
|
||||
this.picture = new Phaser.Image(this.game, x, y - 20, asset);
|
||||
this.picture.anchor.setTo(0.5);
|
||||
|
||||
this.add(this.picture);
|
||||
|
||||
this.tween = this.game.add.tween(this.picture);
|
||||
this.tween.to({ y: this.picture.y - 25, alpha: 0 }, 600);
|
||||
|
||||
this.tween.onComplete.add(this.finish, this);
|
||||
|
||||
this.tween.start()
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.callAll('kill')
|
||||
}
|
||||
}
|
||||
26
client/js/states/boot.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Text } from '../helpers/elements';
|
||||
|
||||
class Boot extends Phaser.State {
|
||||
|
||||
create() {
|
||||
// Make the game keep reacting to messages from the server even when the game window doesn’t have focus.
|
||||
// The game pauses when I open a new tab in the same window, but does not pause when I focus on another application
|
||||
this.game.stage.disableVisibilityChange = true;
|
||||
|
||||
new Text({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY,
|
||||
text: 'Loading...',
|
||||
style: {
|
||||
font: '30px Areal',
|
||||
fill: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
|
||||
this.state.start('Preload');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Boot;
|
||||
90
client/js/states/menu.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Text, TextButton, GameSlots } from '../helpers/elements';
|
||||
|
||||
class Menu extends Phaser.State {
|
||||
|
||||
init() {
|
||||
this.slotsWithGame = null;
|
||||
|
||||
clientSocket.on('display pending games', this.displayPendingGames.bind(this));
|
||||
}
|
||||
|
||||
create() {
|
||||
let background = this.add.image(this.game.world.centerX, this.game.world.centerY, 'main_menu');
|
||||
background.anchor.setTo(0.5);
|
||||
|
||||
|
||||
new Text({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY - 215,
|
||||
text: 'Main Menu',
|
||||
style: {
|
||||
font: '35px Areal',
|
||||
fill: '#9ec0ba',
|
||||
stroke: '#7f9995',
|
||||
strokeThickness: 3
|
||||
}
|
||||
})
|
||||
|
||||
new TextButton({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY + 195,
|
||||
asset: 'buttons',
|
||||
callback: this.hostGameAction,
|
||||
callbackContext: this,
|
||||
overFrame: 1,
|
||||
outFrame: 0,
|
||||
downFrame: 2,
|
||||
upFrame: 0,
|
||||
label: 'New Game',
|
||||
style: {
|
||||
font: '20px Areal',
|
||||
fill: '#000000'
|
||||
}
|
||||
});
|
||||
|
||||
clientSocket.emit('enter lobby', this.displayPendingGames.bind(this));
|
||||
}
|
||||
|
||||
update() {
|
||||
}
|
||||
|
||||
hostGameAction() {
|
||||
clientSocket.emit('leave lobby');
|
||||
this.state.start('SelectMap');
|
||||
}
|
||||
|
||||
displayPendingGames(availableGames) {
|
||||
// NOTE: That is not optimal way to preview slots,
|
||||
// we should implement AddSlotToGroup, RemoveSlotFromGroup
|
||||
|
||||
// I triying to care about readability, not about performance.
|
||||
if (this.slotsWithGame) {
|
||||
this.slotsWithGame.destroy()
|
||||
}
|
||||
|
||||
this.slotsWithGame = new GameSlots({
|
||||
game: this.game,
|
||||
availableGames: availableGames,
|
||||
callback: this.joinGameAction,
|
||||
callbackContext: this,
|
||||
x: this.game.world.centerX - 220,
|
||||
y: 160,
|
||||
style: {
|
||||
font: '35px Areal',
|
||||
fill: '#efefef',
|
||||
stroke: '#ae743a',
|
||||
strokeThickness: 3
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
joinGameAction(game_id) {
|
||||
clientSocket.emit('leave lobby');
|
||||
// https://phaser.io/docs/2.6.2/Phaser.StateManager.html#start
|
||||
this.state.start('PendingGame', true, false, game_id);
|
||||
}
|
||||
}
|
||||
|
||||
export default Menu;
|
||||
118
client/js/states/pending_game.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { Text, Button, TextButton, PlayerSlots } from '../helpers/elements';
|
||||
|
||||
class PendingGame extends Phaser.State {
|
||||
|
||||
init({ game_id }) {
|
||||
this.slotsWithPlayer = null;
|
||||
|
||||
this.game_id = game_id;
|
||||
|
||||
clientSocket.on('update game', this.displayGameInfo.bind(this));
|
||||
clientSocket.on('launch game', this.launchGame.bind(this));
|
||||
|
||||
clientSocket.emit('enter pending game', { game_id: this.game_id });
|
||||
}
|
||||
|
||||
create() {
|
||||
let background = this.add.image(this.game.world.centerX, this.game.world.centerY, 'main_menu');
|
||||
background.anchor.setTo(0.5);
|
||||
|
||||
this.gameTitle = new Text({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY - 215,
|
||||
text: '',
|
||||
style: {
|
||||
font: '35px Areal',
|
||||
fill: '#9ec0ba',
|
||||
stroke: '#6f7975',
|
||||
strokeThickness: 3
|
||||
}
|
||||
})
|
||||
|
||||
this.startGameButton = new TextButton({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX + 105,
|
||||
y: this.game.world.centerY + 195,
|
||||
asset: 'buttons',
|
||||
callback: this.startGameAction,
|
||||
callbackContext: this,
|
||||
overFrame: 1,
|
||||
outFrame: 0,
|
||||
downFrame: 2,
|
||||
upFrame: 0,
|
||||
label: 'Start Game',
|
||||
style: {
|
||||
font: '20px Areal',
|
||||
fill: '#000000'
|
||||
}
|
||||
});
|
||||
|
||||
this.startGameButton.disable()
|
||||
|
||||
new TextButton({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX - 105,
|
||||
y: this.game.world.centerY + 195,
|
||||
asset: 'buttons',
|
||||
callback: this.leaveGameAction,
|
||||
callbackContext: this,
|
||||
overFrame: 1,
|
||||
outFrame: 0,
|
||||
downFrame: 2,
|
||||
upFrame: 0,
|
||||
label: 'Leave Game',
|
||||
style: {
|
||||
font: '20px Areal',
|
||||
fill: '#000000'
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
displayGameInfo({ current_game }) {
|
||||
let players = Object.values(current_game.players);
|
||||
|
||||
this.gameTitle.text = current_game.name
|
||||
|
||||
if (this.slotsWithPlayer) {
|
||||
this.slotsWithPlayer.destroy()
|
||||
}
|
||||
|
||||
this.slotsWithPlayer = new PlayerSlots({
|
||||
game: this.game,
|
||||
max_players: current_game.max_players,
|
||||
players: players,
|
||||
x: this.game.world.centerX - 245,
|
||||
y: this.game.world.centerY - 80,
|
||||
asset_empty: 'bomberman_head_blank',
|
||||
asset_player: 'bomberman_head_',
|
||||
style: {
|
||||
font: '20px Areal',
|
||||
fill: '#48291c'
|
||||
}
|
||||
})
|
||||
|
||||
if(players.length > 1) {
|
||||
this.startGameButton.enable();
|
||||
} else {
|
||||
this.startGameButton.disable();
|
||||
}
|
||||
}
|
||||
|
||||
leaveGameAction() {
|
||||
clientSocket.emit('leave pending game');
|
||||
|
||||
this.state.start('Menu');
|
||||
}
|
||||
|
||||
startGameAction() {
|
||||
clientSocket.emit('start game');
|
||||
}
|
||||
|
||||
launchGame(game) {
|
||||
this.state.start('Play', true, false, game);
|
||||
}
|
||||
}
|
||||
|
||||
export default PendingGame;
|
||||
165
client/js/states/play.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { findFrom, findAndDestroyFrom } from '../utils/utils';
|
||||
import { TILESET, LAYER } from '../utils/constants';
|
||||
|
||||
import Player from '../entities/player';
|
||||
import EnemyPlayer from '../entities/enemy_player';
|
||||
import Bomb from '../entities/bomb';
|
||||
import Spoil from '../entities/spoil';
|
||||
import FireBlast from '../entities/fire_blast';
|
||||
import Bone from '../entities/bone';
|
||||
|
||||
class Play extends Phaser.State {
|
||||
init(game) {
|
||||
this.currentGame = game
|
||||
}
|
||||
|
||||
create() {
|
||||
this.createMap();
|
||||
this.createPlayers();
|
||||
this.setEventHandlers();
|
||||
|
||||
this.game.time.events.loop(400 , this.stopAnimationLoop.bind(this));
|
||||
}
|
||||
|
||||
update() {
|
||||
this.game.physics.arcade.collide(this.player, this.blockLayer);
|
||||
this.game.physics.arcade.collide(this.player, this.enemies);
|
||||
this.game.physics.arcade.collide(this.player, this.bombs);
|
||||
|
||||
this.game.physics.arcade.overlap(this.player, this.spoils, this.onPlayerVsSpoil, null, this);
|
||||
this.game.physics.arcade.overlap(this.player, this.blasts, this.onPlayerVsBlast, null, this);
|
||||
}
|
||||
|
||||
createMap() {
|
||||
this.map = this.add.tilemap(this.currentGame.map_name);
|
||||
|
||||
this.map.addTilesetImage(TILESET);
|
||||
|
||||
this.blockLayer = this.map.createLayer(LAYER);
|
||||
this.blockLayer.resizeWorld();
|
||||
|
||||
this.map.setCollision(this.blockLayer.layer.properties.collisionTiles)
|
||||
|
||||
this.player = null;
|
||||
this.bones = this.game.add.group();
|
||||
this.bombs = this.game.add.group();
|
||||
this.spoils = this.game.add.group();
|
||||
this.blasts = this.game.add.group();
|
||||
this.enemies = this.game.add.group();
|
||||
|
||||
this.game.physics.arcade.enable(this.blockLayer);
|
||||
}
|
||||
|
||||
createPlayers() {
|
||||
for (let player of Object.values(this.currentGame.players)) {
|
||||
let setup = {
|
||||
game: this.game,
|
||||
id: player.id,
|
||||
spawn: player.spawn,
|
||||
skin: player.skin
|
||||
}
|
||||
|
||||
if (player.id === clientSocket.id) {
|
||||
this.player = new Player(setup);
|
||||
} else {
|
||||
this.enemies.add(new EnemyPlayer(setup))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEventHandlers() {
|
||||
clientSocket.on('move player', this.onMovePlayer.bind(this));
|
||||
clientSocket.on('player win', this.onPlayerWin.bind(this));
|
||||
clientSocket.on('show bomb', this.onShowBomb.bind(this));
|
||||
clientSocket.on('detonate bomb', this.onDetonateBomb.bind(this));
|
||||
clientSocket.on('spoil was picked', this.onSpoilWasPicked.bind(this));
|
||||
clientSocket.on('show bones', this.onShowBones.bind(this));
|
||||
clientSocket.on('player disconnect', this.onPlayerDisconnect.bind(this));
|
||||
}
|
||||
|
||||
onPlayerVsSpoil(player, spoil) {
|
||||
clientSocket.emit('pick up spoil', { spoil_id: spoil.id });
|
||||
spoil.kill();
|
||||
}
|
||||
|
||||
onPlayerVsBlast(player, blast) {
|
||||
if (player.alive) {
|
||||
clientSocket.emit('player died', { col: player.currentCol(), row: player.currentRow() });
|
||||
player.becomesDead()
|
||||
}
|
||||
}
|
||||
|
||||
onMovePlayer({ player_id, x, y }) {
|
||||
let enemy = findFrom(player_id, this.enemies);
|
||||
if (!enemy) { return }
|
||||
|
||||
enemy.goTo({ x: x, y: y })
|
||||
}
|
||||
|
||||
stopAnimationLoop() {
|
||||
for (let enemy of this.enemies.children) {
|
||||
if (enemy.lastMoveAt < this.game.time.now - 200) {
|
||||
enemy.animations.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowBomb({ bomb_id, col, row }) {
|
||||
this.bombs.add(new Bomb(this.game, bomb_id, col, row));
|
||||
}
|
||||
|
||||
onDetonateBomb({ bomb_id, blastedCells }) {
|
||||
// Remove Bomb:
|
||||
findAndDestroyFrom(bomb_id, this.bombs)
|
||||
|
||||
// Render Blast:
|
||||
for (let cell of blastedCells) {
|
||||
this.blasts.add(new FireBlast(this.game, cell));
|
||||
};
|
||||
|
||||
// Destroy Tiles:
|
||||
for (let cell of blastedCells) {
|
||||
if (!cell.destroyed) { continue }
|
||||
|
||||
this.map.putTile(this.blockLayer.layer.properties.empty, cell.col, cell.row, this.blockLayer);
|
||||
};
|
||||
|
||||
// Add Spoils:
|
||||
for (let cell of blastedCells) {
|
||||
if (!cell.destroyed) { continue }
|
||||
if (!cell.spoil) { continue }
|
||||
|
||||
this.spoils.add(new Spoil(this.game, cell.spoil));
|
||||
};
|
||||
}
|
||||
|
||||
onSpoilWasPicked({ player_id, spoil_id, spoil_type }){
|
||||
if (player_id === this.player.id){
|
||||
this.player.pickSpoil(spoil_type)
|
||||
}
|
||||
|
||||
findAndDestroyFrom(spoil_id, this.spoils)
|
||||
}
|
||||
|
||||
onShowBones({ player_id, col, row }) {
|
||||
this.bones.add(new Bone(this.game, col, row));
|
||||
|
||||
findAndDestroyFrom(player_id, this.enemies)
|
||||
}
|
||||
|
||||
onPlayerWin(winner_skin) {
|
||||
clientSocket.emit('leave game');
|
||||
|
||||
this.state.start('Win', true, false, winner_skin);
|
||||
}
|
||||
|
||||
onPlayerDisconnect({ player_id }) {
|
||||
findAndDestroyFrom(player_id, this.enemies);
|
||||
|
||||
if (this.enemies.children.length >= 1) { return }
|
||||
|
||||
this.onPlayerWin()
|
||||
}
|
||||
}
|
||||
|
||||
export default Play;
|
||||
79
client/js/states/preload.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
class Preload extends Phaser.State {
|
||||
|
||||
preload() {
|
||||
// Menu:
|
||||
this.load.image('main_menu', 'images/menu/main_menu.png');
|
||||
this.load.image('slot_backdrop', 'images/menu/slot_backdrop.png');
|
||||
|
||||
this.load.spritesheet('buttons', 'images/menu/buttons.png', 200, 75);
|
||||
this.load.spritesheet('check_icon', 'images/menu/accepts.png', 75, 75);
|
||||
this.load.spritesheet('list_icon', 'images/menu/game_enter.png', 75, 75);
|
||||
|
||||
this.load.image('hot_map_preview', 'images/menu/hot_map_preview.png');
|
||||
this.load.image('cold_map_preview', 'images/menu/cold_map_preview.png');
|
||||
this.load.image('prev', 'images/menu/left_arrow.png');
|
||||
this.load.image('next', 'images/menu/right_arrow.png');
|
||||
|
||||
// Map:
|
||||
this.load.image('tiles', 'maps/tileset.png');
|
||||
this.load.tilemap('hot_map', 'maps/hot_map.json', null, Phaser.Tilemap.TILED_JSON);
|
||||
this.load.tilemap('cold_map', 'maps/cold_map.json', null, Phaser.Tilemap.TILED_JSON);
|
||||
|
||||
|
||||
// Game:
|
||||
this.load.spritesheet('explosion_center', 'images/game/explosion_center.png', 35, 35);
|
||||
this.load.spritesheet('explosion_horizontal', 'images/game/explosion_horizontal.png', 35, 35);
|
||||
this.load.spritesheet('explosion_vertical', 'images/game/explosion_vertical.png', 35, 35);
|
||||
this.load.spritesheet('explosion_up', 'images/game/explosion_up.png', 35, 35);
|
||||
this.load.spritesheet('explosion_right', 'images/game/explosion_right.png', 35, 35);
|
||||
this.load.spritesheet('explosion_down', 'images/game/explosion_down.png', 35, 35);
|
||||
this.load.spritesheet('explosion_left', 'images/game/explosion_left.png', 35, 35);
|
||||
|
||||
this.load.spritesheet('spoil_tileset', 'images/game/spoil_tileset.png', 35, 35);
|
||||
this.load.spritesheet('bone_tileset', 'images/game/bone_tileset.png', 35, 35);
|
||||
this.load.spritesheet('bomb_tileset', 'images/game/bombs.png', 35, 35);
|
||||
|
||||
this.load.image('speed_up_bonus', 'images/game/speed_up_bonus.png');
|
||||
this.load.image('speed_up_no_bonus', 'images/game/speed_up_no_bonus.png');
|
||||
this.load.image('delay_up_bonus', 'images/game/delay_up_bonus.png');
|
||||
this.load.image('delay_up_no_bonus', 'images/game/delay_up_no_bonus.png');
|
||||
this.load.image('power_up_bonus', 'images/game/power_up_bonus.png');
|
||||
|
||||
this.load.image('placeholder_power', 'images/game/placeholder_power.png');
|
||||
this.load.image('placeholder_speed', 'images/game/placeholder_speed.png');
|
||||
this.load.image('placeholder_time', 'images/game/placeholder_time.png');
|
||||
|
||||
// Skins:
|
||||
this.load.image('bomberman_head_blank', 'images/game/chars/0-face.png');
|
||||
|
||||
this.load.image('bomberman_head_Theodora', 'images/game/chars/1-face.png');
|
||||
this.load.image('bomberman_head_Ringo', 'images/game/chars/2-face.png');
|
||||
this.load.image('bomberman_head_Jeniffer', 'images/game/chars/3-face.png');
|
||||
this.load.image('bomberman_head_Godard', 'images/game/chars/4-face.png');
|
||||
this.load.image('bomberman_head_Biarid', 'images/game/chars/5-face.png');
|
||||
this.load.image('bomberman_head_Solia', 'images/game/chars/6-face.png');
|
||||
this.load.image('bomberman_head_Kedan', 'images/game/chars/7-face.png');
|
||||
this.load.image('bomberman_head_Nigob', 'images/game/chars/8-face.png');
|
||||
this.load.image('bomberman_head_Baradir', 'images/game/chars/9-face.png');
|
||||
this.load.image('bomberman_head_Raviel', 'images/game/chars/10-face.png');
|
||||
this.load.image('bomberman_head_Valpo', 'images/game/chars/11-face.png');
|
||||
|
||||
this.load.spritesheet('bomberman_Theodora', 'images/game/chars/1-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Ringo', 'images/game/chars/2-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Jeniffer', 'images/game/chars/3-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Godard', 'images/game/chars/4-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Biarid', 'images/game/chars/5-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Solia', 'images/game/chars/6-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Kedan', 'images/game/chars/7-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Nigob', 'images/game/chars/8-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Baradir', 'images/game/chars/9-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Raviel', 'images/game/chars/10-preview.png', 32, 32);
|
||||
this.load.spritesheet('bomberman_Valpo', 'images/game/chars/11-preview.png', 32, 32);
|
||||
}
|
||||
|
||||
create() {
|
||||
this.state.start('Menu');
|
||||
}
|
||||
}
|
||||
|
||||
export default Preload;
|
||||
67
client/js/states/select_map.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { AVAILABLE_MAPS } from '../utils/constants';
|
||||
import { Text, Button } from '../helpers/elements';
|
||||
|
||||
class SelectMap extends Phaser.State {
|
||||
|
||||
init() {
|
||||
this.slider = new phaseSlider(this);
|
||||
}
|
||||
|
||||
create() {
|
||||
let background = this.add.image(this.game.world.centerX, this.game.world.centerY, 'main_menu');
|
||||
background.anchor.setTo(0.5);
|
||||
|
||||
new Text({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY - 215,
|
||||
text: 'Select Map',
|
||||
style: {
|
||||
font: '35px Areal',
|
||||
fill: '#9ec0ba',
|
||||
stroke: '#6f7975',
|
||||
strokeThickness: 3
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// WARN: https://github.com/netgfx/PhaseSlider/issues/1
|
||||
let hotMapImage = new Phaser.Image(this.game, 0, 0, 'hot_map_preview');
|
||||
let coldMapImage = new Phaser.Image(this.game, 0, 0, 'cold_map_preview');
|
||||
|
||||
this.slider.createSlider({
|
||||
x: this.game.world.centerX - hotMapImage.width / 2,
|
||||
y: this.game.world.centerY - coldMapImage.height / 2,
|
||||
width: hotMapImage.width,
|
||||
height: hotMapImage.height,
|
||||
customHandlePrev: 'prev',
|
||||
customHandleNext: 'next',
|
||||
objects: [hotMapImage, coldMapImage]
|
||||
});
|
||||
|
||||
new Button({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY + 195,
|
||||
asset: 'check_icon',
|
||||
callback: this.confirmStageSelection,
|
||||
callbackContext: this,
|
||||
overFrame: 1,
|
||||
outFrame: 0,
|
||||
downFrame: 2,
|
||||
upFrame: 0,
|
||||
})
|
||||
}
|
||||
|
||||
confirmStageSelection() {
|
||||
let map_name = AVAILABLE_MAPS[this.slider.getCurrentIndex()]
|
||||
|
||||
clientSocket.emit('create game', map_name, this.joinToNewGame.bind(this));
|
||||
}
|
||||
|
||||
joinToNewGame(game_id) {
|
||||
this.state.start('PendingGame', true, false, game_id);
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectMap;
|
||||
41
client/js/states/win.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Text } from '../helpers/elements';
|
||||
|
||||
class Win extends Phaser.State {
|
||||
|
||||
init(winner_skin) {
|
||||
this.skin = winner_skin
|
||||
}
|
||||
|
||||
create() {
|
||||
new Text({
|
||||
game: this.game,
|
||||
x: this.game.world.centerX,
|
||||
y: this.game.world.centerY,
|
||||
text: this.winnerText(),
|
||||
style: {
|
||||
font: '30px Areal',
|
||||
fill: '#FFFFFF'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
update() {
|
||||
if( this.game.input.keyboard.isDown(Phaser.Keyboard.ENTER) ) {
|
||||
this.returnToMenu();
|
||||
}
|
||||
}
|
||||
|
||||
returnToMenu() {
|
||||
this.state.start('Menu');
|
||||
}
|
||||
|
||||
winnerText() {
|
||||
if (this.skin) {
|
||||
return `Player: "${this.skin}" won! Press Enter to return to main menu.`
|
||||
}
|
||||
|
||||
return 'Opponent left! Press Enter to return to main menu.'
|
||||
}
|
||||
}
|
||||
|
||||
export default Win;
|
||||
20
client/js/utils/constants.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export const AVAILABLE_MAPS = ['hot_map', 'cold_map']
|
||||
export const TILESET = 'tiles';
|
||||
export const LAYER = 'Blocks';
|
||||
|
||||
export const TILE_SIZE = 35;
|
||||
export const EXPLOSION_TIME = 2000;
|
||||
export const PING = 100;
|
||||
|
||||
export const SPEED = 0
|
||||
export const POWER = 1
|
||||
export const DELAY = 2
|
||||
|
||||
export const INITIAL_SPEED = 150
|
||||
export const STEP_SPEED = 50
|
||||
export const MAX_SPEED = 350
|
||||
export const INITIAL_DELAY = 2000
|
||||
export const STEP_DELAY = 500
|
||||
export const MIN_DELAY = 500
|
||||
export const INITIAL_POWER = 1
|
||||
export const STEP_POWER = 1
|
||||
15
client/js/utils/utils.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export const findFrom = function(id, entities) {
|
||||
for (let entity of entities.children) {
|
||||
if (entity.id !== id) { continue }
|
||||
|
||||
return entity
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const findAndDestroyFrom = function(id, entities) {
|
||||
let entity = findFrom(id, entities);
|
||||
if (!entity) { return }
|
||||
|
||||
entity.destroy()
|
||||
}
|
||||
535
client/lib/phase-slide.js
Normal file
4
client/lib/phaser.min.js
vendored
Normal file
69
client/maps/cold_map.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"width": 28,
|
||||
"height": 18,
|
||||
"tilewidth": 35,
|
||||
"tileheight": 35,
|
||||
"layers": [
|
||||
{
|
||||
"name": "Blocks",
|
||||
"width": 28,
|
||||
"height": 18,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"data": [
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 2, 3, 3, 3, 3, 2, 1, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 1, 2, 3, 3, 3, 2, 3, 1,
|
||||
1, 3, 2, 1, 1, 3, 2, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 1, 3, 3, 2, 2, 2, 3, 1,
|
||||
1, 2, 3, 3, 3, 3, 3, 2, 3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 2, 3, 2, 3, 3, 1, 1, 1, 2, 1,
|
||||
1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 3, 3, 3, 3, 3, 2, 3, 3, 1, 2, 3, 3, 3, 3, 3, 1,
|
||||
1, 3, 3, 3, 2, 3, 3, 3, 3, 2, 2, 3, 3, 2, 1, 1, 3, 3, 2, 3, 1, 2, 3, 3, 2, 3, 2, 1,
|
||||
1, 2, 2, 3, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 3, 2, 1, 1, 1, 2, 1, 1,
|
||||
1, 2, 2, 3, 2, 3, 3, 1, 3, 3, 1, 1, 3, 3, 2, 2, 3, 3, 2, 2, 3, 3, 2, 1, 3, 3, 3, 1,
|
||||
1, 1, 1, 1, 2, 3, 3, 1, 3, 2, 3, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 3, 2, 2, 1,
|
||||
1, 3, 3, 3, 3, 3, 3, 1, 3, 2, 2, 2, 2, 3, 1, 1, 1, 1, 1, 2, 3, 3, 1, 2, 3, 2, 1, 1,
|
||||
1, 3, 2, 1, 1, 2, 3, 1, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 2, 3, 2, 3, 2, 3, 2, 2, 1,
|
||||
1, 1, 2, 2, 2, 2, 3, 1, 1, 1, 2, 2, 2, 1, 1, 2, 3, 2, 3, 2, 3, 2, 3, 2, 2, 3, 3, 1,
|
||||
1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 2, 3, 2, 1, 1, 1, 1, 1, 2, 3, 3, 1,
|
||||
1, 1, 1, 2, 2, 1, 1, 2, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 3, 2, 3, 2, 3, 1, 1, 1, 1, 1,
|
||||
1, 2, 3, 3, 3, 3, 3, 3, 3, 1, 2, 3, 1, 1, 1, 1, 2, 2, 3, 1, 2, 2, 3, 2, 2, 2, 2, 1,
|
||||
1, 3, 2, 2, 1, 2, 3, 2, 3, 1, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 1, 1, 3, 3, 3, 3, 1,
|
||||
1, 3, 3, 2, 3, 2, 1, 2, 3, 1, 3, 3, 2, 3, 2, 3, 2, 3, 3, 1, 3, 3, 3, 3, 2, 2, 2, 1,
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
|
||||
],
|
||||
"opacity": 1,
|
||||
"type": "tilelayer",
|
||||
"visible": true,
|
||||
"properties": {
|
||||
"max_players": 3,
|
||||
"collisionTiles": [1, 2],
|
||||
"empty": 3,
|
||||
"balk": 2,
|
||||
"wall": 1,
|
||||
"spawns": [
|
||||
{ "col": 6, "row": 3 },
|
||||
{ "col": 8, "row": 14 },
|
||||
{ "col": 20, "row": 15 },
|
||||
{ "col": 22, "row": 1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"renderorder": "right-down",
|
||||
"tilesets": [
|
||||
{
|
||||
"name": "tiles",
|
||||
"firstgid": 1,
|
||||
"tilewidth": 35,
|
||||
"tileheight": 35,
|
||||
"imagewidth": 210,
|
||||
"imageheight": 35,
|
||||
"image": "tileset.png",
|
||||
"margin": 0,
|
||||
"properties": {},
|
||||
"spacing": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"orientation": "orthogonal",
|
||||
"version": 1
|
||||
}
|
||||
26
client/maps/cold_map.tmx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.0.0" orientation="orthogonal" renderorder="right-down" width="28" height="18" tilewidth="35" tileheight="35" nextobjectid="1">
|
||||
<tileset firstgid="1" source="untitled.tsx"/>
|
||||
<layer name="Tile Layer 1" width="28" height="18">
|
||||
<data encoding="csv">
|
||||
4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
|
||||
4,6,5,4,6,6,5,6,6,5,6,5,5,6,5,6,5,6,6,6,6,5,6,4,6,5,6,4,
|
||||
4,6,6,5,6,5,5,5,6,6,5,4,6,5,6,5,6,4,5,5,6,5,5,6,5,6,6,4,
|
||||
4,4,6,6,5,6,5,4,5,6,6,4,4,4,6,6,4,4,6,6,4,6,6,5,6,6,4,4,
|
||||
4,5,4,5,6,6,6,5,4,5,6,6,5,5,6,6,5,6,6,4,6,6,6,6,6,4,5,4,
|
||||
4,6,5,5,5,4,6,6,5,4,5,6,6,6,6,4,6,6,4,5,5,5,4,5,4,5,6,4,
|
||||
4,6,6,5,4,6,5,6,6,6,6,5,6,5,4,4,6,5,6,6,5,6,6,4,5,6,6,4,
|
||||
4,6,5,6,5,6,6,4,5,6,4,5,5,5,6,6,6,5,5,5,6,5,6,6,6,5,6,4,
|
||||
4,6,6,6,6,5,6,4,5,6,4,4,4,6,6,5,6,6,6,5,4,6,5,6,5,4,4,4,
|
||||
4,6,4,4,5,6,6,4,5,6,5,5,6,5,5,4,4,4,6,6,4,5,6,5,6,6,6,4,
|
||||
4,6,6,6,6,5,6,5,5,6,6,6,6,6,5,4,5,6,5,6,4,6,6,5,6,5,6,4,
|
||||
4,6,5,5,4,6,6,6,6,5,6,4,4,6,6,6,5,6,5,6,5,5,5,4,5,6,5,4,
|
||||
4,6,5,4,5,4,5,6,6,4,5,5,4,6,5,6,6,6,4,6,6,6,4,5,4,5,6,4,
|
||||
4,5,4,6,5,6,5,6,4,6,6,6,6,6,5,6,6,6,6,4,6,6,5,6,5,4,5,4,
|
||||
4,4,6,6,6,6,6,4,6,6,4,4,4,6,6,4,4,6,5,6,4,6,5,6,6,5,4,4,
|
||||
4,6,6,5,5,5,6,6,5,5,4,6,5,5,5,5,4,5,6,5,6,5,6,6,6,6,6,4,
|
||||
4,6,6,5,4,5,6,6,6,6,5,5,6,6,6,6,5,5,6,6,6,6,6,4,5,6,6,4,
|
||||
4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
69
client/maps/hot_map.json
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"width": 28,
|
||||
"height": 18,
|
||||
"tilewidth": 35,
|
||||
"tileheight": 35,
|
||||
"layers": [
|
||||
{
|
||||
"name": "Blocks",
|
||||
"width": 28,
|
||||
"height": 18,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"data": [
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 6, 5, 4, 6, 6, 5, 6, 6, 5, 6, 5, 5, 6, 5, 6, 5, 6, 6, 6, 6, 5, 6, 4, 6, 5, 6, 4,
|
||||
4, 6, 6, 5, 6, 5, 5, 5, 6, 6, 5, 4, 6, 5, 6, 5, 6, 4, 5, 5, 6, 5, 5, 6, 5, 6, 6, 4,
|
||||
4, 4, 6, 6, 5, 6, 5, 4, 5, 6, 6, 4, 4, 4, 6, 6, 4, 4, 6, 6, 4, 6, 6, 5, 6, 6, 4, 4,
|
||||
4, 5, 4, 5, 6, 6, 6, 5, 4, 5, 6, 6, 5, 5, 6, 6, 5, 6, 6, 4, 6, 6, 6, 6, 6, 4, 5, 4,
|
||||
4, 6, 5, 5, 5, 4, 6, 6, 5, 4, 5, 6, 6, 6, 6, 4, 6, 6, 4, 5, 5, 5, 4, 5, 4, 5, 6, 4,
|
||||
4, 6, 6, 5, 4, 6, 5, 6, 6, 6, 6, 5, 6, 5, 4, 4, 6, 5, 6, 6, 5, 6, 6, 4, 5, 6, 6, 4,
|
||||
4, 6, 5, 6, 5, 6, 6, 4, 5, 6, 4, 5, 5, 5, 6, 6, 6, 5, 5, 5, 6, 5, 6, 6, 6, 5, 6, 4,
|
||||
4, 6, 6, 6, 6, 5, 6, 4, 5, 6, 4, 4, 4, 6, 6, 5, 6, 6, 6, 5, 4, 6, 5, 6, 5, 4, 4, 4,
|
||||
4, 6, 4, 4, 5, 6, 6, 4, 5, 6, 5, 5, 6, 5, 5, 4, 4, 4, 6, 6, 4, 5, 6, 5, 6, 6, 6, 4,
|
||||
4, 6, 6, 6, 6, 5, 6, 5, 5, 6, 6, 6, 6, 6, 5, 4, 5, 6, 5, 6, 4, 6, 6, 5, 6, 5, 6, 4,
|
||||
4, 6, 5, 5, 4, 6, 6, 6, 6, 5, 6, 4, 4, 6, 6, 6, 5, 6, 5, 6, 5, 5, 5, 4, 5, 6, 5, 4,
|
||||
4, 6, 5, 4, 5, 4, 5, 6, 6, 4, 5, 5, 4, 6, 5, 6, 6, 6, 4, 6, 6, 6, 4, 5, 4, 5, 6, 4,
|
||||
4, 5, 4, 6, 5, 6, 5, 6, 4, 6, 6, 6, 6, 6, 5, 6, 6, 6, 6, 4, 6, 6, 5, 6, 5, 4, 5, 4,
|
||||
4, 4, 6, 6, 6, 6, 6, 4, 6, 6, 4, 4, 4, 6, 6, 4, 4, 6, 5, 6, 4, 6, 5, 6, 6, 5, 4, 4,
|
||||
4, 6, 6, 5, 5, 5, 6, 6, 5, 5, 4, 6, 5, 5, 5, 5, 4, 5, 6, 5, 6, 5, 6, 6, 6, 6, 6, 4,
|
||||
4, 6, 6, 5, 4, 5, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 6, 4, 5, 6, 6, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
|
||||
],
|
||||
"opacity": 1,
|
||||
"type": "tilelayer",
|
||||
"visible": true,
|
||||
"properties": {
|
||||
"max_players": 3,
|
||||
"collisionTiles": [4, 5],
|
||||
"empty": 6,
|
||||
"balk": 5,
|
||||
"wall": 4,
|
||||
"spawns": [
|
||||
{ "col": 6, "row": 4 },
|
||||
{ "col": 7, "row": 15 },
|
||||
{ "col": 22, "row": 3 },
|
||||
{ "col": 23, "row": 13 }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"renderorder": "right-down",
|
||||
"tilesets": [
|
||||
{
|
||||
"name": "tiles",
|
||||
"firstgid": 1,
|
||||
"tilewidth": 35,
|
||||
"tileheight": 35,
|
||||
"imagewidth": 210,
|
||||
"imageheight": 35,
|
||||
"image": "tileset.png",
|
||||
"margin": 0,
|
||||
"properties": {},
|
||||
"spacing": 0
|
||||
}
|
||||
],
|
||||
"properties": {},
|
||||
"orientation": "orthogonal",
|
||||
"version": 1
|
||||
}
|
||||
26
client/maps/hot_map.tmx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.0.0" orientation="orthogonal" renderorder="right-down" width="28" height="18" tilewidth="35" tileheight="35" nextobjectid="1">
|
||||
<tileset firstgid="1" source="untitled.tsx"/>
|
||||
<layer name="Tile Layer 1" width="28" height="18">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,2,3,3,3,3,2,1,2,3,2,3,2,3,2,3,2,3,2,3,1,2,3,3,3,2,3,1,
|
||||
1,3,2,1,1,3,2,2,3,2,3,2,3,2,3,2,3,2,3,2,1,3,3,2,2,2,3,1,
|
||||
1,2,3,3,3,3,3,2,3,1,1,1,1,3,1,1,1,1,2,3,2,3,3,1,1,1,2,1,
|
||||
1,1,1,1,2,2,2,2,3,3,3,2,3,3,3,3,3,2,3,3,1,2,3,3,3,3,3,1,
|
||||
1,3,3,3,2,3,3,3,3,2,2,3,3,2,1,1,3,3,2,3,1,2,3,3,2,3,2,1,
|
||||
1,2,2,3,1,1,1,1,2,2,1,1,1,1,1,1,2,2,1,1,3,2,1,1,1,2,1,1,
|
||||
1,2,2,3,2,3,3,1,3,3,1,1,3,3,2,2,3,3,2,2,3,3,2,1,3,3,3,1,
|
||||
1,1,1,1,2,3,3,1,3,2,3,3,2,3,3,3,3,3,3,3,3,3,1,1,3,2,2,1,
|
||||
1,3,3,3,3,3,3,1,3,2,2,2,2,3,1,1,1,1,1,2,3,3,1,2,3,2,1,1,
|
||||
1,3,2,1,1,2,3,1,3,3,3,3,3,3,1,3,3,3,3,2,3,2,3,2,3,2,2,1,
|
||||
1,1,2,2,2,2,3,1,1,1,2,2,2,1,1,2,3,2,3,2,3,2,3,2,2,3,3,1,
|
||||
1,3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,2,3,2,1,1,1,1,1,2,3,3,1,
|
||||
1,1,1,2,2,1,1,2,2,2,3,3,2,2,2,3,3,3,3,2,3,2,3,1,1,1,1,1,
|
||||
1,2,3,3,3,3,3,3,3,1,2,3,1,1,1,1,2,2,3,1,2,2,3,2,2,2,2,1,
|
||||
1,3,2,2,1,2,3,2,3,1,3,2,3,2,3,2,3,2,3,2,3,1,1,3,3,3,3,1,
|
||||
1,3,3,2,3,2,1,2,3,1,3,3,2,3,2,3,2,3,3,1,3,3,3,3,2,2,2,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
BIN
client/maps/tileset.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
13
docker-compose.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
bomberman-vasin:
|
||||
build: .
|
||||
container_name: bomberman-vasin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000"
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
||||
40
package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "bombattack",
|
||||
"version": "0.0.1",
|
||||
"description": "Bomberman like game",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"server": "webpack && node server/app.js",
|
||||
"start": "node server/app.js"
|
||||
},
|
||||
"author": "Dmytro Vasin",
|
||||
"repository": "github.com/DmytroVasin/bomber",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"phaser",
|
||||
"socket.io",
|
||||
"es6",
|
||||
"webpack"
|
||||
],
|
||||
"engines": {
|
||||
"node": "10.4.1",
|
||||
"yarn": "1.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.16.2",
|
||||
"faker": "^4.1.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"path": "^0.12.7",
|
||||
"serve-favicon": "^2.4.5",
|
||||
"socket.io": "^2.0.4",
|
||||
"uuid": "^3.1.0",
|
||||
"yarn": "^1.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-beta.31",
|
||||
"@babel/preset-env": "^7.0.0-beta.31",
|
||||
"babel-loader": "8.0.0-beta.0",
|
||||
"webpack": "4.31.0",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
63
server/app.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const express = require('express');
|
||||
const socketIO = require('socket.io');
|
||||
const favicon = require('serve-favicon');
|
||||
|
||||
const app = express();
|
||||
const server = require('http').createServer(app);
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.static(path.join(__dirname, '..', 'client')));
|
||||
app.use(favicon(path.join(__dirname, '..', 'client', 'favicon.ico')));
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
res.sendFile(path.join(__dirname, 'index'));
|
||||
});
|
||||
|
||||
server.listen(PORT, function(){
|
||||
console.log(`Express server listening on port ${PORT}`)
|
||||
});
|
||||
|
||||
|
||||
const Lobby = require('./lobby');
|
||||
const Play = require('./play');
|
||||
|
||||
serverSocket = socketIO(server);
|
||||
|
||||
serverSocket.sockets.on('connection', function(client) {
|
||||
console.log('New player has connected: ' + client.id);
|
||||
|
||||
client.on('enter lobby', Lobby.onEnterLobby);
|
||||
client.on('leave lobby', Lobby.onLeaveLobby);
|
||||
client.on('create game', Lobby.onCreateGame);
|
||||
|
||||
client.on('enter pending game', Lobby.onEnterPendingGame);
|
||||
client.on('leave pending game', Lobby.onLeavePendingGame);
|
||||
|
||||
client.on('start game', Play.onStartGame);
|
||||
|
||||
client.on('update player position', Play.updatePlayerPosition);
|
||||
client.on('create bomb', Play.createBomb);
|
||||
client.on('pick up spoil', Play.onPickUpSpoil);
|
||||
|
||||
client.on('player died', Play.onPlayerDied);
|
||||
client.on('leave game', Play.onLeaveGame);
|
||||
|
||||
client.on('disconnect', onClientDisconnect);
|
||||
});
|
||||
|
||||
function onClientDisconnect() {
|
||||
if (this.socket_game_id == null) {
|
||||
console.log('Player was not be inside any game...');
|
||||
return
|
||||
}
|
||||
console.log('Player was inside game...');
|
||||
|
||||
// If game is pending then use Lobby.
|
||||
Lobby.onLeavePendingGame.call(this)
|
||||
|
||||
// If game is non-pending then use Play.
|
||||
Play.onDisconnectFromGame.call(this)
|
||||
}
|
||||
|
||||
35
server/constants.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const TILE_SIZE = 35;
|
||||
|
||||
const EXPLOSION_TIME = 2000;
|
||||
|
||||
const SPOIL_CHANCE = 50;
|
||||
const SPEED = 0;
|
||||
const POWER = 1;
|
||||
const DELAY = 2;
|
||||
|
||||
const EMPTY_CELL = 0;
|
||||
const DESTRUCTIBLE_CELL = 2;
|
||||
const NON_DESTRUCTIBLE_CELL = 1;
|
||||
|
||||
const INITIAL_POWER = 1
|
||||
const STEP_POWER = 1
|
||||
|
||||
const SKINS = [
|
||||
'Theodora', 'Ringo', 'Jeniffer', 'Godard',
|
||||
'Biarid', 'Solia', 'Kedan', 'Nigob', 'Baradir', 'Raviel', 'Valpo'
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
TILE_SIZE,
|
||||
EXPLOSION_TIME,
|
||||
SPOIL_CHANCE,
|
||||
SPEED,
|
||||
POWER,
|
||||
DELAY,
|
||||
EMPTY_CELL,
|
||||
DESTRUCTIBLE_CELL,
|
||||
NON_DESTRUCTIBLE_CELL,
|
||||
INITIAL_POWER,
|
||||
STEP_POWER,
|
||||
SKINS
|
||||
}
|
||||
89
server/entity/bomb.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
const { EXPLOSION_TIME, DESTRUCTIBLE_CELL, NON_DESTRUCTIBLE_CELL, SPOIL_CHANCE } = require('../constants');
|
||||
|
||||
const { Spoil } = require('./spoil.js');
|
||||
|
||||
var uuidv4 = require('uuid/v4');
|
||||
|
||||
class Bomb {
|
||||
|
||||
constructor({ game, col, row, power }) {
|
||||
this.id = uuidv4();
|
||||
|
||||
this.game = game;
|
||||
this.power = power
|
||||
this.explosion_time = EXPLOSION_TIME
|
||||
|
||||
this.col = col
|
||||
this.row = row
|
||||
|
||||
this.blastedCells = [];
|
||||
}
|
||||
|
||||
detonate() {
|
||||
let row = this.row;
|
||||
let col = this.col;
|
||||
let power = this.power;
|
||||
|
||||
this.game.nullifyMapCell(row, col);
|
||||
this.addToBlasted(row, col, 'center', false)
|
||||
|
||||
let explosionDirections = [
|
||||
{ x: 0, y: -1, end: 'up', plumb: 'vertical' },
|
||||
{ x: 1, y: 0, end: 'right', plumb: 'horizontal' },
|
||||
{ x: 0, y: 1, end: 'down', plumb: 'vertical' },
|
||||
{ x: -1, y: 0, end: 'left', plumb: 'horizontal' }
|
||||
]
|
||||
|
||||
for (let direction of explosionDirections ) {
|
||||
for(let i = 1; i <= power; i++) {
|
||||
let currentRow = row + (direction.y * i);
|
||||
let currentCol = col + (direction.x * i);
|
||||
|
||||
let cell = this.game.getMapCell(currentRow, currentCol);
|
||||
let isWall = cell == NON_DESTRUCTIBLE_CELL
|
||||
let isBalk = cell == DESTRUCTIBLE_CELL
|
||||
let isLast = (i == power);
|
||||
|
||||
if (cell == DESTRUCTIBLE_CELL) {
|
||||
this.game.nullifyMapCell(currentRow, currentCol);
|
||||
}
|
||||
|
||||
if (isBalk || isWall || isLast) {
|
||||
this.addToBlasted(currentRow, currentCol, direction.end, isBalk)
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.addToBlasted(currentRow, currentCol, direction.plumb, isBalk)
|
||||
}
|
||||
}
|
||||
|
||||
return this.blastedCells;
|
||||
}
|
||||
|
||||
addToBlasted(row, col, direction, destroyed) {
|
||||
let spoil = this.craftSpoil(row, col);
|
||||
|
||||
this.blastedCells.push({
|
||||
row: row,
|
||||
col: col,
|
||||
type: 'explosion_'+direction,
|
||||
destroyed: destroyed,
|
||||
spoil: spoil
|
||||
})
|
||||
}
|
||||
|
||||
craftSpoil(row, col) {
|
||||
var randomNumber = Math.floor(Math.random() * 100)
|
||||
|
||||
if (randomNumber < SPOIL_CHANCE) {
|
||||
let spoil = new Spoil(row, col)
|
||||
this.game.addSpoil(spoil)
|
||||
return spoil
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Bomb = Bomb;
|
||||
136
server/entity/game.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
const { TILE_SIZE, EMPTY_CELL, DESTRUCTIBLE_CELL, NON_DESTRUCTIBLE_CELL, SKINS } = require('../constants');
|
||||
|
||||
var { Player } = require('./player');
|
||||
var { Bomb } = require('./bomb.js');
|
||||
|
||||
var uuidv4 = require('uuid/v4');
|
||||
var faker = require('faker');
|
||||
|
||||
class Game {
|
||||
|
||||
constructor({ map_name }) {
|
||||
this.id = uuidv4();
|
||||
this.name = faker.commerce.color()
|
||||
this.map_name = map_name;
|
||||
|
||||
this.layer_info = require('../../client/maps/' + this.map_name + '.json').layers[0]
|
||||
this.max_players = this.layer_info.properties.max_players
|
||||
|
||||
// NOTE: we can`t use new Map - because Socket.io do not support such format
|
||||
this.players = {}
|
||||
|
||||
// NOTE: Copy objct - not reference
|
||||
this.playerSkins = SKINS.slice()
|
||||
|
||||
// NOTE: Copy objct - not reference
|
||||
this.playerSpawns = this.layer_info.properties.spawns.slice()
|
||||
|
||||
this.shadow_map = this.createMapData();
|
||||
this.spoils = new Map();
|
||||
this.bombs = new Map();
|
||||
}
|
||||
|
||||
addPlayer(id) {
|
||||
let skin = this.getAndRemoveSkin()
|
||||
let [spawn, spawnOnGrid] = this.getAndRemoveSpawn()
|
||||
|
||||
let player = new Player({ id: id, skin: skin, spawn: spawn, spawnOnGrid: spawnOnGrid })
|
||||
this.players[player.id] = player
|
||||
}
|
||||
|
||||
removePlayer(id) {
|
||||
let player = this.players[id];
|
||||
|
||||
this.playerSkins.push(player.skin)
|
||||
this.playerSpawns.push(player.spawnOnGrid)
|
||||
|
||||
delete this.players[id];
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return Object.keys(this.players).length === 0
|
||||
}
|
||||
|
||||
isFull() {
|
||||
return Object.keys(this.players).length === this.max_players
|
||||
}
|
||||
|
||||
getAndRemoveSkin() {
|
||||
// NOTE: we can user here simple .pop()
|
||||
let index = Math.floor(Math.random() * this.playerSkins.length);
|
||||
let randomSkin = this.playerSkins[index];
|
||||
this.playerSkins.splice(index, 1);
|
||||
|
||||
return randomSkin;
|
||||
}
|
||||
|
||||
getAndRemoveSpawn() {
|
||||
let index = Math.floor(Math.random() * this.playerSpawns.length);
|
||||
let spawnOnGrid = this.playerSpawns[index];
|
||||
this.playerSpawns.splice(index, 1);
|
||||
|
||||
let spawn = { x: spawnOnGrid.col * TILE_SIZE, y: spawnOnGrid.row * TILE_SIZE };
|
||||
return [spawn, spawnOnGrid];
|
||||
}
|
||||
|
||||
createMapData() {
|
||||
let tiles = this.layer_info.data
|
||||
let width = this.layer_info.width
|
||||
let height = this.layer_info.height
|
||||
let empty = this.layer_info.properties.empty
|
||||
let wall = this.layer_info.properties.wall
|
||||
let balk = this.layer_info.properties.balk
|
||||
|
||||
let mapMatrix = [];
|
||||
let i = 0;
|
||||
|
||||
for(let row = 0; row < height; row++) {
|
||||
mapMatrix.push([]);
|
||||
|
||||
for(let col = 0; col < width; col++) {
|
||||
mapMatrix[row][col] = EMPTY_CELL;
|
||||
|
||||
if(tiles[i] == balk) {
|
||||
mapMatrix[row][col] = DESTRUCTIBLE_CELL;
|
||||
} else if(tiles[i] == wall) {
|
||||
mapMatrix[row][col] = NON_DESTRUCTIBLE_CELL;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return mapMatrix;
|
||||
}
|
||||
|
||||
addBomb({ col, row, power }) {
|
||||
let bomb = new Bomb({ game: this, col: col, row: row, power: power });
|
||||
if ( this.bombs.get(bomb.id) ) {
|
||||
return false
|
||||
}
|
||||
this.bombs.set(bomb.id, bomb);
|
||||
return bomb
|
||||
}
|
||||
|
||||
getMapCell(row, col) {
|
||||
return this.shadow_map[row][col]
|
||||
}
|
||||
|
||||
nullifyMapCell(row, col) {
|
||||
this.shadow_map[row][col] = EMPTY_CELL
|
||||
}
|
||||
|
||||
findSpoil(spoil_id){
|
||||
return this.spoils.get(spoil_id)
|
||||
}
|
||||
|
||||
addSpoil(spoil) {
|
||||
this.spoils.set(spoil.id, spoil);
|
||||
}
|
||||
|
||||
deleteSpoil(spoil_id){
|
||||
this.spoils.delete(spoil_id)
|
||||
}
|
||||
}
|
||||
|
||||
exports.Game = Game;
|
||||
28
server/entity/player.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
const { POWER, INITIAL_POWER, STEP_POWER } = require('../constants');
|
||||
|
||||
class Player {
|
||||
|
||||
constructor({ id, skin, spawn, spawnOnGrid }) {
|
||||
this.id = id;
|
||||
this.skin = skin;
|
||||
this.spawn = spawn;
|
||||
this.spawnOnGrid = spawnOnGrid;
|
||||
|
||||
this.isAlive = true;
|
||||
|
||||
this.power = INITIAL_POWER;
|
||||
}
|
||||
|
||||
pickSpoil(spoil_type) {
|
||||
if (spoil_type === POWER){
|
||||
this.power += STEP_POWER
|
||||
}
|
||||
}
|
||||
|
||||
dead() {
|
||||
this.isAlive = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.Player = Player;
|
||||
21
server/entity/spoil.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const { SPEED, POWER, DELAY } = require('../constants');
|
||||
|
||||
var uuidv4 = require('uuid/v4');
|
||||
|
||||
class Spoil {
|
||||
|
||||
constructor(row, col) {
|
||||
this.id = uuidv4();
|
||||
|
||||
this.row = row;
|
||||
this.col = col;
|
||||
|
||||
this.spoil_type = this.spoilType()
|
||||
}
|
||||
|
||||
spoilType(){
|
||||
return [SPEED, POWER, DELAY][Math.floor(Math.random() * 3)]
|
||||
}
|
||||
}
|
||||
|
||||
exports.Spoil = Spoil;
|
||||
93
server/lobby.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
var { Game } = require('./entity/game');
|
||||
|
||||
var lobbyId = 'lobby_room';
|
||||
|
||||
var pendingGames = new Map();
|
||||
|
||||
|
||||
var Lobby = {
|
||||
onEnterLobby: function (callback) {
|
||||
// this == socket
|
||||
this.join(lobbyId);
|
||||
|
||||
callback( Lobby.availablePendingGames() )
|
||||
},
|
||||
|
||||
onLeaveLobby: function () {
|
||||
this.leave(lobbyId);
|
||||
},
|
||||
|
||||
onCreateGame: function(map_name, callback) {
|
||||
var newGame = new Game({ map_name: map_name });
|
||||
pendingGames.set(newGame.id, newGame);
|
||||
|
||||
Lobby.updateLobbyGames()
|
||||
|
||||
callback({ game_id: newGame.id });
|
||||
},
|
||||
|
||||
onEnterPendingGame: function ({ game_id }) {
|
||||
let current_game = pendingGames.get(game_id);
|
||||
|
||||
this.join(current_game.id);
|
||||
|
||||
// NOTE: We save current_game_id inside Socket connection.
|
||||
// We should knew it on disconnect
|
||||
this.socket_game_id = current_game.id;
|
||||
|
||||
current_game.addPlayer(this.id);
|
||||
|
||||
if ( current_game.isFull() ){
|
||||
Lobby.updateLobbyGames();
|
||||
}
|
||||
|
||||
Lobby.updateCurrentGame(current_game)
|
||||
},
|
||||
|
||||
onLeavePendingGame: function() {
|
||||
let current_game = pendingGames.get(this.socket_game_id);
|
||||
|
||||
if (current_game) {
|
||||
this.leave(current_game.id);
|
||||
this.socket_game_id = null;
|
||||
|
||||
current_game.removePlayer(this.id);
|
||||
|
||||
if( current_game.isEmpty() ){
|
||||
pendingGames.delete(current_game.id);
|
||||
Lobby.updateLobbyGames();
|
||||
return
|
||||
}
|
||||
|
||||
if ( !current_game.isFull() ){
|
||||
Lobby.updateLobbyGames();
|
||||
}
|
||||
|
||||
Lobby.updateCurrentGame(current_game)
|
||||
}
|
||||
},
|
||||
|
||||
deletePendingGame: function(game_id) {
|
||||
let game = pendingGames.get(game_id);
|
||||
|
||||
pendingGames.delete(game.id);
|
||||
Lobby.updateLobbyGames();
|
||||
|
||||
return game
|
||||
},
|
||||
|
||||
availablePendingGames: function() {
|
||||
return [...pendingGames.values()].filter(item => item.isFull() === false );
|
||||
},
|
||||
|
||||
updateLobbyGames: function() {
|
||||
serverSocket.sockets.in(lobbyId).emit('display pending games', Lobby.availablePendingGames() );
|
||||
},
|
||||
|
||||
updateCurrentGame: function(game) {
|
||||
// Emit to ALL including ME
|
||||
serverSocket.sockets.in(game.id).emit('update game', { current_game: game });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Lobby;
|
||||