├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── README.md ├── audio ├── asteroidBreak.mp3 ├── asteroidDestroy.mp3 ├── gameOver.mp3 └── laser.mp3 ├── flow-typed └── npm │ ├── aphrodite_v0.5.x.js │ ├── react-redux_v4.x.x.js │ └── redux_v3.x.x.js ├── images └── background.jpg ├── index.html ├── js ├── .DS_Store ├── actions.js ├── components │ ├── App.jsx │ ├── Canvases.jsx │ ├── Instructions.jsx │ ├── ModeOption.jsx │ └── SoundControl.jsx ├── constants.js ├── draw.js ├── index.js ├── interfaces │ └── lodash.js ├── reducers │ ├── asteroids.js │ ├── bombs.js │ ├── bullets.js │ ├── debris.js │ ├── difficulty.js │ ├── frameCount.js │ ├── isPaused.js │ ├── isSoundOn.js │ ├── mode.js │ ├── movingObjects.js │ ├── powerups.js │ ├── queuedSounds.js │ ├── root.js │ └── ship.js ├── store.js ├── types │ ├── enums.js │ └── types.js └── utils │ ├── asteroidCollisions.js │ ├── canvas.js │ ├── computeNewVel.js │ ├── durationChecks.js │ ├── getEndMessage.js │ ├── math.js │ ├── newPosition.js │ ├── playSounds.js │ ├── randomAsteroids.js │ └── tupleMap.js ├── main.jsx ├── package.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import", 7 | "babel" 8 | ], 9 | "parser": "babel-eslint", 10 | "parserOptions": { 11 | "ecmaVersion": 7 12 | }, 13 | "globals": { 14 | "document": 1, 15 | "CanvasRenderingContext2D": 1, 16 | "confirm": 1, 17 | "Audio": 1 18 | }, 19 | "rules": { 20 | "react/sort-comp": 0, 21 | "no-alert": 0, 22 | "arrow-parens": 0, 23 | "no-use-before-define": ["error", { "functions": false, "classes": false }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/fbjs/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | js/interfaces/ 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bundle.js 3 | bundle.js.map 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Functional Asteroids 2 | ========= 3 | 4 | ## Overview 5 | 6 | A rebuilt version of my [Asteroids](https://github.com/philpee2/Asteroids) project, but this time 7 | with code that is functional instead of object oriented. 8 | 9 | Asteroids is built using the following tools: 10 | * HTML canvas to draw the game 11 | * [Redux](https://github.com/reactjs/redux) to manage the game state 12 | * [React](https://github.com/facebook/react) to build all the UI that isn't in a canvas 13 | * [Babel](https://github.com/babel/babel) to transpile JS and JSX 14 | * [Flow](https://github.com/facebook/flow) to add type checking 15 | * [Webpack](https://github.com/webpack/webpack) for module bundling 16 | * [Aphrodite](https://github.com/Khan/aphrodite) for styling in JS 17 | * [Keymaster](https://github.com/madrobby/keymaster) for key press handling 18 | * [Lodash](https://github.com/lodash/lodash) for utility functions 19 | * [ESLint](https://github.com/eslint/eslint) for linting 20 | 21 | ## New features 22 | 23 | This version has the same features as the previous version, plus some new ones. 24 | 25 | * The player has a limited number of bombs, which destroy every asteroid in the game. 26 | * Instead of increasing the score multiplier with a powerup, it is now increased by hitting 27 | asteroids in quick succession. 28 | * There are new powerups. 29 | * The gun powerup causes the ship to temporarily shoot a three-bullet spread. Previously it would 30 | make the bullets larger and faster. 31 | * The sound can be turned off. 32 | * When the game ends, a new one can begin without refreshing the page. 33 | 34 | ## Running locally 35 | 36 | * Clone the repo 37 | * `npm install` 38 | * `webpack` 39 | * `open index.html` 40 | -------------------------------------------------------------------------------- /audio/asteroidBreak.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/audio/asteroidBreak.mp3 -------------------------------------------------------------------------------- /audio/asteroidDestroy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/audio/asteroidDestroy.mp3 -------------------------------------------------------------------------------- /audio/gameOver.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/audio/gameOver.mp3 -------------------------------------------------------------------------------- /audio/laser.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/audio/laser.mp3 -------------------------------------------------------------------------------- /flow-typed/npm/aphrodite_v0.5.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a64cf90baeb3dca04068c4f24917df45 2 | // flow-typed version: 7fc28637a1/aphrodite_v0.5.x/flow_>=v0.28.x 3 | 4 | declare module 'aphrodite' { 5 | declare type DehydratedServerContent = { 6 | html: string, 7 | css: { 8 | content: string, 9 | renderedClassNames: Array, 10 | }, 11 | }; 12 | 13 | declare type SheetDefinition = { 14 | [key: string]: Object, 15 | }; 16 | 17 | declare type StyleDefinition = { 18 | [key: string]: { 19 | _name: string, 20 | _definition: Object, 21 | } 22 | }; 23 | 24 | declare export var css: (...definitions: StyleDefinition[]) => string; 25 | 26 | declare export var StyleSheetServer :{ 27 | renderStatic(renderFunc: Function): DehydratedServerContent; 28 | }; 29 | 30 | declare export var StyleSheet: { 31 | create(sheetDefinition: SheetDefinition): { 32 | [key: string]: StyleDefinition 33 | } 34 | }; 35 | 36 | declare export var StyleSheetTestUtils: { 37 | suppressStyleInjection: () => void; 38 | clearBufferAndResumeStyleInjection: () => void; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /flow-typed/npm/react-redux_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7a129727adffc45488b512c32740722a 2 | // flow-typed version: 94e9f7e0a4/react-redux_v4.x.x/flow_>=v0.30.x 3 | 4 | /* @flow */ 5 | type ConnectAll = , SP, DP, Dispatch: Function>( 6 | mapStateToProps: (state: Object, ownProps: $Diff<$Diff<$Diff, SP>, D>) => SP, 7 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff<$Diff<$Diff, SP>, D>) => DP, 8 | mergeProps: null | void, 9 | options?: {pure?: boolean, withRef?: boolean} 10 | ) => (component: Class) => Class, SP>, S>>; 11 | 12 | type ConnectAllStateless = ( 13 | mapStateToProps: (state: Object, ownProps: $Diff<$Diff, SP>) => SP, 14 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff<$Diff, SP>) => DP, 15 | mergeProps: null | void, 16 | options?: {pure?: boolean, withRef?: boolean} 17 | ) => (component: (props: P) => any) => Class, SP>, void>>; 18 | 19 | type ConnectMerged = , SP, DP, MP, Dispatch: Function>( 20 | mapStateToProps: (state: Object, ownProps: $Diff<$Diff, D>) => SP, 21 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff<$Diff, D>) => DP, 22 | mergeProps: (stateProps: SP, dispatchProps: DP, ownProps: $Diff<$Diff, D>) => MP, 23 | options?: {pure?: boolean, withRef?: boolean} 24 | ) => (component: Class) => Class, S>>; 25 | 26 | type ConnectMergedStateless = ( 27 | mapStateToProps: (state: Object, ownProps: $Diff) => SP, 28 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff) => DP, 29 | mergeProps: (stateProps: SP, dispatchProps: DP, ownProps: $Diff) => MP, 30 | options?: {pure?: boolean, withRef?: boolean} 31 | ) => (component: (props: P) => any) => Class, void>>; 32 | 33 | type ConnectNoState = , DP, Dispatch: Function>( 34 | mapStateToProps: null | void, 35 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff<$Diff, D>) => DP, 36 | mergeProps: null | void, 37 | options?: {pure?: boolean, withRef?: boolean} 38 | ) => (component: Class) => Class, S>>; 39 | 40 | type ConnectNoStateStatless = ( 41 | mapStateToProps: null | void, 42 | mapDispatchToProps: (dispatch: Dispatch, ownProps: $Diff) => DP, 43 | mergeProps: null | void, 44 | options?: {pure?: boolean, withRef?: boolean} 45 | ) => (component: (props: P) => any) => Class, void>>; 46 | 47 | type ConnectDispatch = , SP, Dispatch: Function>( 48 | mapStateToProps: (state: Object, ownProps: $Diff<$Diff<$Diff, SP>, D>) => SP, 49 | mapDispatchToProps: null | void, 50 | mergeProps: null | void, 51 | options?: {pure?: boolean, withRef?: boolean} 52 | ) => (component: Class) => Class, SP>, S>>; 53 | 54 | type ConnectDispatchStateless = ( 55 | mapStateToProps: (state: Object, ownProps: $Diff<$Diff, SP>) => SP, 56 | mapDispatchToProps: null | void, 57 | mergeProps: null | void, 58 | options?: {pure?: boolean, withRef?: boolean} 59 | ) => (component: (props: P) => any) => Class, SP>, void>>; 60 | 61 | type ConnectDefault = , Dispatch: Function>() => 62 | (component: Class) => Class, S>>; 63 | 64 | type ConnectDefaultStateless = () => 65 |

