├── 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
--------------------------------------------------------------------------------