├── .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 | logo 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 | [![Github Workflow](https://img.shields.io/github/actions/workflow/status/geckosio/phaser-on-nodejs/ubuntu.yaml?branch=master&label=github%20build&logo=github&style=flat-square)](https://github.com/geckosio/phaser-on-nodejs/actions?query=workflow%3ACI) 12 | [![npm](https://img.shields.io/npm/v/@geckos.io/phaser-on-nodejs.svg?style=flat-square)](https://www.npmjs.com/package/@geckos.io/phaser-on-nodejs) 13 | [![Downloads](https://img.shields.io/npm/dm/@geckos.io/phaser-on-nodejs.svg?style=flat-square)](https://www.npmjs.com/package/@geckos.io/phaser-on-nodejs) 14 | [![NPM](https://img.shields.io/npm/l/@geckos.io/phaser-on-nodejs.svg?style=flat-square)](LICENSE) 15 | [![Codecov](https://img.shields.io/codecov/c/github/geckosio/phaser-on-nodejs?logo=codecov&style=flat-square)](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 | --------------------------------------------------------------------------------