├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── config.yml
└── workflows
│ └── ubuntu.yaml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── assets
├── dude.png
├── gridtiles.png
├── simple-map.json
└── star.png
├── dev
└── dev.js
├── jest.config.js
├── package-lock.json
├── package.json
├── readme
└── phaser-on-nodejs.png
├── src
├── fakeScreen.ts
├── fakeXMLHttpRequest.ts
└── index.ts
├── test
├── axios.test.js
├── basicSetup.test.js
├── customFPS.test.js
├── fakeXMLHttpRequest.test.js
├── globalMock.js
├── index.test.js
├── multipleScenes.test.js
├── node-fetch2.test.js
├── node-fetch3.test.js
├── setBodySize.test.js
├── sprite.test.js
├── spritesheet.test.js
├── startTest.js
└── tilemapTiledJSON.test.js
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: yandeu
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **Have a question?**
13 | Join the [discussions](https://github.com/geckosio/geckos.io/discussions) instead.
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Ask a question
4 | url: https://github.com/geckosio/geckos.io/discussions
5 | about: Ask questions and discuss with other community members
6 |
--------------------------------------------------------------------------------
/.github/workflows/ubuntu.yaml:
--------------------------------------------------------------------------------
1 | name: UbuntuCI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | node-version: [20.x, 22.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install --force
21 | - run: npm run build --if-present
22 | - run: npm test
23 | - uses: codecov/codecov-action@v2
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /lib
3 | /node_modules
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | !/.npmrc
4 | !/LICENSE
5 | !/README.md
6 | !/bundles
7 | !/lib
8 | !/package.json
9 | !/readme
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | "@yandeu/prettier-config"
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | # Phaser on Node.js
8 |
9 | ### Allows you to run Phaser 3 games (including Phaser's physics engines) on Node.js.
10 |
11 | [](https://github.com/geckosio/phaser-on-nodejs/actions?query=workflow%3ACI)
12 | [](https://www.npmjs.com/package/@geckos.io/phaser-on-nodejs)
13 | [](https://www.npmjs.com/package/@geckos.io/phaser-on-nodejs)
14 | [](LICENSE)
15 | [](https://codecov.io/gh/geckosio/phaser-on-nodejs)
16 |
17 |
18 |
19 | ## Arcade Physics
20 |
21 | ⚠️ If your goal is to run the Arcade Physics on the server, I highly recommend using [`arcade-physics`](https://github.com/yandeu/arcade-physics#readme).
22 |
23 | ## Compatibility
24 |
25 | Works with Phaser >=3.55.2.
26 | _Successfully tested with v3.87.0_
27 |
28 | ## Install
29 |
30 | ```console
31 | npm install @geckos.io/phaser-on-nodejs
32 | ```
33 |
34 | ## How to use
35 |
36 | ```js
37 | require('@geckos.io/phaser-on-nodejs')
38 | // or with es6
39 | import '@geckos.io/phaser-on-nodejs'
40 | ```
41 |
42 | ## Features
43 |
44 | - Phaser Physics (Arcade and Matter)
45 | - Load Images and SpriteSheets
46 | - Load TileMaps
47 | - Adjustable Frame Rate
48 | - Allows to use Multiple Scenes
49 |
50 | ## Examples
51 |
52 | - [Simple Phaser on Node.js example](https://github.com/geckosio/phaser-on-nodejs-example)
53 | - [Phaser 3 - Multiplayer game example with geckos.io](https://github.com/geckosio/phaser3-multiplayer-game-example#readme)
54 | - [Phaser 3 - Multiplayer game with physics](https://github.com/yandeu/phaser3-multiplayer-with-physics#readme)
55 |
56 | ## Basic Setup
57 |
58 | Install and require `phaser` and `@geckos.io/phaser-on-nodejs`. Make sure you use Phaser in headless mode on the server `{ type: Phaser.HEADLESS }`
59 |
60 | ```js
61 | require('@geckos.io/phaser-on-nodejs')
62 | const Phaser = require('phaser')
63 |
64 | // set the fps you need
65 | const FPS = 30
66 | global.phaserOnNodeFPS = FPS // default is 60
67 |
68 | // your MainScene
69 | class MainScene extends Phaser.Scene {
70 | constructor() {
71 | super('MainScene')
72 | }
73 | create() {
74 | console.log('it works!')
75 | }
76 | }
77 |
78 | // prepare the config for Phaser
79 | const config = {
80 | type: Phaser.HEADLESS,
81 | width: 1280,
82 | height: 720,
83 | banner: false,
84 | audio: false,
85 | scene: [MainScene],
86 | fps: {
87 | target: FPS
88 | },
89 | physics: {
90 | default: 'arcade',
91 | arcade: {
92 | gravity: { y: 300 }
93 | }
94 | }
95 | }
96 |
97 | // start the game
98 | new Phaser.Game(config)
99 | ```
100 |
101 | ## Loading Assets
102 |
103 | You can load textures (images, spritesheets etc.) on the server.
104 |
105 | ```js
106 | preload() {
107 | // use a relative path
108 | this.load.image('star', '../assets/star.png')
109 | }
110 |
111 | create() {
112 | const star = this.physics.add.sprite(400, 300, 'star')
113 | }
114 | ```
115 |
116 | But to save some memory, I recommend the following approach instead:
117 |
118 | ```js
119 | class Star extends Phaser.Physics.Arcade.Sprite {
120 | constructor(scene, x, y) {
121 | // pass empty string for the texture
122 | super(scene, x, y, '')
123 |
124 | scene.add.existing(this)
125 | scene.physics.add.existing(this)
126 |
127 | // set the width and height of the sprite as the body size
128 | this.body.setSize(24, 22)
129 | }
130 | }
131 | ```
132 |
133 | ## Using node-fetch or axios?
134 |
135 | If you are using **node-fetch**, you do not need to do anything.
136 |
137 | If you are using **axios**, you have to make sure `XMLHttpRequest` will not break:
138 | `XMLHttpRequest` is only use in the browser. Phaser.js is a browsers framework which uses `XMLHttpRequest` so phaser-on-nodejs has to provide a mock implementation. Unfortunately, axios is a isomorphic framework. On initialization, axios checks if `XMLHttpRequest` is available and will think it is running in the browser. To make sure axios works on nodejs, we just have to hide `XMLHttpRequest` from axios during its initialization.
139 | See the snipped below to make it work:
140 |
141 | ```js
142 | // remove fakeXMLHttpRequest
143 | const tmp = XMLHttpRequest
144 | XMLHttpRequest = undefined
145 | // init axios
146 | const axios = require('axios').default
147 | // restore fakeXMLHttpRequest
148 | XMLHttpRequest = tmp
149 | ```
150 |
--------------------------------------------------------------------------------
/assets/dude.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser-on-nodejs/91bc4e6be6f44b11c82ee84c8fde832682f897fd/assets/dude.png
--------------------------------------------------------------------------------
/assets/gridtiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser-on-nodejs/91bc4e6be6f44b11c82ee84c8fde832682f897fd/assets/gridtiles.png
--------------------------------------------------------------------------------
/assets/simple-map.json:
--------------------------------------------------------------------------------
1 | { "height":19,
2 | "infinite":false,
3 | "layers":[
4 | {
5 | "data":[20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 48, 48, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 48, 48, 29, 29, 48, 48, 48, 48, 29, 29, 29, 29, 29, 29, 82, 29, 29, 20, 20, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 48, 48, 48, 48, 48, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 48, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 20, 20, 29, 29, 29, 29, 29, 29, 48, 48, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
6 | "height":19,
7 | "id":3,
8 | "name":"Level1",
9 | "opacity":1,
10 | "type":"tilelayer",
11 | "visible":false,
12 | "width":25,
13 | "x":0,
14 | "y":0
15 | },
16 | {
17 | "data":[20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 82, 29, 29, 29, 29, 82, 29, 29, 82, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 20, 20, 48, 48, 48, 48, 48, 48, 29, 29, 48, 48, 29, 48, 48, 48, 29, 29, 48, 48, 48, 29, 29, 29, 29, 20, 20, 48, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 20, 20, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 20, 20, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 82, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 48, 48, 48, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 20, 20, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 82, 29, 29, 29, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
18 | "height":19,
19 | "id":4,
20 | "name":"Level2",
21 | "opacity":1,
22 | "type":"tilelayer",
23 | "visible":true,
24 | "width":25,
25 | "x":0,
26 | "y":0
27 | }],
28 | "nextlayerid":5,
29 | "nextobjectid":1,
30 | "orientation":"orthogonal",
31 | "renderorder":"right-down",
32 | "tiledversion":"1.2.2",
33 | "tileheight":32,
34 | "tilesets":[
35 | {
36 | "columns":14,
37 | "firstgid":1,
38 | "image":"..\/tiles\/gridtiles.png",
39 | "imageheight":320,
40 | "imagewidth":448,
41 | "margin":0,
42 | "name":"tiles",
43 | "spacing":0,
44 | "tilecount":140,
45 | "tileheight":32,
46 | "tilewidth":32
47 | }],
48 | "tilewidth":32,
49 | "type":"map",
50 | "version":1.2,
51 | "width":25
52 | }
--------------------------------------------------------------------------------
/assets/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser-on-nodejs/91bc4e6be6f44b11c82ee84c8fde832682f897fd/assets/star.png
--------------------------------------------------------------------------------
/dev/dev.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | const Phaser = require('phaser')
3 |
4 | const FPS = 30
5 | global.phaserOnNodeFPS = FPS
6 |
7 | class Player extends Phaser.Physics.Arcade.Sprite {
8 | constructor(scene, x, y) {
9 | // pass empty string for the texture
10 | super(scene, x, y, '')
11 |
12 | scene.add.existing(this)
13 | scene.physics.add.existing(this)
14 |
15 | // set the width and height of the sprite as the body size
16 | this.body.setSize(32, 48)
17 | }
18 | }
19 |
20 | class MainScene extends Phaser.Scene {
21 | constructor() {
22 | super('MainScene')
23 | this.logged = false
24 | this.player
25 | }
26 |
27 | create() {
28 | console.log('create')
29 | this.player = new Player(this, 100, 100)
30 | }
31 |
32 | update(time, delta) {
33 | if (!this.logged && this.player.y > 200) {
34 | this.logged = true
35 | console.log('player', this.player.y.toFixed())
36 | console.log('update')
37 | }
38 | }
39 | }
40 |
41 | const config = {
42 | type: Phaser.HEADLESS,
43 | width: 1280,
44 | height: 720,
45 | banner: false,
46 | audio: false,
47 | scene: [MainScene],
48 | fps: {
49 | target: FPS
50 | },
51 | physics: {
52 | default: 'arcade',
53 | arcade: {
54 | gravity: { y: 1200 }
55 | }
56 | }
57 | }
58 |
59 | new Phaser.Game(config)
60 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | collectCoverage: true,
3 | forceExit: true,
4 | maxConcurrency: 1,
5 | maxWorkers: 1,
6 | testTimeout: 5_000
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@geckos.io/phaser-on-nodejs",
3 | "version": "1.3.0",
4 | "description": "Allows you to run Phaser 3 games (including Phaser's physics engines) on Node.js.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "start": "npm run dev",
8 | "build": "npm run tsc:build",
9 | "dev": "npm-run-all --parallel tsc:watch dev:nodemon",
10 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
11 | "tsc:build": "tsc",
12 | "tsc:watch": "tsc --watch",
13 | "dev:nodemon": "nodemon --watch dev --watch lib --watch src dev/dev.js",
14 | "format": "prettier --check src/** dev/** test/**",
15 | "format:write": "prettier --write src/** dev/** test/**",
16 | "prepublishOnly": "npm run build"
17 | },
18 | "author": "Yannick Deubel (https://github.com/yandeu)",
19 | "license": "MIT",
20 | "keywords": [
21 | "phaser",
22 | "node",
23 | "nodejs",
24 | "multiplayer",
25 | "game",
26 | "physics",
27 | "arcade",
28 | "matter"
29 | ],
30 | "dependencies": {
31 | "canvas": "^3.0.0",
32 | "jsdom": "^25.0.1",
33 | "phaser3spectorjs": "npm:empty-npm-package@1.0.0"
34 | },
35 | "devDependencies": {
36 | "@tsconfig/node18": "^18.2.4",
37 | "@types/jsdom": "^21.1.7",
38 | "@types/node": "^16.4.0",
39 | "@yandeu/prettier-config": "^0.0.3",
40 | "axios": "^0.24.0",
41 | "jest": "^27.0.1",
42 | "node-fetch2": "npm:node-fetch@^2.6.6",
43 | "node-fetch3": "npm:node-fetch@^3.1.0",
44 | "nodemon": "^2.0.4",
45 | "npm-run-all": "^4.1.5",
46 | "phaser": "^3.87.0",
47 | "typescript": "^5.7.2"
48 | },
49 | "directories": {
50 | "lib": "lib"
51 | },
52 | "repository": {
53 | "type": "git",
54 | "url": "git+https://github.com/geckosio/phaser-on-nodejs.git"
55 | },
56 | "bugs": {
57 | "url": "https://github.com/geckosio/phaser-on-nodejs/issues"
58 | },
59 | "homepage": "https://github.com/geckosio/phaser-on-nodejs#readme",
60 | "funding": {
61 | "url": "https://github.com/sponsors/yandeu"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/readme/phaser-on-nodejs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geckosio/phaser-on-nodejs/91bc4e6be6f44b11c82ee84c8fde832682f897fd/readme/phaser-on-nodejs.png
--------------------------------------------------------------------------------
/src/fakeScreen.ts:
--------------------------------------------------------------------------------
1 | global.screen = {
2 | // @ts-expect-error
3 | orientation: {
4 | addEventListener: () => {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/fakeXMLHttpRequest.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import fs from 'fs'
3 |
4 | class FakeXMLHttpRequest {
5 | public url!: string
6 | public status = 200
7 | public response: any
8 | public responseText: string | undefined
9 |
10 | public open(_type: string, url: string) {
11 | this.url = path.resolve(__dirname, url)
12 | }
13 |
14 | public send() {
15 | // use base64 for images and utf8 for json files
16 | const encoding = /\.json$/gm.test(this.url) ? 'utf8' : 'base64'
17 |
18 | fs.readFile(this.url, { encoding }, (err, data) => {
19 | if (err) throw err
20 | this.response = data
21 | this.responseText = data
22 | const event = { target: { status: this.status } }
23 | this.onload(this, event)
24 | })
25 | }
26 | public onload(xhr: any, event: any) {}
27 | public onerror(err: NodeJS.ErrnoException | null) {}
28 | public onprogress() {}
29 | }
30 |
31 | export default FakeXMLHttpRequest
32 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | var phaserOnNodeFPS: number
3 | }
4 |
5 | import Canvas from 'canvas'
6 | import jsdom from 'jsdom'
7 | import FakeXMLHttpRequest from './fakeXMLHttpRequest'
8 | import './fakeScreen'
9 |
10 | const { JSDOM } = jsdom
11 | const dom = new JSDOM(``)
12 | const noop = () => {}
13 |
14 | const document = dom.window.document
15 | const window = dom.window
16 | window.focus = () => {}
17 |
18 | global.document = document as any
19 | global.window = window as any
20 | global.window.Element = undefined as any
21 | global.navigator = { userAgent: 'node' } as any
22 | global.Image = Canvas.Image as any
23 | global.XMLHttpRequest = FakeXMLHttpRequest as any
24 | global.HTMLCanvasElement = window.HTMLCanvasElement
25 | global.HTMLVideoElement = window.HTMLVideoElement
26 |
27 | // @ts-ignore
28 | global.URL = URL || noop
29 | global.URL.createObjectURL = (base64: any) => `data:image/png;base64,${base64}`
30 | global.URL.revokeObjectURL = () => {}
31 |
32 | // phaser on node variables
33 | global.phaserOnNodeFPS = 60
34 |
35 | const animationFrame = (cb: any) => {
36 | const now = performance.now()
37 | if (typeof cb !== 'function') return 0 // this line saves a lot of cpu
38 | window.setTimeout(() => cb(now), 1000 / global.phaserOnNodeFPS)
39 | return 0
40 | }
41 | export { animationFrame }
42 |
43 | window.requestAnimationFrame = cb => {
44 | return animationFrame(cb)
45 | }
46 |
47 | const requestAnimationFrame = window.requestAnimationFrame
48 | export { requestAnimationFrame }
49 |
--------------------------------------------------------------------------------
/test/axios.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const http = require('http')
5 |
6 | // remove fakeXMLHttpRequest
7 | const tmp = XMLHttpRequest
8 | XMLHttpRequest = undefined
9 | // init axios
10 | const axios = require('axios').default
11 | // restore fakeXMLHttpRequest
12 | XMLHttpRequest = tmp
13 |
14 | let server
15 | const PORT = Math.floor(Math.random() * 50000 + 5000)
16 |
17 | beforeAll(done => {
18 | const requestListener = function (req, res) {
19 | if (req.url === '/hello') return res.writeHead(200).end('Hello!')
20 | else return res.writeHead(404).end()
21 | }
22 | server = http.createServer(requestListener)
23 | server.listen(PORT, () => {
24 | done()
25 | })
26 | })
27 |
28 | it('should be able to use axios', done => {
29 | class MainScene extends Phaser.Scene {
30 | preload() {
31 | // load image (only possible of FakeXMLHttpRequest is available)
32 | this.load.image('star', '../assets/star.png')
33 | }
34 | create() {
35 | axios({ method: 'get', url: `http://localhost:${PORT}/hello` })
36 | .then(res => {
37 | expect(res.data).toBe('Hello!')
38 | done()
39 | })
40 | .catch(err => {
41 | console.log(err.message)
42 | })
43 | }
44 | }
45 |
46 | // prepare the config for Phaser
47 | const config = {
48 | type: Phaser.HEADLESS,
49 | width: 1280,
50 | height: 720,
51 | banner: false,
52 | audio: false,
53 | scene: [MainScene],
54 | physics: {
55 | default: 'arcade',
56 | arcade: {
57 | gravity: { y: 300 }
58 | }
59 | }
60 | }
61 |
62 | new Phaser.Game(config)
63 | })
64 |
65 | afterAll(done => {
66 | server.close(() => {
67 | done()
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/test/basicSetup.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 |
5 | it('should render at 30 fps (+-5 fps)', done => {
6 | // set the fps you need
7 | const FPS = 30
8 | global.phaserOnNodeFPS = FPS
9 |
10 | class MainScene extends Phaser.Scene {
11 | constructor() {
12 | super('MainScene')
13 | }
14 | create() {
15 | done()
16 | }
17 | }
18 |
19 | // prepare the config for Phaser
20 | const config = {
21 | type: Phaser.HEADLESS,
22 | width: 1280,
23 | height: 720,
24 | banner: false,
25 | audio: false,
26 | scene: [MainScene],
27 | fps: {
28 | target: FPS
29 | },
30 | physics: {
31 | default: 'arcade',
32 | arcade: {
33 | gravity: { y: 300 }
34 | }
35 | }
36 | }
37 |
38 | new Phaser.Game(config)
39 | })
40 |
--------------------------------------------------------------------------------
/test/customFPS.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 |
5 | jest.setTimeout(10_000)
6 |
7 | // set the fps you need
8 | const FPS = 30
9 | global.phaserOnNodeFPS = FPS
10 |
11 | it('should render at 30 fps (+-5 fps)', done => {
12 | class MainScene extends Phaser.Scene {
13 | constructor() {
14 | super('MainScene')
15 | this.d = []
16 | }
17 |
18 | update(time, delta) {
19 | this.d.push(delta)
20 | if (this.d.length === 90) {
21 | const reducer = (accumulator, currentValue) => accumulator + currentValue
22 | const average = this.d.reduce(reducer) / this.d.length
23 | const difference = Math.abs(average - (1 / FPS) * 1000)
24 | // unfortunately jest testing is slow and we cannot measure the accuracy
25 | expect(difference).toBeLessThanOrEqual(100)
26 | done()
27 | }
28 | }
29 | }
30 |
31 | const config = {
32 | type: Phaser.HEADLESS,
33 | width: 1280,
34 | height: 720,
35 | banner: false,
36 | audio: false,
37 | scene: [MainScene],
38 | fps: {
39 | // at the desired fps as target
40 | target: FPS
41 | },
42 | physics: {
43 | default: 'arcade',
44 | arcade: {
45 | gravity: { y: 300 }
46 | }
47 | }
48 | }
49 |
50 | new Phaser.Game(config)
51 | })
52 |
--------------------------------------------------------------------------------
/test/fakeXMLHttpRequest.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index')
2 | const FakeXMLHttpRequest = require('../lib/fakeXMLHttpRequest').default
3 |
4 | it('should load the file without errors', done => {
5 | try {
6 | const XMLHttpRequest = new FakeXMLHttpRequest()
7 | XMLHttpRequest.open('GET', '../assets/dude.png')
8 | XMLHttpRequest.onload = (xml, event) => {
9 | expect(event.target.status).toBe(200)
10 | done()
11 | }
12 | XMLHttpRequest.send()
13 | } catch (error) {
14 | done(error)
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/test/globalMock.js:
--------------------------------------------------------------------------------
1 | window.focus = jest.fn()
2 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index')
2 | const { requestAnimationFrame, animationFrame } = require('../lib/index')
3 |
4 | jest.setTimeout(10_000)
5 |
6 | it('should create object URL', () => {
7 | expect(URL.createObjectURL('test')).toBe('data:image/png;base64,test')
8 | })
9 |
10 | it('should return 0', done => {
11 | const res = animationFrame('string')
12 |
13 | setTimeout(() => {
14 | expect(res).toBe(0)
15 | done()
16 | }, 0)
17 | })
18 |
19 | it('should increase tick', done => {
20 | let tick = 0
21 | const loop = () => {
22 | tick++
23 | requestAnimationFrame(loop)
24 | }
25 | loop()
26 |
27 | setTimeout(() => {
28 | expect(tick).toBeGreaterThan(10)
29 | done()
30 | }, 2000)
31 | })
32 |
--------------------------------------------------------------------------------
/test/multipleScenes.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const StartTest = require('./startTest')
5 |
6 | it('should load both scenes', done => {
7 | class PreloadScene extends Phaser.Scene {
8 | constructor() {
9 | super('PreloadScene')
10 | }
11 | create() {
12 | this.scene.start('MainScene')
13 | }
14 | }
15 |
16 | class MainScene extends Phaser.Scene {
17 | constructor() {
18 | super('MainScene')
19 | }
20 | create() {
21 | done()
22 | }
23 | }
24 |
25 | StartTest([PreloadScene, MainScene])
26 | })
27 |
--------------------------------------------------------------------------------
/test/node-fetch2.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const http = require('http')
5 | const fetch = require('node-fetch2')
6 |
7 | let server
8 | const PORT = Math.floor(Math.random() * 50000 + 5000)
9 |
10 | beforeAll(done => {
11 | const requestListener = function (req, res) {
12 | if (req.url === '/hello') return res.writeHead(200).end('Hello!')
13 | else return res.writeHead(404).end()
14 | }
15 | server = http.createServer(requestListener)
16 | server.listen(PORT, () => {
17 | done()
18 | })
19 | })
20 |
21 | it('should be able to use node-fetch@2', done => {
22 | class MainScene extends Phaser.Scene {
23 | preload() {
24 | // load image (only possible of FakeXMLHttpRequest is available)
25 | this.load.image('star', '../assets/star.png')
26 | }
27 | create() {
28 | fetch(`http://localhost:${PORT}/hello`)
29 | .then(res => {
30 | return res.text()
31 | })
32 | .then(res => {
33 | expect(res).toBe('Hello!')
34 | done()
35 | })
36 | .catch(err => {
37 | console.log(err.message)
38 | })
39 | }
40 | }
41 |
42 | // prepare the config for Phaser
43 | const config = {
44 | type: Phaser.HEADLESS,
45 | width: 1280,
46 | height: 720,
47 | banner: false,
48 | audio: false,
49 | scene: [MainScene],
50 | physics: {
51 | default: 'arcade',
52 | arcade: {
53 | gravity: { y: 300 }
54 | }
55 | }
56 | }
57 |
58 | new Phaser.Game(config)
59 | })
60 |
61 | afterAll(done => {
62 | server.close(() => {
63 | done()
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/test/node-fetch3.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const http = require('http')
5 | let fetch
6 |
7 | let server
8 | const PORT = Math.floor(Math.random() * 50000 + 5000)
9 |
10 | beforeAll(done => {
11 | import('node-fetch3').then(pkg => {
12 | fetch = pkg.default
13 |
14 | const requestListener = function (req, res) {
15 | if (req.url === '/hello') return res.writeHead(200).end('Hello!')
16 | else return res.writeHead(404).end()
17 | }
18 | server = http.createServer(requestListener)
19 | server.listen(PORT, () => {
20 | done()
21 | })
22 | })
23 | })
24 |
25 | it('should be able to use node-fetch@3', done => {
26 | class MainScene extends Phaser.Scene {
27 | preload() {
28 | // load image (only possible of FakeXMLHttpRequest is available)
29 | this.load.image('star', '../assets/star.png')
30 | }
31 | create() {
32 | fetch(`http://localhost:${PORT}/hello`)
33 | .then(res => {
34 | return res.text()
35 | })
36 | .then(res => {
37 | expect(res).toBe('Hello!')
38 | done()
39 | })
40 | .catch(err => {
41 | console.log(err.message)
42 | })
43 | }
44 | }
45 |
46 | // prepare the config for Phaser
47 | const config = {
48 | type: Phaser.HEADLESS,
49 | width: 1280,
50 | height: 720,
51 | banner: false,
52 | audio: false,
53 | scene: [MainScene],
54 | physics: {
55 | default: 'arcade',
56 | arcade: {
57 | gravity: { y: 300 }
58 | }
59 | }
60 | }
61 |
62 | new Phaser.Game(config)
63 | })
64 |
65 | afterAll(done => {
66 | server.close(() => {
67 | done()
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/test/setBodySize.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const StartTest = require('./startTest')
5 |
6 | it('body size should be 32 * 48 (1536)', done => {
7 | class Player extends Phaser.Physics.Arcade.Sprite {
8 | constructor(scene, x, y) {
9 | // pass empty string for the texture
10 | super(scene, x, y, '')
11 |
12 | scene.add.existing(this)
13 | scene.physics.add.existing(this)
14 |
15 | // set the width and height of the sprite as the body size
16 | this.body.setSize(32, 48)
17 | }
18 | }
19 |
20 | class MainScene extends Phaser.Scene {
21 | create() {
22 | this.player = new Player(this, 100, 100)
23 | }
24 |
25 | update(time, delta) {
26 | expect(this.player.body.width * this.player.body.height).toBe(1536)
27 | done()
28 | }
29 | }
30 |
31 | StartTest([MainScene])
32 | })
33 |
--------------------------------------------------------------------------------
/test/sprite.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const StartTest = require('./startTest')
5 |
6 | it('should multiply to 528 (24*22)', done => {
7 | class MainScene extends Phaser.Scene {
8 | preload() {
9 | this.load.image('star', '../assets/star.png')
10 | }
11 |
12 | create() {
13 | const star = this.physics.add.sprite(400, 300, 'star')
14 | expect(star.body.width * star.body.height).toBe(528)
15 | done()
16 | }
17 | }
18 |
19 | StartTest([MainScene])
20 | })
21 |
--------------------------------------------------------------------------------
/test/spritesheet.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const StartTest = require('./startTest')
5 |
6 | it('should multiply to 1536 (32*48)', done => {
7 | class MainScene extends Phaser.Scene {
8 | preload() {
9 | this.load.spritesheet('dude', '../assets/dude.png', {
10 | frameWidth: 32,
11 | frameHeight: 48
12 | })
13 | }
14 |
15 | create() {
16 | const dude = this.physics.add.sprite(100, 450, 'dude')
17 | expect(dude.body.width * dude.body.height).toBe(1536)
18 | done()
19 | }
20 | }
21 |
22 | StartTest([MainScene])
23 | })
24 |
--------------------------------------------------------------------------------
/test/startTest.js:
--------------------------------------------------------------------------------
1 | const StartGame = Scenes => {
2 | const config = {
3 | type: Phaser.HEADLESS,
4 | width: 1280,
5 | height: 720,
6 | banner: false,
7 | audio: false,
8 | scene: Scenes,
9 | physics: {
10 | default: 'arcade',
11 | arcade: {
12 | gravity: { y: 300 }
13 | }
14 | }
15 | }
16 |
17 | new Phaser.Game(config)
18 | }
19 |
20 | module.exports = StartGame
21 |
--------------------------------------------------------------------------------
/test/tilemapTiledJSON.test.js:
--------------------------------------------------------------------------------
1 | require('../lib/index.js')
2 | require('./globalMock')
3 | const Phaser = require('phaser')
4 | const StartTest = require('./startTest')
5 |
6 | /**
7 | * From the example:
8 | * https://phaser.io/examples/v3/view/game-objects/tilemap/collision/simple-map
9 | */
10 | it('player should collide with the tile map', done => {
11 | class MainScene extends Phaser.Scene {
12 | preload() {
13 | this.load.image('tiles', '../assets/gridtiles.png')
14 | this.load.tilemapTiledJSON('map', '../assets/simple-map.json')
15 | }
16 |
17 | create() {
18 | const map = this.make.tilemap({
19 | key: 'map',
20 | tileWidth: 32,
21 | tileHeight: 32
22 | })
23 | const tileset = map.addTilesetImage('tiles')
24 | const layer = map.createLayer('Level1', tileset)
25 |
26 | map.setCollision([20, 48])
27 |
28 | this.player = this.physics.add.sprite(96, 96, '')
29 | this.player.body.setSize(24, 38)
30 |
31 | this.physics.add.collider(this.player, layer)
32 | }
33 |
34 | update() {
35 | if (this.player.body.onFloor()) {
36 | done()
37 | }
38 | }
39 | }
40 |
41 | StartTest([MainScene])
42 | })
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "./src",
5 | "outDir": "./lib",
6 |
7 | "lib": ["es2023", "dom"],
8 |
9 | "sourceMap": true,
10 | "declaration": true,
11 | "declarationMap": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------