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