Spaces:
Running
Running
Upload 27 files
Browse files- .github/workflows/main.yml +28 -0
- .gitignore +5 -0
- .prettierrc +1 -0
- .vscode/extensions.json +3 -0
- LICENSE +21 -0
- README.md +24 -10
- client/assets/controls.png +0 -0
- client/assets/fullscreen.png +0 -0
- client/assets/player.png +0 -0
- client/client.js +22 -0
- client/components/controls.js +95 -0
- client/components/cursors.js +41 -0
- client/components/fullscreenButton.js +22 -0
- client/components/fullscreenEvent.js +17 -0
- client/components/player.js +12 -0
- client/scenes/bootScene.js +18 -0
- client/scenes/gameScene.js +120 -0
- index.html +16 -17
- package-lock.json +0 -0
- package.json +39 -0
- server/game/components/player.js +80 -0
- server/game/config.js +20 -0
- server/game/game.js +8 -0
- server/game/gameScene.js +114 -0
- server/server.js +37 -0
- test/test.js +64 -0
- webpack.config.cjs +12 -0
.github/workflows/main.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# read: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
|
| 2 |
+
|
| 3 |
+
name: CI
|
| 4 |
+
|
| 5 |
+
on: [push]
|
| 6 |
+
|
| 7 |
+
jobs:
|
| 8 |
+
build:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
|
| 11 |
+
strategy:
|
| 12 |
+
matrix:
|
| 13 |
+
node-version: [14.x, 16.x, 18.x]
|
| 14 |
+
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout repository
|
| 17 |
+
uses: actions/checkout@v3
|
| 18 |
+
|
| 19 |
+
- name: Use Node.js ${{ matrix.node-version }}
|
| 20 |
+
uses: actions/setup-node@v3
|
| 21 |
+
with:
|
| 22 |
+
node-version: ${{ matrix.node-version }}
|
| 23 |
+
|
| 24 |
+
- name: Install Dependencies
|
| 25 |
+
run: npm install
|
| 26 |
+
|
| 27 |
+
- name: Build Packages
|
| 28 |
+
run: npm run build
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/.cache
|
| 2 |
+
/.parcel-cache
|
| 3 |
+
/client/bundle.js
|
| 4 |
+
/dist
|
| 5 |
+
/node_modules
|
.prettierrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"@yandeu/prettier-config"
|
.vscode/extensions.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"recommendations": ["esbenp.prettier-vscode"]
|
| 3 |
+
}
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2022 Yannick Deubel (https://github.com/yandeu)
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,10 +1,24 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phaser 3 Multiplayer Game Example with geckos.io
|
| 2 |
+
|
| 3 |
+
## How To Start
|
| 4 |
+
|
| 5 |
+
To clone and run this game, you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
|
| 6 |
+
|
| 7 |
+
**Note:** Test it on Chrome. On some browsers like Firefox you need to add a STUN server to make it work.
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# Clone this repository
|
| 11 |
+
$ npx gitget https://github.com/geckosio/phaser3-multiplayer-game-example phaser3-multiplayer-game
|
| 12 |
+
|
| 13 |
+
# Go into the repository
|
| 14 |
+
$ cd phaser3-multiplayer-game
|
| 15 |
+
|
| 16 |
+
# Install dependencies
|
| 17 |
+
$ npm install
|
| 18 |
+
|
| 19 |
+
# Start the local development server (on port 1444)
|
| 20 |
+
$ npm run start
|
| 21 |
+
|
| 22 |
+
# Add bots to the game (via puppeteer) to test it
|
| 23 |
+
$ npm run test
|
| 24 |
+
```
|
client/assets/controls.png
ADDED
|
client/assets/fullscreen.png
ADDED
|
client/assets/player.png
ADDED
|
client/client.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//// <reference path="../phaser.d.ts" />
|
| 2 |
+
|
| 3 |
+
import Phaser, { Game } from 'phaser'
|
| 4 |
+
import BootScene from './scenes/bootScene.js'
|
| 5 |
+
import GameScene from './scenes/gameScene.js'
|
| 6 |
+
import FullScreenEvent from './components/fullscreenEvent.js'
|
| 7 |
+
|
| 8 |
+
const config = {
|
| 9 |
+
type: Phaser.AUTO,
|
| 10 |
+
scale: {
|
| 11 |
+
mode: Phaser.Scale.FIT,
|
| 12 |
+
autoCenter: Phaser.Scale.CENTER_BOTH,
|
| 13 |
+
width: 896,
|
| 14 |
+
height: 504
|
| 15 |
+
},
|
| 16 |
+
scene: [BootScene, GameScene]
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
window.addEventListener('load', () => {
|
| 20 |
+
const game = new Game(config)
|
| 21 |
+
FullScreenEvent(() => resize(game))
|
| 22 |
+
})
|
client/components/controls.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default class Controls {
|
| 2 |
+
constructor(scene, channel) {
|
| 3 |
+
this.scene = scene
|
| 4 |
+
this.channel = channel
|
| 5 |
+
this.left = false
|
| 6 |
+
this.right = false
|
| 7 |
+
this.up = false
|
| 8 |
+
this.controls = []
|
| 9 |
+
this.none = true
|
| 10 |
+
this.prevNone = true
|
| 11 |
+
|
| 12 |
+
// add a second pointer
|
| 13 |
+
scene.input.addPointer()
|
| 14 |
+
|
| 15 |
+
const detectPointer = (gameObject, down) => {
|
| 16 |
+
if (gameObject.btn) {
|
| 17 |
+
switch (gameObject.btn) {
|
| 18 |
+
case 'left':
|
| 19 |
+
this.left = down
|
| 20 |
+
break
|
| 21 |
+
case 'right':
|
| 22 |
+
this.right = down
|
| 23 |
+
break
|
| 24 |
+
case 'up':
|
| 25 |
+
this.up = down
|
| 26 |
+
break
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
scene.input.on('gameobjectdown', (pointer, gameObject) => detectPointer(gameObject, true))
|
| 31 |
+
scene.input.on('gameobjectup', (pointer, gameObject) => detectPointer(gameObject, false))
|
| 32 |
+
|
| 33 |
+
let left = new Control(scene, 0, 0, 'left').setRotation(-0.5 * Math.PI)
|
| 34 |
+
let right = new Control(scene, 0, 0, 'right').setRotation(0.5 * Math.PI)
|
| 35 |
+
let up = new Control(scene, 0, 0, 'up')
|
| 36 |
+
this.controls.push(left, right, up)
|
| 37 |
+
this.resize()
|
| 38 |
+
|
| 39 |
+
this.scene.events.on('update', this.update, this)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
controlsDown() {
|
| 43 |
+
return { left: this.left, right: this.right, up: this.up, none: this.none }
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
resize() {
|
| 47 |
+
const SCALE = 1
|
| 48 |
+
const controlsRadius = (192 / 2) * SCALE
|
| 49 |
+
const w = this.scene.cameras.main.width - 10 - controlsRadius
|
| 50 |
+
const h = this.scene.cameras.main.height - 10 - controlsRadius
|
| 51 |
+
|
| 52 |
+
let positchannelns = [
|
| 53 |
+
{
|
| 54 |
+
x: controlsRadius + 10,
|
| 55 |
+
y: h
|
| 56 |
+
},
|
| 57 |
+
{ x: controlsRadius + 214, y: h },
|
| 58 |
+
{ x: w, y: h }
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
this.controls.forEach((ctl, i) => {
|
| 62 |
+
ctl.setPosition(positchannelns[i].x, positchannelns[i].y)
|
| 63 |
+
ctl.setScale(SCALE)
|
| 64 |
+
})
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
update() {
|
| 68 |
+
this.none = this.left || this.right || this.up ? false : true
|
| 69 |
+
|
| 70 |
+
if (!this.none || this.none !== this.prevNone) {
|
| 71 |
+
let total = 0
|
| 72 |
+
if (this.left) total += 1
|
| 73 |
+
if (this.right) total += 2
|
| 74 |
+
if (this.up) total += 4
|
| 75 |
+
let str36 = total.toString(36)
|
| 76 |
+
|
| 77 |
+
this.channel.emit('playerMove', str36)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
this.prevNone = this.none
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
class Control extends Phaser.GameObjects.Image {
|
| 85 |
+
constructor(scene, x, y, btn) {
|
| 86 |
+
super(scene, x, y, 'controls')
|
| 87 |
+
scene.add.existing(this)
|
| 88 |
+
|
| 89 |
+
this.btn = btn
|
| 90 |
+
|
| 91 |
+
this.setInteractive().setScrollFactor(0).setAlpha(0.2).setDepth(2)
|
| 92 |
+
|
| 93 |
+
// if (!scene.sys.game.device.input.touch) this.setAlpha(0)
|
| 94 |
+
}
|
| 95 |
+
}
|
client/components/cursors.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default class Cursors {
|
| 2 |
+
constructor(scene, channel) {
|
| 3 |
+
this.channel = channel
|
| 4 |
+
this.cursors = scene.input.keyboard.createCursorKeys()
|
| 5 |
+
|
| 6 |
+
scene.events.on('update', this.update, this)
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
update() {
|
| 10 |
+
let move = {
|
| 11 |
+
left: false,
|
| 12 |
+
right: false,
|
| 13 |
+
up: false,
|
| 14 |
+
none: true
|
| 15 |
+
}
|
| 16 |
+
if (this.cursors.left.isDown) {
|
| 17 |
+
move.left = true
|
| 18 |
+
move.none = false
|
| 19 |
+
} else if (this.cursors.right.isDown) {
|
| 20 |
+
move.right = true
|
| 21 |
+
move.none = false
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
if (this.cursors.up.isDown) {
|
| 25 |
+
move.up = true
|
| 26 |
+
move.none = false
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
if (move.left || move.right || move.up || move.none !== this.prevNoMovement) {
|
| 30 |
+
let total = 0
|
| 31 |
+
if (move.left) total += 1
|
| 32 |
+
if (move.right) total += 2
|
| 33 |
+
if (move.up) total += 4
|
| 34 |
+
let str36 = total.toString(36)
|
| 35 |
+
|
| 36 |
+
this.channel.emit('playerMove', str36)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
this.prevNoMovement = move.none
|
| 40 |
+
}
|
| 41 |
+
}
|
client/components/fullscreenButton.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const FullscreenButton = scene => {
|
| 2 |
+
let button = scene.add
|
| 3 |
+
.image(scene.cameras.main.width - 20, 20, 'fullscreen', 0)
|
| 4 |
+
.setOrigin(1, 0)
|
| 5 |
+
.setInteractive()
|
| 6 |
+
.setScrollFactor(0)
|
| 7 |
+
.setDepth(100)
|
| 8 |
+
.setAlpha(0.2)
|
| 9 |
+
|
| 10 |
+
button.on('pointerup', () => {
|
| 11 |
+
if (scene.scale.isFullscreen) {
|
| 12 |
+
button.setFrame(0)
|
| 13 |
+
scene.scale.stopFullscreen()
|
| 14 |
+
} else {
|
| 15 |
+
button.setFrame(1)
|
| 16 |
+
scene.scale.startFullscreen()
|
| 17 |
+
}
|
| 18 |
+
})
|
| 19 |
+
return button
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default FullscreenButton
|
client/components/fullscreenEvent.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// listen for fullscreen change event
|
| 2 |
+
const FullScreenEvent = callback => {
|
| 3 |
+
const fullScreenChange = () => {
|
| 4 |
+
let times = [50, 100, 200, 500, 1000, 2000, 5000]
|
| 5 |
+
times.forEach(time => {
|
| 6 |
+
window.setTimeout(() => {
|
| 7 |
+
callback()
|
| 8 |
+
}, time)
|
| 9 |
+
})
|
| 10 |
+
}
|
| 11 |
+
var vendors = ['webkit', 'moz', 'ms', '']
|
| 12 |
+
vendors.forEach(prefix => {
|
| 13 |
+
document.addEventListener(prefix + 'fullscreenchange', fullScreenChange, false)
|
| 14 |
+
})
|
| 15 |
+
document.addEventListener('MSFullscreenChange', fullScreenChange, false)
|
| 16 |
+
}
|
| 17 |
+
export default FullScreenEvent
|
client/components/player.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Phaser from 'phaser'
|
| 2 |
+
|
| 3 |
+
export default class Player extends Phaser.GameObjects.Sprite {
|
| 4 |
+
constructor(scene, channelId, x, y) {
|
| 5 |
+
super(scene, x, y, 'player')
|
| 6 |
+
scene.add.existing(this)
|
| 7 |
+
|
| 8 |
+
this.channelId = channelId
|
| 9 |
+
|
| 10 |
+
this.setFrame(4)
|
| 11 |
+
}
|
| 12 |
+
}
|
client/scenes/bootScene.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Scene } from 'phaser'
|
| 2 |
+
import geckos from '@geckos.io/client'
|
| 3 |
+
|
| 4 |
+
export default class BootScene extends Scene {
|
| 5 |
+
constructor() {
|
| 6 |
+
super({ key: 'BootScene' })
|
| 7 |
+
|
| 8 |
+
const channel = geckos({ port: 1444 })
|
| 9 |
+
|
| 10 |
+
channel.onConnect(error => {
|
| 11 |
+
if (error) console.error(error.message)
|
| 12 |
+
|
| 13 |
+
channel.on('ready', () => {
|
| 14 |
+
this.scene.start('GameScene', { channel: channel })
|
| 15 |
+
})
|
| 16 |
+
})
|
| 17 |
+
}
|
| 18 |
+
}
|
client/scenes/gameScene.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Scene } from 'phaser'
|
| 2 |
+
import axios from 'axios'
|
| 3 |
+
import Player from '../components/player.js'
|
| 4 |
+
import Cursors from '../components/cursors.js'
|
| 5 |
+
import Controls from '../components/controls.js'
|
| 6 |
+
import FullscreenButton from '../components/fullscreenButton.js'
|
| 7 |
+
|
| 8 |
+
export default class GameScene extends Scene {
|
| 9 |
+
constructor() {
|
| 10 |
+
super({ key: 'GameScene' })
|
| 11 |
+
this.objects = {}
|
| 12 |
+
this.playerId
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
init({ channel }) {
|
| 16 |
+
this.channel = channel
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
preload() {
|
| 20 |
+
this.load.image('controls', 'assets/controls.png')
|
| 21 |
+
this.load.spritesheet('fullscreen', 'assets/fullscreen.png', {
|
| 22 |
+
frameWidth: 64,
|
| 23 |
+
frameHeight: 64
|
| 24 |
+
})
|
| 25 |
+
this.load.spritesheet('player', 'assets/player.png', {
|
| 26 |
+
frameWidth: 32,
|
| 27 |
+
frameHeight: 48
|
| 28 |
+
})
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
async create() {
|
| 32 |
+
new Cursors(this, this.channel)
|
| 33 |
+
new Controls(this, this.channel)
|
| 34 |
+
|
| 35 |
+
FullscreenButton(this)
|
| 36 |
+
|
| 37 |
+
let addDummyDude = this.add
|
| 38 |
+
.text(this.cameras.main.width / 2, this.cameras.main.height / 2 - 100, 'CLICK ME', { fontSize: 48 })
|
| 39 |
+
.setOrigin(0.5)
|
| 40 |
+
addDummyDude.setInteractive().on('pointerdown', () => {
|
| 41 |
+
this.channel.emit('addDummy')
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
const parseUpdates = updates => {
|
| 45 |
+
if (typeof updates === undefined || updates === '') return []
|
| 46 |
+
|
| 47 |
+
// parse
|
| 48 |
+
let u = updates.split(',')
|
| 49 |
+
u.pop()
|
| 50 |
+
|
| 51 |
+
let u2 = []
|
| 52 |
+
|
| 53 |
+
u.forEach((el, i) => {
|
| 54 |
+
if (i % 4 === 0) {
|
| 55 |
+
u2.push({
|
| 56 |
+
playerId: u[i + 0],
|
| 57 |
+
x: parseInt(u[i + 1], 36),
|
| 58 |
+
y: parseInt(u[i + 2], 36),
|
| 59 |
+
dead: parseInt(u[i + 3]) === 1 ? true : false
|
| 60 |
+
})
|
| 61 |
+
}
|
| 62 |
+
})
|
| 63 |
+
return u2
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const updatesHandler = updates => {
|
| 67 |
+
updates.forEach(gameObject => {
|
| 68 |
+
const { playerId, x, y, dead } = gameObject
|
| 69 |
+
const alpha = dead ? 0 : 1
|
| 70 |
+
|
| 71 |
+
if (Object.keys(this.objects).includes(playerId)) {
|
| 72 |
+
// if the gameObject does already exist,
|
| 73 |
+
// update the gameObject
|
| 74 |
+
let sprite = this.objects[playerId].sprite
|
| 75 |
+
sprite.setAlpha(alpha)
|
| 76 |
+
sprite.setPosition(x, y)
|
| 77 |
+
} else {
|
| 78 |
+
// if the gameObject does NOT exist,
|
| 79 |
+
// create a new gameObject
|
| 80 |
+
let newGameObject = {
|
| 81 |
+
sprite: new Player(this, playerId, x || 200, y || 200),
|
| 82 |
+
playerId: playerId
|
| 83 |
+
}
|
| 84 |
+
newGameObject.sprite.setAlpha(alpha)
|
| 85 |
+
this.objects = { ...this.objects, [playerId]: newGameObject }
|
| 86 |
+
}
|
| 87 |
+
})
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
this.channel.on('updateObjects', updates => {
|
| 91 |
+
let parsedUpdates = parseUpdates(updates[0])
|
| 92 |
+
updatesHandler(parsedUpdates)
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
this.channel.on('removePlayer', playerId => {
|
| 96 |
+
try {
|
| 97 |
+
this.objects[playerId].sprite.destroy()
|
| 98 |
+
delete this.objects[playerId]
|
| 99 |
+
} catch (error) {
|
| 100 |
+
console.error(error.message)
|
| 101 |
+
}
|
| 102 |
+
})
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
let res = await axios.get(`${location.protocol}//${location.hostname}:1444/getState`)
|
| 106 |
+
|
| 107 |
+
let parsedUpdates = parseUpdates(res.data.state)
|
| 108 |
+
updatesHandler(parsedUpdates)
|
| 109 |
+
|
| 110 |
+
this.channel.on('getId', playerId36 => {
|
| 111 |
+
this.playerId = parseInt(playerId36, 36)
|
| 112 |
+
this.channel.emit('addPlayer')
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
this.channel.emit('getId')
|
| 116 |
+
} catch (error) {
|
| 117 |
+
console.error(error.message)
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|
index.html
CHANGED
|
@@ -1,19 +1,18 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
</body>
|
| 19 |
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
| 7 |
+
<script type="module" src="bundle.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
html,
|
| 10 |
+
body {
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
}
|
| 14 |
+
</style>
|
| 15 |
+
<title>Phaser 3 Game</title>
|
| 16 |
+
</head>
|
| 17 |
+
<body></body>
|
|
|
|
| 18 |
</html>
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "phaser3-multiplayer-example-with-geckos.io",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"engines": {
|
| 7 |
+
"node": "^14.15 || >=16"
|
| 8 |
+
},
|
| 9 |
+
"scripts": {
|
| 10 |
+
"start": "npm run dev",
|
| 11 |
+
"dev": "npm-run-all --parallel dev:*",
|
| 12 |
+
"build": "webpack -c webpack.config.cjs",
|
| 13 |
+
"play": "cross-env-shell NODE_ENV=production node server/server.js",
|
| 14 |
+
"test": "node test/test.js",
|
| 15 |
+
"dev:webpack": "webpack -c webpack.config.cjs --watch",
|
| 16 |
+
"dev:nodemon": "nodemon --delay 500ms server/server.js"
|
| 17 |
+
},
|
| 18 |
+
"keywords": [],
|
| 19 |
+
"author": "",
|
| 20 |
+
"license": "MIT",
|
| 21 |
+
"dependencies": {
|
| 22 |
+
"@geckos.io/client": "^2.1.3",
|
| 23 |
+
"@geckos.io/phaser-on-nodejs": "^1.2.8",
|
| 24 |
+
"@geckos.io/server": "^2.1.3",
|
| 25 |
+
"axios": "^0.21.1",
|
| 26 |
+
"cors": "^2.8.5",
|
| 27 |
+
"express": "^4.17.1",
|
| 28 |
+
"phaser": "3.55.2"
|
| 29 |
+
},
|
| 30 |
+
"devDependencies": {
|
| 31 |
+
"@yandeu/prettier-config": "^0.0.2",
|
| 32 |
+
"cross-env": "^7.0.3",
|
| 33 |
+
"nodemon": "^2.0.3",
|
| 34 |
+
"npm-run-all": "^4.1.5",
|
| 35 |
+
"puppeteer": "^19.4.1",
|
| 36 |
+
"webpack": "^5.75.0",
|
| 37 |
+
"webpack-cli": "^4.9.1"
|
| 38 |
+
}
|
| 39 |
+
}
|
server/game/components/player.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export class Player extends Phaser.Physics.Arcade.Sprite {
|
| 2 |
+
constructor(scene, playerId, x = 200, y = 200, dummy = false) {
|
| 3 |
+
super(scene, x, y, '')
|
| 4 |
+
scene.add.existing(this)
|
| 5 |
+
scene.physics.add.existing(this)
|
| 6 |
+
|
| 7 |
+
this.scene = scene
|
| 8 |
+
|
| 9 |
+
this.prevX = -1
|
| 10 |
+
this.prevY = -1
|
| 11 |
+
|
| 12 |
+
this.dead = false
|
| 13 |
+
this.prevDead = false
|
| 14 |
+
|
| 15 |
+
this.playerId = playerId
|
| 16 |
+
this.move = {}
|
| 17 |
+
|
| 18 |
+
this.setDummy(dummy)
|
| 19 |
+
|
| 20 |
+
this.body.setSize(32, 48)
|
| 21 |
+
|
| 22 |
+
this.prevNoMovement = true
|
| 23 |
+
|
| 24 |
+
this.setCollideWorldBounds(true)
|
| 25 |
+
|
| 26 |
+
scene.events.on('update', this.update, this)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
setDummy(dummy) {
|
| 30 |
+
if (dummy) {
|
| 31 |
+
this.body.setBounce(1)
|
| 32 |
+
this.scene.time.addEvent({
|
| 33 |
+
delay: Phaser.Math.RND.integerInRange(45, 90) * 1000,
|
| 34 |
+
callback: () => this.kill()
|
| 35 |
+
})
|
| 36 |
+
} else {
|
| 37 |
+
this.body.setBounce(0)
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
kill() {
|
| 42 |
+
this.dead = true
|
| 43 |
+
this.setActive(false)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
revive(playerId, dummy) {
|
| 47 |
+
this.playerId = playerId
|
| 48 |
+
this.dead = false
|
| 49 |
+
this.setActive(true)
|
| 50 |
+
this.setDummy(dummy)
|
| 51 |
+
this.setVelocity(0)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
setMove(data) {
|
| 55 |
+
let int = parseInt(data, 36)
|
| 56 |
+
|
| 57 |
+
let move = {
|
| 58 |
+
left: int === 1 || int === 5,
|
| 59 |
+
right: int === 2 || int === 6,
|
| 60 |
+
up: int === 4 || int === 6 || int === 5,
|
| 61 |
+
none: int === 8
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
this.move = move
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
update() {
|
| 68 |
+
if (this.move.left) this.setVelocityX(-160)
|
| 69 |
+
else if (this.move.right) this.setVelocityX(160)
|
| 70 |
+
else this.setVelocityX(0)
|
| 71 |
+
|
| 72 |
+
if (this.move.up && this.body.onFloor()) this.setVelocityY(-550)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
postUpdate() {
|
| 76 |
+
this.prevX = this.x
|
| 77 |
+
this.prevY = this.y
|
| 78 |
+
this.prevDead = this.dead
|
| 79 |
+
}
|
| 80 |
+
}
|
server/game/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import '@geckos.io/phaser-on-nodejs'
|
| 2 |
+
|
| 3 |
+
import Phaser from 'phaser'
|
| 4 |
+
import { GameScene } from './gameScene.js'
|
| 5 |
+
|
| 6 |
+
export const config = {
|
| 7 |
+
type: Phaser.HEADLESS,
|
| 8 |
+
parent: 'phaser-game',
|
| 9 |
+
width: 896,
|
| 10 |
+
height: 504,
|
| 11 |
+
banner: false,
|
| 12 |
+
audio: false,
|
| 13 |
+
scene: [GameScene],
|
| 14 |
+
physics: {
|
| 15 |
+
default: 'arcade',
|
| 16 |
+
arcade: {
|
| 17 |
+
gravity: { y: 1200 }
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
}
|
server/game/game.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { config } from './config.js'
|
| 2 |
+
|
| 3 |
+
export class PhaserGame extends Phaser.Game {
|
| 4 |
+
constructor(server) {
|
| 5 |
+
super(config)
|
| 6 |
+
this.server = server
|
| 7 |
+
}
|
| 8 |
+
}
|
server/game/gameScene.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import geckos from '@geckos.io/server'
|
| 2 |
+
import { iceServers } from '@geckos.io/server'
|
| 3 |
+
|
| 4 |
+
import pkg from 'phaser'
|
| 5 |
+
const { Scene } = pkg
|
| 6 |
+
|
| 7 |
+
import { Player } from './components/player.js'
|
| 8 |
+
|
| 9 |
+
export class GameScene extends Scene {
|
| 10 |
+
constructor() {
|
| 11 |
+
super({ key: 'GameScene' })
|
| 12 |
+
this.playerId = 0
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
init() {
|
| 16 |
+
this.io = geckos({
|
| 17 |
+
iceServers: process.env.NODE_ENV === 'production' ? iceServers : []
|
| 18 |
+
})
|
| 19 |
+
this.io.addServer(this.game.server)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
getId() {
|
| 23 |
+
return this.playerId++
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
prepareToSync(player) {
|
| 27 |
+
return `${player.playerId},${Math.round(player.x).toString(36)},${Math.round(player.y).toString(36)},${
|
| 28 |
+
player.dead === true ? 1 : 0
|
| 29 |
+
},`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
getState() {
|
| 33 |
+
let state = ''
|
| 34 |
+
this.playersGroup.children.iterate(player => {
|
| 35 |
+
state += this.prepareToSync(player)
|
| 36 |
+
})
|
| 37 |
+
return state
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
create() {
|
| 41 |
+
this.playersGroup = this.add.group()
|
| 42 |
+
|
| 43 |
+
const addDummy = () => {
|
| 44 |
+
let x = Phaser.Math.RND.integerInRange(50, 800)
|
| 45 |
+
let y = Phaser.Math.RND.integerInRange(100, 400)
|
| 46 |
+
let id = Math.random()
|
| 47 |
+
|
| 48 |
+
let dead = this.playersGroup.getFirstDead()
|
| 49 |
+
if (dead) {
|
| 50 |
+
dead.revive(id, true)
|
| 51 |
+
dead.setPosition(x, y)
|
| 52 |
+
} else {
|
| 53 |
+
this.playersGroup.add(new Player(this, id, x, y, true))
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
this.io.onConnection(channel => {
|
| 58 |
+
channel.onDisconnect(() => {
|
| 59 |
+
console.log('Disconnect user ' + channel.id)
|
| 60 |
+
this.playersGroup.children.each(player => {
|
| 61 |
+
if (player.playerId === channel.playerId) {
|
| 62 |
+
player.kill()
|
| 63 |
+
}
|
| 64 |
+
})
|
| 65 |
+
channel.room.emit('removePlayer', channel.playerId)
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
channel.on('addDummy', addDummy)
|
| 69 |
+
|
| 70 |
+
channel.on('getId', () => {
|
| 71 |
+
channel.playerId = this.getId()
|
| 72 |
+
channel.emit('getId', channel.playerId.toString(36))
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
channel.on('playerMove', data => {
|
| 76 |
+
this.playersGroup.children.iterate(player => {
|
| 77 |
+
if (player.playerId === channel.playerId) {
|
| 78 |
+
player.setMove(data)
|
| 79 |
+
}
|
| 80 |
+
})
|
| 81 |
+
})
|
| 82 |
+
|
| 83 |
+
channel.on('addPlayer', data => {
|
| 84 |
+
let dead = this.playersGroup.getFirstDead()
|
| 85 |
+
if (dead) {
|
| 86 |
+
dead.revive(channel.playerId, false)
|
| 87 |
+
} else {
|
| 88 |
+
this.playersGroup.add(new Player(this, channel.playerId, Phaser.Math.RND.integerInRange(100, 700)))
|
| 89 |
+
}
|
| 90 |
+
})
|
| 91 |
+
|
| 92 |
+
channel.emit('ready')
|
| 93 |
+
})
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
update() {
|
| 97 |
+
let updates = ''
|
| 98 |
+
this.playersGroup.children.iterate(player => {
|
| 99 |
+
let x = Math.abs(player.x - player.prevX) > 0.5
|
| 100 |
+
let y = Math.abs(player.y - player.prevY) > 0.5
|
| 101 |
+
let dead = player.dead != player.prevDead
|
| 102 |
+
if (x || y || dead) {
|
| 103 |
+
if (dead || !player.dead) {
|
| 104 |
+
updates += this.prepareToSync(player)
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
player.postUpdate()
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
if (updates.length > 0) {
|
| 111 |
+
this.io.room().emit('updateObjects', [updates])
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
server/server.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express'
|
| 2 |
+
import http from 'http'
|
| 3 |
+
import cors from 'cors'
|
| 4 |
+
import path from 'path'
|
| 5 |
+
import { PhaserGame } from './game/game.js'
|
| 6 |
+
|
| 7 |
+
import { dirname } from 'path'
|
| 8 |
+
import { fileURLToPath } from 'url'
|
| 9 |
+
const __filename = fileURLToPath(import.meta.url)
|
| 10 |
+
const __dirname = dirname(__filename)
|
| 11 |
+
|
| 12 |
+
const app = express()
|
| 13 |
+
const server = http.createServer(app)
|
| 14 |
+
|
| 15 |
+
const game = new PhaserGame(server)
|
| 16 |
+
const port = 1444
|
| 17 |
+
|
| 18 |
+
app.use(cors())
|
| 19 |
+
|
| 20 |
+
app.use('/', express.static(path.join(__dirname, '../client')))
|
| 21 |
+
|
| 22 |
+
app.get('/', (req, res) => {
|
| 23 |
+
res.sendFile(path.join(__dirname, '../index.html'))
|
| 24 |
+
})
|
| 25 |
+
|
| 26 |
+
app.get('/getState', (req, res) => {
|
| 27 |
+
try {
|
| 28 |
+
let gameScene = game.scene.keys['GameScene']
|
| 29 |
+
return res.json({ state: gameScene.getState() })
|
| 30 |
+
} catch (error) {
|
| 31 |
+
return res.status(500).json({ error: error.message })
|
| 32 |
+
}
|
| 33 |
+
})
|
| 34 |
+
|
| 35 |
+
server.listen(port, () => {
|
| 36 |
+
console.log('Express is listening on http://localhost:' + port)
|
| 37 |
+
})
|
test/test.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Adds 10 player to the scene
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import puppeteer from 'puppeteer'
|
| 6 |
+
|
| 7 |
+
const browser = await puppeteer.launch({
|
| 8 |
+
defaultViewport: { width: 896, height: 504 }
|
| 9 |
+
})
|
| 10 |
+
|
| 11 |
+
const wait = ms => {
|
| 12 |
+
return new Promise(resolve => {
|
| 13 |
+
setTimeout(() => {
|
| 14 |
+
resolve()
|
| 15 |
+
}, ms)
|
| 16 |
+
})
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const randomTime = () => {
|
| 20 |
+
return Math.random() * 2000 + 2000
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const goRight = async page => {
|
| 24 |
+
await page.keyboard.up('ArrowRight')
|
| 25 |
+
await page.keyboard.down('ArrowLeft')
|
| 26 |
+
await wait(randomTime())
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const goLeft = async page => {
|
| 30 |
+
await page.keyboard.up('ArrowLeft')
|
| 31 |
+
await page.keyboard.down('ArrowRight')
|
| 32 |
+
await wait(randomTime())
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
const newPage = async () => {
|
| 36 |
+
try {
|
| 37 |
+
const page = await browser.newPage()
|
| 38 |
+
await page.goto('http://localhost:1444/')
|
| 39 |
+
|
| 40 |
+
await wait(randomTime() + 5000)
|
| 41 |
+
await page.keyboard.down('ArrowUp')
|
| 42 |
+
await wait(randomTime())
|
| 43 |
+
|
| 44 |
+
await goLeft(page)
|
| 45 |
+
await goRight(page)
|
| 46 |
+
await goLeft(page)
|
| 47 |
+
await goRight(page)
|
| 48 |
+
await goLeft(page)
|
| 49 |
+
await goRight(page)
|
| 50 |
+
await goLeft(page)
|
| 51 |
+
await goRight(page)
|
| 52 |
+
await goLeft(page)
|
| 53 |
+
await goRight(page)
|
| 54 |
+
|
| 55 |
+
await browser.close()
|
| 56 |
+
} catch (error) {
|
| 57 |
+
console.error(error.message)
|
| 58 |
+
}
|
| 59 |
+
process.exit()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
for (let i = 0; i < 10; i++) {
|
| 63 |
+
newPage()
|
| 64 |
+
}
|
webpack.config.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const path = require('path')
|
| 2 |
+
|
| 3 |
+
module.exports = {
|
| 4 |
+
mode: 'development',
|
| 5 |
+
devtool: 'eval-cheap-source-map',
|
| 6 |
+
stats: 'minimal',
|
| 7 |
+
entry: './client/client.js',
|
| 8 |
+
output: {
|
| 9 |
+
filename: 'bundle.js',
|
| 10 |
+
path: path.resolve(__dirname, 'client')
|
| 11 |
+
}
|
| 12 |
+
}
|