├── src ├── components │ ├── main │ │ ├── main.less │ │ ├── renderer.js │ │ ├── stats.js │ │ ├── canvas.js │ │ └── main.jsx │ ├── entities │ │ ├── shell.js │ │ ├── ghost.js │ │ ├── shipComponents │ │ │ ├── hull.js │ │ │ ├── thruster.js │ │ │ ├── shipComponent.js │ │ │ ├── hardpoints.js │ │ │ └── turret.js │ │ ├── modules │ │ │ ├── thrust.js │ │ │ ├── debug.js │ │ │ ├── README.md │ │ │ └── attack.js │ │ ├── entity.js │ │ ├── projectile.js │ │ ├── ship.js │ │ └── physical.js │ ├── bootstrap │ │ ├── bootstrap.less │ │ └── bootstrap.jsx │ ├── debug │ │ ├── debug.less │ │ └── debug.jsx │ ├── world │ │ ├── materials.js │ │ ├── stars.js │ │ └── engine.js │ └── user │ │ └── user.js ├── polyfill.js ├── assets │ └── circle4.png ├── utils │ ├── noop.js │ ├── font.js │ ├── logical.js │ ├── timing.js │ ├── compose.js │ └── logger.js ├── constants │ ├── err.js │ ├── shipComponentTypes.js │ └── events.js ├── core │ └── styles │ │ ├── modules │ │ ├── queries.less │ │ └── colors.less │ │ ├── base.less │ │ └── utils.less ├── styles.less ├── app.js ├── tmpl │ └── index.hjs ├── stores │ ├── config.js │ ├── resources.js │ ├── stateFactory.js │ ├── appState.js │ └── shipComponents.js └── dispatchers │ ├── appDispatcher.js │ └── engineDispatcher.js ├── .gitignore ├── .npmignore ├── .hodevrc ├── .babelrc ├── spec └── index.js ├── .eslintrc ├── CHANGELOG.md ├── README.md └── package.json /src/components/main/main.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.log* 4 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | 2 | require( 'babel/polyfill' ) 3 | require( 'whatwg-fetch' ) 4 | -------------------------------------------------------------------------------- /src/assets/circle4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattstyles/odyssey-nova/HEAD/src/assets/circle4.png -------------------------------------------------------------------------------- /src/utils/noop.js: -------------------------------------------------------------------------------- 1 | export default function noop() { 2 | // Named to allow a proper stack trace 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .eslintrc 3 | .travis.yml 4 | dist 5 | **/*.test.js 6 | **/__tests__ 7 | **/__test__ 8 | -------------------------------------------------------------------------------- /.hodevrc: -------------------------------------------------------------------------------- 1 | { 2 | "autoprefixer-transform": { 3 | "browsers": [ 4 | "last 3 versions" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/entities/shell.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Shell entities consist of only a physics body, no renderable 4 | */ 5 | -------------------------------------------------------------------------------- /src/utils/font.js: -------------------------------------------------------------------------------- 1 | import WebFont from 'webfontloader' 2 | 3 | WebFont.load({ 4 | google: { 5 | families: [ 'Open+Sans:400,600:latin' ] 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 2, 3 | "optional": [ 4 | "es7.classProperties" 5 | ], 6 | "loose": [ 7 | "es6.modules", 8 | "es6.classes" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/logical.js: -------------------------------------------------------------------------------- 1 | 2 | // logical exclusive OR 3 | // TT F 4 | // TF T 5 | // FT T 6 | // FF F 7 | export function XOR( a, b ) { 8 | return !a !== !b 9 | } 10 | -------------------------------------------------------------------------------- /src/components/entities/ghost.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * A ghost entity has a renderable representation but is not part of the 4 | * physics world. Perfect for visual effects. 5 | */ 6 | -------------------------------------------------------------------------------- /src/constants/err.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates custom errors and error heirarchies 3 | */ 4 | 5 | import { create } from 'errno' 6 | 7 | export const DispatchError = create( 'DispatchError' ) 8 | -------------------------------------------------------------------------------- /src/utils/timing.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Returns a dirty promise to appease await 4 | */ 5 | export async function wait( ms ) { 6 | return { 7 | then: cb => setTimeout( cb, ms ) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /spec/index.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path' 3 | import minimist from 'minimist' 4 | 5 | let argv = minimist( process.argv.slice( 2 ) ) 6 | 7 | argv._.forEach( file => { 8 | require( path.resolve( file ) ) 9 | }) 10 | -------------------------------------------------------------------------------- /src/core/styles/modules/queries.less: -------------------------------------------------------------------------------- 1 | /* Media query aliases */ 2 | @--sm-view: ~"only screen and ( max-width: 479px )"; 3 | @--md-view: ~"only screen and ( max-width: 789px )"; 4 | @--lg-view: ~"only screen and ( max-width: 1023px )"; 5 | -------------------------------------------------------------------------------- /src/components/bootstrap/bootstrap.less: -------------------------------------------------------------------------------- 1 | 2 | .BS-loading { 3 | display: block; 4 | position: absolute; 5 | width: 100%; 6 | bottom: 20%; 7 | text-align: center; 8 | font-size: 20px; 9 | color: @--color-blue; 10 | text-shadow: 0px 0px 12px @--color-darkblue; 11 | } 12 | -------------------------------------------------------------------------------- /src/constants/shipComponentTypes.js: -------------------------------------------------------------------------------- 1 | 2 | import toMap from 'to-map' 3 | 4 | // Immutable key-value map of 5 | // Use a flat map, makes life a little easier 6 | const SC_TYPES = toMap({ 7 | HULL: 'sc:hull', 8 | THRUSTER: 'sc:thruster', 9 | TURRET: 'sc:turret' 10 | }) 11 | 12 | 13 | export default SC_TYPES 14 | -------------------------------------------------------------------------------- /src/constants/events.js: -------------------------------------------------------------------------------- 1 | 2 | import toMap from 'to-map' 3 | 4 | // Immutable key-value map of 5 | const EVENTS = toMap({ 6 | UPDATE: 'app:update', 7 | 8 | CHANGE_STATE: 'app:changeState', 9 | 10 | /** 11 | * Engine level dispatches 12 | */ 13 | ENTITY_ADD: 'engine:entity:add', 14 | ENTITY_REMOVE: 'engine:entity:remove' 15 | }) 16 | 17 | 18 | export default EVENTS 19 | -------------------------------------------------------------------------------- /src/styles.less: -------------------------------------------------------------------------------- 1 | 2 | /* Vendor */ 3 | @import (inline) 'normalize.css/normalize.css'; 4 | 5 | 6 | /* Module includes */ 7 | @import 'modules/colors'; 8 | @import 'modules/queries'; 9 | 10 | 11 | /* Global includes */ 12 | @import 'base'; 13 | @import 'utils'; 14 | 15 | 16 | 17 | /* Component includes */ 18 | @import 'main/main'; 19 | @import 'bootstrap/bootstrap'; 20 | @import 'debug/debug'; 21 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 2 | import logger from 'utils/logger' 3 | import appState from 'stores/appState' 4 | 5 | import resources from 'stores/resources' 6 | 7 | // Let's go 8 | appState.run( document.querySelector( '.js-app' ) ) 9 | 10 | if ( process.env.DEBUG ) { 11 | window.appState = appState 12 | 13 | window.resources = resources 14 | 15 | window.Pixi = require( 'pixi.js' ) 16 | window.P2 = require( 'p2' ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/debug/debug.less: -------------------------------------------------------------------------------- 1 | 2 | .Debug { 3 | position: absolute; 4 | top: 2px; 5 | left: 2px; 6 | z-index: 1000; 7 | padding: 4px; 8 | background: rgba( 2, 136, 209, .15 ); 9 | font-size: 10px; 10 | color: @--color-blue; 11 | text-shadow: 0px 0px 3px @--color-darkblue; 12 | } 13 | 14 | .Debug-title { 15 | margin-top: 1em; 16 | margin-bottom: .6em; 17 | } 18 | 19 | .Debug-section:first-child .Debug-title { 20 | margin-top: .2em; 21 | } 22 | -------------------------------------------------------------------------------- /src/core/styles/modules/colors.less: -------------------------------------------------------------------------------- 1 | @--color-black: #000000; 2 | @--color-black10: #181818; 3 | @--color-black20: #404040; 4 | @--color-black30: #545454; 5 | @--color-black40: #898989; 6 | 7 | @--color-white: #ffffff; 8 | @--color-white10: #fafafa; 9 | @--color-white20: #f1f1f1; 10 | @--color-white30: #eaecf0; 11 | 12 | @--color-green: rgb( 77, 216, 10 ); 13 | 14 | // These are hashed in quick quick 15 | @--color-darkblue: #0288D1; 16 | @--color-blue: #03A9F4; 17 | @--color-lightblue: #B3E5FC; 18 | @--color-blueaccent: #448AFF; 19 | -------------------------------------------------------------------------------- /src/tmpl/index.hjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | odyssey-nova 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/entities/shipComponents/hull.js: -------------------------------------------------------------------------------- 1 | 2 | import P2 from 'p2' 3 | 4 | import ShipComponent from 'entities/shipComponents/shipComponent' 5 | import materials from 'world/materials' 6 | import SC_TYPES from 'constants/shipComponentTypes' 7 | 8 | export default class Hull extends ShipComponent { 9 | constructor( opts ) { 10 | super( opts ) 11 | 12 | this.type = SC_TYPES.get( 'HULL' ) 13 | 14 | this.shape = new P2.Circle({ 15 | radius: this.radius, 16 | material: this.material 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/styles/base.less: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | position: fixed; 4 | width: 100vw; 5 | height: 100vh; 6 | overflow:hidden; 7 | -webkit-overflow-scrolling: touch; 8 | } 9 | 10 | 11 | body { 12 | margin: 0; 13 | font-family: 'Coolville', 'Helvetica Neue', 'Helvetica', 'Lucida Grande', 'Arial', sans-serif; 14 | font-size: 10px; 15 | color: @--color-white10; 16 | background: @--color-black; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | 21 | 22 | .App { 23 | z-index: 0; 24 | } 25 | 26 | canvas { 27 | position: absolute; 28 | display: block; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/entities/shipComponents/thruster.js: -------------------------------------------------------------------------------- 1 | 2 | import P2 from 'p2' 3 | 4 | import ShipComponent from 'entities/shipComponents/shipComponent' 5 | import SC_TYPES from 'constants/shipComponentTypes' 6 | import logger from 'utils/logger' 7 | import materials from 'world/materials' 8 | 9 | export default class Thruster extends ShipComponent { 10 | constructor( opts ) { 11 | super( opts ) 12 | 13 | this.type = SC_TYPES.get( 'THRUSTER' ) 14 | this.angle = Math.PI 15 | 16 | // @TODO magnitude should be calculated from angle and a value 17 | // so that the thruster can be rotated 18 | this.magnitude = opts.magnitude || [ 0, 150 ] 19 | // this.offset = opts.offset || [ 0, 0 ] 20 | 21 | this.shape = new P2.Circle({ 22 | radius: this.radius, 23 | material: this.material 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/stores/config.js: -------------------------------------------------------------------------------- 1 | 2 | import toMap from 'to-map' 3 | 4 | const conf = Symbol( 'conf' ) 5 | 6 | 7 | /** 8 | * @class 9 | * Wrapper around a native map 10 | * Holds app-level config in a flat map, dont get fancy and use a nested structure, 11 | * its more trouble than its worth here 12 | */ 13 | class Config { 14 | constructor() { 15 | this[ conf ] = toMap({ 16 | width: window.innerWidth, 17 | height: window.innerHeight, 18 | dp: window.devicePixelRatio, 19 | 20 | worldTime: 0 21 | }) 22 | } 23 | 24 | set( key, value ) { 25 | this[ conf ].set( key, value ) 26 | return this 27 | } 28 | 29 | has( key ) { 30 | return this[ conf ].has( key ) 31 | } 32 | 33 | get( key ) { 34 | return this[ conf ].get( key ) 35 | } 36 | } 37 | 38 | export default new Config() 39 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "rules": { 8 | "strict": 0, 9 | "semi": [ 1, "never" ], 10 | "quotes": [ 2, "single" ], 11 | "no-underscore-dangle": 0, 12 | "curly": 1, 13 | "no-multi-spaces": 1, 14 | "no-shadow": 1, 15 | "default-case": 2, 16 | "indent": [ 2, 4 ], 17 | "camelcase": 2, 18 | "comma-spacing": [ 2, { 19 | "before": false, 20 | "after": true 21 | }], 22 | "key-spacing": [ 2, { 23 | "beforeColon": false, 24 | "afterColon": true 25 | }], 26 | "no-nested-ternary": 2 27 | }, 28 | "env": { 29 | "browser": true, 30 | "es6": true, 31 | "node": true 32 | }, 33 | "globals": { 34 | "fetch": true, 35 | "performance": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/main/renderer.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import canvas from './canvas' 4 | import config from 'stores/config' 5 | 6 | 7 | /** 8 | * Creates and manages a list of Pixi.renderers 9 | */ 10 | class RenderManager { 11 | constructor() { 12 | this.renderers = new Map() 13 | } 14 | 15 | create( id = 'js-main', view = null ) { 16 | let renderer = new Pixi.autoDetectRenderer( config.get( 'width' ), config.get( 'height' ), { 17 | antialiasing: false, 18 | transparency: false, 19 | resolution: config.get( 'dp' ), 20 | view: view || canvas.get() 21 | }) 22 | this.renderers.set( id, renderer ) 23 | return renderer 24 | } 25 | 26 | get( id ) { 27 | if ( !this.renderers.has( id ) ) { 28 | return this.create( id ) 29 | } 30 | 31 | return this.renderers.get( id ) 32 | } 33 | } 34 | 35 | export default new RenderManager() 36 | -------------------------------------------------------------------------------- /src/components/entities/modules/thrust.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default Base => class ThrustModule extends Base { 4 | applyMainThruster = () => { 5 | this.linearThrust.forEach( thruster => { 6 | this.body.applyForceLocal( thruster.magnitude, thruster.offset ) 7 | }) 8 | } 9 | 10 | applyTurnLeft = () => { 11 | this.body.angularVelocity = -this.turnThrust 12 | } 13 | 14 | applyTurnRight = () => { 15 | this.body.angularVelocity = this.turnThrust 16 | } 17 | 18 | // Banking is almost like strafing, but results in a slight opposite turn as well 19 | // The slight offset implies the banking thrusters are located behind the 20 | // center of gravity, which accounts for the slight turn imparted 21 | applyBankLeft = () => { 22 | this.body.applyForceLocal( [ this.bankThrust, 0 ], [ 0, -1 ] ) 23 | } 24 | 25 | applyBankRight = () => { 26 | this.body.applyForceLocal( [ -this.bankThrust, 0 ], [ 0, -1 ] ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/stores/resources.js: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path' 3 | 4 | import Preloader from 'preload.io' 5 | import PixiLoader from 'preload.io-pixi' 6 | 7 | 8 | class Resources { 9 | constructor() { 10 | this.preloader = new Preloader() 11 | this.preloader.register( new PixiLoader() ) 12 | } 13 | 14 | loadTextures() { 15 | return new Promise( ( resolve, reject ) => { 16 | this.textures = new Map() 17 | 18 | let toLoad = [ 'circle4.png' ] 19 | toLoad.forEach( url => { 20 | this.preloader.load({ 21 | id: url, 22 | resource: path.join( '/assets', url ) 23 | }) 24 | }) 25 | 26 | this.preloader.on( 'load', resource => this.textures.set( resource.id, resource.texture ) ) 27 | this.preloader.on( 'preload:complete', resources => resolve( resources ) ) 28 | }) 29 | } 30 | 31 | getTexture( id ) { 32 | return this.textures.get( id ) 33 | } 34 | 35 | } 36 | 37 | export default new Resources() 38 | -------------------------------------------------------------------------------- /src/components/main/stats.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Stat from 'stats.js' 4 | 5 | export default class Stats { 6 | constructor( stats ) { 7 | this.parent = document.createElement( 'div' ) 8 | Object.assign( this.parent.style, { 9 | position: 'absolute', 10 | top: '2px', 11 | right: '2px', 12 | 'z-index': 1000 13 | }) 14 | 15 | document.body.appendChild( this.parent ) 16 | 17 | this.stats = new Set() 18 | stats.forEach( statMode => { 19 | let s = new Stat() 20 | s.setMode( statMode ) 21 | Object.assign( s.domElement.style, { 22 | float: 'left' 23 | }) 24 | this.parent.appendChild( s.domElement ) 25 | this.stats.add( s ) 26 | }) 27 | } 28 | 29 | begin() { 30 | this.stats.forEach( stat => { 31 | stat.begin() 32 | }) 33 | } 34 | 35 | end() { 36 | this.stats.forEach( stat => { 37 | stat.end() 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/debug/debug.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | 5 | const Section = props => { 6 | // Same deal, use a sequence and convert into a list ready to render 7 | let fields = props.fields.toSeq().map( ( value, key ) => { 8 | return ( 9 |
  • 10 | { key + ' : ' + value } 11 |
  • 12 | ) 13 | }).toList() 14 | return ( 15 |
  • 16 |

    { props.id }

    17 | 20 |
  • 21 | ) 22 | } 23 | 24 | 25 | /** 26 | * Pure stateless function FTW! 27 | */ 28 | export default props => { 29 | // Convert map into a list and pass through with the section title key 30 | let info = props.data.toSeq().map( ( value, key ) => { 31 | return
    32 | }).toList() 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /src/dispatchers/appDispatcher.js: -------------------------------------------------------------------------------- 1 | 2 | import { Dispatcher } from 'flux' 3 | import { DispatchError } from 'constants/err' 4 | 5 | /** 6 | * Main dispatcher class 7 | * --- 8 | * @class 9 | */ 10 | class AppDispatcher extends Dispatcher { 11 | /** 12 | * @constructs 13 | */ 14 | constructor() { 15 | super( arguments ) 16 | } 17 | 18 | /** 19 | * AppDispatcher should only dispatch app events 20 | * @param payload type field is mandatory and should be an app event type 21 | */ 22 | dispatch( payload ) { 23 | if ( !payload || !payload.type ) { 24 | console.error( 'Trying to call AppDispatcher.dispatch without payload type' ) 25 | throw new DispatchError( 'missing payload type' ) 26 | } 27 | 28 | if ( !/^app/.test( payload.type ) ) { 29 | console.error( 'Invalid payload type for AppDispatcher' ) 30 | throw new DispatchError( 'invalid payload type' ) 31 | } 32 | 33 | super.dispatch( payload ) 34 | } 35 | } 36 | 37 | // Export singleton 38 | export default new AppDispatcher() 39 | -------------------------------------------------------------------------------- /src/dispatchers/engineDispatcher.js: -------------------------------------------------------------------------------- 1 | 2 | import { Dispatcher } from 'flux' 3 | import { DispatchError } from 'constants/err' 4 | 5 | /** 6 | * Handles dispatches related to the world engine 7 | * --- 8 | * @class 9 | */ 10 | class EngineDispatcher extends Dispatcher { 11 | /** 12 | * @constructs 13 | */ 14 | constructor() { 15 | super( arguments ) 16 | } 17 | 18 | /** 19 | * AppDispatcher should only dispatch app events 20 | * @param payload type field is mandatory and should be an app event type 21 | */ 22 | dispatch( payload ) { 23 | if ( !payload || !payload.type ) { 24 | console.error( 'Trying to call EngineDispatcher.dispatch without payload type' ) 25 | throw new DispatchError( 'missing payload type' ) 26 | } 27 | 28 | if ( !/^engine/.test( payload.type ) ) { 29 | console.error( 'Invalid payload type for EngineDispatcher' ) 30 | throw new DispatchError( 'invalid payload type' ) 31 | } 32 | 33 | super( payload ) 34 | } 35 | } 36 | 37 | // Export singleton 38 | export default new EngineDispatcher() 39 | -------------------------------------------------------------------------------- /src/utils/compose.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on [this gist](https://gist.github.com/sebmarkbage/fac0830dbb13ccbff596) 3 | * by [Sebastian Markbåge](https://github.com/sebmarkbage) 4 | */ 5 | 6 | import noop from 'utils/noop' 7 | 8 | /** 9 | * @see modules/README.md for the use of the compose function 10 | * In essence, the first argument can be a function that simply returns a class 11 | * (this allows that class to extend whatever it likes), 12 | * although all other argument must return a function that extends the its 13 | * argument to return a new class that inherits from the argument. 14 | * This function then composes those modules together by chaining one class 15 | * into the last, hence, order is important and the first class can inherit 16 | * nothing or whatever it likes whereas subsequent classes must inherit the 17 | * previous class in the list. 18 | */ 19 | export default function compose( ...modules ) { 20 | // Add methods here to avoid throwing errors 21 | var base = class { 22 | update() {} 23 | render() {} 24 | } 25 | 26 | modules.forEach( mod => { 27 | base = mod( base ) 28 | }) 29 | 30 | return base 31 | } 32 | -------------------------------------------------------------------------------- /src/components/main/canvas.js: -------------------------------------------------------------------------------- 1 | 2 | import config from 'stores/config' 3 | 4 | /** 5 | * Creates and manages raw native canvas elements 6 | */ 7 | class CanvasManager { 8 | constructor() { 9 | this.canvases = new Map() 10 | } 11 | 12 | create( id = 'js-canvas', parent = document.body ) { 13 | if ( this.canvases.has( id ) ) { 14 | throw new Error( 'Canvas ID ' + id + ' already created' ) 15 | } 16 | 17 | let canvas = parent.querySelector( '.' + id ) || document.createElement( 'canvas' ) 18 | canvas.classList.add( id ) 19 | canvas.width = config.get( 'width' ) * config.get( 'dp' ) 20 | canvas.height = config.get( 'height' ) * config.get( 'dp' ) 21 | Object.assign( canvas.style, { 22 | 'width': config.get( 'width' ) + 'px', 23 | 'height': config.get( 'height' ) + 'px', 24 | 'z-index': 0 25 | }) 26 | 27 | parent.appendChild( canvas ) 28 | 29 | this.canvases.set( id, canvas ) 30 | return canvas 31 | } 32 | 33 | get( id ) { 34 | if ( !this.canvases.has( id ) ) { 35 | return this.create() 36 | } 37 | 38 | return this.canvases.get( id ) 39 | } 40 | } 41 | 42 | export default new CanvasManager() 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | * _add_ hardpoint mount types 4 | * _add_ turret component 5 | 6 | # 0.0.5 7 | 8 | * _add_ attack module adds projectile entities to the engine 9 | * _add_ ship components 10 | * _add_ thrust and hull components 11 | * _add_ ship hardpoints for mounting components 12 | * _update_ use hardpoint offsets rather than component offset 13 | 14 | # 0.0.4 15 | 16 | * _update_ refactor entities and entity inheritance chain 17 | * _add_ functional compositional to add behaviours to entities 18 | * _add_ thrust and debug rendering behaviour modules to entities 19 | * _add_ interpolation has saved my lag! 20 | * _update_ small perf updates to world tick 21 | 22 | # 0.0.3 23 | 24 | * _update_ render loop interpolation to keep logic update rate consistent 25 | * _fix_ 0,0 render twitch when creating new entities 26 | * _add_ rendering is now fairly smooth while fps fluctuates thanks to interpolated values 27 | 28 | # 0.0.2 29 | 30 | * _add_ user craft banking maneuver 31 | * _add_ crude bullet entity system 32 | * _add_ entities base class 33 | * _add_ material properties for collisions 34 | 35 | # 0.0.1 36 | 37 | * _add_ Debug panel—starting work on panels 38 | * _add_ Main user class—the beginning of entities, interaction and physics 39 | * _add_ Starfield backdrop 40 | * _add_ App Main state 41 | * _add_ App Load state 42 | * _add_ Centralised immutable state 43 | * _add_ Major dependencies and build 44 | -------------------------------------------------------------------------------- /src/components/entities/entity.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import P2 from 'p2' 4 | import random from 'lodash.random' 5 | 6 | import materials from 'world/materials' 7 | 8 | /** 9 | * Entity is a fairly abstract class, usually it’ll be instantiated with either 10 | * a body (ShellEntity), a renderable (GhostEntity) or both (PhysicalEntity). 11 | */ 12 | export default class Entity { 13 | constructor( opts = {} ) { 14 | // Id is assigned when the entity joins an engine 15 | this.id = null 16 | 17 | // All entities need a position 18 | this.position = opts.position || [ 0, 0 ] 19 | } 20 | 21 | /** 22 | * Render is called as infrequently as possible, usually at instantiation 23 | * but also after entity mutations which alter appearance 24 | */ 25 | render() { 26 | // abstract 27 | } 28 | 29 | /** 30 | * Entities usually need to update themselves. A world update sorts out 31 | * the physics update but those values will need bouncing back to the 32 | * renderable 33 | */ 34 | update() { 35 | 36 | } 37 | 38 | /** 39 | * In order to save state there needs to be a way to stringify each entity, 40 | * this gets tricky when p2.bodies and p2.shapes are pixi stuff are added 41 | * the entity object. A dedicated toString or toJSON will help. 42 | */ 43 | toJSON() { 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/entities/modules/debug.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | 4 | export default Base => class DebugModule extends Base { 5 | constructor( opts ) { 6 | super( opts ) 7 | 8 | this._debug = true 9 | this._debugSprite = new Pixi.Graphics() 10 | 11 | this.container.addChild( this._debugSprite ) 12 | } 13 | 14 | render() { 15 | if ( !this._debug ) { 16 | super() 17 | return 18 | } 19 | 20 | this._debugSprite.clear() 21 | 22 | if ( !this.body.shapes.length ) { 23 | return 24 | } 25 | 26 | this.body.shapes.forEach( shape => { 27 | this._debugSprite.moveTo( ...shape.position ) 28 | this._debugSprite.beginFill( 0xffffff, .1 ) 29 | this._debugSprite.drawCircle( ...shape.position, shape.radius ) 30 | this._debugSprite.endFill() 31 | this._debugSprite.lineStyle( 1, 0xffffff, .3 ) 32 | this._debugSprite.arc( 33 | ...shape.position, 34 | shape.radius, 35 | shape.angle + Math.PI * .5, shape.angle + Math.PI * 2.5, false 36 | ) 37 | this._debugSprite.lineTo( ...shape.position ) 38 | }) 39 | 40 | super() 41 | } 42 | 43 | update() { 44 | super() 45 | 46 | this._debugSprite.position.set( ...this.position ) 47 | this._debugSprite.rotation = this.angle 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # odyssey-nova 3 | 4 | > Odyssey Nova Web-only Client 5 | 6 | Standalone front-end version of Odyssey:Nova 7 | 8 | ## Getting Started 9 | 10 | Version 0.1.0 will mark the start of hosting the game somewhere, and that milestone will be dictated mostly by some form of decent interactive layer being implemented. That means that entities and panels need to be implemented, at least to the extent that a player has the chance to interact with the game world, even if fairly sparsely. Until then, if you want to see whats going on you’ll have to build from source, thankfully, for the web—only version, it’s a fairly trivial affair to get going. 11 | 12 | You’ll just need [node](https://nodejs.org/) and [npm](https://www.npmjs.com/) to get going, both can be downloaded from the [node foundation](https://nodejs.org/). Current builds are running against version 4, but it’ll probably build just fine with just about any fairly recent version. 13 | 14 | ```sh 15 | git clone git@github.com:mattstyles/odyssey-nova.git 16 | npm install 17 | npm run make 18 | npm run serve 19 | ``` 20 | 21 | The `serve` script should start a server listening on port 3000 but serving all the stuff you need will be in `dist` once built if you prefer to serve it a different way. 22 | 23 | ## Contributing 24 | 25 | At this stage all the code is open (and will probably remain so) but I’m not currently accepting pull requests. Bug reports, feature requests or any other feedback are encouraged so please feel free to open an issue. 26 | 27 | ## License 28 | 29 | MIT 30 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | 2 | import toMap from 'to-map' 3 | import noop from 'utils/noop' 4 | 5 | /** 6 | * Wrapper for collecting logs 7 | */ 8 | 9 | /** 10 | * @const 11 | * Map log levels to console logging 12 | */ 13 | const logMethods = toMap({ 14 | 'fatal': { console: 'error', level: 60 }, 15 | 'error': { console: 'error', level: 50 }, 16 | 'warn': { console: 'warn', level: 40 }, 17 | 'info': { console: 'log', level: 30 }, 18 | 'debug': { console: 'info', level: 20 }, 19 | 'trace': { console: 'info', level: 10 } 20 | }) 21 | 22 | 23 | /** 24 | * Wrapper for console 25 | * Tolerates IE9 by providing noops 26 | * @TODO manually set level to emit, or just use bunyan for structure 27 | * @TODO punt to an external log file or indexeddb/localstorage 28 | * @class 29 | */ 30 | class Logger { 31 | /** 32 | * @constructs 33 | */ 34 | constructor( opts ) { 35 | opts = Object.assign({ 36 | level: 'error' 37 | }, opts ) 38 | 39 | // Grab the loglevel from the log method map for the specified level 40 | let logLevel = logMethods.get( opts.level ).level 41 | 42 | // Attach log methods only if they exist 43 | logMethods.forEach( ( value, key ) => { 44 | // Set a noop if specified level is higher than allowed 45 | if ( value.level < logLevel ) { 46 | this[ key ] = noop 47 | return 48 | } 49 | 50 | // Otherwise assign console methods if they exist 51 | this[ key ] = console[ value.console ] 52 | ? Function.prototype.bind.call( console[ value.console ], console ) 53 | : noop 54 | }, this ) 55 | } 56 | } 57 | 58 | export default new Logger({ 59 | level: process.env.DEBUG ? 'debug' : 'error' 60 | }) 61 | -------------------------------------------------------------------------------- /src/components/world/materials.js: -------------------------------------------------------------------------------- 1 | 2 | import P2 from 'p2' 3 | 4 | 5 | 6 | const _mats = Symbol( '_materials' ) 7 | 8 | 9 | /** 10 | * Holds a map of materials 11 | * --- 12 | * Contacts must be registered with the world for the properties to have an effect 13 | * when entities collide 14 | */ 15 | class Materials { 16 | constructor() { 17 | this[ _mats ] = new Map() 18 | 19 | this.add( '_default', new P2.Material() ) 20 | this.addContact( '_defaultContact', new P2.ContactMaterial( 21 | this.get( '_default' ), 22 | this.get( '_default' ), 23 | { 24 | friction: .175, 25 | restitution: .45 26 | } 27 | )) 28 | this.add( 'metal', new P2.Material() ) 29 | this.addContact( 'metalContact', new P2.ContactMaterial( 30 | this.get( 'metal' ), 31 | this.get( 'metal' ), 32 | { 33 | friction: .175, 34 | restitution: .45 35 | } 36 | )) 37 | this.add( 'plasma', new P2.Material() ) 38 | } 39 | 40 | get( id ) { 41 | if ( !this[ _mats ].has( id ) ) { 42 | throw new Error( 'Material ' + id + ' has not been created' ) 43 | } 44 | 45 | return this[ _mats ].get( id ) 46 | } 47 | 48 | addContact( id, contactMaterial ) { 49 | if ( !( contactMaterial instanceof P2.ContactMaterial ) ) { 50 | throw new Error( 'Material ' + id + ' must be a P2.ContactMaterial' ) 51 | } 52 | 53 | this[ _mats ].set( id, contactMaterial ) 54 | } 55 | 56 | add( id, material ) { 57 | if ( !( material instanceof P2.Material ) ) { 58 | throw new Error( 'Material ' + id + ' must be a P2.Material' ) 59 | } 60 | 61 | this[ _mats ].set( id, material ) 62 | } 63 | 64 | } 65 | 66 | export default new Materials() 67 | -------------------------------------------------------------------------------- /src/components/entities/shipComponents/shipComponent.js: -------------------------------------------------------------------------------- 1 | 2 | import materials from 'world/materials' 3 | 4 | /** 5 | * Components hold most of the functional aspects of a ship 6 | * This is an abstract base class for components 7 | * @class 8 | */ 9 | export default class ShipComponent { 10 | constructor( opts ) { 11 | /** 12 | * Make it identifiable 13 | */ 14 | this.id = opts.id || '_defaultID' 15 | 16 | /** 17 | * The component type, found at `constants/shipComponentTypes` 18 | */ 19 | this.type = null 20 | 21 | /** 22 | * Most components have a physical location, this is the overall radius, 23 | * @TODO it should probably be calculated from the shape, which would also 24 | * account for non-circular components 25 | */ 26 | this.radius = opts.radius || 10 27 | 28 | /** 29 | * Everything is made of something right? Unless its made of some nothing 30 | */ 31 | this.material = opts.material || materials.get( 'metal' ) 32 | 33 | /** 34 | * The p2.Shape associated with this component 35 | */ 36 | this.shape = null 37 | 38 | /** 39 | * The parent this component is mounted to, null if unmounted 40 | * @TODO not sure I'm a fan of passing through the parent like this, feels 41 | * very dangerous, but some components need to know ship world position 42 | * and velocity. p2.vec2.toGlobalFrame might do position, but velocity 43 | * is trickier—e.g. turrets need velocity to add the velocity to bullets 44 | * they create 45 | * @TODO a proxy might be a good solution to monitor what the parent is 46 | * doing without any actual control over it 47 | */ 48 | this.parent = null 49 | } 50 | 51 | /** 52 | * @TODO this should be unnecessary when this.radius disappears, just manipulate 53 | * the shape directly 54 | */ 55 | setRadius( r ) { 56 | this.radius = r 57 | if ( this.shape ) { 58 | this.shape.radius = this.radius 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/entities/shipComponents/hardpoints.js: -------------------------------------------------------------------------------- 1 | 2 | import SC_TYPES from 'constants/shipComponentTypes' 3 | 4 | /** 5 | * Hard points hold reference points on ships that can mount ship components 6 | * Hardpoints accept only certain types of components, eventually they should 7 | * probably also only accept certain component levels i.e. a low-level hardpoint 8 | * can only accept low-level components etc etc 9 | * Some hard-points can add extra hardpoints i.e. the hull is critical as defines 10 | * the shape of the ship and the hardpoints it has 11 | */ 12 | export default class Hardpoint { 13 | constructor( opts ) { 14 | this.id = opts.id || '_defaultHardpointID' 15 | /** 16 | * Holds a ref to the component currently mounted here 17 | */ 18 | this.mounted = null 19 | 20 | /** 21 | * Holds a set of the component types viable for mounting 22 | */ 23 | this.viableMounts = new Set() 24 | 25 | /** 26 | * Offset from the center of the ship 27 | */ 28 | this.offset = opts.offset || [ 0, 0 ] 29 | } 30 | 31 | mountComponent( component ) { 32 | if ( !this.viableMounts.has( component.type ) ) { 33 | throw new Error( 'Component ' + component.id + ' can not be mounted to hardpoint ' + this.id + '. Incorrect mount type' ) 34 | } 35 | 36 | this.mounted = component 37 | } 38 | 39 | unmountComponent() { 40 | let component = this.mounted 41 | this.mounted = null 42 | return component 43 | } 44 | 45 | setOffset( x, y ) { 46 | this.offset = [ x, y ] 47 | } 48 | } 49 | 50 | export class HullHardpoint extends Hardpoint { 51 | constructor( opts ) { 52 | super( opts ) 53 | this.viableMounts.add( SC_TYPES.get( 'HULL' ) ) 54 | } 55 | } 56 | 57 | export class ThrusterHardpoint extends Hardpoint { 58 | constructor( opts ) { 59 | super( opts ) 60 | this.viableMounts.add( SC_TYPES.get( 'THRUSTER' ) ) 61 | } 62 | } 63 | 64 | export class TurretHardpoint extends Hardpoint { 65 | constructor( opts ) { 66 | super( opts ) 67 | this.viableMounts.add( SC_TYPES.get( 'TURRET' ) ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/core/styles/utils.less: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Micro-clearfix 4 | * http://nicolasgallagher.com/micro-clearfix-hack/ 5 | */ 6 | .u-cf { 7 | &:before, 8 | &:after { 9 | content: " "; 10 | display: table 11 | } 12 | &:after { 13 | clear: both; 14 | } 15 | } 16 | 17 | /* 18 | * Pullers 19 | */ 20 | .u-pullLeft { 21 | float: left !important; 22 | } 23 | 24 | .u-pullRight { 25 | float: right !important; 26 | } 27 | 28 | // Note this wont always centralise, quick and dirty 29 | .u-pullCenter { 30 | margin: 0 auto; 31 | } 32 | 33 | 34 | /** 35 | * Alignment 36 | */ 37 | .u-textLeft { 38 | text-align: left; 39 | } 40 | .u-textCenter { 41 | text-align: center; 42 | } 43 | .u-textRight { 44 | text-align: right; 45 | } 46 | 47 | 48 | /* 49 | * Sizing 50 | */ 51 | .u-fit { 52 | position: absolute; 53 | left: 0; 54 | top: 0; 55 | right: 0; 56 | bottom: 0; 57 | } 58 | .u-fit-fix { 59 | position: fixed; 60 | left: 0; 61 | top: 0; 62 | right: 0; 63 | bottom: 0; 64 | overflow: hidden; 65 | } 66 | .u-fill { 67 | position: absolute; 68 | left: 0; 69 | top: 0; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | .u-stretchX { 74 | display: block; 75 | width: 100%; 76 | } 77 | .u-stretchY { 78 | display: block; 79 | height: 100%; 80 | } 81 | 82 | 83 | /* 84 | * Visibility 85 | */ 86 | .u-invisible { 87 | opacity: 0 !important; 88 | visibility: hidden !important; 89 | position: absolute; 90 | } 91 | .u-hide { 92 | display: none !important; 93 | } 94 | .u-transparent { 95 | opacity: 0 !important; 96 | pointer-events: none !important; 97 | } 98 | 99 | 100 | /* 101 | * Font rendering 102 | */ 103 | .u-typeOnDark { 104 | -webkit-font-smoothing: subpixel-antialiased; 105 | -moz-osx-font-smoothing: auto; 106 | } 107 | .u-typeOnLight { 108 | -webkit-font-smoothing: antialiased; 109 | -moz-osx-font-smoothing: grayscale; 110 | } 111 | 112 | 113 | /* 114 | * Shaping 115 | */ 116 | .u-circular { 117 | border-radius: 200px; 118 | } 119 | 120 | 121 | /* 122 | * list 123 | */ 124 | .u-nakedList { 125 | list-style-type: none; 126 | margin: 0; 127 | padding: 0; 128 | } 129 | -------------------------------------------------------------------------------- /src/components/bootstrap/bootstrap.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import random from 'lodash.random' 4 | 5 | import logger from 'utils/logger' 6 | import appDispatcher from 'dispatchers/appDispatcher' 7 | import EVENTS from 'constants/events' 8 | import { wait } from 'utils/timing' 9 | 10 | import appState from 'stores/appState' 11 | import resources from 'stores/resources' 12 | 13 | const ID = 'bootstrap' 14 | 15 | /** 16 | * Handles a loading progress indicator, albeit a fake one at present 17 | */ 18 | class Loading extends React.Component { 19 | constructor( props ) { 20 | super( props ) 21 | } 22 | 23 | /** 24 | * Do some fake loading, should probably be done in the Bootstrap component 25 | */ 26 | async componentDidMount() { 27 | let count = 5 28 | let time = 750 / count 29 | while( count-- ) { 30 | await wait( time + random( -time * .75, time * .75 ) ) 31 | 32 | logger.info( 'Bootstrap event' ) 33 | 34 | this.props.progress.update( cursor => { 35 | let progress = this.props.progress.push( true ) 36 | return progress 37 | }) 38 | } 39 | 40 | // Do some actual loading 41 | resources.loadTextures() 42 | .then( this.onComplete ) 43 | } 44 | 45 | onComplete() { 46 | logger.info( 'Bootstrap complete' ) 47 | 48 | // Change app state to the main frame 49 | appDispatcher.dispatch({ 50 | type: EVENTS.get( 'CHANGE_STATE' ), 51 | payload: { 52 | requestedStateID: 'main' 53 | } 54 | }) 55 | } 56 | 57 | render() { 58 | let progress = 'Loading' + this.props.progress.reduce( prev => { 59 | return prev + '.' 60 | }, '' ) 61 | 62 | return { progress } 63 | } 64 | } 65 | 66 | /** 67 | * Master component 68 | * Passes cursors down to children to actually do the work 69 | */ 70 | export default class Bootstrap extends React.Component { 71 | constructor( props ) { 72 | super( props ) 73 | } 74 | 75 | componentWillMount() { 76 | logger.info( 'Bootstrapping...' ) 77 | } 78 | 79 | render() { 80 | // Pass cursors to child components 81 | return ( 82 |
    83 | 84 |
    85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/stores/stateFactory.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | import Bootstrap from 'bootstrap/bootstrap' 5 | import Main from 'main/main' 6 | 7 | 8 | /** 9 | * State factory 10 | * Creates the various high level application states and default data to accompany them 11 | * Creating data within each component is preferable (as this.state does), however, 12 | * to diff between states and use pure render functions everything needs to be passed 13 | * down as props so components can not create their own data as it must be passed. 14 | * @class 15 | */ 16 | export default class StateFactory { 17 | /** 18 | * @constructs 19 | * @param appState immutable centralised state object 20 | * Needs a ref to the appState object to pass high-level keys through to states 21 | */ 22 | constructor( appState ) { 23 | this.appState = appState 24 | } 25 | 26 | /** 27 | * Returns a state if it exists by invoking a creation function 28 | * @param id id's are referenced by string 29 | * @param opts options to pass to creation functions 30 | */ 31 | get( id, opts ) { 32 | if ( this[ id ] ) { 33 | return this[ id ]( opts ) 34 | } 35 | } 36 | 37 | /*-----------------------------------------------------------* 38 | * 39 | * Creation functions 40 | * --- 41 | * Creates default data trees in the immutable state object 42 | * if necessary and returns the state component 43 | * 44 | *-----------------------------------------------------------*/ 45 | 46 | 47 | /** 48 | * The bootstrap state 49 | * Responsible for loading app resources and setting up the app 50 | */ 51 | bootstrap( opts ) { 52 | let key = 'bootstrap' 53 | // Create default bootstrap data if none exists 54 | if ( !this.appState.get( key ) ) { 55 | this.appState.create( key, { 56 | progress: [] 57 | }) 58 | } 59 | 60 | return 61 | } 62 | 63 | /** 64 | * Main 65 | * Here be dragons! 66 | */ 67 | main( opts ) { 68 | let key = 'main' 69 | // Create default data if none exists 70 | if ( !this.appState.get( key ) ) { 71 | this.appState.create( key, { 72 | debug: {} 73 | }) 74 | } 75 | 76 | return
    81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/stores/appState.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import Immreact from 'immreact' 5 | import toMap from 'to-map' 6 | 7 | import logger from 'utils/logger' 8 | import appDispatcher from 'dispatchers/appDispatcher' 9 | import EVENTS from 'constants/events' 10 | 11 | import StateFactory from 'stores/stateFactory' 12 | 13 | 14 | const _state = Symbol( 'state' ) 15 | const _render = Symbol( 'render' ) 16 | const STATE_ID = 'app' 17 | 18 | 19 | 20 | /** 21 | * Holds the centralised immutable state of the application 22 | * Renders are triggered only by mutations to the state object 23 | * @class 24 | */ 25 | class AppState { 26 | /** 27 | * @constructs 28 | * Registers a new immutable state object and sets up the main render function 29 | * Nothing too outrageous will happen until the app is called to run 30 | */ 31 | constructor() { 32 | this.el = null 33 | this.Component = null 34 | 35 | /** 36 | * App state 37 | */ 38 | this[ _state ] = new Immreact.State() 39 | 40 | /** 41 | * App state factory 42 | */ 43 | this.factory = new StateFactory( this[ _state ] ) 44 | 45 | // Set default app state to bootstrap 46 | this[ _state ].create( STATE_ID, { 47 | currentState: 'bootstrap' 48 | }) 49 | 50 | /** 51 | * Main app render function, can only be accessed by state mutation 52 | */ 53 | this[ _render ] = () => { 54 | // Pass entire appState through to main app component 55 | ReactDOM.render( this.factory.get( this[ _state ].get([ STATE_ID, 'currentState' ]) ), this.el ) 56 | } 57 | 58 | /** 59 | * Set up app dispatch listener 60 | */ 61 | appDispatcher.register( dispatch => { 62 | // Convert to get function to execute on dispatch 63 | if ( dispatch.type === EVENTS.get( 'CHANGE_STATE' ) ) { 64 | this[ _state ].cursor([ STATE_ID, 'currentState' ] ).update( cursor => { 65 | return dispatch.payload.requestedStateID 66 | }) 67 | } 68 | }) 69 | } 70 | 71 | /** 72 | * Returns state for stores to allow mutation 73 | * @TODO should this be clamped to only privileged classes? 74 | */ 75 | get() { 76 | return this[ _state ] 77 | } 78 | 79 | 80 | /** 81 | * Sets up the render listener and starts things off 82 | * Akin to App.run in other frameworks 83 | * @param el element to render into 84 | */ 85 | run( el ) { 86 | this.el = el || document.querySelector( '.js-app' ) 87 | 88 | // Register render function 89 | this[ _state ].on( 'update', this[ _render ] ) 90 | 91 | // Initial render 92 | this[ _render ]() 93 | } 94 | } 95 | 96 | export default new AppState() 97 | -------------------------------------------------------------------------------- /src/components/entities/modules/README.md: -------------------------------------------------------------------------------- 1 | Mixin functions define behaviours that can be applied to entities. 2 | 3 | Mixins create an inheritance chain meaning that mixins can add methods to a prototype but also benefit from being able to call functions defined further down the inheritance chain. 4 | 5 | The utils/mixin function is where the magic composition occurs. 6 | 7 | As the composer function works through the list of supplied functions, the first 8 | function can effectively operate as a regular extend (albeit the class needs to be thunkified to return the class method), whilst the extra arguments all provide regular mixin functions that create classes using the argument supplied to them (in this case the other composer functions) as a base 9 | 10 | Take this inheritance tree 11 | 12 | Ship 13 | | 14 | PhysicalEntity 15 | | 16 | Entity 17 | 18 | This can be set up with regular classes and extends 19 | 20 | Trying to set up this is impossible 21 | 22 | Ship 23 | / | \ 24 | ThrustMod PhysicalEntity DebugMod 25 | | 26 | Entity 27 | 28 | However, with functional composition it is possible. 29 | 30 | By using a function that returns a regular PhysicalEntity class (which extends from Entity) the composer is free to make the assumption that PhysicalEntity will form the bottom of the trunk of the inheritance tree (it doesnt matter that PhysicalEntity itself inherits another class) so it is the only instance which does not inherit from other modules in the mixin arguments. 31 | 32 | With PhysicalEntity set as a base it is passed to the composer function of the other modules in the argument list as a base, whereby the resultant class extends from that base and the subsequent class inherits from that class i.e. ThrustMod would inherit from PhysicalEntity, and ThrustMod is passed to the composer function for DebugMod with the end goal that DebugMod inherits from ThrustMod. 33 | 34 | The resultant inheritance tree, given these params, is 35 | 36 | physicalEntityMixin, ThrustMod, DebugMod 37 | 38 | Ship 39 | | 40 | DebugMod 41 | | 42 | ThrustMod 43 | | 44 | PhysicalEntity 45 | | 46 | Entity 47 | 48 | In this way the first argument passed to the mixin function forms the trunk of the inheritance tree whilst the subsequent arguments form the branches with the resultant class at the top, this resultant class (in this case Ship) can also be used as a trunk for another round of mixins. 49 | 50 | Mixin order is important if one mixin relies on another, this gets complicated and resolving this is no small matter. React ES6 class style does not yet support mixins in favour of higher-order components—the system outlined here is the same form of functional composition whereby one class is used as a wrapper for another one. 51 | 52 | The upshoot of all this is simple: 53 | 54 | Inherit a class trunk using a thunk, and apply additional modules to get a final class with all the functions and properties it needs to operate. 55 | -------------------------------------------------------------------------------- /src/components/world/stars.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import Bezier from 'bezier-easing' 4 | import Starfield from 'pixi-starfield' 5 | 6 | import config from 'stores/config' 7 | import resources from 'stores/resources' 8 | 9 | /** 10 | * Holds the various bits that make up the background. 11 | * Dust, clouds and stars all form layers, the dust moving quicker than everything 12 | * else to represent the local environment whereas clouds, nebulae, stars etc all 13 | * form the distant background. 14 | */ 15 | export default class Stars { 16 | constructor() { 17 | 18 | this.container = new Pixi.Container 19 | 20 | this.layer = [] 21 | this.layer.push({ 22 | speed: .1, 23 | field: new Starfield({ 24 | schema: { 25 | tex: [ resources.getTexture( 'circle4.png' ) ], 26 | rotation: false, 27 | alpha: { 28 | min: .1, 29 | max: 1 30 | }, 31 | scale: { 32 | min: .25, 33 | max: .8 34 | }, 35 | tempCurve: new Bezier( .75, .1, .85, 1 ), 36 | threshold: .05 37 | }, 38 | density: .0005 * config.get( 'width' ) * config.get( 'height' ), 39 | size: { 40 | width: config.get( 'width' ), 41 | height: config.get( 'height' ) 42 | } 43 | }) 44 | }) 45 | 46 | this.layer.push({ 47 | speed: .2, 48 | field: new Starfield({ 49 | schema: { 50 | tex: [ resources.getTexture( 'circle4.png' ) ], 51 | rotation: false, 52 | alpha: { 53 | min: .1, 54 | max: 1 55 | }, 56 | scale: { 57 | min: .25, 58 | max: .8 59 | }, 60 | color: { 61 | from: [ 0x02, 0x88, 0xd1 ], 62 | to: [ 0xb3, 0xe5, 0xfc ] 63 | }, 64 | tempCurve: new Bezier( .75, .1, .85, 1 ), 65 | threshold: .1 66 | }, 67 | density: .00025 * config.get( 'width' ) * config.get( 'height' ), 68 | size: { 69 | width: config.get( 'width' ), 70 | height: config.get( 'height' ) 71 | } 72 | }) 73 | }) 74 | 75 | this.layer.forEach( layer => { 76 | this.container.addChild( layer.field.container ) 77 | }) 78 | } 79 | 80 | update() { 81 | this.layer.forEach( layer => layer.field.update() ) 82 | } 83 | 84 | setPosition( x, y ) { 85 | this.layer.forEach( layer => layer.field.setPosition( x * layer.speed, y * layer.speed ) ) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/stores/shipComponents.js: -------------------------------------------------------------------------------- 1 | 2 | import toMap from 'to-map' 3 | 4 | import Hull from 'entities/shipComponents/hull' 5 | import Thruster from 'entities/shipComponents/thruster' 6 | import Turret from 'entities/shipComponents/turret' 7 | import materials from 'world/materials' 8 | 9 | const _comps = Symbol( '_components' ) 10 | 11 | var uuid = 0 12 | function getUuid() { 13 | return ++uuid 14 | } 15 | 16 | /** 17 | * Holds all the data regarding the different components that can be added to 18 | * ship entities. 19 | * All ids for components return functions which can be used to instantiate 20 | * new components. Creating new ones is important as p2.body.addShape requires 21 | * unique shapes to be added, so we can’t reuse components, which isn’t ideal 22 | * but should be fine. 23 | * @class 24 | */ 25 | class ShipComponents { 26 | constructor() { 27 | /** 28 | * @TODO this should be gathered from data stored externally 29 | */ 30 | this[ _comps ] = new toMap({ 31 | /** 32 | * Hulls 33 | */ 34 | 'defaultHull': () => new Hull({ 35 | id: 'defaultHull' + getUuid(), 36 | radius: 15, 37 | material: materials.get( 'metal' ) 38 | }), 39 | 'userHull': () => new Hull({ 40 | id: 'userHull' + getUuid(), 41 | radius: 10, 42 | material: materials.get( 'metal' ) 43 | }), 44 | 'cruiserHull': () => new Hull({ 45 | id: 'cruiserHull' + getUuid(), 46 | radius: 40, 47 | material: materials.get( 'metal' ) 48 | }), 49 | 50 | /** 51 | * Thrusters 52 | */ 53 | 'defaultThruster': () => new Thruster({ 54 | id: 'defaultThruster' + getUuid(), 55 | radius: 5, 56 | magnitude: [ 0, 150 ], 57 | offset: [ 0, -1 ], 58 | material: materials.get( 'metal' ) 59 | }), 60 | 'megaThruster': () => new Thruster({ 61 | id: 'megaThruster' + getUuid(), 62 | radius: 20, 63 | magnitude: [ 0, 350 ], 64 | offset: [ 0, -1 ], 65 | material: materials.get( 'metal' ) 66 | }), 67 | 68 | /** 69 | * Turrets 70 | */ 71 | 'defaultTurret': () => new Turret({ 72 | id: 'defaultTurret' + getUuid(), 73 | radius: 2, 74 | offset: [ 0, -1 ], 75 | material: materials.get( 'metal' ) 76 | }), 77 | 'peaShooter': () => new Turret({ 78 | id: 'peaShooter' + getUuid(), 79 | radius: 2, 80 | offset: [ 0, -1 ], 81 | material: materials.get( 'metal' ) 82 | }) 83 | }) 84 | } 85 | 86 | /** 87 | * Returns a new instance of the component 88 | * @param id grabbing a new instance of component is done via name 89 | */ 90 | get( id ) { 91 | return this[ _comps ].get( id )() 92 | } 93 | } 94 | 95 | export default new ShipComponents() 96 | -------------------------------------------------------------------------------- /src/components/entities/modules/attack.js: -------------------------------------------------------------------------------- 1 | 2 | import Projectile from 'entities/projectile' 3 | 4 | import config from 'stores/config' 5 | import EVENTS from 'constants/events' 6 | import engineDispatcher from 'dispatchers/engineDispatcher' 7 | 8 | /** 9 | * Composed modules should call the super constructor to use the inheritance 10 | * chain and then add properties within a namespace, in this case 'weapon' is 11 | * used as a namespace 12 | * @function returns @class 13 | */ 14 | export default Base => class WeaponModule extends Base { 15 | /** 16 | * 'attack' namespaced properties 17 | * @constructs 18 | */ 19 | constructor( opts ) { 20 | super( opts ) 21 | 22 | /** 23 | * @namespace attack 24 | */ 25 | this.weapon = {} 26 | 27 | // Attack is currently rate limited to the user event registered with Quay, 28 | // which is clamped to the animation frame refresh rate. 29 | this.weapon.fireRate = 1 / 60 30 | 31 | // Turrets should implement their own refresh rate, which determines their 32 | // rate of fire. This module should just attempt to fire as quickly as 33 | // possible and let the turret component sort out fulfilling that request. 34 | this.weapon.lastTime = 0 35 | } 36 | 37 | /** 38 | * Currently fires all turrets associated with the entity 39 | * @TODO should loop through turret, for now just assume one front-mounted 40 | */ 41 | fire() { 42 | if ( config.get( 'worldTime' ) - this.weapon.lastTime < this.weapon.fireRate ) { 43 | return 44 | } 45 | 46 | // For now assume a turret position of ship center and dictate that 47 | // the projectile creation zone is directly in front, to stop instant 48 | // collisions with the firer 49 | // @TODO so much can be done to make this faster, although the main 50 | // bottleneck is adding lots of entities to the game world 51 | // let r = ( this.hardpoints.get( 'hull' ).mounted.radius + 3 ) * 1.5 52 | // let angle = this.angle + Math.PI * .5 53 | // let mag = 50 54 | // 55 | // let turretPos = [ 56 | // r * Math.cos( angle ) + this.position[ 0 ], 57 | // r * Math.sin( angle ) + this.position[ 1 ], 58 | // ] 59 | // 60 | // let velocity = [ 61 | // mag * Math.cos( angle ) + this.body.velocity[ 0 ], 62 | // mag * Math.sin( angle ) + this.body.velocity[ 1 ] 63 | // ] 64 | // 65 | // let projectile = new Projectile({ 66 | // position: turretPos, 67 | // velocity: velocity, 68 | // angle: this.angle 69 | // }) 70 | // 71 | // // Now need some way of adding the entity to the engine world 72 | // engineDispatcher.dispatch({ 73 | // type: EVENTS.get( 'ENTITY_ADD' ), 74 | // payload: { 75 | // entities: [ projectile ] 76 | // } 77 | // }) 78 | 79 | // @TODO loop through hardpoints grabbing turrets 80 | // @TODO associate turrets with a firing group and only fire groups attached 81 | // the current firing group 82 | this.hardpoints.get( 'mainTurret' ).mounted.fire() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/entities/shipComponents/turret.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import P2 from 'p2' 4 | 5 | import Projectile from 'entities/projectile' 6 | import ShipComponent from 'entities/shipComponents/shipComponent' 7 | import SC_TYPES from 'constants/shipComponentTypes' 8 | import materials from 'world/materials' 9 | import engineDispatcher from 'dispatchers/engineDispatcher' 10 | import EVENTS from 'constants/events' 11 | 12 | /** 13 | * A turret component is responsible for firing projectiles 14 | * Its an emitter yay! @TODO add EmitterComponent, thrusters will also need 15 | * to be emitters when they start creating exhaust trails/particles/fire 16 | */ 17 | export default class Turret extends ShipComponent { 18 | constructor( opts ) { 19 | super( opts ) 20 | 21 | this.type = SC_TYPES.get( 'TURRET' ) 22 | this.angle = 0 23 | 24 | // this.offset = opts.offset || [ 0, 0 ] 25 | 26 | this.projectile = null 27 | 28 | this.shape = new P2.Circle({ 29 | radius: this.radius, 30 | material: this.material 31 | }) 32 | } 33 | 34 | /** 35 | * For now `schema` is a set of options to create a generic projectile 36 | * entity, it should ref a projectile id 37 | * @TODO use a projectile factory and ref by id to create the projectile 38 | * Does nothing currently as fire() just creates a new projectile itself 39 | */ 40 | loadProjectile( schema ) { 41 | if ( !schema ) { 42 | throw new Error( 'No schema defined to load projectile with' ) 43 | } 44 | 45 | this.projectileSchema = schema 46 | } 47 | 48 | /** 49 | * Creates a projectile, adds velocity to it and tells the engine to 50 | * register it as an entity 51 | */ 52 | fire() { 53 | // if ( !this.projectileSchema ) { 54 | // throw new Error( 'No schema defined to fire a projectile' ) 55 | // } 56 | 57 | let r = ( this.radius + 3 ) * 1.5 58 | let angle = ( this.parent.angle + this.angle ) + Math.PI * .5 59 | let mag = 50 60 | 61 | // Translate turret local position to parent position + turret position 62 | let position = [] 63 | P2.vec2.toGlobalFrame( position, this.shape.position, this.parent.position, this.parent.angle ) 64 | 65 | // Now that we have the relative turret position given the parent position 66 | // and angle we can extend to the get the firing position 67 | let firingPos = [ 68 | r * Math.cos( angle ) + position[ 0 ], 69 | r * Math.sin( angle ) + position[ 1 ], 70 | ] 71 | 72 | // Add a velocity, relative to the turret current travel momentum (which 73 | // is equal to the parent velocity) 74 | let velocity = [ 75 | mag * Math.cos( angle ) + this.parent.body.velocity[ 0 ], 76 | mag * Math.sin( angle ) + this.parent.body.velocity[ 1 ] 77 | ] 78 | 79 | let projectile = new Projectile({ 80 | position: firingPos, 81 | velocity: velocity, 82 | angle: this.angle 83 | }) 84 | 85 | // Now need some way of adding the entity to the engine world 86 | engineDispatcher.dispatch({ 87 | type: EVENTS.get( 'ENTITY_ADD' ), 88 | payload: { 89 | entities: [ projectile ] 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/components/user/user.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import P2 from 'p2' 4 | import { Vector2, toRadians, toDegrees, wrap } from 'mathutil' 5 | 6 | import Ship from 'entities/ship' 7 | import { HullHardpoint, 8 | ThrusterHardpoint, 9 | TurretHardpoint} from 'entities/shipComponents/hardpoints' 10 | import materials from 'world/materials' 11 | import shipComponents from 'stores/shipComponents' 12 | import SC_TYPES from 'constants/shipComponentTypes' 13 | 14 | // @TODO only for registering debug info 15 | import appState from 'stores/appState' 16 | 17 | function updateDebug( obj ) { 18 | // These are expensive for cycles, not sure its going to work like this 19 | appState.get().cursor([ 'main', 'debug' ]).update( cursor => { 20 | return cursor.merge( obj ) 21 | }) 22 | } 23 | 24 | 25 | /** 26 | * User data should be bounced back to the appState, but, benchmark it once there 27 | * are some tick updates updating the physics. 28 | */ 29 | export default class User extends Ship { 30 | constructor() { 31 | super() 32 | 33 | this.id = 'user' 34 | 35 | // Update damping for the user to make it more controllable 36 | this.body.damping = .1 37 | this.body.angularDamping = .75 38 | 39 | this.lineColor = 0xb3e5fc 40 | 41 | // @TODO replace with sprite 42 | this.sprite = new Pixi.Graphics() 43 | this.container.addChild( this.sprite ) 44 | 45 | // Add hardpoints 46 | this.addHardpoint( new HullHardpoint({ 47 | id: 'hull', 48 | offset: [ 0, 0 ] 49 | })) 50 | this.addHardpoint( new ThrusterHardpoint({ 51 | id: 'linearThrust', 52 | offset: [ 0, -1 ] 53 | })) 54 | this.addHardpoint( new TurretHardpoint({ 55 | id: 'mainTurret', 56 | offset: [ 0, 1 ] 57 | })) 58 | 59 | // Add a hull component 60 | let hull = shipComponents.get( 'userHull' ) 61 | this.mountHardpoint( 'hull', hull ) 62 | 63 | // Add a main engine thruster 64 | let thruster = shipComponents.get( 'defaultThruster' ) 65 | // Update linear thrust hardpoint based on component size 66 | this.hardpoints 67 | .get( 'linearThrust' ) 68 | .setOffset( 0, thruster.radius - hull.radius ) 69 | this.mountHardpoint( 'linearThrust', thruster ) 70 | 71 | // Add a main turret component 72 | let turret = shipComponents.get( 'peaShooter' ) 73 | this.hardpoints 74 | .get( 'mainTurret' ) 75 | .setOffset( 0, hull.radius ) 76 | this.mountHardpoint( 'mainTurret', turret ) 77 | } 78 | 79 | render() { 80 | super() 81 | 82 | let radius = this.hardpoints.get( 'hull' ).mounted.radius 83 | 84 | this.sprite.beginFill( 0x040414 ) 85 | this.sprite.lineStyle( 1, this.lineColor, 1 ) 86 | this.sprite.arc( 87 | 0, 88 | 0, 89 | radius * .5, 90 | toRadians( 220 ), toRadians( 320 ), false 91 | ) 92 | this.sprite.lineTo( 0, radius * .75 ) 93 | this.sprite.endFill() 94 | } 95 | 96 | update() { 97 | super() 98 | 99 | // update this debug info 100 | updateDebug({ 101 | 'user': { 102 | 'px': this.position[ 0 ].toFixed( 2 ), 103 | 'py': this.position[ 1 ].toFixed( 2 ), 104 | 'pa': wrap( toDegrees( this.angle ), 0, 360 ).toFixed( 2 ), 105 | 'vx': this.body.velocity[ 0 ].toFixed( 4 ), 106 | 'vy': this.body.velocity[ 1 ].toFixed( 4 ), 107 | 'va': this.body.angularVelocity.toFixed( 4 ), 108 | 'fx': this.body.force[ 0 ].toFixed( 6 ), 109 | 'fy': this.body.force[ 1 ].toFixed( 6 ) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/entities/projectile.js: -------------------------------------------------------------------------------- 1 | 2 | import P2 from 'p2' 3 | import Pixi from 'pixi.js' 4 | 5 | import compose from 'utils/compose' 6 | import PhysicalEntity from 'entities/physical' 7 | import DebugModule from 'entities/modules/debug' 8 | 9 | 10 | /** 11 | * @class 12 | */ 13 | export default class Projectile extends compose( 14 | PhysicalEntity.compose, 15 | DebugModule ) { 16 | 17 | /** 18 | * @constructs 19 | */ 20 | constructor( opts = {} ) { 21 | super( opts ) 22 | 23 | // Setting body.damping to 0 isnt as effective as a fully kinematic body 24 | // but it is a perf boost and allows mass-based collision response to 25 | // occur which is probably desirable 26 | this.body.damping = 0 27 | 28 | // Turning off angular damping also gives a perf boost (damping and 29 | // angular damping at 0 is a good fps gain) but there is a consideration: 30 | // Currently bullet velocity is worked out by taking ship angle and 31 | // velocity and simply increasing it, meaning that the bullets own angle 32 | // doesnt matter, therefore it can spin (some visual effects could rely 33 | // on this spinning), this should be ok even if a large force is added to 34 | // it when it fires (like a real gun). However, if the bullet adds force 35 | // during its lifetime, like a rocket, then it'll need some angular damping. 36 | // Thats probably cool though as rocket-type projectiles wont fire as 37 | // often (they might also need targeting or tracking AI depending on the 38 | // type of rocket) 39 | this.body.angularDamping = 0 40 | 41 | // Kinematic might work with no collision response, certainly see a massive 42 | // fps boost with kinematic over dynamic bodies. The problem with kinematic 43 | // bodies is the massive mass 44 | // this.body.type = P2.Body.KINEMATIC 45 | 46 | // Turning off collision response actually doesnt save fps, in a quick 47 | // test fps was actually worse, but thats probably only indicative that 48 | // nuking response isnt going to help. When combined with a kinematic 49 | // body type it could work though as bullets/projectiles generally dont 50 | // require damping to be exerted (setting world damping off gives a big 51 | // perf boost, its damping thats expensive) 52 | this.body.collisionResponse = false 53 | 54 | // @TODO set velocity to match that of firer 55 | // Easy enough, extract this into a craft class and use it on 56 | // craft.fire to create the bullet particle, then add velocity and 57 | // finally add a little more retro thrust to kick it out from the craft. 58 | // Can not just add force because acceleration is proportionate to force 59 | // over mass and the mass of the projectile will not match that of the 60 | // craft, resulting is massively higher acceleration. 61 | 62 | 63 | // Force is linked to mass, but this should all be controlled somewhere 64 | // else. To negate mass manually set velocity, but thats a bit shit. 65 | // This should probably be set by the firer based on some properties. 66 | //this.applyForceLocal( [ 0, .1 ] ) 67 | 68 | // An option for bullets is to turn off collision response (body.collisionResponse) 69 | // which would fire events so kill bullet on event, but save 70 | // cycles on collision physics 71 | // Bullets should probably also be kinematic to save cycles, but 72 | // only if kinematic emit collision events - nope, they emit but they also 73 | // provide collisions, they just dont have force or damping and max mass 74 | // Bullets should almost certainly not collide with each other 75 | 76 | // Set up a shape and a debug level, will this get overwritten by mixin? 77 | this.addShape( new P2.Circle({ 78 | radius: .75 79 | })) 80 | 81 | this._debug = true 82 | this.render() 83 | 84 | } 85 | 86 | update() { 87 | super() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/components/world/engine.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import P2 from 'p2' 4 | 5 | //import Bullet from 'entities/bullet' 6 | import materials from 'world/materials' 7 | import config from 'stores/config' 8 | import engineDispatcher from 'dispatchers/engineDispatcher' 9 | import EVENTS from 'constants/events' 10 | 11 | import { XOR } from 'utils/logical' 12 | 13 | // @TODO only for registering debug info 14 | import appState from 'stores/appState' 15 | 16 | function updateDebug( obj ) { 17 | // These are expensive for cycles, not sure its going to work like this 18 | appState.get().cursor([ 'main', 'debug', 'world' ]).update( cursor => { 19 | return cursor.merge( obj ) 20 | }) 21 | } 22 | 23 | const FRAMERATE = 1 / 60 24 | const FRAME_SUBSTEPS = 10 25 | 26 | /** 27 | * Engine handles both the physics world and the main game container for graphics 28 | * @class 29 | */ 30 | export default class Engine { 31 | constructor() { 32 | this.world = new P2.World({ 33 | gravity: [ 0, 0 ] 34 | }) 35 | 36 | // This is maybe a tiny perf gain 37 | this.world.applyGravity = false 38 | this.world.applySpringForces = false 39 | 40 | this.lastTime = null 41 | 42 | this.world.addContactMaterial( materials.get( '_defaultContact' ) ) 43 | this.world.addContactMaterial( materials.get( 'metalContact' ) ) 44 | 45 | this.container = new Pixi.Container() 46 | this.container.position.set( config.get( 'width' ) / 2, config.get( 'height' ) / 2 ) 47 | 48 | // Master list of all entities 49 | this.entities = [] 50 | 51 | // Play with detecting collisions 52 | // this.world.on( 'impact', event => { 53 | // if ( !XOR( event.bodyA instanceof Bullet, event.bodyB instanceof Bullet ) ) { 54 | // // Not a bullet involved, ignore for now 55 | // // Or maybe 2 bullets? I've gone cross-eyed 56 | // return 57 | // } 58 | // 59 | // let bullet = event.bodyA instanceof Bullet ? event.bodyA : event.bodyB 60 | // 61 | // // If perf becomes an issue consider pooling rather than GC and create 62 | // this.world.removeBody( bullet ) 63 | // this.container.removeChild( bullet.container ) 64 | // }) 65 | 66 | engineDispatcher.register( dispatch => { 67 | if ( dispatch.type === EVENTS.get( 'ENTITY_ADD' ) ) { 68 | if ( !dispatch.payload.entities || !dispatch.payload.entities.length ) { 69 | console.warn( 'Trying to add entities without adding them to the dispatch payload' ) 70 | return 71 | } 72 | 73 | //this.entities = this.entities.concat( dispatch.payload.entities ) 74 | 75 | dispatch.payload.entities.forEach( entity => { 76 | // @TODO is this quicker than just concatting? Got to iterate anyway. 77 | //this.entities.push( entity ) 78 | 79 | this.addEntity( entity ) 80 | }) 81 | } 82 | }) 83 | 84 | // Add a world debug prop 85 | appState.get().cursor([ 'main', 'debug' ]).update( cursor => { 86 | return cursor.merge({ 87 | 'world': {} 88 | }) 89 | }) 90 | } 91 | 92 | addEntity( entity ) { 93 | // @TODO draw the entity into the world container here 94 | 95 | 96 | if ( entity.body ) { 97 | this.world.addBody( entity.body ) 98 | } 99 | 100 | if ( entity.container ) { 101 | this.container.addChild( entity.container ) 102 | } 103 | 104 | this.entities.push( entity ) 105 | } 106 | 107 | update( dt ) { 108 | this.entities.forEach( entity => entity.update() ) 109 | 110 | var t = performance.now() / 1000 111 | this.lastTime = this.lastTime || t 112 | 113 | this.world.step( FRAMERATE, t - this.lastTime, FRAME_SUBSTEPS ) 114 | 115 | config.set( 'worldTime', this.world.time ) 116 | 117 | updateDebug({ 118 | 'entities': this.entities.length 119 | }) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odyssey-nova", 3 | "private": true, 4 | "version": "0.0.5", 5 | "description": "Odyssey Nova Web-only Client", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "prepack": "npm run make", 9 | "watch": "parallelshell 'npm run watch:styles' 'npm run watch:scripts' 'npm run watch:html'", 10 | "test": "babel-node ./spec/index.js ./src/**/*.test.js", 11 | "clean": "rm -rf ./dist/ && mkdir ./dist", 12 | "serve": "serve -CJS --no-less ./dist", 13 | "livereload": "livereload ./dist -i 400", 14 | "lint": "eslint ./src/*.js", 15 | "build:styles": "ho compile -d -e src/styles.less -o dist/styles.css -p src:node_modules:src/components:src/core/styles -c .hodevrc", 16 | "make:styles": "ho compile -e src/styles.less -o dist/styles.css -p src:node_modules:src/components:src/core/styles -c package.json", 17 | "watch:styles": "ho watch -d -w 'src/**/*.less' -e src/styles.less -o dist/styles.css -p src:node_modules:src/components:src/core/styles -c .hodevrc", 18 | "build:scripts": "DEBUG=true NODE_PATH=./src/components:./src browserify ./src/app.js --extension .jsx -t babelify -t flowcheck -t envify -t brfs --debug > ./dist/main.js", 19 | "make:scripts": "NODE_PATH=./src/components:./src NODE_ENV=prod browserify ./src/app.js --extension .jsx -t babelify -t brfs | uglifyjs > ./dist/main.js", 20 | "watch:scripts": "DEBUG=true NODE_PATH=./src/components:./src watchify ./src/app.js --extension .jsx -t babelify -t flowcheck -t envify -t brfs --debug -o ./dist/main.js -v", 21 | "make:polyfill": "browserify ./src/polyfill.js | uglifyjs > ./dist/polyfill.js", 22 | "make:assets": "cp -r ./src/assets ./dist/", 23 | "make:html": "mustache package.json ./src/tmpl/index.hjs > ./dist/index.html", 24 | "watch:html": "watch 'npm run make:html' ./src/tmpl", 25 | "prebuild": "npm run clean", 26 | "build": "npm run make:assets && npm run make:html && npm run make:polyfill && npm run build:scripts && npm run build:styles", 27 | "premake": "npm run clean", 28 | "make": "NODE_ENV=prod npm run make:assets && npm run make:html && npm run make:polyfill && npm run make:scripts && npm run make:styles", 29 | "predeploy": "npm run make", 30 | "deploy": "echo \"sort out the deploy, you chump\"" 31 | }, 32 | "ho": { 33 | "autoprefixer-transform": { 34 | "browsers": [ 35 | "last 3 versions" 36 | ] 37 | }, 38 | "cleancss-transform": { 39 | "compatibility": "ie9" 40 | } 41 | }, 42 | "bundler": { 43 | "paths": [ 44 | "src", 45 | "node_modules", 46 | "src/components" 47 | ], 48 | "extensions": [ 49 | ".js", 50 | ".jsx" 51 | ] 52 | }, 53 | "author": "Matt Styles", 54 | "repository": { 55 | "type": "git", 56 | "url": "git://github.com/mattstyles/odyssey-nova.git" 57 | }, 58 | "bugs": { 59 | "url": "https://github.com/mattstyles/odyssey-nova/issues" 60 | }, 61 | "homepage": "https://github.com/mattstyles/odyssey-nova", 62 | "license": "MIT", 63 | "dependencies": { 64 | "@mattstyles/tick": "^0.1.0", 65 | "babel": "^5.8.23", 66 | "bezier-easing": "^1.1.1", 67 | "classnames": "^2.1.5", 68 | "debounce": "^1.0.0", 69 | "errno": "^0.1.4", 70 | "eventemitter3": "^1.1.1", 71 | "fast-simplex-noise": "^1.0.0", 72 | "flux": "^2.1.1", 73 | "heightmap": "^0.2.1", 74 | "ho-grid": "^2.0.0", 75 | "immreact": "^0.1.0", 76 | "immstruct": "^2.0.0", 77 | "immutable": "^3.7.5", 78 | "lodash.random": "^3.0.1", 79 | "mathutil": "^0.2.0", 80 | "normalize.css": "^3.0.3", 81 | "p2": "^0.7.0", 82 | "pixi-starfield": "^0.7.0", 83 | "pixi.js": "^3.0.7", 84 | "preload.io": "^1.2.0", 85 | "preload.io-pixi": "^0.2.0", 86 | "qs": "^5.2.0", 87 | "quay": "^0.2.0", 88 | "react": "^0.14.0", 89 | "react-dom": "^0.14.0", 90 | "stats.js": "mrdoob/stats.js", 91 | "to-map": "^1.0.0", 92 | "underscore.string": "^3.2.2", 93 | "webfontloader": "^1.6.10", 94 | "whatwg-fetch": "^0.10.0" 95 | }, 96 | "devDependencies": { 97 | "autoprefixer-transform": "^0.3.1", 98 | "babel-eslint": "^4.1.3", 99 | "babelify": "^6.3.0", 100 | "brfs": "^1.4.1", 101 | "browserify": "^11.2.0", 102 | "cleancss-transform": "^0.1.1", 103 | "envify": "^3.4.0", 104 | "eslint": "^1.6.0", 105 | "eslint-plugin-react": "^3.5.1", 106 | "flowcheck": "^0.2.7", 107 | "ho": "^1.1.2", 108 | "livereload": "^0.3.7", 109 | "minimist": "^1.2.0", 110 | "mustache": "^2.1.3", 111 | "parallelshell": "^2.0.0", 112 | "serve": "^1.4.0", 113 | "tape": "^4.2.1", 114 | "uglifyjs": "^2.4.10", 115 | "watch": "^0.16.0", 116 | "watchify": "^3.4.0" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/entities/ship.js: -------------------------------------------------------------------------------- 1 | 2 | import P2 from 'p2' 3 | import Pixi from 'pixi.js' 4 | 5 | import compose from 'utils/compose' 6 | import materials from 'world/materials' 7 | import SC_TYPES from 'constants/shipComponentTypes' 8 | import shipComponents from 'stores/shipComponents' 9 | 10 | import PhysicalEntity from 'entities/physical' 11 | import ThrustModule from 'entities/modules/thrust' 12 | import AttackModule from 'entities/modules/attack' 13 | import DebugModule from 'entities/modules/debug' 14 | 15 | /** 16 | * Main ship entity 17 | * @see modules/README.md for the use of the mixin function 18 | * @class 19 | */ 20 | export default class Ship extends compose( 21 | PhysicalEntity.compose, 22 | AttackModule, 23 | ThrustModule, 24 | DebugModule ) { 25 | /** 26 | * @constructs 27 | * @return this 28 | */ 29 | constructor( opts = {} ) { 30 | super( opts ) 31 | 32 | this.id = opts.id || '_defaultShip' 33 | 34 | /** 35 | * Hardpoints refer to external positions that components can be mounted 36 | * on to. 37 | * @TODO hardpoints should have a function associated with them, i.e. 38 | * only thrusters can go on thrust-mounts etc etc 39 | */ 40 | this.hardpoints = new Map() 41 | 42 | // Set application forces 43 | // @TODO should be calculated from components 44 | // Thrust components should determine offset and magnitude of thrusts 45 | // applied per action i.e. main forward thrust should be a composite of 46 | // all the thrusters connected with the 'main:thrust' behaviour 47 | this.turnThrust = .25 48 | this.bankThrust = 50 49 | 50 | // Linear thrust is now recalculated from thrust components 51 | // @TODO Currently all thrust components contribute, this needs further 52 | // refinement 53 | this.linearThrust = [ 54 | { 55 | offset: [ 0, 0 ], 56 | magnitude: [ 0, 150 ] 57 | } 58 | ] 59 | 60 | // Set up damping 61 | // @TODO again, this should be calculated as damping components are added 62 | // or removed from the entity 63 | this.body.damping = .05 64 | this.body.angularDamping = .01 65 | 66 | return this 67 | } 68 | 69 | addHardpoint( hardpoint ) { 70 | if ( this.hardpoints.has( hardpoint.id ) ) { 71 | throw new Error( 'A hardpoint with the id ' + hardpoint.id + ' is already added to ship ' + this.id ) 72 | } 73 | 74 | this.hardpoints.set( hardpoint.id, hardpoint ) 75 | } 76 | 77 | /** 78 | * Adds a component to a ship hardpoint 79 | */ 80 | mountHardpoint( hardpointID, component ) { 81 | if ( !component ) { 82 | throw new Error( 'Adding a component requires a component and hardpoint be specified' ) 83 | } 84 | if ( !hardpointID ) { 85 | throw new Error( 'Component must be mounted to a hardpoint' ) 86 | } 87 | if ( !this.hardpoints.has( hardpointID ) ) { 88 | throw new Error( 'Hardpoint not recognised on this entity' ) 89 | } 90 | 91 | let hardpoint = this.hardpoints.get( hardpointID ) 92 | 93 | hardpoint.mountComponent( component ) 94 | 95 | component.parent = this 96 | 97 | if ( component.shape ) { 98 | this.addShape( component.shape, hardpoint.offset || [ 0, 0 ], component.angle || 0 ) 99 | } 100 | 101 | // @TODO not sure I like this, a component onMount would be better, which 102 | // gets passed the entity. This gives the component a lot of control but 103 | // makes more sense that handling the logic here. 104 | if ( component.type === SC_TYPES.get( 'THRUSTER' ) ) { 105 | this.calcLinearThrust() 106 | } 107 | 108 | return this 109 | } 110 | 111 | /** 112 | * Removes a component from a hardpoint 113 | */ 114 | unmountHardpoint( hardpointID ) { 115 | let component = this.hardpoints 116 | .get( hardpointID ) 117 | .unmountComponent() 118 | 119 | if ( component.shape ) { 120 | this.removeShape( component.shape ) 121 | } 122 | 123 | return this 124 | } 125 | 126 | /** 127 | * Loops through hardpoints, grabs mounted thruster components and appends 128 | * their data to the linearThrust array. 129 | * This is done every time a component is added, which is better than doing 130 | * it every time we need to use the linear thrust to calculate momentum. 131 | * @returns this 132 | */ 133 | calcLinearThrust() { 134 | // Filter component map to thruster types and generate a new array of 135 | // linear thrust components 136 | this.linearThrust = [] 137 | 138 | // @TODO proper filter functions for maps 139 | this.hardpoints.forEach( ( hardpoint, id ) => { 140 | let component = hardpoint.mounted 141 | 142 | // A null value for a hardpoint is valid, so just bail 143 | if ( !component ) { 144 | return 145 | } 146 | 147 | // Filter for thruster types 148 | if ( component.type !== SC_TYPES.get( 'THRUSTER' ) ) { 149 | return 150 | } 151 | 152 | this.linearThrust.push({ 153 | offset: component.offset, 154 | magnitude: component.magnitude 155 | }) 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/components/entities/physical.js: -------------------------------------------------------------------------------- 1 | 2 | import Pixi from 'pixi.js' 3 | import P2 from 'p2' 4 | 5 | import Entity from 'entities/entity' 6 | import compose from 'utils/compose' 7 | 8 | 9 | /** 10 | * A physical entity has both a renderable and a physics body. 11 | * In theory it should inherit from both Ghost and Shell but, JS being what it 12 | * is, its worth taking the risk of a clean implementation. 13 | * @class 14 | */ 15 | export default class PhysicalEntity extends Entity { 16 | 17 | /** 18 | * Can be used to add to the compose function for creating an inheritance 19 | * chain 20 | * @static 21 | * @see utils/compose 22 | */ 23 | static compose = () => { 24 | return PhysicalEntity 25 | } 26 | 27 | /** 28 | * @constructs 29 | * @return this 30 | */ 31 | constructor( options = {} ) { 32 | super( options ) 33 | 34 | // Extend with defaults, then with instantiation parameter override 35 | let opts = Object.assign({ 36 | velocity: [ 0, 0 ], 37 | angle: 0 38 | }, options ) 39 | 40 | 41 | // Create the body, it has super mass until shapes are assigned 42 | // Using 0 mass would change the body type, which is just a pain later on 43 | this.body = new P2.Body({ 44 | mass: Number.MAX_VALUE, 45 | position: opts.position || this.position, 46 | velocity: opts.velocity, 47 | angle: opts.angle, 48 | }) 49 | 50 | if ( opts.position ) { 51 | this.setPosition( ...opts.position ) 52 | } 53 | 54 | //this.body.interpolatedPosition = [ this.position[ 0 ], this.position[ 1 ] ] 55 | 56 | this.position = this.body.interpolatedPosition 57 | this.angle = this.body.interpolatedAngle 58 | 59 | if ( opts.mass ) { 60 | this.setMass( opts.mass ) 61 | this.isMassLocked = true 62 | } 63 | 64 | // For now hard-code these, although might be nice if they were calculated 65 | // somehow based on the materials used to build the entity. 66 | this.body.damping = .05 67 | this.body.angularDamping = .01 68 | 69 | // Renderable—this is stuck as a sprite for now, but can be changed 70 | // after instantiation. It may end up as multiple sprites etc etc 71 | this.sprite = new Pixi.Sprite() 72 | 73 | // Renderables need a main container. This could effectively make other 74 | // renderable properties private, probably should do 75 | this.container = new Pixi.Container() 76 | 77 | // For now add sprite and debug sprite to the container, should probably 78 | // be a bit smarter about this when optimisations start happening 79 | this.container.addChild( this.sprite ) 80 | 81 | return this 82 | } 83 | 84 | /** 85 | * Wrapper around p2.Body.shape which also sets mass and provides an 86 | * extendable method for subclasses 87 | * @param shape 88 | * @param offset position relative to entity center 89 | * @param angle rotational angle 90 | * @return this 91 | */ 92 | addShape( shape, offset, angle ) { 93 | this.body.addShape( shape, offset, angle ) 94 | 95 | this.setMass() 96 | this.body.updateBoundingRadius() 97 | 98 | return this 99 | } 100 | 101 | /** 102 | * Wrapper around p2.Body.shape, also resets mass 103 | * @param shape 104 | * @return this 105 | */ 106 | removeShape( shape ) { 107 | this.body.removeShape( shape ) 108 | 109 | this.setMass() 110 | this.body.updateBoundingRadius() 111 | 112 | return this 113 | } 114 | 115 | /** 116 | * Apply position to all the various position attributes 117 | * @param x 118 | * @param y 119 | * @return this 120 | */ 121 | setPosition( x, y ) { 122 | this.position = this.body.interpolatedPosition = this.body.position = [ x, y ] 123 | return this 124 | } 125 | 126 | /** 127 | * If mass is supplied then it’ll just use it to set the body.mass, otherwise 128 | * it’ll work it out from the shapes and shape materials attributed to 129 | * this entity 130 | * @param mass _optional_ if omitted will calculate mass from components 131 | * @return this 132 | */ 133 | setMass( mass ) { 134 | if ( mass ) { 135 | this.body.mass = mass 136 | this.body.updateMassProperties() 137 | return this 138 | } 139 | 140 | // If trying to auto set mass (no mass param supplied) but is mass 141 | // locked then bail 142 | if ( this.isMassLocked ) { 143 | return this 144 | } 145 | 146 | if ( !this.body.shapes.length ) { 147 | throw new Error( 'Can not set mass on physical entity ' + this.id + ' with no shapes attached' ) 148 | } 149 | 150 | this.body.mass = this.body.shapes.reduce( ( total, shape ) => { 151 | // Just use area for now, should multiply by material.density to 152 | // give final mass of the shape 153 | return total + ( shape.area * .006 ) 154 | }, 0 ) 155 | this.body.updateMassProperties() 156 | 157 | return this 158 | } 159 | 160 | /** 161 | * Actual render of entity 162 | */ 163 | render() { 164 | // nothing at present. abstract. 165 | } 166 | 167 | /** 168 | * Called every tick, although most of the physics calculations are handled 169 | * by P2.World.step 170 | * Should probably accept a delta time 171 | */ 172 | update() { 173 | this.angle = this.body.interpolatedAngle 174 | 175 | this.sprite.position.set( ...this.position ) 176 | this.sprite.rotation = this.angle 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/components/main/main.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Pixi from 'pixi.js' 4 | import Tick from '@mattstyles/tick' 5 | import Bezier from 'bezier-easing' 6 | import Quay from 'quay' 7 | import P2 from 'p2' 8 | import random from 'lodash.random' 9 | import debounce from 'debounce' 10 | 11 | import canvas from './canvas' 12 | import renderer from './renderer' 13 | import Stats from './stats' 14 | import Debug from 'debug/debug' 15 | 16 | import Engine from 'world/engine' 17 | import Stars from 'world/stars' 18 | import Ship from 'entities/ship' 19 | import User from 'user/user' 20 | 21 | import { HullHardpoint, ThrusterHardpoint } from 'entities/shipComponents/hardpoints' 22 | import shipComponents from 'stores/shipComponents' 23 | import materials from 'world/materials' 24 | import resources from 'stores/resources' 25 | import config from 'stores/config' 26 | 27 | 28 | /** 29 | * @class 30 | * Main class holds the main game canvas renderer 31 | */ 32 | export default class Main extends React.Component { 33 | static propTypes = { 34 | canvas: React.PropTypes.string 35 | } 36 | 37 | static defaultProps = { 38 | canvas: 'js-main' 39 | } 40 | 41 | constructor( props ) { 42 | super( props ) 43 | 44 | this.renderer = null 45 | this.renderTick = null 46 | } 47 | 48 | componentWillMount() { 49 | this.stats = new Stats([ 0, 2 ]) 50 | } 51 | 52 | componentDidMount() { 53 | // Set up the canvas & renderer 54 | let id = this.props.canvas 55 | canvas.create( id, this.refs.main ) 56 | renderer.create( id, canvas.get( id ) ) 57 | this.renderer = renderer.get( id ) 58 | 59 | try { 60 | // Set up a user 61 | this.user = new User() 62 | this.user.setPosition( 0, 80 ) 63 | 64 | // Set up input 65 | this.quay = new Quay() 66 | this.addHandlers() 67 | 68 | 69 | // Master stage, renderer renders this 70 | this.stage = new Pixi.Container() 71 | 72 | // Use Engine class 73 | this.engine = new Engine() 74 | this.engine.addEntity( this.user ) 75 | 76 | // Generate background 77 | this.stars = new Stars() 78 | 79 | 80 | // Add actors to the stage 81 | this.stage.addChild( this.stars.container ) 82 | this.stage.addChild( this.engine.container ) 83 | 84 | //Create a complex entity 85 | let entity = new Ship({ 86 | position: [ 0, 0 ], 87 | angle: 0 88 | }) 89 | 90 | // Add hardpoints 91 | entity.addHardpoint( new HullHardpoint({ 92 | id: 'hull', 93 | offset: [ 0, 0 ] 94 | })) 95 | entity.addHardpoint( new ThrusterHardpoint({ 96 | id: 'thruster1', 97 | offset: [ 32, -32 ] 98 | })) 99 | entity.addHardpoint( new ThrusterHardpoint({ 100 | id: 'thruster2', 101 | offset: [ -32, -32 ] 102 | })) 103 | 104 | let hull = shipComponents.get( 'cruiserHull' ) 105 | entity.mountHardpoint( 'hull', hull ) 106 | 107 | let thruster1 = shipComponents.get( 'megaThruster' ) 108 | entity.mountHardpoint( 'thruster1', thruster1 ) 109 | 110 | let thruster2 = shipComponents.get( 'megaThruster' ) 111 | entity.mountHardpoint( 'thruster2', thruster2 ) 112 | 113 | this.engine.addEntity( entity ) 114 | 115 | 116 | // Create a few extra entities, just for funsies 117 | let numEntities = random( 10, 20 ) 118 | let bound = numEntities * 100 119 | for ( let i = 0; i < numEntities; i++ ) { 120 | let entity = new Ship({ 121 | position: [ ~random( -bound, bound ), ~random( -bound, bound ) ] 122 | }) 123 | 124 | let hull = shipComponents.get( 'defaultHull' ) 125 | let thruster = shipComponents.get( 'defaultThruster' ) 126 | 127 | entity.addHardpoint( new HullHardpoint({ 128 | id: 'hull', 129 | offset: [ 0, 0 ] 130 | })) 131 | entity.addHardpoint( new ThrusterHardpoint({ 132 | id: 'linearThrust', 133 | offset: [ 0, thruster.radius - hull.radius ] 134 | })) 135 | 136 | entity.mountHardpoint( 'hull', hull ) 137 | entity.mountHardpoint( 'linearThrust', thruster ) 138 | 139 | this.engine.addEntity( entity ) 140 | } 141 | 142 | // @TODO debug user render 143 | window.stage = this.stage 144 | window.engine = this.engine 145 | window.user = this.user 146 | window.starfield = this.starfield 147 | window.config = config 148 | window.materials = materials 149 | window.entity = entity 150 | 151 | } catch ( err ) { 152 | console.warn( err, err.stack ) 153 | } 154 | 155 | 156 | // Set up the render tick 157 | this.renderTick = new Tick() 158 | // .on( 'data', this.onUpdate ) 159 | .on( 'data', this.onRender ) 160 | .once( 'data', this.onInitialRender ) 161 | 162 | 163 | window.pause = () => { 164 | this.renderTick.pause() 165 | } 166 | window.resume = () => { 167 | this.renderTick.resume() 168 | } 169 | } 170 | 171 | addHandlers() { 172 | if ( !this.quay ) { 173 | logger.warn( 'Quay not instantiated' ) 174 | return 175 | } 176 | this.quay.on( '', this.user.applyMainThruster ) 177 | // this.quay.on( '', this.user.backward ) 178 | this.quay.on( '', this.user.applyTurnLeft ) 179 | this.quay.on( '', this.user.applyTurnRight ) 180 | this.quay.on( 'Q', this.user.applyBankLeft ) 181 | this.quay.on( 'E', this.user.applyBankRight ) 182 | 183 | this.quay.stream( '' ) 184 | .on( 'keydown', () => { 185 | this.user.linearThrust[ 0 ].magnitude[ 1 ] = 220 186 | }) 187 | .on( 'keyup', () => { 188 | this.user.linearThrust[ 0 ].magnitude[ 1 ] = 150 189 | }) 190 | 191 | var lastFire = 0 192 | var reloadTime = 1 / 60 // Fire data stream is clamped to 60 fps so max reload is 1000/60 193 | 194 | this.quay.stream( '' ) 195 | .on( 'data', () => { 196 | this.user.fire() 197 | }) 198 | // this.quay.stream( '' ) 199 | // .on( 'data', () => { 200 | // if ( this.engine.world.time - lastFire < reloadTime ) { 201 | // return 202 | // } 203 | // 204 | // console.log( 'firing' ) 205 | // 206 | // lastFire = this.engine.world.time 207 | // 208 | // // User radius plus bullet radius plus a little extra 209 | // // @TODO User radius probably wont exist for much longer 210 | // let radius = ( this.user.radius + 3 ) * 1.5 211 | // let angle = this.user.angle + Math.PI * .5 212 | // let mag = 50 213 | // let turretPos = [ 214 | // radius * Math.cos( angle ) + this.user.position[ 0 ], 215 | // radius * Math.sin( angle ) + this.user.position[ 1 ] 216 | // ] 217 | // let bulletVel = [ 218 | // mag * Math.cos( angle ) + this.user.body.velocity[ 0 ], 219 | // mag * Math.sin( angle ) + this.user.body.velocity[ 1 ] 220 | // ] 221 | // // @TODO create bullet with a different material then set up the 222 | // // material scalar for calculating PhysicalEntity mass 223 | // let bullet = new Bullet({ 224 | // position: turretPos, 225 | // velocity: bulletVel, 226 | // angle: this.user.angle 227 | // }) 228 | // 229 | // bullet.addShape( new P2.Circle({ 230 | // radius: .75 231 | // })) 232 | // 233 | // bullet._debug = true 234 | // bullet.render() 235 | // 236 | // this.engine.addEntity( bullet ) 237 | // }) 238 | } 239 | 240 | onUpdate = dt => { 241 | // Update the physics world 242 | this.engine.update( dt ) 243 | 244 | // Dampen star movement 245 | // Entities should move fast compared to each other, not compared to the backdrop 246 | // There might also need to be a planet layer that sits somewhere in between speeds 247 | this.stars.setPosition( ...this.user.position ) 248 | 249 | // This translation effectively simulates the camera moving, although simple 250 | // it should still be extracted into a camera class 251 | this.engine.container.position.set( 252 | ( config.get( 'width' ) / 2 ) - this.user.position[ 0 ], 253 | ( config.get( 'height' ) / 2 ) - this.user.position[ 1 ] 254 | ) 255 | 256 | this.stars.update() 257 | } 258 | 259 | /** 260 | * Initial render is a bit of a hack to kickstart the physics simulation to 261 | * position everything etc etc and render everything in its initial state 262 | */ 263 | onInitialRender = () => { 264 | //this.user.render() 265 | this.engine.update( 1 / 60 ) 266 | //this.user.render() 267 | 268 | this.engine.entities.forEach( entity => entity.render() ) 269 | } 270 | 271 | onRender = dt => { 272 | this.stats.begin() 273 | 274 | this.onUpdate( dt ) 275 | this.renderer.render( this.stage ) 276 | 277 | this.stats.end() 278 | } 279 | 280 | render() { 281 | // console.log( 'main render' ) 282 | 283 | // @TODO does the canvas want to be buried this deep in the DOM? 284 | // No problems with creating them from document.body and just reffing them 285 | return ( 286 |
    287 | 288 |
    289 | ) 290 | } 291 | } 292 | --------------------------------------------------------------------------------