├── src ├── js │ ├── game │ │ ├── states │ │ │ ├── finish.js │ │ │ ├── preload.js │ │ │ ├── boot.js │ │ │ ├── play.js │ │ │ ├── menu.js │ │ │ └── intro.js │ │ ├── utils │ │ │ └── observed_value.js │ │ ├── ui │ │ │ └── components │ │ │ │ ├── mute_button.js │ │ │ │ └── label_button.js │ │ ├── managers │ │ │ └── local_data_store.js │ │ ├── index.js │ │ ├── controlers │ │ │ ├── camera_rig_mouse_controler.js │ │ │ └── camera_rig_keyboard_controler.js │ │ └── index.spec.js │ ├── app │ │ └── container │ │ │ ├── app.jsx │ │ │ └── game.jsx │ └── index.jsx └── assets │ └── ps │ └── logo_prototype.psd ├── .gitignore ├── public ├── assets │ ├── logo.png │ ├── mute.png │ ├── button.png │ ├── preloader.png │ └── soundtrack.ogg ├── css │ └── styles.css ├── favicon.ico └── index.html ├── conf ├── webpack.config.dev.js ├── karma.config.js └── webpack.config.live.js ├── readme.md └── package.json /src/js/game/states/finish.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hg 2 | node_modules/* 3 | build/assets/* 4 | build/js/* 5 | build/css/* 6 | /.hgignore 7 | -------------------------------------------------------------------------------- /public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/public/assets/logo.png -------------------------------------------------------------------------------- /public/assets/mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/public/assets/mute.png -------------------------------------------------------------------------------- /public/assets/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/public/assets/button.png -------------------------------------------------------------------------------- /public/assets/preloader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/public/assets/preloader.png -------------------------------------------------------------------------------- /public/assets/soundtrack.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/public/assets/soundtrack.ogg -------------------------------------------------------------------------------- /src/assets/ps/logo_prototype.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xesenix/game-webpack-react-phaser-scaffold/HEAD/src/assets/ps/logo_prototype.psd -------------------------------------------------------------------------------- /public/css/styles.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background-color: #000; 8 | overflow: hidden; 9 | } -------------------------------------------------------------------------------- /src/js/app/container/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Game from './game' 3 | 4 | class App extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | } 13 | 14 | export default App -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=view-source:http://xesenix.pl/resources/assets/favicon.ico 3 | IDList= 4 | HotKey=0 5 | IconFile=C:\Users\Xesenix\AppData\Local\Mozilla\Firefox\Profiles\dkqudrwr.default\shortcutCache\hExPxzkZNlX7zkIdxOiCYg==.ico 6 | IconIndex=0 7 | -------------------------------------------------------------------------------- /src/js/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './app/container/app' 4 | 5 | window.onload = () => { 6 | window.PhaserGlobal = { 7 | disableWebAudio: true// that bit is important for ram consumption 8 | } 9 | 10 | render(, document.getElementById('app')) 11 | } -------------------------------------------------------------------------------- /src/js/game/utils/observed_value.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual' 2 | 3 | class ObservedValue extends Phaser.Signal { 4 | constructor(value) { 5 | super() 6 | this.value = value 7 | } 8 | 9 | check(newValue) { 10 | if (!isEqual(this.value, newValue)) { 11 | this.dispatch(newValue, this.value) 12 | this.value = newValue 13 | } 14 | } 15 | } 16 | 17 | export default ObservedValue -------------------------------------------------------------------------------- /src/js/app/container/game.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import gameConstructor from 'js/game' 3 | 4 | export default class Game extends React.Component { 5 | componentDidMount() { 6 | gameConstructor(980, 600) 7 | } 8 | 9 | storeContainer = (container) => { this.gameContainer = container } 10 | 11 | render() { 12 | return ( 13 |
14 |
15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Game 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/js/game/ui/components/mute_button.js: -------------------------------------------------------------------------------- 1 | class MuteButton extends Phaser.Button { 2 | constructor(game, x, y, key) { 3 | super(game, x, y, key, () => { 4 | game.dataStore.get('audioMute').then((state) => { 5 | state = !state 6 | game.dataStore.set('audioMute', state) 7 | }) 8 | }) 9 | 10 | this.game.dataStore.watcher('audioMute').add(this.updateState) 11 | this.game.dataStore.get('audioMute').then(this.updateState) 12 | } 13 | 14 | updateState = (state) => { 15 | if (state) { 16 | this.setFrames(0, 1, 0, 1) 17 | } else { 18 | this.setFrames(1, 0, 1, 0) 19 | } 20 | 21 | this.game.sound.mute = state 22 | } 23 | 24 | destroy() { 25 | this.game.dataStore.watcher('audioMute').remove(this.updateState) 26 | super.destroy() 27 | } 28 | } 29 | 30 | module.exports = MuteButton; -------------------------------------------------------------------------------- /src/js/game/states/preload.js: -------------------------------------------------------------------------------- 1 | class PreloadState { 2 | construct() { 3 | this.asset = null 4 | this.ready = false 5 | } 6 | 7 | preload() { 8 | this.asset = this.add.sprite(this.game.world.centerX, this.game.world.centerY, 'preloader') 9 | this.asset.anchor.setTo(0.5, 0.5) 10 | 11 | this.load.onLoadComplete.addOnce(this.onLoadComplete, this) 12 | this.load.setPreloadSprite(this.asset) 13 | this.load.image('game-logo', 'assets/logo.png') 14 | this.load.image('button', 'assets/button.png') 15 | 16 | this.load.spritesheet('mute', 'assets/mute.png', 64, 64) 17 | 18 | this.load.audio('melody', 'assets/soundtrack.ogg') 19 | } 20 | 21 | create() { 22 | this.asset.cropEnabled = false 23 | } 24 | 25 | onLoadComplete() { 26 | this.game.state.start('intro') 27 | } 28 | } 29 | 30 | export default PreloadState -------------------------------------------------------------------------------- /src/js/game/managers/local_data_store.js: -------------------------------------------------------------------------------- 1 | class LocalDataStoreManager { 2 | constructor(game) { 3 | this.game = game 4 | this.observers = {} 5 | } 6 | 7 | set(key, value) { 8 | localStorage.setItem(key, JSON.stringify(value)) 9 | 10 | if (typeof this.observers[key] !== 'undefined') { 11 | this.observers[key].dispatch(value) 12 | } 13 | } 14 | 15 | get(key, defaultValue) { 16 | return new Promise((resolve, reject) => { 17 | const value = localStorage.getItem(key) 18 | 19 | if (value !== null) { 20 | resolve(JSON.parse(value)) 21 | } else { 22 | resolve(defaultValue) 23 | } 24 | }) 25 | } 26 | 27 | watcher(key) { 28 | if (typeof this.observers[key] === 'undefined') { 29 | this.observers[key] = new Phaser.Signal(); 30 | } 31 | 32 | return this.observers[key] 33 | } 34 | } 35 | 36 | export default LocalDataStoreManager -------------------------------------------------------------------------------- /conf/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | console.log(path.resolve(__dirname, 'src')); 5 | 6 | module.exports = { 7 | entry: [ 8 | 'babel-polyfill', 9 | './src/js' 10 | ], 11 | output: { 12 | path: 'public/', 13 | publicPath: '/', 14 | filename: 'js/app.js' 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.jsx?$/, 20 | include: [ 21 | path.resolve(__dirname, '../src') 22 | ], 23 | loader: 'babel-loader', 24 | query: { 25 | plugins: ['transform-runtime'], 26 | presets: ['latest', 'react', 'stage-2'] 27 | } 28 | } 29 | ] 30 | }, 31 | plugins: [ 32 | new webpack.HotModuleReplacementPlugin() 33 | ], 34 | debug: true, 35 | devtool: 'source-map', 36 | devServer: { 37 | contentBase: 'public/', 38 | inline: true 39 | }, 40 | resolve: { 41 | extensions: ['', '.js', '.jsx'], 42 | root: [ 43 | path.resolve('./src') 44 | ] 45 | } 46 | }; -------------------------------------------------------------------------------- /src/js/game/index.js: -------------------------------------------------------------------------------- 1 | import BootState from 'js/game/states/boot' 2 | import PreloadState from 'js/game/states/preload' 3 | import IntroState from 'js/game/states/intro' 4 | import MenuState from 'js/game/states/menu' 5 | import PlayState from 'js/game/states/play' 6 | import FinishState from 'js/game/states/finish' 7 | 8 | import DataStoreManager from 'js/game/managers/local_data_store' 9 | 10 | const gameConstructor = (width = window.innerWidth, height = window.innerHeight, renderer = Phaser.AUTO, container = 'game') => { 11 | const game = new Phaser.Game( 12 | width, 13 | height, 14 | renderer, // Phaser. AUTO, WEBGL, CANVAS, HEADLESS 15 | container // id or element 16 | ) 17 | 18 | game.state.add('boot', BootState) 19 | game.state.add('preload', PreloadState) 20 | game.state.add('intro', IntroState) 21 | game.state.add('menu', MenuState) 22 | game.state.add('play', PlayState) 23 | //game.state.add('finish', FinishState) 24 | 25 | game.dataStore = new DataStoreManager(game) 26 | 27 | game.state.start('boot') 28 | 29 | return game 30 | } 31 | 32 | export default gameConstructor -------------------------------------------------------------------------------- /src/js/game/ui/components/label_button.js: -------------------------------------------------------------------------------- 1 | class LabelButton extends Phaser.Button { 2 | constructor(game, x, y, key, text, callback, callbackContext, overFrame, outFrame, downFrame, upFrame) { 3 | super(game, x, y, key, callback, callbackContext, overFrame, outFrame, downFrame, upFrame) 4 | this.label = new Phaser.Text(game, 0, 0, text, { align: 'center', boundsAlignH: 'center', boundsAlignV: 'middle' }) 5 | this.label.anchor.setTo(0.5, 0.5) 6 | this.addChild(this.label) 7 | } 8 | 9 | get height() { 10 | return this.scale.x * this.texture.frame.height 11 | } 12 | 13 | set height(value) { 14 | this.scale.y = value / this.texture.frame.height 15 | this._height = value 16 | this.label.scale.y = this.texture.frame.height / value 17 | this.label.y = this.label.scale.y * value * (0.5 - this.anchor.y) 18 | } 19 | 20 | get width() { 21 | return this.scale.x * this.texture.frame.width 22 | } 23 | 24 | set width(value) { 25 | this.scale.x = value / this.texture.frame.width 26 | this._width = value 27 | this.label.scale.x = this.texture.frame.width / value 28 | this.label.x = this.label.scale.x * value * (0.5 - this.anchor.x) 29 | } 30 | } 31 | 32 | export default LabelButton -------------------------------------------------------------------------------- /conf/karma.config.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.config.dev'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: '../src', 6 | frameworks: ['mocha', 'chai', 'sinon'], 7 | files: [ 8 | '../public/js/phaser.min.js', 9 | { pattern: 'js/**/*.spec.js', watched: false } 10 | ], 11 | exclude: [ 12 | ], 13 | preprocessors: { 14 | 'js/**/*.spec.js': ['webpack', 'sourcemap', 'coverage'] 15 | }, 16 | webpack: { 17 | module: webpackConfig.module, 18 | resolve: webpackConfig.resolve, 19 | devtool: 'inline-source-map' 20 | }, 21 | htmlReporter: { 22 | outputFile: '../logs/test/result.html', 23 | // Optional 24 | pageTitle: 'Game', 25 | subPageTitle: 'unit test results', 26 | groupSuites: true, 27 | useCompactStyle: false, 28 | useLegacyStyle: false 29 | }, 30 | mochaReporter: { 31 | showDiff: true 32 | }, 33 | reporters: ['progress', 'html'], 34 | port: 9876, 35 | colors: true, 36 | logLevel: config.LOG_ERROR, 37 | autoWatch: true, 38 | browsers: ['Chrome'], 39 | singleRun: false, 40 | concurrency: Infinity, 41 | plugins: [ 42 | require('karma-chrome-launcher'), 43 | require('karma-htmlfile-reporter'), 44 | require('karma-webpack'), 45 | require('karma-mocha'), 46 | require('karma-chai'), 47 | //require('karma-chai-spies'), 48 | require('karma-sinon'), 49 | require('karma-sourcemap-loader'), 50 | require('karma-coverage') 51 | ] 52 | }) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/js/game/controlers/camera_rig_mouse_controler.js: -------------------------------------------------------------------------------- 1 | class CameraRigMouseControler { 2 | constructor(game, camera, speed) { 3 | this.game = game 4 | this.camera = camera 5 | this.speed = speed 6 | 7 | this.isDragging = false 8 | this.dragOriginPoint = null 9 | this.cameraDragOriginPoint = null 10 | 11 | this.prepareControls() 12 | } 13 | 14 | prepareControls() { 15 | this.game.input.activePointer.middleButton.onDown.add(this.startDrag) 16 | this.game.input.activePointer.middleButton.onUp.add(this.stopDrag) 17 | } 18 | 19 | startDrag = (button) => { 20 | this.isDragging = true 21 | 22 | this.dragOriginPoint = { 23 | x: button.parent.x, 24 | y: button.parent.y 25 | } 26 | 27 | this.cameraDragOriginPoint = { 28 | x: this.camera.x, 29 | y: this.camera.y 30 | } 31 | } 32 | 33 | stopDrag = (button) => { 34 | this.isDragging = false 35 | this.dragOriginPoint = null 36 | } 37 | 38 | update() { 39 | this.handleControls() 40 | } 41 | 42 | handleControls() { 43 | if (this.isDragging) { 44 | this.camera.x = this.cameraDragOriginPoint.x - (this.dragOriginPoint.x - this.game.input.x) * this.speed 45 | this.camera.y = this.cameraDragOriginPoint.y - (this.dragOriginPoint.y - this.game.input.y) * this.speed 46 | } 47 | } 48 | 49 | destroy() { 50 | this.game.input.activePointer.middleButton.onDown.remove(this.startDrag) 51 | this.game.input.activePointer.middleButton.onUp.remove(this.stopDrag) 52 | } 53 | } 54 | 55 | export default CameraRigMouseControler -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # About 2 | This scaffold makes use of webpack for loading and building scripts written in es6 (or you can use es5 if you dont want to use it). It is integrated with hot realoading development server and karma testing framework. 3 | 4 | Source code is put into src/js folder. Public folder contains development build that requires running webpack-dev-server to work. And build folder contains deployment ready version. If you want change how it works configurations for this enviroment is present in conf folder. 5 | 6 | # Features 7 | Project contains basic states for loading assets, intro, screen, menu and play states. 8 | * basic managment for soundtrack 9 | * google fonts 10 | * simple camera mouse and keyboard controller 11 | 12 | Everything is contained in react component so you can build react application around your game. 13 | 14 | # How to start 15 | 16 | You need to install all dependecies first with: 17 | 18 | ``` bash 19 | npm install 20 | ``` 21 | 22 | To start developing server with hot realoading run: 23 | 24 | ``` bash 25 | npm start 26 | ``` 27 | 28 | And open public/index.html in your browser. 29 | 30 | # How to test 31 | 32 | Write your test in *.spec.js files inside src/js folder as in example src/js/index.spec.js and when ready run: 33 | 34 | 35 | ``` bash 36 | npm test 37 | ``` 38 | 39 | You can check result of your tests in logs/test/index.html 40 | 41 | 42 | # How to build for deployment 43 | 44 | If you just want to build production ready script run: 45 | 46 | 47 | ``` bash 48 | npm run build 49 | ``` 50 | 51 | Everything needed to deploy will be in build folder. 52 | -------------------------------------------------------------------------------- /src/js/game/controlers/camera_rig_keyboard_controler.js: -------------------------------------------------------------------------------- 1 | class CameraRigKeyboardControler { 2 | constructor(game, camera, speed) { 3 | this.game = game 4 | this.camera = camera 5 | this.speed = speed 6 | 7 | this.prepareControls() 8 | } 9 | 10 | prepareControls() { 11 | this.controls = this.game.input.keyboard.addKeys({ 12 | left: Phaser.Keyboard.LEFT, 13 | right: Phaser.Keyboard.RIGHT, 14 | up: Phaser.Keyboard.UP, 15 | down: Phaser.Keyboard.DOWN, 16 | a: Phaser.Keyboard.A, 17 | s: Phaser.Keyboard.S, 18 | d: Phaser.Keyboard.D, 19 | w: Phaser.Keyboard.W 20 | }) 21 | 22 | this.game.input.keyboard.addKeyCapture([ 23 | Phaser.Keyboard.LEFT, 24 | Phaser.Keyboard.RIGHT, 25 | Phaser.Keyboard.UP, 26 | Phaser.Keyboard.DOWN 27 | ]) 28 | } 29 | 30 | update() { 31 | this.handleControls() 32 | } 33 | 34 | handleControls() { 35 | const up = this.controls.up.isDown || this.controls.w.isDown 36 | const left = this.controls.left.isDown || this.controls.a.isDown 37 | const right = this.controls.right.isDown || this.controls.d.isDown 38 | const down = this.controls.down.isDown || this.controls.s.isDown 39 | const delta = this.game.time.elapsed 40 | 41 | if (up) { 42 | this.camera.y = this.camera.y - this.speed * delta 43 | } else if (down) { 44 | this.camera.y = this.camera.y + this.speed * delta 45 | } 46 | 47 | if (left) { 48 | this.camera.x = this.camera.x - this.speed * delta 49 | } else if (right) { 50 | this.camera.x = this.camera.x + this.speed * delta 51 | } 52 | } 53 | } 54 | 55 | export default CameraRigKeyboardControler -------------------------------------------------------------------------------- /src/js/game/index.spec.js: -------------------------------------------------------------------------------- 1 | import PreloadState from 'js/game/states/preload' 2 | import BootState from 'js/game/states/boot' 3 | import IntroState from 'js/game/states/intro' 4 | import MenuState from 'js/game/states/menu' 5 | import PlayState from 'js/game/states/play' 6 | 7 | import gameConstructor from 'js/game/index' 8 | 9 | describe('Game Constructor', function() { 10 | const element = document.createElement('div') 11 | let game 12 | 13 | before(() => { 14 | // for testing only Phaser.WEBGL works and yes i know about Phaser.HEADLESS 15 | game = gameConstructor(640, 480, Phaser.WEBGL, element) 16 | // dont wait for event 17 | game.boot() 18 | }) 19 | 20 | after(() => { 21 | game.destroy() 22 | }) 23 | 24 | it('should contain preload state', function() { 25 | expect(game.state.states.preload).to.be.instanceOf(PreloadState) 26 | }) 27 | 28 | it('should contain boot state', function() { 29 | expect(game.state.states.boot).to.be.instanceOf(BootState) 30 | }) 31 | 32 | it('should contain intro state', function() { 33 | expect(game.state.states.intro).to.be.instanceOf(IntroState) 34 | }) 35 | 36 | it('should contain menu state', function() { 37 | expect(game.state.states.menu).to.be.instanceOf(MenuState) 38 | }) 39 | 40 | it('should contain play state', function() { 41 | expect(game.state.states.play).to.be.instanceOf(PlayState) 42 | }) 43 | 44 | it('should be of constructed size', function() { 45 | expect(game.width).to.be.equal(640) 46 | expect(game.height).to.be.equal(480) 47 | }) 48 | 49 | it('should be initialized in choosen dom element', function() { 50 | expect(game.parent).to.be.equal(element) 51 | }) 52 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-webpack-react-phaser-scaffold", 3 | "version": "0.0.1", 4 | "description": "scaffold for games written in es6 with help of webpack phaser and react for page layout", 5 | "scripts": { 6 | "start": "webpack-dev-server --config conf/webpack.config.dev.js", 7 | "build": "webpack --config conf/webpack.config.live.js", 8 | "test": "karma start conf/karma.config.js" 9 | }, 10 | "author": "xesenix", 11 | "license": "GPL", 12 | "devDependencies": { 13 | "babel-cli": "^6.18.0", 14 | "babel-core": "^6.18.2", 15 | "babel-loader": "^6.2.9", 16 | "babel-plugin-transform-runtime": "^6.15.0", 17 | "babel-preset-es2015": "^6.18.0", 18 | "babel-preset-latest": "^6.16.0", 19 | "babel-preset-react": "^6.16.0", 20 | "babel-preset-stage-2": "^6.18.0", 21 | "chai": "^3.5.0", 22 | "chai-shallow-deep-equal": "^1.4.4", 23 | "chai-spies": "^0.7.1", 24 | "copy-webpack-plugin": "^4.0.1", 25 | "karma": "^1.3.0", 26 | "karma-chai": "^0.1.0", 27 | "karma-chai-spies": "^0.1.4", 28 | "karma-chrome-launcher": "^2.0.0", 29 | "karma-coverage": "^1.1.1", 30 | "karma-htmlfile-reporter": "^0.3.4", 31 | "karma-mocha": "^1.3.0", 32 | "karma-sinon": "^1.0.5", 33 | "karma-sourcemap-loader": "^0.3.7", 34 | "karma-webpack": "^1.8.1", 35 | "mocha": "^3.2.0", 36 | "sinon": "^1.17.6", 37 | "webpack": "^1.14.0", 38 | "webpack-dev-server": "^1.16.2", 39 | "webpack-obfuscator": "^0.8.3" 40 | }, 41 | "dependencies": { 42 | "babel-polyfill": "^6.16.0", 43 | "babel-runtime": "^6.18.0", 44 | "react": "^15.4.1", 45 | "react-dom": "^15.4.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /conf/webpack.config.live.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | console.log(path.resolve(__dirname, 'src')); 6 | 7 | module.exports = { 8 | entry: [ 9 | 'babel-polyfill', 10 | './src/js' 11 | ], 12 | output: { 13 | path: 'build/', 14 | publicPath: '/', 15 | filename: 'js/app.js' 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.jsx?$/, 21 | include: [ 22 | path.resolve(__dirname, '../src') 23 | ], 24 | loader: 'babel-loader', 25 | query: { 26 | plugins: ['transform-runtime'], 27 | presets: ['latest', 'react', 'stage-2'] 28 | } 29 | } 30 | ] 31 | }, 32 | plugins: [ 33 | new webpack.optimize.UglifyJsPlugin(), 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | NODE_ENV: JSON.stringify('production') 37 | } 38 | }), 39 | //new webpack.HotModuleReplacementPlugin(), 40 | new CopyWebpackPlugin([{ 41 | from: path.resolve(__dirname, '../public/js'), 42 | to: path.resolve(__dirname, '../build/js') 43 | }, { 44 | from: path.resolve(__dirname, '../public/css'), 45 | to: path.resolve(__dirname, '../build/css') 46 | }, { 47 | from: path.resolve(__dirname, '../public/assets'), 48 | to: path.resolve(__dirname, '../build/assets') 49 | }, { 50 | from: path.resolve(__dirname, '../public/fonts'), 51 | to: path.resolve(__dirname, '../build/fonts') 52 | }]) 53 | ], 54 | debug: true, 55 | devtool: 'source-map', 56 | devServer: { 57 | contentBase: 'public/', 58 | inline: true, 59 | outputPath: 'build/', 60 | }, 61 | resolve: { 62 | extensions: ['', '.js', '.jsx'], 63 | root: [ 64 | path.resolve('./src') 65 | ] 66 | } 67 | }; -------------------------------------------------------------------------------- /src/js/game/states/boot.js: -------------------------------------------------------------------------------- 1 | class BootState { 2 | preload() { 3 | this.stage.disableVisibilityChange = true // so that on first state application will progress regardless if there was player interaction or not 4 | this.game.time.advancedTiming = true // for fps counter 5 | this.game.scale.fullScreenScaleMode = Phaser.ScaleManager.SHOW_ALL //SHOW_ALL, NO_SCALE, EXACT_FIT 6 | this.game.clearBeforeRender = false // if your game contains full background 7 | 8 | this.loaded = 1 9 | 10 | // turn on additional plugins: 11 | //this.game.add.plugin(Fabrique.Plugins.Spine) 12 | 13 | // setup fonts and interface apperance (google fonts used here should be loaded via link tag in page layout header) 14 | this.game.theme = { 15 | font: 'VT323' 16 | //font: 'Bungee' 17 | //font: 'Russo One' 18 | //font: 'Fruktur' 19 | //font: 'Press Start 2P' 20 | } 21 | 22 | document.querySelector('canvas').oncontextmenu = (e) => { 23 | e.preventDefault(); 24 | return false; 25 | } 26 | 27 | this.load.onLoadComplete.addOnce(this.onLoadComplete, this) 28 | this.load.image('preloader', 'assets/preloader.png') 29 | 30 | // if you dont want to use link tag in layout for fonts you can use this part but it isnt 100% reliable 31 | /* 32 | this.loaded = 2 33 | window.WebFontConfig = { 34 | active: () => { 35 | this.game.time.events.add(Phaser.Timer.SECOND, this.onLoadComplete) 36 | }, 37 | google: { 38 | families: [ this.game.theme.font ] 39 | } 40 | } 41 | 42 | this.load.script("webfont", "//ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js") 43 | */ 44 | } 45 | 46 | onLoadComplete = () => { 47 | if (--this.loaded === 0) { 48 | this.game.state.start('preload') 49 | } 50 | } 51 | } 52 | 53 | export default BootState -------------------------------------------------------------------------------- /src/js/game/states/play.js: -------------------------------------------------------------------------------- 1 | import CameraRigKeyboardControler from 'js/game/controlers/camera_rig_keyboard_controler' 2 | import CameraRigMouseControler from 'js/game/controlers/camera_rig_mouse_controler' 3 | 4 | import ObservedValue from 'js/game/utils/observed_value' 5 | 6 | class GameState { 7 | create() { 8 | this.game.time.advancedTiming = true 9 | 10 | this.prepareWorld() 11 | this.prepareCamera() 12 | this.prepareControls() 13 | } 14 | 15 | prepareWorld() { 16 | this.game.world.setBounds(0, 0, 4000, 4000) 17 | 18 | this.game.add.sprite(300, 100, 'game-logo') 19 | 20 | this.infoText = this.game.add.text(20, 20, 'Drag with centre mouse button or use arrows a/s/d/w.\nEsc to go back to menu R to restart.', { font: '32px ' + this.game.theme.font, fill: '#666666', align: 'left'}) 21 | this.infoText.anchor.setTo(0, 0) 22 | } 23 | 24 | prepareCamera() { 25 | this.cameraKeyboardControler = new CameraRigKeyboardControler(this.game, this.game.camera, 1) 26 | this.cameraMouseControler = new CameraRigMouseControler(this.game, this.game.camera, 8) 27 | 28 | const { x , y } = this.game.camera.view 29 | 30 | this.cameraPositionObserver = new ObservedValue({x, y}) 31 | this.onViewChanged = this.cameraPositionObserver 32 | } 33 | 34 | prepareControls() { 35 | this.controls = this.game.input.keyboard.addKeys({ 36 | restart: Phaser.KeyCode.R, 37 | esc: Phaser.KeyCode.ESC 38 | }) 39 | 40 | this.controls.restart.onDown.add(() => { 41 | this.game.state.start('play', true, false) 42 | }) 43 | 44 | this.controls.esc.onDown.add(() => { 45 | this.game.state.start('menu') 46 | }) 47 | 48 | this.game.input.keyboard.addKeyCapture([ 49 | Phaser.KeyCode.ESC 50 | ]) 51 | } 52 | 53 | update() { 54 | this.cameraKeyboardControler.update() 55 | this.cameraMouseControler.update() 56 | } 57 | 58 | render() { 59 | this.game.debug.text('FPS: ' + (this.game.time.fps || '--'), 10, 15, "#ffffff") 60 | } 61 | } 62 | 63 | export default GameState -------------------------------------------------------------------------------- /src/js/game/states/menu.js: -------------------------------------------------------------------------------- 1 | import LabelButton from 'js/game/ui/components/label_button' 2 | import MuteButton from 'js/game/ui/components/mute_button' 3 | 4 | class MenuState { 5 | init() { 6 | this.menuItems = [] 7 | } 8 | 9 | create() { 10 | // remember to restore size when getting back from play state 11 | this.game.world.setBounds(0, 0, this.game.width, this.game.height) 12 | 13 | if (this.game.menuSoundtrack === null) { 14 | this.game.menuSoundtrack = this.game.sound.play('melody', 1, true) 15 | } else { 16 | this.game.menuSoundtrack.resume() 17 | } 18 | 19 | this.createBackground() 20 | this.createUi() 21 | } 22 | 23 | createBackground() { 24 | this.game.stage.backgroundColor = '#000000' 25 | 26 | this.sprite = this.game.add.sprite(this.game.world.centerX / 2, 220, 'game-logo') 27 | this.sprite.fixedToCamera = true 28 | this.sprite.anchor.setTo(0.5, 0.5) 29 | 30 | this.sprite.angle = -5 31 | this.game.add.tween(this.sprite).to({angle: 5}, 1000, Phaser.Easing.Linear.NONE, true, 0, 1000, true) 32 | } 33 | 34 | createUi() { 35 | this.createMenuItem('start', this.onStartClick) 36 | this.createMenuItem('full screen', this.onToggleFullScreen) 37 | 38 | this.muteButton = new MuteButton(this.game, this.game.world.width - 10, 10, 'mute', this.game.soundManager) 39 | this.muteButton.fixedToCamera = true 40 | this.muteButton.anchor.setTo(1, 0) 41 | this.muteButton.width = 32 42 | this.muteButton.height = 32 43 | this.world.add(this.muteButton) 44 | } 45 | 46 | onStartClick = () => { 47 | this.game.state.start('play') 48 | } 49 | 50 | onToggleFullScreen = () => { 51 | if (this.game.scale.isFullScreen) { 52 | this.game.scale.stopFullScreen() 53 | } else { 54 | this.game.scale.startFullScreen(false) 55 | } 56 | } 57 | 58 | createMenuItem(label, callback) { 59 | const menuItem = new LabelButton(this.game, 1.5 * this.game.world.centerX, this.menuItems.length * 64 + 120, 'button', label, callback) 60 | menuItem.fixedToCamera = true 61 | menuItem.label.setStyle({ font: '24px ' + this.game.theme.font, fill: '#000000', align: 'center' }, true) 62 | menuItem.width = 256 63 | menuItem.height = 60 64 | 65 | this.world.add(menuItem) 66 | this.menuItems.push(menuItem) 67 | } 68 | } 69 | 70 | export default MenuState -------------------------------------------------------------------------------- /src/js/game/states/intro.js: -------------------------------------------------------------------------------- 1 | import LabelButton from 'js/game/ui/components/label_button' 2 | import MuteButton from 'js/game/ui/components/mute_button' 3 | 4 | class IntroState { 5 | create() { 6 | this.game.dataStore.get('audioMute').then((state) => { 7 | this.game.menuSoundtrack = this.game.sound.play('melody', 1, true) 8 | this.game.menuSoundtrack.mute = state 9 | }) 10 | 11 | this.createUi() 12 | this.animateUi() 13 | } 14 | 15 | createUi() { 16 | this.logo = this.game.add.sprite(this.game.world.centerX, 120, 'game-logo') 17 | this.logo.fixedToCamera = true 18 | this.logo.anchor.setTo(0.5, 0) 19 | 20 | this.titleText = this.game.add.text(this.game.world.centerX, 360, 'Game Title', { font: '64px ' + this.game.theme.font, fill: '#ffffff', align: 'center'}) 21 | this.titleText.fixedToCamera = true 22 | this.titleText.anchor.setTo(0.5, 0) 23 | 24 | this.authorText = this.game.add.text(this.game.world.centerX, 440, 'game by Xesenix', { font: '32px ' + this.game.theme.font, fill: '#ffffff', align: 'center'}) 25 | this.authorText.fixedToCamera = true 26 | this.authorText.anchor.setTo(0.5, 0) 27 | 28 | this.continueButton = new LabelButton(this.game, this.game.world.centerX, 520, 'button', 'continue', () => { this.game.state.start('menu') }) 29 | this.continueButton.fixedToCamera = true 30 | this.continueButton.label.setStyle({ font: '24px ' + this.game.theme.font, fill: '#000000', align: 'center' }, true) 31 | this.continueButton.anchor.setTo(0.5, 0.5) 32 | this.continueButton.width = 256 33 | this.continueButton.height = 64 34 | this.world.add(this.continueButton) 35 | 36 | this.muteButton = new MuteButton(this.game, this.game.world.width - 10, 10, 'mute') 37 | this.muteButton.fixedToCamera = true 38 | this.muteButton.anchor.setTo(1, 0) 39 | this.muteButton.width = 32 40 | this.muteButton.height = 32 41 | this.world.add(this.muteButton) 42 | } 43 | 44 | animateUi() { 45 | this.game.add.tween(this.logo).from({ y: -20 }, 500, Phaser.Easing.Linear.NONE, true, 0, 0, false) 46 | this.game.add.tween(this.titleText).from({ y: this.game.world.height }, 500, Phaser.Easing.Linear.NONE, true, 500, 0, false) 47 | this.game.add.tween(this.authorText).from({ y: this.game.world.height }, 500, Phaser.Easing.Linear.NONE, true, 700, 0, false) 48 | this.game.add.tween(this.continueButton).from({ alpha: 0.0 }, 500, Phaser.Easing.Linear.NONE, true, 1250, 0, false) 49 | } 50 | } 51 | 52 | export default IntroState --------------------------------------------------------------------------------