(component: (props: P) => any) => Class, void>>; 66 | 67 | declare module 'react-redux' { 68 | declare var exports: { 69 | connect: ConnectAll 70 | & ConnectAllStateless 71 | & ConnectMerged 72 | & ConnectMergedStateless 73 | & ConnectNoState 74 | & ConnectNoStateStatless 75 | & ConnectDispatch 76 | & ConnectDispatchStateless 77 | & ConnectDefault 78 | & ConnectDefaultStateless; 79 | Provider: ReactClass<{store: Object, children?: any}>; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bb030fab7757eed18b0307e415853401 2 | // flow-typed version: 94e9f7e0a4/redux_v3.x.x/flow_>=v0.23.x 3 | 4 | declare module 'redux' { 5 | declare type State = any; 6 | declare type Action = Object; 7 | declare type AsyncAction = any; 8 | declare type Reducer = (state: S, action: A) => S; 9 | declare type BaseDispatch = (a: Action) => Action; 10 | declare type Dispatch = (a: Action | AsyncAction) => any; 11 | declare type ActionCreator = (...args: any) => Action | AsyncAction; 12 | declare type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }; 13 | declare type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch; 14 | declare type Store = { 15 | dispatch: Dispatch, 16 | getState: () => State, 17 | subscribe: (listener: () => void) => () => void, 18 | replaceReducer: (reducer: Reducer) => void 19 | }; 20 | declare type StoreCreator = (reducer: Reducer, initialState: ?State) => Store; 21 | declare type StoreEnhancer = (next: StoreCreator) => StoreCreator; 22 | declare type ActionCreatorOrObjectOfACs = ActionCreator | { [key: string]: ActionCreator }; 23 | declare type Reducers = { [key: string]: Reducer }; 24 | declare class Redux { 25 | bindActionCreators(actionCreators: actionCreators, dispatch: Dispatch): actionCreators; 26 | combineReducers(reducers: Reducers): Reducer; 27 | createStore(reducer: Reducer, initialState?: State, enhancer?: StoreEnhancer): Store; 28 | applyMiddleware(...middlewares: Array): StoreEnhancer; 29 | compose(...functions: Array): Function; 30 | } 31 | declare var exports: Redux; 32 | } 33 | -------------------------------------------------------------------------------- /images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/images/background.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Asteroids 5 | 6 | 7 | 8 | 9 |

10 | 11 | 12 | -------------------------------------------------------------------------------- /js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnachum/Functional-Asteroids/5c76a429588faceba4495059a1e86d37e931f22a/js/.DS_Store -------------------------------------------------------------------------------- /js/actions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | Ship, 5 | DifficultyState, 6 | TurnDirection, 7 | ToggleSoundAction, 8 | TriggerBombAction, 9 | AddInitialAsteroidsAction, 10 | SetModeAction, 11 | ResetAction, 12 | ShootAction, 13 | TogglePauseAction, 14 | StopThrustingAction, 15 | RotateShipAction, 16 | ThrustShipAction, 17 | MoveAction, 18 | } from './types/types'; 19 | import type { Mode } from './types/enums'; 20 | 21 | export const MOVE = 'MOVE'; 22 | export const ADD_ASTEROID = 'ADD_ASTEROID'; 23 | export const THRUST_SHIP = 'THRUST_SHIP'; 24 | export const ROTATE_SHIP = 'ROTATE_SHIP'; 25 | export const STOP_THRUSTING_SHIP = 'STOP_THRUSTING_SHIP'; 26 | export const SHOOT = 'SHOOT'; 27 | export const TOGGLE_PAUSE = 'TOGGLE_PAUSE'; 28 | export const RESET = 'RESET'; 29 | export const SET_MODE = 'SET_MODE'; 30 | export const ADD_INITIAL_ASTEROIDS = 'ADD_INITIAL_ASTEROIDS'; 31 | export const TRIGGER_BOMB = 'TRIGGER_BOMB'; 32 | export const TOGGLE_SOUND = 'TOGGLE_SOUND'; 33 | 34 | export function move(payload: { 35 | difficulty: DifficultyState, 36 | frameCount: number, 37 | mode: Mode, 38 | lives: number, 39 | bombs: number, 40 | freezePowerupStartFrame: ?number, 41 | }): MoveAction { 42 | return { 43 | type: MOVE, 44 | payload, 45 | }; 46 | } 47 | 48 | export function thrustShip(): ThrustShipAction { 49 | return { type: 'THRUST_SHIP' }; 50 | } 51 | 52 | export function rotateShip(direction: TurnDirection): RotateShipAction { 53 | return { type: 'ROTATE_SHIP', payload: direction }; 54 | } 55 | 56 | export function stopThrustingShip(): StopThrustingAction { 57 | return { type: 'STOP_THRUSTING_SHIP' }; 58 | } 59 | 60 | export function shoot( 61 | ship: Ship, 62 | mode: Mode, 63 | bulletPowerupStartFrame: ?number, 64 | frameCount: number 65 | ): ShootAction { 66 | return { 67 | type: 'SHOOT', 68 | payload: { 69 | ship, 70 | mode, 71 | bulletPowerupStartFrame, 72 | frameCount, 73 | }, 74 | }; 75 | } 76 | 77 | export function togglePause(): TogglePauseAction { 78 | return { type: 'TOGGLE_PAUSE' }; 79 | } 80 | 81 | export function reset(mode: Mode, isSoundOn: boolean): ResetAction { 82 | return { 83 | type: RESET, 84 | payload: { 85 | mode, 86 | isSoundOn, 87 | }, 88 | }; 89 | } 90 | 91 | export function setMode(newMode: Mode): SetModeAction { 92 | return { 93 | type: 'SET_MODE', 94 | payload: { 95 | mode: newMode, 96 | }, 97 | }; 98 | } 99 | 100 | export function addInitialAsteroids(mode: Mode): AddInitialAsteroidsAction { 101 | return { 102 | type: 'ADD_INITIAL_ASTEROIDS', 103 | payload: { 104 | mode, 105 | }, 106 | }; 107 | } 108 | 109 | export function triggerBomb(): TriggerBombAction { 110 | return { type: 'TRIGGER_BOMB' }; 111 | } 112 | 113 | export function toggleSound(): ToggleSoundAction { 114 | return { type: 'TOGGLE_SOUND' }; 115 | } 116 | -------------------------------------------------------------------------------- /js/components/App.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import { connect } from 'react-redux'; 6 | import { MODES } from '../constants'; 7 | import ModeOption from './ModeOption'; 8 | import Instructions from './Instructions'; 9 | import Canvases from './Canvases'; 10 | import { setMode } from '../actions'; 11 | import type { Mode } from '../types/enums'; 12 | 13 | type State = { 14 | isSelectingMode: boolean, 15 | }; 16 | 17 | type Props = { 18 | modeSelected: (mode: Mode) => void, 19 | }; 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | fontFamily: 'Arial', 24 | }, 25 | }); 26 | 27 | class App extends React.Component { 28 | props: Props; 29 | state: State; 30 | 31 | constructor(props: Props) { 32 | super(props); 33 | 34 | this.state = { 35 | isSelectingMode: true, 36 | }; 37 | } 38 | 39 | selectMode(mode: Mode) { 40 | this.setState({ isSelectingMode: false }); 41 | this.props.modeSelected(mode); 42 | } 43 | 44 | render() { 45 | const { isSelectingMode } = this.state; 46 | 47 | return ( 48 |
49 | {isSelectingMode ? ( 50 |
51 |

Choose a Mode

52 |
53 | {MODES.map(mode => ( 54 | this.selectMode(mode)} 58 | /> 59 | ))} 60 |
61 |
62 | ) : ( 63 | 64 | )} 65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | export default connect(null, dispatch => ({ 72 | modeSelected: (mode: Mode) => dispatch(setMode(mode)), 73 | }))(App); 74 | -------------------------------------------------------------------------------- /js/components/Canvases.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import beginGame from '../index'; 6 | import SoundControl from './SoundControl'; 7 | 8 | const styles = StyleSheet.create({ 9 | container: { 10 | display: 'inline-block', 11 | }, 12 | canvas: { 13 | border: '2px solid #000000', 14 | }, 15 | gameCanvas: { 16 | backgroundImage: "url('./images/background.jpg')", 17 | }, 18 | }); 19 | 20 | export default class Canvases extends React.Component { 21 | gameCanvas: Object; 22 | uiCanvas: Object; 23 | 24 | componentDidMount() { 25 | const contexts: CanvasRenderingContext2D[] = [this.gameCanvas, this.uiCanvas].map(canvas => ( 26 | canvas.getContext('2d') 27 | )); 28 | beginGame(...contexts); 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 | { 40 | this.gameCanvas = gameCanvas; 41 | }} 42 | /> 43 | 44 | { 50 | this.uiCanvas = uiCanvas; 51 | }} 52 | /> 53 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /js/components/Instructions.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import { POWERUP_TYPES, SETTINGS } from '../constants'; 6 | 7 | const styles = StyleSheet.create({ 8 | container: { 9 | display: 'inline-block', 10 | verticalAlign: 'top', 11 | marginLeft: 20, 12 | }, 13 | description: { 14 | fontWeight: 'bold', 15 | }, 16 | }); 17 | 18 | export default function Instructions() { 19 | return ( 20 |
21 |
22 |

Controls:

23 |
    24 |
  • 25 | Rotate ship: Left/Right arrows 26 |
  • 27 | 28 |
  • 29 | Move ship: Up arrow 30 |
  • 31 | 32 |
  • 33 | Shoot: Space bar 34 |
  • 35 | 36 |
  • Pause: P
  • 37 | 38 |
  • Bomb: B
  • 39 | 40 |
  • Toggle sound: M
  • 41 |
42 |
43 | 44 |
45 |

Powerups:

46 |
    47 | {POWERUP_TYPES.map(powerupType => { 48 | const description = SETTINGS.powerups.description[powerupType]; 49 | const color = SETTINGS.powerups.color[powerupType]; 50 | return ( 51 |
  • 52 | {description} 53 |
  • 54 | ); 55 | })} 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /js/components/ModeOption.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import { StyleSheet, css } from 'aphrodite'; 5 | import { SETTINGS } from '../constants'; 6 | import type { Mode } from '../types/enums'; 7 | 8 | const styles = StyleSheet.create({ 9 | modeOption: { 10 | width: 200, 11 | height: 120, 12 | display: 'inline-block', 13 | border: '3px solid lightgreen', 14 | margin: 10, 15 | verticalAlign: 'top', 16 | backgroundColor: 'transparent', 17 | ':hover': { 18 | backgroundColor: 'lightgreen', 19 | }, 20 | }, 21 | modeTitle: { 22 | textAlign: 'center', 23 | fontSize: 20, 24 | marginTop: 10, 25 | marginBottom: 10, 26 | }, 27 | modeText: { 28 | fontSize: 16, 29 | textAlign: 'left', 30 | padding: 5, 31 | }, 32 | }); 33 | 34 | type Props = { 35 | mode: Mode, 36 | onClick: () => void, 37 | }; 38 | 39 | export default function ModeOption({ mode, onClick }: Props) { 40 | return ( 41 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /js/components/SoundControl.jsx: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import VolumeOff from 'react-icons/lib/fa/volume-off'; 5 | import VolumeUp from 'react-icons/lib/fa/volume-up'; 6 | import { connect } from 'react-redux'; 7 | import { toggleSound } from '../actions'; 8 | import type { Store } from '../types/types'; 9 | 10 | type Props = { 11 | toggle: () => void, 12 | isSoundOn: boolean, 13 | }; 14 | 15 | function SoundControl({ toggle, isSoundOn }: Props) { 16 | return ( 17 |
18 | {isSoundOn ? : } 19 |
20 | ); 21 | } 22 | 23 | export default connect( 24 | ({ isSoundOn }: Store) => ({ isSoundOn }: { isSoundOn: boolean }), 25 | dispatch => ({ toggle: () => dispatch(toggleSound()) }) 26 | )(SoundControl); 27 | -------------------------------------------------------------------------------- /js/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Ship } from './types/types'; 3 | import type { Mode, PowerupType, Sound } from './types/enums'; 4 | 5 | export const DIMENSION = 500; 6 | export const FRAMES_PER_SECOND = 30; 7 | 8 | export const MODES: Mode[] = ['CLASSIC', 'DODGEBALL', 'BOSS', 'SUPER_BOSS']; 9 | export const BOSS_MODES: Mode[] = ['BOSS', 'SUPER_BOSS']; 10 | export const POWERUP_TYPES: PowerupType[] = [ 11 | 'LIFE', 12 | 'BULLET', 13 | 'BOMB', 14 | 'FREEZE', 15 | 'INVINCIBLE', 16 | ]; 17 | 18 | export const DEFAULT_MODE: Mode = 'CLASSIC'; 19 | 20 | const bulletColor = 'red'; 21 | const shipColor = 'blue'; 22 | 23 | type ModeToNumber = { [key: Mode]: number }; 24 | type ModeToString = { [key: Mode]: string }; 25 | type PowerupTypeToString = { [key: PowerupType]: string }; 26 | 27 | type SettingsType = { 28 | asteroids: { 29 | startingMinimumArea: ModeToNumber, 30 | startingSpawnRadius: ModeToNumber, 31 | startingNumber: ModeToNumber, 32 | minimumRadius: number, 33 | color: string, 34 | startingSpeed: number, 35 | }, 36 | ship: { 37 | radius: number, 38 | color: string, 39 | maxSpeed: number, 40 | turnSpeed: number, 41 | acceleration: number, 42 | thrusterRadius: number, 43 | thrusterColor: string, 44 | turretRadius: number, 45 | airResistance: number, 46 | invincibilityTime: number, 47 | defaultShip: Ship, 48 | }, 49 | bullets: { 50 | radius: number, 51 | color: string, 52 | speed: number, 53 | distance: number, 54 | }, 55 | debris: { 56 | distance: number, 57 | number: number, 58 | speed: number, 59 | }, 60 | difficulty: { 61 | timeInterval: ModeToNumber, 62 | asteroidSpeedIncrease: number, 63 | asteroidSpawnRadiusMultiplier: number, 64 | minimumAsteroidAreaMultiplier: number, 65 | }, 66 | powerups: { 67 | radius: number, 68 | duration: { [key: PowerupType]: number }, 69 | description: PowerupTypeToString, 70 | color: PowerupTypeToString, 71 | bullet: { 72 | spreadDegrees: number, 73 | }, 74 | }, 75 | startingLives: ModeToNumber, 76 | startingBombs: ModeToNumber, 77 | pointsForBreak: number, 78 | pointsForDestroy: number, 79 | audioFile: { [key: Sound]: string }, 80 | modes: { 81 | name: ModeToString, 82 | description: ModeToString, 83 | powerups: { [key: Mode]: PowerupType[] }, 84 | }, 85 | }; 86 | 87 | export const SETTINGS: SettingsType = { 88 | asteroids: { 89 | startingMinimumArea: { 90 | CLASSIC: 5000, 91 | DODGEBALL: 5000, 92 | BOSS: 0, 93 | SUPER_BOSS: 0, 94 | }, 95 | startingSpawnRadius: { 96 | CLASSIC: 30, 97 | DODGEBALL: 30, 98 | BOSS: 100, 99 | SUPER_BOSS: 173, 100 | }, 101 | startingNumber: { 102 | CLASSIC: 2, 103 | DODGEBALL: 2, 104 | BOSS: 1, 105 | SUPER_BOSS: 1, 106 | }, 107 | minimumRadius: 10, 108 | color: 'sienna', 109 | startingSpeed: 0.5, 110 | }, 111 | 112 | ship: { 113 | radius: 10, 114 | color: shipColor, 115 | maxSpeed: 5, 116 | turnSpeed: 10, 117 | acceleration: 0.3, 118 | thrusterRadius: 5, 119 | thrusterColor: 'orange', 120 | turretRadius: 3, 121 | airResistance: 0.07, 122 | invincibilityTime: 3, // seconds 123 | defaultShip: { 124 | pos: [250, 250], 125 | vel: [0, 0], 126 | degrees: 90, 127 | isThrusting: false, 128 | invincibilityStartFrame: 0, 129 | }, 130 | }, 131 | 132 | bullets: { 133 | radius: 2, 134 | color: bulletColor, 135 | speed: 20, 136 | distance: 400, 137 | }, 138 | 139 | debris: { 140 | distance: 400, 141 | number: 10, 142 | speed: 20, 143 | }, 144 | 145 | difficulty: { 146 | timeInterval: { 147 | // seconds 148 | CLASSIC: 10, 149 | DODGEBALL: 5, 150 | BOSS: 10, 151 | SUPER_BOSS: 10, 152 | }, 153 | asteroidSpeedIncrease: 0.15, 154 | asteroidSpawnRadiusMultiplier: 1.0, 155 | minimumAsteroidAreaMultiplier: 1.25, 156 | }, 157 | 158 | powerups: { 159 | radius: 10, 160 | duration: { 161 | // seconds 162 | BULLET: 5, 163 | FREEZE: 3, 164 | }, 165 | description: { 166 | LIFE: 'Extra life', 167 | BULLET: 'Gun upgrade', 168 | BOMB: 'Extra bomb', 169 | FREEZE: 'Freeze asteroids', 170 | INVINCIBLE: 'Invincibility', 171 | }, 172 | color: { 173 | BULLET: bulletColor, 174 | LIFE: shipColor, 175 | BOMB: 'orange', 176 | FREEZE: 'lightblue', 177 | INVINCIBLE: 'purple', 178 | }, 179 | bullet: { 180 | spreadDegrees: 15, 181 | }, 182 | }, 183 | 184 | startingLives: { 185 | CLASSIC: 2, 186 | DODGEBALL: 0, 187 | BOSS: 2, 188 | SUPER_BOSS: 6, 189 | }, 190 | startingBombs: { 191 | CLASSIC: 2, 192 | DODGEBALL: 0, 193 | BOSS: 0, 194 | SUPER_BOSS: 0, 195 | }, 196 | pointsForBreak: 2, 197 | pointsForDestroy: 10, 198 | 199 | audioFile: { 200 | ASTEROID_BREAK: 'asteroidBreak', 201 | ASTEROID_DESTROY: 'asteroidDestroy', 202 | GAME_OVER: 'gameOver', 203 | LASER: 'laser', 204 | }, 205 | 206 | modes: { 207 | name: { 208 | CLASSIC: 'Classic', 209 | DODGEBALL: 'Dodgeball', 210 | BOSS: 'Bossteroid', 211 | SUPER_BOSS: 'Super Bossteroid', 212 | }, 213 | description: { 214 | CLASSIC: 'Score as many points as you can before running out of lives', 215 | DODGEBALL: 'If you can dodge an asteroid, you can dodge a ball', 216 | BOSS: 'Kill the Bossteroid as quickly as possible. Like a boss', 217 | SUPER_BOSS: 'Like the Bossteroid, but three times bigger', 218 | }, 219 | powerups: { 220 | CLASSIC: [ 221 | 'LIFE', 222 | 'BULLET', 223 | 'BOMB', 224 | 'FREEZE', 225 | 'INVINCIBLE', 226 | ], 227 | DODGEBALL: [], 228 | BOSS: [ 229 | 'LIFE', 230 | 'BULLET', 231 | 'FREEZE', 232 | 'INVINCIBLE', 233 | ], 234 | SUPER_BOSS: [ 235 | 'LIFE', 236 | 'BULLET', 237 | 'FREEZE', 238 | 'INVINCIBLE', 239 | ], 240 | }, 241 | }, 242 | }; 243 | -------------------------------------------------------------------------------- /js/draw.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { compact, flatten, times } from 'lodash'; 4 | import { 5 | SETTINGS, 6 | FRAMES_PER_SECOND, 7 | } from './constants'; 8 | import { getRotateablePosition } from './utils/math'; 9 | import { 10 | drawCircleInGame, 11 | drawCircleInUI, 12 | drawTextInGame, 13 | drawTextInUI, 14 | drawRectangleInUI, 15 | clear, 16 | } from './utils/canvas'; 17 | import type { 18 | Asteroid, 19 | Ship, 20 | Bullet, 21 | Debris, 22 | DrawableCircle, 23 | Powerup, 24 | Store, 25 | } from './types/types'; 26 | import { isShipInvincible } from './utils/durationChecks'; 27 | import type { Mode } from './types/enums'; 28 | 29 | let shipDrawFrame = 0; 30 | 31 | // Convert an asteroid's state into the data needed to draw it. 32 | function asteroidDrawInfo({ pos, radius }: Asteroid): DrawableCircle { 33 | const { 34 | asteroids: { 35 | color, 36 | }, 37 | } = SETTINGS; 38 | 39 | return { 40 | pos, 41 | radius, 42 | color, 43 | }; 44 | } 45 | 46 | // Convert a ship's state into the data needed to draw its body. 47 | function shipBodyDrawInfo({ pos }: Ship): DrawableCircle { 48 | const { 49 | ship: { 50 | color, 51 | radius, 52 | }, 53 | } = SETTINGS; 54 | return { 55 | color, 56 | pos, 57 | radius, 58 | }; 59 | } 60 | 61 | // Convert a ship's state into the data needed to draw its turret. 62 | function turretDrawInfo({ pos, degrees }: Ship): DrawableCircle { 63 | const { 64 | ship: { 65 | radius: shipRadius, 66 | color, 67 | turretRadius, 68 | }, 69 | } = SETTINGS; 70 | return { 71 | pos: getRotateablePosition(shipRadius, pos, degrees), 72 | radius: turretRadius, 73 | color, 74 | }; 75 | } 76 | 77 | // Convert a ship's state into the data needed to draw its thruster 78 | function thrusterDrawInfo({ isThrusting, pos, degrees }: Ship): ?DrawableCircle { 79 | const { 80 | ship: { 81 | radius: shipRadius, 82 | thrusterColor, 83 | thrusterRadius, 84 | }, 85 | } = SETTINGS; 86 | if (isThrusting) { 87 | return { 88 | color: thrusterColor, 89 | // The thruster is behind the ship, so add 180 to its degrees 90 | pos: getRotateablePosition(shipRadius, pos, degrees + 180), 91 | radius: thrusterRadius, 92 | }; 93 | } 94 | return null; 95 | } 96 | 97 | function shipDrawInfo(ship: Ship, frameCount: ?number = null): DrawableCircle[] { 98 | const isInvincible: boolean = frameCount != null && isShipInvincible(ship, frameCount); 99 | const drawInfos = compact([ 100 | shipBodyDrawInfo(ship), 101 | turretDrawInfo(ship), 102 | thrusterDrawInfo(ship), 103 | ]); 104 | 105 | // Make the ship flash while it's invincible; 106 | if (isInvincible) { 107 | shipDrawFrame = (shipDrawFrame + 1) % 20; 108 | if (shipDrawFrame < 10) { 109 | return drawInfos; 110 | } 111 | return []; 112 | } 113 | return drawInfos; 114 | } 115 | 116 | function bulletDrawInfo({ pos, radius }: Bullet): DrawableCircle { 117 | const { 118 | bullets: { 119 | color, 120 | }, 121 | } = SETTINGS; 122 | return { 123 | color, 124 | radius, 125 | pos, 126 | }; 127 | } 128 | 129 | function debrisDrawInfo({ pos }: Debris): DrawableCircle { 130 | const { 131 | asteroids: { 132 | color, 133 | minimumRadius, 134 | }, 135 | debris: { 136 | number, 137 | }, 138 | } = SETTINGS; 139 | const radius = minimumRadius / number; 140 | return { 141 | color, 142 | radius, 143 | pos, 144 | }; 145 | } 146 | 147 | function powerupDrawInfo({ pos, type }: Powerup): DrawableCircle { 148 | const { 149 | powerups: { 150 | radius, 151 | }, 152 | } = SETTINGS; 153 | const color = SETTINGS.powerups.color[type]; 154 | return { 155 | color, 156 | radius, 157 | pos, 158 | }; 159 | } 160 | 161 | function drawPause() { 162 | drawTextInGame({ 163 | text: 'Paused', 164 | size: 20, 165 | pos: [205, 270], 166 | color: 'white', 167 | }); 168 | } 169 | 170 | function drawUIText({ text, pos }: { text: string, pos: [number, number] }) { 171 | drawTextInUI({ 172 | color: 'black', 173 | size: 20, 174 | text, 175 | pos, 176 | }); 177 | } 178 | 179 | function drawScore(score: number) { 180 | drawUIText({ 181 | text: `Score: ${score.toLocaleString()}`, 182 | pos: [5, 30], 183 | }); 184 | } 185 | 186 | function drawLives(lives: number) { 187 | drawRepeated(lives, i => ( 188 | shipDrawInfo({ 189 | ...SETTINGS.ship.defaultShip, 190 | pos: [20 + (25 * i), 100], 191 | }) 192 | )); 193 | } 194 | 195 | function drawBombs(bombs: number) { 196 | drawRepeated(bombs, i => ( 197 | powerupDrawInfo({ 198 | type: 'BOMB', 199 | pos: [20 + (25 * i), 130], 200 | }) 201 | )); 202 | } 203 | 204 | function drawRepeated(num: number, drawOne: (i: number) => DrawableCircle | DrawableCircle[]) { 205 | flatten(times(num, drawOne)).forEach(drawCircleInUI); 206 | } 207 | 208 | function drawTime(frameCount: number) { 209 | const seconds: number = Math.floor(frameCount / FRAMES_PER_SECOND); 210 | drawUIText({ 211 | text: `Time: ${seconds}`, 212 | pos: [5, 200], 213 | }); 214 | } 215 | 216 | function drawMultiplier(multiplier: number, multiplierBar: number) { 217 | const outerWidth = 180; 218 | const outerHeight = 40; 219 | const borderThickness = 2; 220 | const innerWidth = outerWidth - (2 * borderThickness); 221 | const innerHeight = outerHeight - (2 * borderThickness); 222 | const outerPos = [5, 40]; 223 | const innerPos = outerPos.map(d => d + borderThickness); 224 | // Draw an outer black rectangle to create the border 225 | drawRectangleInUI({ 226 | pos: outerPos, 227 | color: 'black', 228 | width: outerWidth, 229 | height: outerHeight, 230 | }); 231 | // Draw an inner white rectangle to create make it a border 232 | drawRectangleInUI({ 233 | pos: innerPos, 234 | color: 'white', 235 | width: innerWidth, 236 | height: innerHeight, 237 | }); 238 | // Fill it up with the green bar 239 | drawRectangleInUI({ 240 | pos: innerPos, 241 | color: 'green', 242 | width: (multiplierBar / 100) * innerWidth, 243 | height: innerHeight, 244 | }); 245 | drawUIText({ 246 | text: `x${multiplier}`, 247 | pos: [outerPos[0] + outerWidth + 15, outerPos[1] + 30], 248 | }); 249 | } 250 | 251 | function drawMode(mode: Mode) { 252 | const text = SETTINGS.modes.name[mode]; 253 | drawUIText({ 254 | text: `Mode: ${text}`, 255 | pos: [5, 170], 256 | }); 257 | } 258 | 259 | export default function draw({ 260 | movingObjects, 261 | isPaused, 262 | frameCount, 263 | mode, 264 | }: Store) { 265 | const { 266 | asteroids, 267 | ship, 268 | bullets, 269 | debris, 270 | score, 271 | lives, 272 | multiplier, 273 | multiplierBar, 274 | powerups, 275 | bombs, 276 | } = movingObjects; 277 | clear(); 278 | const drawableInfos: DrawableCircle[] = [ 279 | ...asteroids.map(asteroidDrawInfo), 280 | ...bullets.map(bulletDrawInfo), 281 | ...debris.map(debrisDrawInfo), 282 | ...powerups.map(powerupDrawInfo), 283 | ...shipDrawInfo(ship, frameCount), 284 | ]; 285 | drawableInfos.forEach(drawCircleInGame); 286 | 287 | if (isPaused) { 288 | drawPause(); 289 | } 290 | drawScore(score); 291 | drawLives(lives); 292 | drawBombs(bombs); 293 | drawTime(frameCount); 294 | drawMultiplier(multiplier, multiplierBar); 295 | drawMode(mode); 296 | } 297 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import key from 'keymaster'; 4 | import { 5 | move, 6 | thrustShip, 7 | rotateShip, 8 | stopThrustingShip, 9 | shoot, 10 | togglePause, 11 | reset, 12 | addInitialAsteroids, 13 | triggerBomb, 14 | toggleSound, 15 | } from './actions'; 16 | import { FRAMES_PER_SECOND, BOSS_MODES } from './constants'; 17 | import { initContext } from './utils/canvas'; 18 | import playSounds from './utils/playSounds'; 19 | import store from './store'; 20 | import draw from './draw'; 21 | import getEndMessage from './utils/getEndMessage'; 22 | import type { Store } from './types/types'; 23 | 24 | let intervalId; 25 | 26 | function keyPressListener() { 27 | if (key.isPressed('up')) { 28 | store.dispatch(thrustShip()); 29 | } else { 30 | store.dispatch(stopThrustingShip()); 31 | } 32 | 33 | if (key.isPressed('left')) { 34 | store.dispatch(rotateShip(1)); 35 | } 36 | 37 | if (key.isPressed('right')) { 38 | store.dispatch(rotateShip(-1)); 39 | } 40 | } 41 | 42 | function stop() { 43 | clearInterval(intervalId); 44 | } 45 | 46 | function gameOver(hasWon: boolean) { 47 | const { 48 | movingObjects: { 49 | score, 50 | }, 51 | frameCount, 52 | mode, 53 | isSoundOn, 54 | }: Store = store.getState(); 55 | stop(); 56 | if (!hasWon && isSoundOn) { 57 | playSounds(['GAME_OVER']); 58 | } 59 | const endMessage = getEndMessage({ 60 | score, 61 | frameCount, 62 | mode, 63 | hasWon, 64 | }); 65 | if (confirm(endMessage)) { 66 | store.dispatch(reset(mode, isSoundOn)); 67 | start(); 68 | } 69 | } 70 | 71 | function step() { 72 | const { 73 | movingObjects: { 74 | lives, 75 | freezePowerupStartFrame, 76 | bombs, 77 | asteroids, 78 | }, 79 | frameCount, 80 | difficulty, 81 | mode, 82 | }: Store = store.getState(); 83 | store.dispatch(move({ 84 | lives, 85 | freezePowerupStartFrame, 86 | bombs, 87 | difficulty, 88 | frameCount, 89 | mode, 90 | })); 91 | keyPressListener(); 92 | if (lives < 0) { 93 | gameOver(false); 94 | } 95 | const isBossMode = BOSS_MODES.includes(mode); 96 | if (frameCount !== 0 && asteroids.length === 0 && isBossMode) { 97 | gameOver(true); 98 | } 99 | } 100 | 101 | function start() { 102 | const interval = Math.floor(1000 / FRAMES_PER_SECOND); 103 | intervalId = setInterval(step, interval); 104 | } 105 | 106 | const guardForPaused = (f, getState) => (...args) => !getState().isPaused && f(...args); 107 | 108 | function bindKeyHandlers() { 109 | key('space', guardForPaused(() => { 110 | const { 111 | movingObjects: { 112 | ship, 113 | bulletPowerupStartFrame, 114 | }, 115 | mode, 116 | frameCount, 117 | }: Store = store.getState(); 118 | store.dispatch(shoot(ship, mode, bulletPowerupStartFrame, frameCount)); 119 | }, store.getState)); 120 | 121 | key('p', () => { 122 | store.dispatch(togglePause()); 123 | return store.getState().isPaused ? stop() : start(); 124 | }); 125 | 126 | key('b', guardForPaused(() => { 127 | store.dispatch(triggerBomb()); 128 | }, store.getState)); 129 | 130 | key('m', () => { 131 | store.dispatch(toggleSound()); 132 | }); 133 | } 134 | 135 | store.subscribe(() => { 136 | const state: Store = store.getState(); 137 | draw(state); 138 | if (state.isSoundOn) { 139 | playSounds(state.movingObjects.queuedSounds); 140 | } 141 | }); 142 | 143 | export default function beginGame( 144 | gameContext: CanvasRenderingContext2D, 145 | uiContext: CanvasRenderingContext2D 146 | ) { 147 | initContext(gameContext, uiContext); 148 | bindKeyHandlers(); 149 | store.dispatch(addInitialAsteroids(store.getState().mode)); 150 | start(); 151 | } 152 | -------------------------------------------------------------------------------- /js/interfaces/lodash.js: -------------------------------------------------------------------------------- 1 | declare module "lodash" { 2 | declare function flatten(a: S[][] | S[]): S[]; 3 | 4 | declare function times(num: number, func: (index: number) => S): S[]; 5 | 6 | declare function compact(a: Array): S[]; 7 | 8 | declare function pick(obj: Object): Object; 9 | 10 | declare function random(a: number, b: number): number; 11 | declare function random(a: number): number; 12 | 13 | declare function sample(a: S[]): S; 14 | 15 | declare function sumBy(a: S[], func: (x: S) => number): number; 16 | } 17 | -------------------------------------------------------------------------------- /js/reducers/asteroids.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import newPosition from '../utils/newPosition'; 4 | import { MOVE, ADD_INITIAL_ASTEROIDS, RESET } from '../actions'; 5 | import type { Asteroid, Action } from '../types/types'; 6 | import { SETTINGS } from '../constants'; 7 | import randomAsteroids from '../utils/randomAsteroids'; 8 | import { areAsteroidsFrozen } from '../utils/durationChecks'; 9 | 10 | const defaultState: Asteroid[] = []; 11 | 12 | export default function asteroids(state: Asteroid[] = defaultState, action: Action): Asteroid[] { 13 | switch (action.type) { 14 | case MOVE: { 15 | const { frameCount, freezePowerupStartFrame } = action.payload; 16 | const isFrozen = areAsteroidsFrozen(freezePowerupStartFrame, frameCount); 17 | return state.map(asteroid => ({ 18 | ...asteroid, 19 | pos: isFrozen ? asteroid.pos : newPosition(asteroid), 20 | })); 21 | } 22 | case RESET: 23 | case ADD_INITIAL_ASTEROIDS: { 24 | const { mode } = action.payload; 25 | const num = SETTINGS.asteroids.startingNumber[mode]; 26 | const radius = SETTINGS.asteroids.startingSpawnRadius[mode]; 27 | return state.concat(randomAsteroids(num, { radius })); 28 | } 29 | default: 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /js/reducers/bombs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { TRIGGER_BOMB, SET_MODE, RESET } from '../actions'; 4 | import { SETTINGS } from '../constants'; 5 | import type { Action } from '../types/types'; 6 | 7 | const defaultState: number = 0; 8 | 9 | export default function bombs(state: number = defaultState, action: Action): number { 10 | const { startingBombs } = SETTINGS; 11 | switch (action.type) { 12 | case TRIGGER_BOMB: 13 | return Math.max(state - 1, 0); 14 | case SET_MODE: 15 | case RESET: { 16 | const { mode } = action.payload; 17 | return startingBombs[mode]; 18 | } 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /js/reducers/bullets.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import newPosition from '../utils/newPosition'; 4 | import { MOVE, SHOOT } from '../actions'; 5 | import { SETTINGS } from '../constants'; 6 | import { 7 | getRotateablePosition, 8 | direction, 9 | } from '../utils/math'; 10 | import { isBulletPoweredUp } from '../utils/durationChecks'; 11 | import { map } from '../utils/tupleMap'; 12 | import type { Bullet, Action } from '../types/types'; 13 | 14 | const defaultState: Bullet[] = []; 15 | 16 | function bullet(state: Bullet, action: Action): Bullet { 17 | switch (action.type) { 18 | case MOVE: { 19 | const newPos = newPosition(state); 20 | return { 21 | ...state, 22 | pos: newPos, 23 | distance: state.distance - state.speed, 24 | }; 25 | } 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | export default function bullets(state: Bullet[] = defaultState, action: Action): Bullet[] { 32 | const { 33 | bullets: { 34 | speed, 35 | distance, 36 | radius: bulletRadius, 37 | }, 38 | ship: { 39 | radius: shipRadius, 40 | }, 41 | powerups: { 42 | bullet: { 43 | spreadDegrees, 44 | }, 45 | }, 46 | } = SETTINGS; 47 | 48 | switch (action.type) { 49 | case MOVE: 50 | return state 51 | .map(b => bullet(b, action)) 52 | .filter(b => b.distance > 0); 53 | case SHOOT: { 54 | const { 55 | ship: { 56 | pos, 57 | degrees, 58 | }, 59 | mode, 60 | bulletPowerupStartFrame, 61 | frameCount, 62 | } = action.payload; 63 | // Disable shooting in DODGEBALL mode 64 | if (mode === 'DODGEBALL') { 65 | return state; 66 | } 67 | const isPoweredUp = isBulletPoweredUp(bulletPowerupStartFrame, frameCount); 68 | 69 | // Add three bullet spread when powered up 70 | const newBullets = (isPoweredUp ? [-1, 0, 1] : [0]).map(k => { 71 | const modifiedDegrees = degrees + (k * spreadDegrees); 72 | const position = getRotateablePosition(shipRadius, pos, modifiedDegrees); 73 | return { 74 | vel: map(direction(modifiedDegrees), d => d * speed), 75 | radius: bulletRadius, 76 | pos: position, 77 | distance, 78 | speed, 79 | }; 80 | }); 81 | return state.concat(newBullets); 82 | } 83 | default: 84 | return state; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /js/reducers/debris.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { MOVE } from '../actions'; 4 | import newPosition from '../utils/newPosition'; 5 | import { SETTINGS } from '../constants'; 6 | import type { Debris, Action } from '../types/types'; 7 | 8 | // TODO: These reducers are nearly identical to the bullet reducers. Share the code 9 | 10 | const defaultState: Debris[] = []; 11 | 12 | function oneDebris(state: Debris, action: Action): Debris { 13 | const { 14 | debris: { 15 | number, 16 | speed, 17 | }, 18 | asteroids: { 19 | minimumRadius, 20 | }, 21 | } = SETTINGS; 22 | switch (action.type) { 23 | case MOVE: { 24 | const newPos = newPosition({ 25 | ...state, 26 | radius: minimumRadius / number, 27 | }); 28 | return { 29 | ...state, 30 | pos: newPos, 31 | distance: state.distance - speed, 32 | }; 33 | } 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | export default function debris(state: Debris[] = defaultState, action: Action): Debris[] { 40 | switch (action.type) { 41 | case MOVE: 42 | return state 43 | .map(deb => oneDebris(deb, action)) 44 | .filter(deb => deb.distance > 0); 45 | default: 46 | return state; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /js/reducers/difficulty.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { SETTINGS, FRAMES_PER_SECOND, DEFAULT_MODE, BOSS_MODES } from '../constants'; 4 | import { MOVE, SET_MODE, RESET } from '../actions'; 5 | import type { DifficultyState, Action } from '../types/types'; 6 | 7 | function increasedDifficulty(prevDifficulty: DifficultyState): DifficultyState { 8 | const { 9 | asteroidSpawnRadiusMultiplier, 10 | minimumAsteroidAreaMultiplier, 11 | asteroidSpeedIncrease, 12 | } = SETTINGS.difficulty; 13 | 14 | return { 15 | asteroidSpawnRadius: prevDifficulty.asteroidSpawnRadius * asteroidSpawnRadiusMultiplier, 16 | minimumAsteroidArea: prevDifficulty.minimumAsteroidArea * minimumAsteroidAreaMultiplier, 17 | asteroidSpeed: prevDifficulty.asteroidSpeed + asteroidSpeedIncrease, 18 | }; 19 | } 20 | 21 | const defaultState: DifficultyState = { 22 | asteroidSpawnRadius: SETTINGS.asteroids.startingSpawnRadius[DEFAULT_MODE], 23 | minimumAsteroidArea: SETTINGS.asteroids.startingMinimumArea[DEFAULT_MODE], 24 | asteroidSpeed: SETTINGS.asteroids.startingSpeed, 25 | }; 26 | 27 | export default function difficulty( 28 | state: DifficultyState = defaultState, 29 | action: Action 30 | ): DifficultyState { 31 | switch (action.type) { 32 | case MOVE: { 33 | const { frameCount, mode } = action.payload; 34 | // There are no difficulty increases for these modes 35 | if (BOSS_MODES.includes(mode)) { 36 | return state; 37 | } 38 | const elapsedSeconds = frameCount / FRAMES_PER_SECOND; 39 | // Don't do a difficulty increase when the game starts 40 | if (frameCount !== 0 && elapsedSeconds % SETTINGS.difficulty.timeInterval[mode] === 0) { 41 | return increasedDifficulty(state); 42 | } 43 | return state; 44 | } 45 | case RESET: 46 | case SET_MODE: { 47 | const { mode } = action.payload; 48 | return { 49 | ...state, 50 | asteroidSpawnRadius: SETTINGS.asteroids.startingSpawnRadius[mode], 51 | minimumAsteroidArea: SETTINGS.asteroids.startingMinimumArea[mode], 52 | }; 53 | } 54 | default: 55 | return state; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /js/reducers/frameCount.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { MOVE } from '../actions'; 4 | import type { Action } from '../types/types'; 5 | 6 | const defaultState = 0; 7 | 8 | export default function frameCount(state: number = defaultState, action: Action): number { 9 | switch (action.type) { 10 | case MOVE: 11 | return state + 1; 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/reducers/isPaused.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { TOGGLE_PAUSE } from '../actions'; 4 | import type { Action } from '../types/types'; 5 | 6 | const defaultState = false; 7 | 8 | export default function isPaused(state: boolean = defaultState, action: Action): boolean { 9 | switch (action.type) { 10 | case TOGGLE_PAUSE: 11 | return !state; 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/reducers/isSoundOn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { TOGGLE_SOUND, RESET } from '../actions'; 4 | import type { Action } from '../types/types'; 5 | 6 | export default function isSoundOn(state: boolean = true, action: Action): boolean { 7 | switch (action.type) { 8 | case TOGGLE_SOUND: 9 | return !state; 10 | case RESET: 11 | return action.payload.isSoundOn; 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/reducers/mode.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { SET_MODE, RESET } from '../actions'; 4 | import { DEFAULT_MODE } from '../constants'; 5 | import type { Action } from '../types/types'; 6 | import type { Mode } from '../types/enums'; 7 | 8 | const defaultState = DEFAULT_MODE; 9 | 10 | export default function mode(state: Mode = defaultState, action: Action): Mode { 11 | switch (action.type) { 12 | // Maintain the previous mode when the game resets 13 | case RESET: 14 | case SET_MODE: 15 | return action.payload.mode; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /js/reducers/movingObjects.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { pick, times } from 'lodash'; 4 | import { combineReducers } from 'redux'; 5 | import ship from './ship'; 6 | import bullets from './bullets'; 7 | import asteroids from './asteroids'; 8 | import debris from './debris'; 9 | import powerups from './powerups'; 10 | import bombs from './bombs'; 11 | import queuedSounds from './queuedSounds'; 12 | import { MOVE, SET_MODE, RESET, TRIGGER_BOMB } from '../actions'; 13 | import { SETTINGS, DEFAULT_MODE } from '../constants'; 14 | import { 15 | debrisForDestroyedAsteroids, 16 | subASteroidsForCollidedAsteroids, 17 | additionalAsteroidsForCurrentAsteroids, 18 | handleCollisions, 19 | updateMultipliers, 20 | } from '../utils/asteroidCollisions'; 21 | import type { 22 | Asteroid, 23 | Action, 24 | WithRadius, 25 | MovingObjectsState, 26 | } from '../types/types'; 27 | 28 | const defaultState: MovingObjectsState = { 29 | asteroids: [], 30 | bullets: [], 31 | ship: SETTINGS.ship.defaultShip, 32 | debris: [], 33 | powerups: [], 34 | score: 0, 35 | lives: SETTINGS.startingLives[DEFAULT_MODE], 36 | multiplier: 1, 37 | multiplierBar: 0, 38 | bulletPowerupStartFrame: null, 39 | freezePowerupStartFrame: null, 40 | bombs: 0, 41 | queuedSounds: [], 42 | }; 43 | 44 | function smallerRadius(distance: number): (obj: WithRadius) => boolean { 45 | return ({ radius }) => radius < distance; 46 | } 47 | 48 | const shouldBeDestroyed = smallerRadius(SETTINGS.asteroids.minimumRadius * Math.sqrt(2)); 49 | 50 | function pointsForCollision(multiplier: number): (asteroid: Asteroid) => number { 51 | const { pointsForBreak, pointsForDestroy } = SETTINGS; 52 | return (asteroid: Asteroid): number => ( 53 | multiplier * (shouldBeDestroyed(asteroid) ? pointsForDestroy : pointsForBreak) 54 | ); 55 | } 56 | 57 | const subReducer = combineReducers({ 58 | asteroids, 59 | bullets, 60 | debris, 61 | ship, 62 | powerups, 63 | bombs, 64 | queuedSounds, 65 | }); 66 | 67 | // This reducer allows for state changes which rely on interactions between various moving objects, 68 | // specifically to handle collisions. 69 | export default function movingObjects( 70 | state: MovingObjectsState = defaultState, 71 | action: Action 72 | ): MovingObjectsState { 73 | // TODO: This seems pretty messy 74 | const subState: MovingObjectsState = subReducer(pick(state, [ 75 | 'asteroids', 76 | 'ship', 77 | 'bullets', 78 | 'debris', 79 | 'powerups', 80 | 'bombs', 81 | 'queuedSounds', 82 | ]), action); 83 | const defaultNewState: MovingObjectsState = { 84 | ...state, 85 | ...subState, 86 | }; 87 | switch (action.type) { 88 | case MOVE: { 89 | const { 90 | difficulty, 91 | frameCount, 92 | } = action.payload; 93 | const { 94 | livesDiff, 95 | notCollidedBullets, 96 | collidedAsteroids, 97 | notCollidedAsteroids, 98 | notCollidedPowerups, 99 | pointsAwarded, 100 | newShip, 101 | beginBulletPowerup, 102 | beginFreezePowerup, 103 | bombsDiff, 104 | resetMultiplier, 105 | } = handleCollisions({ 106 | ship: defaultNewState.ship, 107 | asteroids: defaultNewState.asteroids, 108 | bullets: defaultNewState.bullets, 109 | powerups: defaultNewState.powerups, 110 | pointsForCollision: pointsForCollision(state.multiplier), 111 | frameCount, 112 | }); 113 | 114 | const bulletPowerupStartFrame = beginBulletPowerup 115 | ? frameCount 116 | : defaultNewState.bulletPowerupStartFrame; 117 | const freezePowerupStartFrame = beginFreezePowerup 118 | ? frameCount 119 | : defaultNewState.freezePowerupStartFrame; 120 | const subAsteroids = subASteroidsForCollidedAsteroids(collidedAsteroids); 121 | const destroyedAsteroids = collidedAsteroids.filter(shouldBeDestroyed); 122 | const newDebris = debrisForDestroyedAsteroids(destroyedAsteroids); 123 | const newAsteroids = notCollidedAsteroids.concat(subAsteroids); 124 | const additionalAsteroids = additionalAsteroidsForCurrentAsteroids( 125 | newAsteroids, 126 | difficulty 127 | ); 128 | 129 | const newSounds = [ 130 | ...times(collidedAsteroids.length, () => 'ASTEROID_BREAK'), 131 | ...times(destroyedAsteroids.length, () => 'ASTEROID_DESTROY'), 132 | ]; 133 | 134 | const { multiplier, multiplierBar } = updateMultipliers({ 135 | previousMultiplier: defaultNewState.multiplier, 136 | previousMultiplierBar: defaultNewState.multiplierBar, 137 | numHits: collidedAsteroids.length, 138 | resetMultiplier, 139 | }); 140 | 141 | return { 142 | ship: newShip, 143 | bullets: notCollidedBullets, 144 | asteroids: newAsteroids.concat(additionalAsteroids), 145 | debris: subState.debris.concat(newDebris), 146 | powerups: notCollidedPowerups, 147 | score: defaultNewState.score + pointsAwarded, 148 | multiplier, 149 | multiplierBar, 150 | lives: defaultNewState.lives + livesDiff, 151 | bombs: defaultNewState.bombs + bombsDiff, 152 | queuedSounds: defaultNewState.queuedSounds.concat(newSounds), 153 | bulletPowerupStartFrame, 154 | freezePowerupStartFrame, 155 | }; 156 | } 157 | case TRIGGER_BOMB: 158 | if (state.bombs > 0) { 159 | return { 160 | ...defaultNewState, 161 | asteroids: [], 162 | debris: defaultNewState.debris.concat( 163 | debrisForDestroyedAsteroids(defaultNewState.asteroids) 164 | ), 165 | queuedSounds: defaultNewState.queuedSounds.concat( 166 | times(state.asteroids.length, () => 'ASTEROID_DESTROY') 167 | ), 168 | }; 169 | } 170 | return defaultNewState; 171 | case RESET: 172 | case SET_MODE: 173 | return { ...defaultNewState, lives: SETTINGS.startingLives[action.payload.mode] }; 174 | default: 175 | return defaultNewState; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /js/reducers/powerups.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { random, sample } from 'lodash'; 4 | import { 5 | SETTINGS, 6 | FRAMES_PER_SECOND, 7 | DIMENSION, 8 | } from '../constants'; 9 | import { MOVE } from '../actions'; 10 | import { makePair } from '../utils/tupleMap'; 11 | import type { Powerup, Action } from '../types/types'; 12 | import type { Mode } from '../types/enums'; 13 | 14 | function newPowerup(mode: Mode, lives: number, bombs: number): Powerup { 15 | // Do not give more lives if current lives >= 2 16 | // Do not give more bombs if current bombs >= 2 17 | const possiblePowerups = SETTINGS.modes.powerups[mode]; 18 | const options = possiblePowerups.filter(powerupType => { 19 | if (powerupType === 'LIFE' && lives >= 2) { 20 | return false; 21 | } 22 | if (powerupType === 'BOMB' && bombs >= 2) { 23 | return false; 24 | } 25 | return true; 26 | }); 27 | return { 28 | pos: makePair(() => random(0, DIMENSION)), 29 | type: sample(options), 30 | }; 31 | } 32 | 33 | const defaultState: Powerup[] = []; 34 | 35 | export default function powerups(state: Powerup[] = defaultState, action: Action): Powerup[] { 36 | const { 37 | difficulty: { 38 | timeInterval, 39 | }, 40 | } = SETTINGS; 41 | switch (action.type) { 42 | case MOVE: { 43 | const { frameCount, mode, lives, bombs } = action.payload; 44 | // No powerups in DODGEBALL 45 | if (mode === 'DODGEBALL') { 46 | return state; 47 | } 48 | const elapsedSeconds = frameCount / FRAMES_PER_SECOND; 49 | if (frameCount !== 0 && elapsedSeconds % timeInterval[mode] === 0) { 50 | return [...state, newPowerup(mode, lives, bombs)]; 51 | } 52 | return state; 53 | } 54 | default: 55 | return state; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /js/reducers/queuedSounds.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { MOVE, SHOOT } from '../actions'; 4 | import type { Sound } from '../types/enums'; 5 | import type { Action } from '../types/types'; 6 | 7 | const defaultState: Sound[] = []; 8 | 9 | export default function queuedSounds( 10 | state: Sound[] = defaultState, 11 | action: Action, 12 | ): Sound[] { 13 | switch (action.type) { 14 | case MOVE: 15 | return defaultState; 16 | case SHOOT: { 17 | const { mode } = action.payload; 18 | // Disable shooting in DODGEBALL mode 19 | if (mode === 'DODGEBALL') { 20 | return state; 21 | } 22 | return [...state, 'LASER']; 23 | } 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /js/reducers/root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { combineReducers } from 'redux'; 4 | import movingObjects from './movingObjects'; 5 | import isPaused from './isPaused'; 6 | import frameCount from './frameCount'; 7 | import difficulty from './difficulty'; 8 | import mode from './mode'; 9 | import isSoundOn from './isSoundOn'; 10 | import { RESET } from '../actions'; 11 | import type { Action, Store } from '../types/types'; 12 | 13 | const appReducer = combineReducers({ 14 | movingObjects, 15 | isPaused, 16 | frameCount, 17 | difficulty, 18 | mode, 19 | isSoundOn, 20 | }); 21 | 22 | export default function rootReducer(state: Store, action: Action): Store { 23 | if (action.type === RESET) { 24 | return appReducer(undefined, action); 25 | } 26 | return appReducer(state, action); 27 | } 28 | -------------------------------------------------------------------------------- /js/reducers/ship.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { MOVE, THRUST_SHIP, ROTATE_SHIP, STOP_THRUSTING_SHIP } from '../actions'; 4 | import newPosition from '../utils/newPosition'; 5 | import computeNewVel from '../utils/computeNewVel'; 6 | import { SETTINGS } from '../constants'; 7 | import { map } from '../utils/tupleMap'; 8 | import type { Ship, Action } from '../types/types'; 9 | 10 | function airResistedVelocity(oldVel: [number, number], airResistance: number): [number, number] { 11 | return map(oldVel, (d) => { 12 | if (d > airResistance) { 13 | return d - airResistance; 14 | } else if (d < -airResistance) { 15 | return d + airResistance; 16 | } 17 | return 0; 18 | }); 19 | } 20 | 21 | const defaultState = SETTINGS.ship.defaultShip; 22 | 23 | export default function ship(state: Ship = defaultState, action: Action): Ship { 24 | const { 25 | radius: shipRadius, 26 | airResistance, 27 | acceleration, 28 | maxSpeed, 29 | turnSpeed, 30 | } = SETTINGS.ship; 31 | switch (action.type) { 32 | case MOVE: { 33 | const newPos = newPosition({ 34 | ...state, 35 | radius: shipRadius, 36 | }); 37 | return { 38 | ...state, 39 | pos: newPos, 40 | vel: airResistedVelocity(state.vel, airResistance), 41 | }; 42 | } 43 | case THRUST_SHIP: { 44 | const vel = computeNewVel( 45 | state.vel, 46 | state.degrees, 47 | acceleration, 48 | maxSpeed 49 | ); 50 | return { ...state, isThrusting: true, vel }; 51 | } 52 | case ROTATE_SHIP: { 53 | if (action.payload == null) { 54 | return state; 55 | } 56 | const degrees = (state.degrees + (action.payload * turnSpeed)) % 360; 57 | return { ...state, degrees }; 58 | } 59 | case STOP_THRUSTING_SHIP: 60 | return { ...state, isThrusting: false }; 61 | default: 62 | return state; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /js/store.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import rootReducer from './reducers/root'; 5 | 6 | const middleWare = []; 7 | 8 | const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore); 9 | 10 | const store = createStoreWithMiddleware(rootReducer); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /js/types/enums.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Mode = 'CLASSIC' | 'DODGEBALL' | 'BOSS' | 'SUPER_BOSS'; 4 | export type PowerupType = 'LIFE' | 'BULLET' | 'BOMB' | 'FREEZE' | 'INVINCIBLE'; 5 | export type Sound = 'ASTEROID_BREAK' | 'ASTEROID_DESTROY' | 'GAME_OVER' | 'LASER'; 6 | -------------------------------------------------------------------------------- /js/types/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Mode, PowerupType, Sound } from './enums'; 4 | 5 | export type Asteroid = { 6 | pos: [number, number], 7 | radius: number, 8 | vel: [number, number], 9 | spawnSpeed: number, 10 | }; 11 | 12 | export type Ship = { 13 | pos: [number, number], 14 | vel: [number, number], 15 | degrees: number, 16 | isThrusting: boolean, 17 | invincibilityStartFrame: number, 18 | }; 19 | 20 | export type Bullet = { 21 | pos: [number, number], 22 | vel: [number, number], 23 | distance: number, 24 | speed: number, 25 | radius: number, 26 | }; 27 | 28 | export type Debris = { 29 | pos: [number, number], 30 | vel: [number, number], 31 | distance: number, 32 | }; 33 | 34 | export type WithRadius = { 35 | radius: number, 36 | }; 37 | 38 | export type Distanceable = { 39 | radius: number, 40 | pos: [number, number], 41 | }; 42 | 43 | export type Moveable = { 44 | radius: number, 45 | pos: [number, number], 46 | vel: [number, number], 47 | } 48 | 49 | export type DrawableCircle = { 50 | radius: number, 51 | pos: [number, number], 52 | color: string, 53 | } 54 | 55 | export type DrawableText = { 56 | text: string, 57 | size: number, 58 | pos: [number, number], 59 | color: string 60 | }; 61 | 62 | export type DrawableRectangle = { 63 | pos: [number, number], 64 | color: string, 65 | width: number, 66 | height: number, 67 | }; 68 | 69 | export type DifficultyState = { 70 | asteroidSpawnRadius: number, 71 | minimumAsteroidArea: number, 72 | asteroidSpeed: number, 73 | }; 74 | 75 | export type TurnDirection = 1 | -1; 76 | 77 | export type Powerup = { 78 | pos: [number, number], 79 | type: PowerupType, 80 | }; 81 | 82 | export type MovingObjectsState = { 83 | asteroids: Asteroid[], 84 | bullets: Bullet[], 85 | ship: Ship, 86 | debris: Debris[], 87 | powerups: Powerup[], 88 | score: number, 89 | lives: number, 90 | multiplier: number, 91 | multiplierBar: number, 92 | bulletPowerupStartFrame: ?number, 93 | freezePowerupStartFrame: ?number, 94 | bombs: number, 95 | queuedSounds: Sound[], 96 | }; 97 | 98 | export type Store = { 99 | movingObjects: MovingObjectsState, 100 | isPaused: boolean, 101 | frameCount: number, 102 | difficulty: DifficultyState, 103 | mode: Mode, 104 | isSoundOn: boolean, 105 | }; 106 | 107 | export type MoveAction = { 108 | type: 'MOVE', 109 | payload: { 110 | difficulty: DifficultyState, 111 | frameCount: number, 112 | mode: Mode, 113 | lives: number, 114 | bombs: number, 115 | freezePowerupStartFrame: ?number, 116 | } 117 | } 118 | 119 | export type ThrustShipAction = { type: 'THRUST_SHIP' }; 120 | 121 | export type RotateShipAction = { 122 | type: 'ROTATE_SHIP', 123 | payload: TurnDirection, 124 | }; 125 | 126 | export type StopThrustingAction = { type: 'STOP_THRUSTING_SHIP' }; 127 | 128 | export type ShootAction = { 129 | type: 'SHOOT', 130 | payload: { 131 | ship: Ship, 132 | mode: Mode, 133 | bulletPowerupStartFrame: ?number, 134 | frameCount: number, 135 | }, 136 | }; 137 | 138 | export type TogglePauseAction = { type: 'TOGGLE_PAUSE' }; 139 | 140 | export type ResetAction = { 141 | type: 'RESET', 142 | payload: { 143 | mode: Mode, 144 | isSoundOn: boolean, 145 | }, 146 | } 147 | 148 | export type SetModeAction = { 149 | type: 'SET_MODE', 150 | payload: { 151 | mode: Mode, 152 | }, 153 | } 154 | 155 | export type AddInitialAsteroidsAction = { 156 | type: 'ADD_INITIAL_ASTEROIDS', 157 | payload: { 158 | mode: Mode, 159 | }, 160 | } 161 | 162 | export type TriggerBombAction = { type: 'TRIGGER_BOMB' }; 163 | 164 | export type ToggleSoundAction = { type: 'TOGGLE_SOUND' }; 165 | 166 | export type Action = 167 | | ToggleSoundAction 168 | | TriggerBombAction 169 | | AddInitialAsteroidsAction 170 | | SetModeAction 171 | | ResetAction 172 | | ShootAction 173 | | TogglePauseAction 174 | | StopThrustingAction 175 | | RotateShipAction 176 | | ThrustShipAction 177 | | MoveAction 178 | -------------------------------------------------------------------------------- /js/utils/asteroidCollisions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { times, sumBy } from 'lodash'; 4 | import { SETTINGS } from '../constants'; 5 | import randomAsteroids from './randomAsteroids'; 6 | import { direction, sumOfAreas, isCollided } from './math'; 7 | import { isShipInvincible } from './durationChecks'; 8 | import type { 9 | Asteroid, 10 | Debris, 11 | WithRadius, 12 | DifficultyState, 13 | Ship, 14 | Bullet, 15 | Powerup, 16 | } from '../types/types'; 17 | import type { PowerupType } from '../types/enums'; 18 | 19 | type AsteroidCollision = { 20 | asteroid: Asteroid, 21 | points: number, 22 | }; 23 | 24 | // Returns the debris objects resulting from the destroyed asteroids 25 | export function debrisForDestroyedAsteroids(destroyedAsteroids: Asteroid[]): Debris[] { 26 | const { 27 | debris: { 28 | number: numDebris, 29 | distance: debrisDistance, 30 | }, 31 | } = SETTINGS; 32 | const angle: number = 360 / numDebris; 33 | return destroyedAsteroids.reduce((prev, current) => { 34 | const debrisForAsteroid = times(numDebris, index => ({ 35 | pos: current.pos, 36 | vel: direction(angle * index), 37 | distance: debrisDistance, 38 | })); 39 | return prev.concat(debrisForAsteroid); 40 | }, []); 41 | } 42 | 43 | // Return the new small asteroids resulting from the asteroids that have been collided with 44 | export function subASteroidsForCollidedAsteroids(collidedAsteroids: Asteroid[]): Asteroid[] { 45 | const { minimumRadius } = SETTINGS.asteroids; 46 | 47 | return collidedAsteroids.reduce((prev, current) => ( 48 | prev.concat(randomAsteroids(2, { 49 | radius: current.radius / Math.sqrt(2), 50 | pos: current.pos, 51 | // Split asteroids maintain the same speed as their parent 52 | spawnSpeed: current.spawnSpeed, 53 | })) 54 | ), []).filter(asteroid => asteroid.radius > minimumRadius); 55 | } 56 | 57 | // Return the asteroids needed in addition to the current ones to meet the minimum area requirement 58 | export function additionalAsteroidsForCurrentAsteroids( 59 | currentAsteroids: Asteroid[], 60 | { asteroidSpawnRadius, asteroidSpeed, minimumAsteroidArea }: DifficultyState, 61 | ): Asteroid[] { 62 | // Flow can't cast Asteroid[] to WithRadius[], so map over the array to do it explicitly 63 | const withRadii: WithRadius[] = currentAsteroids.map(asteroid => (asteroid: WithRadius)); 64 | return sumOfAreas(withRadii) < minimumAsteroidArea 65 | ? randomAsteroids(1, { radius: asteroidSpawnRadius, spawnSpeed: asteroidSpeed }) 66 | : []; 67 | } 68 | 69 | const numPowerupsOfType = (powerups: Powerup[]) => (type: PowerupType): number => ( 70 | powerups.filter(powerup => powerup.type === type).length 71 | ); 72 | 73 | export function handleCollisions({ 74 | ship, 75 | asteroids, 76 | bullets, 77 | powerups, 78 | frameCount, 79 | pointsForCollision, 80 | } : { 81 | ship: Ship, 82 | asteroids: Asteroid[], 83 | bullets: Bullet[], 84 | powerups: Powerup[], 85 | frameCount: number, 86 | pointsForCollision: (asteroid: Asteroid) => number, 87 | }): { 88 | livesDiff: number, 89 | notCollidedBullets: Bullet[], 90 | collidedAsteroids: Asteroid[], 91 | notCollidedAsteroids: Asteroid[], 92 | notCollidedPowerups: Powerup[], 93 | pointsAwarded: number, 94 | newShip: Ship, 95 | beginBulletPowerup: boolean, 96 | beginFreezePowerup: boolean, 97 | bombsDiff: number, 98 | resetMultiplier: boolean, 99 | } { 100 | const { 101 | ship: { 102 | radius: shipRadius, 103 | defaultShip, 104 | }, 105 | bullets: { 106 | radius: bulletRadius, 107 | }, 108 | powerups: { 109 | radius: powerupRadius, 110 | }, 111 | } = SETTINGS; 112 | let livesDiff = 0; 113 | let bombsDiff = 0; 114 | let resetMultiplier = false; 115 | const collidedBullets: Bullet[] = []; 116 | const asteroidCollisions: AsteroidCollision[] = []; 117 | let newShip = ship; 118 | asteroids.forEach((asteroid) => { 119 | bullets.forEach((bullet) => { 120 | if (isCollided({ ...bullet, radius: bulletRadius }, asteroid)) { 121 | collidedBullets.push(bullet); 122 | asteroidCollisions.push({ 123 | points: pointsForCollision(asteroid), 124 | asteroid, 125 | }); 126 | } 127 | }); 128 | const didShipCollideWithAsteroid = isCollided( 129 | { pos: ship.pos, radius: shipRadius }, 130 | asteroid 131 | ); 132 | if (!isShipInvincible(ship, frameCount) && didShipCollideWithAsteroid) { 133 | livesDiff -= 1; 134 | asteroidCollisions.push({ points: 0, asteroid }); 135 | // Maintain the ship's current direction and reset its spawnFrame 136 | newShip = { 137 | ...defaultShip, 138 | degrees: ship.degrees, 139 | invincibilityStartFrame: frameCount, 140 | }; 141 | resetMultiplier = true; 142 | } 143 | }); 144 | 145 | const collidedAsteroids = asteroidCollisions.map(ac => ac.asteroid); 146 | const pointsAwarded = sumBy(asteroidCollisions, ac => ac.points); 147 | const notCollidedAsteroids = asteroids.filter(asteroid => ( 148 | !collidedAsteroids.includes(asteroid) 149 | )); 150 | const notCollidedBullets = bullets.filter(bullet => !collidedBullets.includes(bullet)); 151 | 152 | const collidedPowerups = powerups.filter(powerup => ( 153 | isCollided({ ...ship, radius: shipRadius }, { ...powerup, radius: powerupRadius }) 154 | )); 155 | const notCollidedPowerups = powerups.filter(powerup => !collidedPowerups.includes(powerup)); 156 | 157 | const numCollidedPowerupsOfType = numPowerupsOfType(collidedPowerups); 158 | livesDiff += numCollidedPowerupsOfType('LIFE'); 159 | bombsDiff = numCollidedPowerupsOfType('BOMB'); 160 | 161 | const beginBulletPowerup = numCollidedPowerupsOfType('BULLET') > 0; 162 | const beginFreezePowerup = numCollidedPowerupsOfType('FREEZE') > 0; 163 | const beginInvinciblePowerup = numCollidedPowerupsOfType('INVINCIBLE') > 0; 164 | if (beginInvinciblePowerup) { 165 | newShip = { 166 | ...newShip, 167 | invincibilityStartFrame: frameCount, 168 | }; 169 | } 170 | return { 171 | newShip: newShip || ship, 172 | livesDiff, 173 | notCollidedBullets, 174 | collidedAsteroids, 175 | notCollidedAsteroids, 176 | notCollidedPowerups, 177 | pointsAwarded, 178 | beginBulletPowerup, 179 | beginFreezePowerup, 180 | bombsDiff, 181 | resetMultiplier, 182 | }; 183 | } 184 | 185 | export function updateMultipliers({ 186 | previousMultiplier, 187 | previousMultiplierBar, 188 | numHits, 189 | resetMultiplier, 190 | }: { 191 | previousMultiplier: number, 192 | previousMultiplierBar: number, 193 | numHits: number, 194 | resetMultiplier: boolean, 195 | }): { multiplier: number, multiplierBar: number } { 196 | if (resetMultiplier) { 197 | return { multiplier: 1, multiplierBar: 0 }; 198 | } 199 | let newMultiplier = previousMultiplier; 200 | let newMultiplierBar = Math.max(0, previousMultiplierBar + (10 * numHits) - 0.2); 201 | if (newMultiplierBar === 0) { 202 | newMultiplier = 1; 203 | } else if (newMultiplierBar > 100) { 204 | newMultiplierBar %= 100; 205 | newMultiplier += 1; 206 | } 207 | return { 208 | multiplier: Math.max(1, newMultiplier), 209 | multiplierBar: newMultiplierBar, 210 | }; 211 | } 212 | -------------------------------------------------------------------------------- /js/utils/canvas.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { DIMENSION } from '../constants'; 4 | import type { DrawableCircle, DrawableText, DrawableRectangle } from '../types/types'; 5 | 6 | let gameCtx: CanvasRenderingContext2D; 7 | let uiCtx: CanvasRenderingContext2D; 8 | 9 | function guardForInitialized(callback: Function): Function { 10 | return (...args) => { 11 | if (gameCtx && uiCtx) { 12 | callback(...args); 13 | } 14 | }; 15 | } 16 | 17 | export function initContext( 18 | gameContext: CanvasRenderingContext2D, 19 | uiContext: CanvasRenderingContext2D 20 | ) { 21 | gameCtx = gameContext; 22 | uiCtx = uiContext; 23 | } 24 | 25 | function unguardedClear() { 26 | gameCtx.clearRect(0, 0, DIMENSION, DIMENSION); 27 | uiCtx.clearRect(0, 0, DIMENSION / 2, DIMENSION); 28 | } 29 | 30 | export const clear = guardForInitialized(unguardedClear); 31 | 32 | function drawCircle(ctx: CanvasRenderingContext2D, { color, pos, radius }: DrawableCircle) { 33 | ctx.fillStyle = color; 34 | ctx.beginPath(); 35 | ctx.arc( 36 | pos[0], 37 | pos[1], 38 | radius, 39 | 0, 40 | 2 * Math.PI, 41 | false 42 | ); 43 | ctx.fill(); 44 | } 45 | 46 | function unguardedDrawCircleInGame(obj: DrawableCircle) { 47 | drawCircle(gameCtx, obj); 48 | } 49 | export const drawCircleInGame = guardForInitialized(unguardedDrawCircleInGame); 50 | 51 | function unguardedDrawCircleInUI(obj: DrawableCircle) { 52 | drawCircle(uiCtx, obj); 53 | } 54 | export const drawCircleInUI = guardForInitialized(unguardedDrawCircleInUI); 55 | 56 | function drawText( 57 | ctx: CanvasRenderingContext2D, 58 | { text, size, pos, color }: DrawableText, 59 | ) { 60 | ctx.fillStyle = color; 61 | ctx.font = `${size}pt Arial `; 62 | ctx.fillText(text, ...pos); 63 | } 64 | 65 | function ungardedDrawTextInGame(obj: DrawableText) { 66 | drawText(gameCtx, obj); 67 | } 68 | export const drawTextInGame = guardForInitialized(ungardedDrawTextInGame); 69 | 70 | function ungardedDrawTextInUI(obj: DrawableText) { 71 | drawText(uiCtx, obj); 72 | } 73 | export const drawTextInUI = guardForInitialized(ungardedDrawTextInUI); 74 | 75 | function drawRectangle( 76 | ctx: CanvasRenderingContext2D, 77 | { pos, width, height, color }: DrawableRectangle 78 | ) { 79 | ctx.fillStyle = color; 80 | ctx.fillRect(pos[0], pos[1], width, height); 81 | } 82 | 83 | function unguardedDrawRectangleInUI(obj: DrawableRectangle) { 84 | drawRectangle(uiCtx, obj); 85 | } 86 | 87 | export const drawRectangleInUI = guardForInitialized(unguardedDrawRectangleInUI); 88 | -------------------------------------------------------------------------------- /js/utils/computeNewVel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { direction } from './math'; 4 | import { map, mapPair, add } from './tupleMap'; 5 | 6 | // Computes the new velocity of the ship when it accelerates 7 | export default function computeNewVel( 8 | oldVel: [number, number], 9 | degree: number, 10 | accel: number, 11 | maxSpeed: number 12 | ): [number, number] { 13 | const impulse = map(direction(degree), d => d * accel); 14 | const newVel = add(oldVel, impulse); 15 | // Enforce that the ship's speed does not exceed MAXSPEED 16 | const minVel = map(newVel, d => Math.min(maxSpeed, Math.abs(d))); 17 | return mapPair(newVel, minVel, (n, m) => n >= 0 ? m : -m); 18 | } 19 | -------------------------------------------------------------------------------- /js/utils/durationChecks.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { FRAMES_PER_SECOND, SETTINGS } from '../constants'; 4 | import type { Ship } from '../types/types'; 5 | 6 | function hasTimePassed( 7 | startFrame: ?number, 8 | duration: number, 9 | currentFrame: number, 10 | ): boolean { 11 | if (startFrame == null) { 12 | return true; 13 | } 14 | return startFrame + (FRAMES_PER_SECOND * duration) < currentFrame; 15 | } 16 | 17 | export function isShipInvincible({ invincibilityStartFrame }: Ship, frameCount: number): boolean { 18 | return !hasTimePassed(invincibilityStartFrame, SETTINGS.ship.invincibilityTime, frameCount); 19 | } 20 | 21 | export function isBulletPoweredUp(bulletPowerupStartFrame: ?number, frameCount: number): boolean { 22 | return !hasTimePassed( 23 | bulletPowerupStartFrame, 24 | SETTINGS.powerups.duration.BULLET, 25 | frameCount 26 | ); 27 | } 28 | 29 | export function areAsteroidsFrozen(freezePowerupStartFrame: ?number, frameCount: number): boolean { 30 | return !hasTimePassed( 31 | freezePowerupStartFrame, 32 | SETTINGS.powerups.duration.FREEZE, 33 | frameCount 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /js/utils/getEndMessage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { FRAMES_PER_SECOND } from '../constants'; 4 | import type { Mode } from '../types/enums'; 5 | 6 | type Argument = { 7 | score: number, 8 | frameCount: number, 9 | mode: Mode, 10 | hasWon: boolean, 11 | }; 12 | 13 | const ending = 'Would you like to play again?'; 14 | 15 | function getEndMessageHelper({ score, frameCount, mode, hasWon }: Argument): string { 16 | const seconds = Math.floor(frameCount / FRAMES_PER_SECOND); 17 | if (hasWon) { 18 | return `You win! That took you ${seconds} seconds.`; 19 | } 20 | if (mode === 'DODGEBALL') { 21 | return `Game Over! You survived for ${seconds} seconds.`; 22 | } 23 | return `Game Over! Your score is ${score.toLocaleString()}. You survived for ${seconds} seconds.`; 24 | } 25 | 26 | export default function getEndMessage(options: Argument): string { 27 | return `${getEndMessageHelper(options)} ${ending}`; 28 | } 29 | -------------------------------------------------------------------------------- /js/utils/math.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { sumBy } from 'lodash'; 4 | import { map, add, subtract } from './tupleMap'; 5 | import type { WithRadius, Distanceable } from '../types/types'; 6 | 7 | export function toRadians(degrees: number): number { 8 | return degrees * (Math.PI / 180); 9 | } 10 | 11 | export function direction(degrees: number): [number, number] { 12 | const radians = toRadians(degrees); 13 | return [Math.cos(radians), -Math.sin(radians)]; 14 | } 15 | 16 | // Get the position of a rotateable (ie the turret or the thruster), given the radius, and position 17 | // of the ship, and degrees that the rotateable is pointing in. 18 | export function getRotateablePosition( 19 | radius: number, 20 | pos: [number, number], 21 | degrees: number 22 | ): [number, number] { 23 | const distances = map(direction(degrees), d => d * radius); 24 | return add(pos, distances); 25 | } 26 | 27 | export function distance(obj1: Distanceable, obj2: Distanceable): number { 28 | const [xDiff, yDiff] = subtract(obj1.pos, obj2.pos); 29 | return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2)); 30 | } 31 | 32 | export function isCollided(obj1: Distanceable, obj2: Distanceable): boolean { 33 | if (obj1 === obj2) { 34 | return false; 35 | } 36 | return distance(obj1, obj2) < obj1.radius + obj2.radius; 37 | } 38 | 39 | function area({ radius }: WithRadius): number { 40 | return Math.PI * Math.pow(radius, 2); 41 | } 42 | 43 | export function sumOfAreas(objects: WithRadius[]) { 44 | return sumBy(objects, area); 45 | } 46 | -------------------------------------------------------------------------------- /js/utils/newPosition.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { DIMENSION } from '../constants'; 4 | import { map, add } from './tupleMap'; 5 | import type { Moveable } from '../types/types'; 6 | 7 | // Given the position and radius of an object, and the dimension of the screen, map the object 8 | // to the screeen so that it wraps around the edge of screen. 9 | function mapToScreen( 10 | pos: [number, number], 11 | radius: number, 12 | dimension: number = DIMENSION 13 | ): [number, number] { 14 | return map(pos, (d) => { 15 | if (d >= dimension + radius) { 16 | return d - (dimension + (2 * radius)); 17 | } else if (d <= 0 - radius) { 18 | return d + (dimension + (2 * radius)); 19 | } 20 | return d; 21 | }); 22 | } 23 | 24 | export default function newPosition({ vel, radius, pos }: Moveable): [number, number] { 25 | const newPos = add(pos, vel); 26 | return mapToScreen(newPos, radius); 27 | } 28 | -------------------------------------------------------------------------------- /js/utils/playSounds.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { SETTINGS } from '../constants'; 4 | import type { Sound } from '../types/enums'; 5 | 6 | function playSound(sound: Sound) { 7 | const soundPath = `audio/${SETTINGS.audioFile[sound]}.mp3`; 8 | const audio = new global.Audio(soundPath); 9 | audio.play(); 10 | } 11 | 12 | export default function playSounds(sounds: Sound[]) { 13 | sounds.forEach(playSound); 14 | } 15 | -------------------------------------------------------------------------------- /js/utils/randomAsteroids.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { random, sample, times } from 'lodash'; 4 | import { SETTINGS, DIMENSION } from '../constants'; 5 | import { makePair } from './tupleMap'; 6 | import type { Asteroid } from '../types/types'; 7 | 8 | type Options = { 9 | pos?: [number, number], 10 | radius: number, 11 | spawnSpeed?: number, 12 | } 13 | 14 | // Pick a random position along the edge of the game for the asteroid to 15 | // spawn at 16 | function randomPos(radius, dimension: number): [number, number] { 17 | const [randomX, randomY] = makePair(() => random(-radius, dimension + radius)); 18 | const [edgeX, edgeY] = makePair(() => sample([-radius, dimension + radius])); 19 | const candidate1 = [edgeX, randomY]; 20 | const candidate2 = [randomX, edgeY]; 21 | return sample([candidate1, candidate2]); 22 | } 23 | 24 | // Pick a random direction for the asteroid to begin moving in 25 | function randomVel(dimension: number, intensity: number): [number, number] { 26 | return makePair(() => { 27 | const range = (intensity * dimension) / 125; 28 | const direction = sample([-1, 1]); 29 | return random(1, range) * direction; 30 | }); 31 | } 32 | 33 | function randomAsteroid(options: Options): Asteroid { 34 | // Asteroids in dodgeball have a predefined set of sizes 35 | // const radius = options.dodgeball ? sample([15, 21.2, 30]) : Asteroid.spawnRadius; 36 | 37 | const radius = options.radius; 38 | const pos = options.pos || randomPos(radius, DIMENSION); 39 | const spawnSpeed = options.spawnSpeed || SETTINGS.asteroids.startingSpeed; 40 | const vel = randomVel(DIMENSION, spawnSpeed); 41 | 42 | return { 43 | pos, 44 | vel, 45 | radius, 46 | spawnSpeed, 47 | }; 48 | } 49 | 50 | export default function randomAsteroids(num: number, options: Options): Asteroid[] { 51 | return times(num, () => randomAsteroid(options)); 52 | } 53 | -------------------------------------------------------------------------------- /js/utils/tupleMap.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type Pair = [number, number] 4 | 5 | export function mapPair(pair1: Pair, pair2: Pair, f: (a: number, b: number)=>number): Pair { 6 | return [ 7 | f(pair1[0], pair2[0]), 8 | f(pair1[1], pair2[1]), 9 | ]; 10 | } 11 | 12 | export function map(pair: Pair, f: (a: number, b: ?number)=>number): Pair { 13 | return mapPair(pair, [0, 1], f); 14 | } 15 | 16 | export function add(pair1: Pair, pair2: Pair): Pair { 17 | return mapPair(pair1, pair2, (a, b) => a + b); 18 | } 19 | 20 | export function subtract(pair1: Pair, pair2: Pair): Pair { 21 | return mapPair(pair1, pair2, (a, b) => a - b); 22 | } 23 | 24 | export function makePair(f: (a: ?number)=>number): Pair { 25 | return map([0, 1], f); 26 | } 27 | -------------------------------------------------------------------------------- /main.jsx: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import React from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import App from './js/components/App'; 5 | import store from './js/store'; 6 | 7 | document.addEventListener('DOMContentLoaded', () => { 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Asteroids", 3 | "version": "1.0.0", 4 | "description": "Asteroids =========", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "./node_modules/.bin/eslint js", 9 | "deploy": "surge -d philnachumasteroids.surge.sh" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/philpee2/Asteroids.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/philpee2/Asteroids/issues" 19 | }, 20 | "homepage": "https://github.com/philpee2/Asteroids", 21 | "devDependencies": { 22 | "babel-core": "^6.5.0", 23 | "babel-eslint": "^6.1.2", 24 | "babel-loader": "^6.2.2", 25 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 26 | "eslint": "^3.5.0", 27 | "eslint-config-airbnb": "^11.1.0", 28 | "eslint-plugin-babel": "^3.3.0", 29 | "eslint-plugin-import": "^1.15.0", 30 | "eslint-plugin-jsx-a11y": "^2.2.2", 31 | "eslint-plugin-react": "^6.2.2" 32 | }, 33 | "dependencies": { 34 | "aphrodite": "^0.5.0", 35 | "babel-core": "^6.5.0", 36 | "babel-loader": "^6.2.2", 37 | "babel-preset-es2015": "^6.5.0", 38 | "babel-preset-react": "^6.11.1", 39 | "babel-preset-stage-0": "^6.5.0", 40 | "enumify": "^1.0.4", 41 | "keymaster": "^1.6.2", 42 | "lodash": "^4.2.1", 43 | "react": "^15.3.2", 44 | "react-dom": "^15.3.2", 45 | "react-icons": "^2.2.1", 46 | "react-redux": "^4.4.5", 47 | "redux": "^3.6.0", 48 | "webpack": "^1.12.13" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname, 3 | entry: "./main.jsx", 4 | output: { 5 | path: "./", 6 | filename: "bundle.js" 7 | }, 8 | module: { 9 | loaders: [{ 10 | test: /\.jsx?$/, 11 | exclude: /node_modules/, 12 | loader: "babel-loader", 13 | query: { 14 | presets: ['react', 'es2015', 'stage-0'], 15 | plugins: ['transform-flow-strip-types'] 16 | } 17 | }] 18 | }, 19 | devtool: 'source-map', 20 | resolve: { 21 | extensions: ["", ".js", ".jsx"] 22 | } 23 | }; 24 | --------------------------------------------------------------------------------