├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── compile.js └── dev-server.js ├── config ├── environments.config.js ├── karma.config.js ├── project.config.js └── webpack.config.js ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── humans.txt ├── js │ └── lib │ │ └── Oimo.1.2.js ├── robots.txt ├── sfx │ ├── boom1.wav │ ├── boom2.wav │ ├── boom3.wav │ ├── boom4.wav │ ├── boom5.wav │ ├── xplode1.wav │ ├── xplode2.wav │ ├── xplode3.wav │ └── xplode4.wav ├── shaders │ ├── gradient.fragment.fx │ └── gradient.vertex.fx ├── skybox │ ├── TropicalSunnyDay_nx.jpg │ ├── TropicalSunnyDay_ny.jpg │ ├── TropicalSunnyDay_nz.jpg │ ├── TropicalSunnyDay_px.jpg │ ├── TropicalSunnyDay_py.jpg │ └── TropicalSunnyDay_pz.jpg └── style │ └── bootstrap.min.css ├── quarto_screenshot.png ├── server └── main.js ├── src ├── appHistory.js ├── components │ └── Header │ │ ├── Header.js │ │ ├── Header.scss │ │ └── index.js ├── containers │ └── AppContainer.js ├── index.html ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.js │ │ ├── CoreLayout.scss │ │ └── index.js ├── main.js ├── routes │ ├── Counter │ │ ├── components │ │ │ └── Counter.js │ │ ├── containers │ │ │ └── CounterContainer.js │ │ ├── index.js │ │ └── modules │ │ │ └── counter.js │ ├── Home │ │ ├── assets │ │ │ └── Duck.jpg │ │ ├── components │ │ │ ├── HomeView.js │ │ │ └── HomeView.scss │ │ └── index.js │ ├── Quarto │ │ ├── assets │ │ │ ├── pick_icon.png │ │ │ └── put_icon.png │ │ ├── components │ │ │ ├── Base.js │ │ │ ├── Gameboard.js │ │ │ ├── Piece.js │ │ │ ├── Player.js │ │ │ ├── Quarto.js │ │ │ ├── Quarto.scss │ │ │ ├── Timer.js │ │ │ └── Tutorial.js │ │ ├── containers │ │ │ └── QuartoContainer.js │ │ ├── index.js │ │ └── modules │ │ │ └── quarto.js │ └── index.js ├── sagas │ ├── index.js │ └── quartoGameLogic.js ├── store │ ├── createStore.js │ ├── location.js │ └── reducers.js └── styles │ ├── _base.scss │ └── core.scss ├── tests ├── .eslintrc ├── components │ └── Header │ │ └── Header.spec.js ├── layouts │ └── CoreLayout.spec.js ├── routes │ ├── Counter │ │ ├── components │ │ │ └── Counter.spec.js │ │ ├── index.spec.js │ │ └── modules │ │ │ └── counter.spec.js │ └── Home │ │ ├── components │ │ └── HomeView.spec.js │ │ └── index.spec.js ├── store │ ├── createStore.spec.js │ └── location.spec.js └── test-bundler.js ├── typings.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | src/index.html 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "plugins": [ 8 | "babel", 9 | "react", 10 | "promise" 11 | ], 12 | "env": { 13 | "browser" : true 14 | }, 15 | "globals": { 16 | "__DEV__" : false, 17 | "__TEST__" : false, 18 | "__PROD__" : false, 19 | "__COVERAGE__" : false, 20 | "__BASENAME__" : false 21 | }, 22 | "rules": { 23 | "key-spacing" : 0, 24 | "jsx-quotes" : [2, "prefer-single"], 25 | "max-len" : [2, 160, 2], 26 | "object-curly-spacing" : [2, "always"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | 6 | dist 7 | coverage 8 | 9 | typings 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install -g yarn 12 | - yarn install 13 | 14 | script: 15 | - npm run deploy:dev 16 | - npm run deploy:prod 17 | 18 | after_success: 19 | - npm run codecov 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 3.0.0-alpha.0 5 | ------------- 6 | 7 | ### Improvements 8 | * Migrated to Fractal Project Structure, huge thanks to [justingreenberg](https://github.com/justingreenberg). See https://github.com/davezuko/react-redux-starter-kit/pull/684 for details and discussion. 9 | 10 | 2.0.0 11 | ----- 12 | 13 | ### Features 14 | * Upgraded `eslint-plugin-react` to `^5.0.0` 15 | * Upgraded `fs-extra` to `^0.28.0` 16 | 17 | ### Improvements 18 | * Updated syntax used for `createStore` to match `redux@^3.1.0` 19 | * Cleaned up `connect` decorator in `HomeView` 20 | * Cleaned up flow types in `HomeView` 21 | 22 | 2.0.0-alpha.5 23 | ------------- 24 | 25 | ### Features 26 | * Upgraded `flow-bin` to `0.23.0` 27 | * Upgraded `fs-extra` to `^0.27.0` 28 | 29 | ### Improvements 30 | * Minor cleanup in Karma configuration 31 | * Added missing node-style index files in blueprints 32 | 33 | ### Fixes 34 | * Modified webpack manifest initialization to prevent syntax errors in some environments (https://github.com/davezuko/react-redux-starter-kit/issues/572) 35 | 36 | 2.0.0-alpha.4 37 | ------------- 38 | 39 | ### Features 40 | * Upgraded `react` to `^15.0.0` 41 | * Upgraded `react-dom` to `^15.0.0` 42 | * Upgraded `react-addons-test-utils` to `^15.0.0` 43 | * Upgraded `eslint-plugin-flow-vars` to `^0.3.0` 44 | 45 | ### Improvements 46 | * Updated `npm run deploy` to be environment agnostic (no longer forces `production`) 47 | * Added `npm run deploy:prod` (forces `production`, acts as old `npm run deploy`) 48 | * Added `npm run deploy:dev` (forces `development`) 49 | 50 | ### Fixes 51 | * Removed `strip_root` option in Flow to support Nuclide 52 | * Updated webpack development configuration to use correct `public_path` 53 | 54 | 55 | 2.0.0-alpha.3 56 | ------------- 57 | 58 | ### Features 59 | * Upgraded `flow-interfaces` to `^0.6.0` 60 | 61 | ### Improvements 62 | * Moved dependencies needed for production builds from devDependencies to regular dependencies 63 | 64 | ### Fixes 65 | * Production configuration now generates assets with absolute rather than relative paths 66 | 67 | ### Deprecations 68 | * Removed `eslint-loader` for performance reasons 69 | 70 | 2.0.0-alpha.2 71 | ------------- 72 | 73 | ### Features 74 | * Upgraded `eslint` to `^2.4.0` 75 | * Upgraded `babel-eslint` to `^6.0.0-beta.6` 76 | * Upgraded `better-npm-run` to `0.0.8` 77 | * Upgraded `phantomjs-polyfill` to `0.0.2` 78 | * Upgraded `karma-mocha-reporter` to `^2.0.0` 79 | * Upgraded `webpack` to `^1.12.14` 80 | * Upgraded `redux-thunk` to `^2.0.0` 81 | 82 | ### Improvements 83 | * Added `index.js` files for blueprints for convenient imports 84 | 85 | ### Fixes 86 | * Removed some `cssnano` options that caused potential conflicts with css modules 87 | * Updated flow to understand global webpack definitions 88 | 89 | 2.0.0-alpha.1 90 | ------------- 91 | 92 | ### Features 93 | * Upgraded `react-router-redux` from `^4.0.0-beta` to `^4.0.0` 94 | 95 | 2.0.0-alpha.0 96 | ------------- 97 | 98 | ### Features 99 | * Integrated with [redux-cli](https://github.com/SpencerCDixon/redux-cli) 100 | * Added support for [Flowtype](http://flowtype.org/) 101 | * Added `npm run flow:check` script 102 | * Added [chai-enzyme](https://github.com/producthunt/chai-enzyme) 103 | * Added `babel-plugin-transform-react-constant-elements` in production 104 | * Added `babel-plugin-transform-react-remove-prop-types` in production 105 | * Added `eslint-plugin-flowvars` 106 | * Added `better-npm-run` 107 | * Added loader for `.otf` files 108 | * Added `nodemon` for local server development 109 | * Added placeholder favicon, `humans.txt`, and `robots.txt` 110 | * Replaced `express` with `koa@^2.0.0-alpha` 111 | * Added `koa-proxy` with config support 112 | * Added `koa-conntect-history-api-fallback` 113 | * Upgraded `eslint` to `^2.0.0` 114 | * Upgraded `babel-eslint` to `^5.0.0` 115 | * Upgraded `eslint-plugin-react` to `^4.0.0` 116 | * Upgraded `yargs` to `^4.0.0` 117 | * Upgraded `html-webpack-plugin` from `^1.6.1` to `^2.7.1` 118 | * Upgraded `react-router` to `^2.0.0` 119 | * Replaced `redux-simple-router` with `react-router-redux` 120 | * Replaced `phantomjs` with `phantomjs-prebuilt` 121 | * Replaced Karma spec reporter with mocha reporter 122 | 123 | ### Improvements 124 | * Webpack optimization plugins are now correctly used only in production 125 | * Added ability to simultaneously use CSS modules and regular CSS 126 | * Added `karma-webpack-with-fast-source-maps` for selective and faster test rebuilds 127 | * Simplified environment-based webpack configuration 128 | * Fixed CSS being minified twice with both `cssnano` and `css-loader` 129 | * Updated `cssnano` to not use unsafe options by default 130 | * Redux devtools now looks for the browser extension if available 131 | * Added webpack entry point for tests to replace file globs in Karma 132 | * Made Webpack compiler script generic so it can accept any webpack configuration file 133 | * Added sample tests for counter redux module 134 | * Replaced `react-hmre` with `redbox-react` and `react-transform-hmr` 135 | * Disabled verbose uglify warnings during compilation 136 | * Updated route definition file to have access to the redux store 137 | * Updated server start message so link is clickable 138 | * `ExtractTextPlugin` is now correctly used whenever HMR is disabled 139 | * `npm run deploy` now cleans out `~/dist` directory 140 | * Miscellaneous folder structure improvements 141 | * Removed unnecessary `bin` file for Karma 142 | * Removed unnecessary `NotFoundView` 143 | * Re-enabled support for `.jsx` files 144 | * Specified compatible Node and NPM engines 145 | 146 | ### Fixes 147 | * Fixed some development-only code not being stripped from production bundles 148 | * Added rimraf for `~/dist` clearing to support Windows users 149 | * Fixed miscellaneous path issues for Windows users 150 | * Fixed source maps for Sass files 151 | * Updated server start debug message to display correct host 152 | 153 | ### Deprecations 154 | * Removed `redux-actions` 155 | * Removed `dotenv` 156 | * Removed `add-module-exports` babel plugin 157 | 158 | 1.0.0 159 | ----- 160 | 161 | ### Features 162 | * Upgraded from Babel 5 to Babel 6 :tada: 163 | * Added script to copy static assets from ~src/assets to ~/dist during compilation 164 | * Added CSS Modules (can be toggled on/off in config file) 165 | * Enabled source maps for CSS 166 | * Added `postcss-loader` 167 | * Added `debug` module to replace `console.log` 168 | * Added `json-loader` 169 | * Added `url-loader` for `(png|jpg)` files 170 | * Added `redux-actions` with demo 171 | * Upgraded `redux-devtools` from `^3.0.0-beta` to `^3.0.0` 172 | * Upgraded `redux-simple-router` from `^0.0.10` to `^1.0.0` 173 | * Upgraded `isparta` from `^2.0.0` to `^3.0.0` 174 | * Replaced `karma-sinon-chai` with `karma-chai-sinon` for peerDependencies fix 175 | * Added sample asynchronous action 176 | * Added example `composes` style to demo CSS modules in `HomeView` 177 | * Added `lint:fix` npm script 178 | * Added CONTRIBUTING document 179 | * Added placeholder favicon 180 | 181 | ### Improvements 182 | * Refactored application to follow ducks-like architecture 183 | * Improved how configuration determines when to apply HMR-specific Babel transforms 184 | * Replaced explicit aliases with `resolve.root` 185 | * Renamed karma configuration file to more widely-known `karma.conf` 186 | * Made `CoreLayout` a pure (stateless) component 187 | * Renamed debug namespace from `kit:*` to `app:*` 188 | * Standardized coding conventions 189 | * Added ability to easily specify environment-specific configuration overrides 190 | * Extended available configuration options 191 | * Improved miscellaneous documentation 192 | * Refactored webpack middleware in express server into separate files 193 | 194 | ### Fixes 195 | * Fixed DevTools imports so they are longer included in production builds 196 | * Added CSS best practices to root tag, node, and `core.scss` file 197 | * Disabled manifest extraction due to broken production builds 198 | * Updated Webpack dev server uses explicit publicPath during live development 199 | * Fixed Karma running tests twice after file change during watch mode 200 | 201 | ### Deprecations 202 | * Removed `eslint-config-airbnb` 203 | * Deprecated support for Node `^4.0.0` 204 | 205 | 0.18.0 206 | ----- 207 | 208 | ### Features 209 | * Replaces `webpack-dev-server` with `Express` and webpack middleware 210 | * Replaces `redux-router` with `redux-simple-router` 211 | * Use `postcss-loader` for autoprefixing rather than autoprefixer-loader 212 | * Configuration will now warn you of missing dependencies for vendor bundle 213 | * Upgrade `react-router` from `1.0.0-rc1` -> `^1.0.0` 214 | * Upgrade `css-loader` from `0.21.0` -> `0.23.0` 215 | * Upgrade `eslint-config-airbnb` from `0.1.0` to `^1.0.0` 216 | * Upgrade `karma-spec-reporter` from `0.0.21` to `0.0.22` 217 | * Upgrade `extract-text-webpack-plugin` from `^0.8.0` to `^0.9.0` 218 | 219 | ### Improvements 220 | * Compiled `index.html` is now minified 221 | * Content hashes are now injected directly into the filename rather than appended as query strings 222 | * Better reporting of webpack build status 223 | * Use object-style configuration for `sass-loader` rather than inline query string 224 | * Rename `test:lint` task to `lint:tests` 225 | * Various documentation improvements 226 | 227 | ### Fixes 228 | * Content hash is now longer duplicated in CSS bundle 229 | * Karma plugins are autoloaded now, rather than explicitly defined 230 | * Removes extraneous wrapping div in `Root` container 231 | * Fixes awkwardly named arguments to `createReducer` utility 232 | * Add missing alias to `~/src/store` 233 | 234 | 0.17.0 235 | ------ 236 | 237 | ### Features 238 | * Karma coverage now generates proper coverage reports 239 | * Added chai-as-promised 240 | * Added `npm run lint` script to lint all `~/src` code 241 | * Added `npm run test:lint` script to lint all `*.spec.js` files in `~/tests` 242 | * Updated `npm run deploy` to explicitly run linter on source code 243 | * Added `dotenv` (thanks [dougvk](https://github.com/dougvk)) 244 | 245 | ### Improvements 246 | * Renamed application entry point from `index.js` -> `app.js` (clarifies intent and helps with coverage reports) 247 | * Refactored sample counter constants and actions to their appropriate locations (thanks [kyleect](https://github.com/kyleect)) 248 | * Devtools in `npm run dev:nw` now take up the full window (thanks [jhgg](https://github.com/jhgg)) 249 | * Webpack no longer runs an eslint pre-loader (cleans up console messages while developing) 250 | * Moved tests into their own directory (alleviates lint/organization issues) 251 | * Renamed `stores` to `store` to be more intuitive 252 | * Webpack-dev-server now uses a configurable host (thanks [waynelkh](https://github.com/waynelkh)) 253 | * Sass-loader is now configured independently of its loader definition 254 | * Upgraded `redux-devtools` from `^2.0.0` -> `^3.0.0` 255 | * Upgraded `react-transform-catch-errors` from `^0.1.0` -> `^1.0.0` 256 | 257 | ### Fixes 258 | * Fix .editorconfig missing a setting that caused it to not be picked up in all IDE's 259 | * Cleans up miscellaneous lint errors. 260 | 261 | 262 | 0.16.0 263 | ------ 264 | 265 | ### Features 266 | * Adds redux-router (thanks to [dougvk](https://github.com/dougvk)) 267 | * Adds redux-thunk middleware 268 | * Adds loaders for font files (thanks to [nodkz](https://github.com/nodkz)) 269 | * Adds url loader 270 | * Upgrades React dependencies to stable `^0.14.0` 271 | * Upgrades react-redux to `^4.0.0` 272 | 273 | ### Improvements 274 | * Cleans up unused configuration settings 275 | * configureStore no longer relies on a global variable to determine whether or not to enable devtool middleware 276 | * Removes unused invariant and ImmutableJS vendor dependencies 277 | * Removes unused webpack-clean plugin 278 | * Tweaks .js loader configuration to make it easier to add json-loader 279 | * Updates counter example to demonstrate `mapDispatchToProps` 280 | * Force `components` directory inclusion 281 | * Documentation improvements 282 | 283 | 0.15.2 284 | ------ 285 | 286 | ### Fixes 287 | * Remove unused/broken "minify" property provided to HtmlWebpackPlugin configuration. 288 | 289 | 0.15.1 290 | ------ 291 | 292 | ### Fixes 293 | * Dev server now loads the correct Webpack configuration with HMR enabled. 294 | * Redbox-React error catcher is now loaded correctly in development. 295 | 296 | 0.15.0 297 | ------ 298 | 299 | ### Fixes 300 | * HMR is now longer enabled for simple compilations. You can now compile development builds that won't constantly ping a non-existent dev server. 301 | * react-transform-hmr now only runs when HMR is enabled. 302 | 303 | ### Improvements 304 | * Unit tests now only run in watch mode when explicitly requested. This makes it much more convenient to run tests on any environment without having to struggle with the `singleRun` flag in Karma. 305 | * There is now only a single webpack configuration (rather than one for the client and one for the server). As a result, configuration has once again been split into a base configuration which is then extended based on the current `NODE_ENV`. 306 | 307 | ### Deprecations 308 | * Removed Koa server (sad days). 309 | 310 | 0.14.0 311 | ------ 312 | 313 | #### Features 314 | * Replaces `react-transform-webpack-hmr` with its replacement `react-transform-hmr`. Thanks to [daviferreira](https://github.com/daviferreira). 315 | * Replaces `delicate-error-reporter` with `redbox-react`. Thanks to [bulby97](https://github.com/bulby97). 316 | * Created a `no-server` branch [here](https://github.com/davezuko/react-redux-starter-kit/tree/no-server) to make it easier for users who don't care about Koa. 317 | 318 | #### Improvements 319 | * Renames `client` directory to `src` to be more intuitive. 320 | * `inline-source-map` has been replaced by `source-map` as the default webpack devTool to reduce build sizes. 321 | * Refactors configuration file to focus on commonly-configured options rather than mixing them with internal configuration. 322 | * Swaps `dev` and `dev:debug` so debug tools are now enabled by default and can be disabled instead with `dev:no-debug`. 323 | * Repositions Redux devtools so they no longer block errors displayed by `redbox-react`. 324 | * Adds explicit directory references to some `import` statements to clarify which are from from `npm` and which are local. 325 | 326 | #### Fixes 327 | * Fixes naming in `HomeView` where `mapStateToProps` was incorrectly written as `mapDispatchToProps`. 328 | 329 | #### Deprecations 330 | * Removes local test utilities (in `~/src/utils/test`). 331 | 332 | 0.13.0 333 | ------ 334 | 335 | #### Features 336 | * Adds `react-transform-catch-errors` along with `delicate-error-reporter`. Thanks [bulby97](https://github.com/bulby97) for this! 337 | 338 | #### Fixes 339 | * ExtractTextPlugin is once again production only. This fixes an issue where styles wouldn't be hot reloaded with Webpack. 340 | 341 | 0.12.0 342 | ------ 343 | 344 | #### Features 345 | * Upgrades react-router to `^3.0.0`. This is the only reason for the minor-level version bump. 346 | * Webpack now uses OccurrenceOrderPlugin to produce consistent bundle hashes. 347 | 348 | #### Fixes 349 | * Adds `history` to vendor dependencies to fix HMR caused by upgrade to react-router `1.0.0-rc` 350 | 351 | #### Improvements 352 | * Server no longer modifies initial counter state by default. 353 | * Adds invariant error in route rendering method to enforce router state definition through props. 354 | 355 | 0.11.0 356 | ------ 357 | 358 | #### Features 359 | * Upgrades all React dependencies to `0.14.0-rc1` 360 | * Upgrades react-router to `1.0.0-rc` 361 | * Updates client and server rendering accordingly 362 | * Adds Sinon-Chai for improved assertions and function spies 363 | * Adds option to disable eslint when in development 364 | 365 | #### Improvements 366 | * Improved example unit tests using react-addons-test-utils and Sinon Chai 367 | 368 | 0.10.0 369 | ------ 370 | 371 | #### Features 372 | * Initial state can now be injected from the server (still WIP). 373 | * Adds react-addons-test-utils as a devDependency. 374 | 375 | #### Improvements 376 | * Eslint no longer prevents webpack from bundling in development mode if an error is emitted. 377 | * See: https://github.com/MoOx/eslint-loader/issues/23 378 | * Updates all `.jsx` files to `.js`. (https://github.com/davezuko/react-redux-starter-kit/issues/37) 379 | * Updates all React component file names to be ProperCased. 380 | 381 | 0.9.0 382 | ----- 383 | 384 | #### Features 385 | * Koa server now uses gzip middleware. 386 | 387 | #### Improvements 388 | * Switches out react-hot-loader in favor of [react-transform-webpack-hmr](https://github.com/gaearon/react-transform-webpack-hmr). 389 | * Eslint configuration now uses Airbnb's configuration (slightly softened). 390 | * Migrates all actual development dependencies to devDependencies in `package.json`. 391 | * Example store and view are now more intuitive (simple counter display). 392 | * CSS-loader dependency upgraded from `0.16.0` to `0.17.0`. 393 | 394 | #### Deprecations 395 | * Removes unnecessary object-assign dependency. 396 | 397 | 0.8.0 398 | ----- 399 | 400 | #### Improvements 401 | * All build-, server-, and client-related code is now ES6. 402 | * Significantly refactors how client and server webpack configs are built. 403 | * `reducers/index.js` now exports combined root reducer. 404 | * Client application code now lives in `~/client` instead of `~/src` in order to conform to Redux standards. 405 | 406 | #### Fixes 407 | * Redux store now explicitly handles HMR. 408 | 409 | #### Changes 410 | * Webpack compiler configurations are no longer merged on top of a base default configuration. This can become unwieldy and even though explicitly writing each configuration file out is more verbose, it ends up being more maintainable. 411 | 412 | #### Deprecations 413 | * Quiet mode has been removed (`npm run dev:quiet`). 414 | 415 | 0.7.0 416 | ----- 417 | #### New Features 418 | * Support for redux-devtools in separate window with `dev:debugnw` 419 | - Thanks to [mlusetti](https://github.com/mlusetti) 420 | 421 | #### Improvements 422 | * Upgrades react to `0.14.0-beta3` 423 | * Upgrades react to `0.14.0-beta3` 424 | * Upgrades redux to `^2.0.0` 425 | * Upgrades redux-devtools to `^2.0.0` 426 | * Upgrades react-redux to `^2.0.0` 427 | 428 | #### Fixes 429 | * Configuration file name trimming on Windows machines 430 | - Thanks to [nuragic](https://github.com/nuragic) 431 | 432 | 0.6.0 433 | ----- 434 | 435 | #### Fixes 436 | * Fixes potential spacing issues when Webpack tries to load a config file. 437 | - Thanks to [nuragic](https://github.com/nuragic) for his [PR](https://github.com/davezuko/react-redux-starter-kit/pull/32) 438 | 439 | #### Improvements 440 | * Upgrades koa to `1.0.0` 441 | * Upgrades react-redux to `1.0.0` 442 | * Upgrades object-assign to `0.4.0` 443 | 444 | 0.5.0 445 | ----- 446 | 447 | #### Improvements 448 | * Restructures src directory so filenames are more identifiable. 449 | 450 | #### Breaking Changes 451 | * Removes action-creators alias as it's unlikely to be used. 452 | 453 | 0.4.0 454 | ----- 455 | 456 | #### Improvements 457 | * Cleans up/removes example code per https://github.com/davezuko/react-redux-starter-kit/issues/20 458 | 459 | 0.3.1 460 | ----- 461 | 462 | #### Fixes 463 | * https://github.com/davezuko/react-redux-starter-kit/issues/19 464 | - Invalid initialStates from server-side router will now yield to the next middleware. 465 | 466 | 0.3.0 467 | ----- 468 | 469 | #### Improvements 470 | * Bumps Redux version to first major release. 471 | * Bumps Redux-devtools version to first major release. 472 | 473 | #### Fixes 474 | * Fixes broken hot-reload in `:debug` mode. 475 | - Temporarily fixed by moving `redux-devtools` into the vendor bundle. 476 | 477 | 0.2.0 478 | ----- 479 | 480 | #### Improvements 481 | * Weakens various eslint rules that were too opinionated. 482 | - notable: `one-var` and `key-spacing`. 483 | 484 | Thanks to [StevenLangbroek](https://github.com/StevenLangbroek) for the following: 485 | * Adds alias `utils` to reference `~/src/utils` 486 | * Adds `createConstants` utility. 487 | * Adds `createReducer` utility. 488 | * Refactors `todos` reducer to use a function map rather than switch statements. 489 | 490 | #### Fixes 491 | * Nested routes are now loaded correctly in react-router when using BrowserHistory. 492 | * Bundle compilation now fails if an eslint error is encountered when running a production build. 493 | - Thanks [clearjs](https://github.com/clearjs) 494 | * Upgrades all outdated dependencies. 495 | - Karma, eslint, babel, sass-loader, and a handful more. 496 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #271 feat(standard): add style config and refactor to match 29 | #270 fix(config): only override publicPath when served by webpack 30 | #269 feat(eslint-config-defaults): replace eslint-config-airbnb 31 | #268 feat(config): allow user to configure webpack stats output 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `babel`, `redux` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Zukowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Redux BabylonJS Starter Kit 2 | I was interested in 3D engines and looked into BabylonJS and wanted to look at how to get BabylonJS working within a React/Redux project. 3 | 4 | My original starting point was the Quarto game from http://www.pixelcodr.com/projects.html (https://github.com/Temechon/Quarto) 5 | 6 | Temechon last updated that game in 2014 and I wanted to port it over using React for the UI and try to build a bridge between Redux and BabylonJS. 7 | I wanted it also written in ES6 or TypeScript, so the davezuko starter kit was chosen as a starting point (this is a fork). 8 | 9 | # Deprecated 10 | The starter kit that this project is a fork of added a deprecation notice 2017-07. I have updated BabylonJS to 3.2 alpha (2018-01), but am not going to catch up the last 24 commits. Check out my CRA starter repo: 11 | [Create React App Typescript BabylonJS](https://github.com/brianzinn/create-react-app-typescript-babylonjs) 12 | 13 | # Overview 14 | In the Redux world you have action creators generating 'events' that flow through the reducer to generate a minimal state. The structure of this starter project is that there is a container (Selector) to generate the props for your components. This starter project uses a fractal design, so the game is not loaded until you visit the Route. I've used this starter kit on a couple of other projects and I really like how easy it is to work with. 15 | 16 | The original starter kit tries to be as unopinionated as possible. I brought in a few libraries to convert over more to my way of thinking: 17 | 1. redux-saga - Used to monitor events and generate events (ie: game won). This is because the reducer should be pure and not have side-effects. Using sagas I was able to keep my reducer pure and just generate state for the React components on the page. 18 | 2. react-babylonJS - This is the Scene component and redux middleware. I created an NPM so that it could be worked on separately and demonstrated here. 19 | 20 | The original game was using BabylonJS 1.13. I updated it to the latest version, which was since updated to TypeScript. There were only a couple of breaking changes. I also added Bootstrap, but it's really only used for the button to show/hide BabylonJS debug window. 21 | 22 | I also bought the book [Learning BabylonJS] (http://learningbabylonjs.com/), which was authored by the same person that wrote the original Quarto. I added a couple of concepts from the book like shadows and a skybox - it's a great book to dial a lot of different concepts and the book has been updated since. 23 | 24 | # Getting Started 25 | There are more detailed instructions from [github.com/davezuko/react-redux-starter-kit](https://github.com/davezuko/react-redux-starter-kit/). 26 | 27 | ```sh 28 | $ git clone https://github.com/brianzinn/react-redux-babylonjs-starter-kit.git 29 | $ cd 30 | ``` 31 | Then install dependencies and check to see it works. It is recommended that you use [Yarn](https://yarnpkg.com/) for deterministic installs, but `npm install` will work just as well. 32 | 33 | ```bash 34 | $ yarn install # Install project dependencies 35 | $ yarn start # Compile and launch (same as `npm start`) 36 | ``` 37 | 38 | You can see a full working demo here (with BJS 2.5), which is a copy of /dist/ folder generated by starter kit: 39 | ```sh 40 | $ npm run deploy:prod 41 | ``` 42 | [Starter Kit Demo](https://brianzinn.github.io/react-redux-babylonjs-starter-kit/) 43 | 44 | If everything works, you should see the following (shown here with Redux devtools): 45 | ![Quarto Screenshot](https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/master/quarto_screenshot.png) 46 | 47 | It only takes a few minutes to get it running on your own machine... -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const webpack = require('webpack') 3 | const debug = require('debug')('app:bin:compile') 4 | const webpackConfig = require('../config/webpack.config') 5 | const project = require('../config/project.config') 6 | 7 | // Wrapper around webpack to promisify its compiler and supply friendly logging 8 | const webpackCompiler = (webpackConfig) => 9 | new Promise((resolve, reject) => { 10 | const compiler = webpack(webpackConfig) 11 | 12 | compiler.run((err, stats) => { 13 | if (err) { 14 | debug('Webpack compiler encountered a fatal error.', err) 15 | return reject(err) 16 | } 17 | 18 | const jsonStats = stats.toJson() 19 | debug('Webpack compile completed.') 20 | debug(stats.toString(project.compiler_stats)) 21 | 22 | if (jsonStats.errors.length > 0) { 23 | debug('Webpack compiler encountered errors.') 24 | debug(jsonStats.errors.join('\n')) 25 | return reject(new Error('Webpack compiler encountered errors')) 26 | } else if (jsonStats.warnings.length > 0) { 27 | debug('Webpack compiler encountered warnings.') 28 | debug(jsonStats.warnings.join('\n')) 29 | } else { 30 | debug('No errors or warnings encountered.') 31 | } 32 | resolve(jsonStats) 33 | }) 34 | }) 35 | 36 | const compile = () => { 37 | debug('Starting compiler.') 38 | return Promise.resolve() 39 | .then(() => webpackCompiler(webpackConfig)) 40 | .then(stats => { 41 | if (stats.warnings.length && project.compiler_fail_on_warning) { 42 | throw new Error('Config set to fail on warning, exiting with status code "1".') 43 | } 44 | debug('Copying static assets to dist folder.') 45 | fs.copySync(project.paths.public(), project.paths.dist()) 46 | }) 47 | .then(() => { 48 | debug('Compilation completed successfully.') 49 | }) 50 | .catch((err) => { 51 | debug('Compiler encountered an error.', err) 52 | process.exit(1) 53 | }) 54 | } 55 | 56 | compile() 57 | -------------------------------------------------------------------------------- /bin/dev-server.js: -------------------------------------------------------------------------------- 1 | const project = require('../config/project.config') 2 | const server = require('../server/main') 3 | const debug = require('debug')('app:bin:dev-server') 4 | 5 | server.listen(project.server_port) 6 | debug(`Server is now running at http://localhost:${project.server_port}.`) 7 | -------------------------------------------------------------------------------- /config/environments.config.js: -------------------------------------------------------------------------------- 1 | // Here is where you can define configuration overrides based on the execution environment. 2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and 3 | // the base configuration will apply your overrides before exporting itself. 4 | module.exports = { 5 | // ====================================================== 6 | // Overrides when NODE_ENV === 'development' 7 | // ====================================================== 8 | // NOTE: In development, we use an explicit public path when the assets 9 | // are served webpack by to fix this issue: 10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 11 | development : (config) => ({ 12 | compiler_public_path : `http://${config.server_host}:${config.server_port}/` 13 | }), 14 | 15 | // ====================================================== 16 | // Overrides when NODE_ENV === 'production' 17 | // ====================================================== 18 | production : (config) => ({ 19 | compiler_public_path : '/react-redux-babylonjs-starter-kit/', 20 | compiler_fail_on_warning : false, 21 | compiler_hash_type : 'chunkhash', 22 | compiler_devtool : null, 23 | compiler_stats : { 24 | chunks : true, 25 | chunkModules : true, 26 | colors : true 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /config/karma.config.js: -------------------------------------------------------------------------------- 1 | const argv = require('yargs').argv 2 | const project = require('./project.config') 3 | const webpackConfig = require('./webpack.config') 4 | const debug = require('debug')('app:config:karma') 5 | 6 | debug('Creating configuration.') 7 | const karmaConfig = { 8 | basePath : '../', // project root in relation to bin/karma.js 9 | files : [ 10 | { 11 | pattern : `./${project.dir_test}/test-bundler.js`, 12 | watched : false, 13 | served : true, 14 | included : true 15 | } 16 | ], 17 | singleRun : !argv.watch, 18 | frameworks : ['mocha'], 19 | reporters : ['mocha'], 20 | preprocessors : { 21 | [`${project.dir_test}/test-bundler.js`] : ['webpack'] 22 | }, 23 | browsers : ['PhantomJS'], 24 | webpack : { 25 | devtool : 'cheap-module-source-map', 26 | resolve : Object.assign({}, webpackConfig.resolve, { 27 | alias : Object.assign({}, webpackConfig.resolve.alias, { 28 | sinon : 'sinon/pkg/sinon.js' 29 | }) 30 | }), 31 | plugins : webpackConfig.plugins, 32 | module : { 33 | noParse : [ 34 | /\/sinon\.js/ 35 | ], 36 | loaders : webpackConfig.module.loaders.concat([ 37 | { 38 | test : /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 39 | loader : 'imports?define=>false,require=>false' 40 | } 41 | ]) 42 | }, 43 | // Enzyme fix, see: 44 | // https://github.com/airbnb/enzyme/issues/47 45 | externals : Object.assign({}, webpackConfig.externals, { 46 | 'react/addons' : true, 47 | 'react/lib/ExecutionEnvironment' : true, 48 | 'react/lib/ReactContext' : 'window' 49 | }), 50 | sassLoader : webpackConfig.sassLoader, 51 | postcss : webpackConfig.postcss 52 | }, 53 | webpackMiddleware : { 54 | noInfo : true 55 | }, 56 | coverageReporter : { 57 | reporters : project.coverage_reporters 58 | } 59 | } 60 | 61 | if (project.globals.__COVERAGE__) { 62 | karmaConfig.reporters.push('coverage') 63 | karmaConfig.webpack.module.preLoaders = [{ 64 | test : /\.(js|jsx)$/, 65 | include : new RegExp(project.dir_client), 66 | exclude : /node_modules/, 67 | loader : 'babel', 68 | query : Object.assign({}, project.compiler_babel, { 69 | plugins : (project.compiler_babel.plugins || []).concat('istanbul') 70 | }) 71 | }] 72 | } 73 | 74 | module.exports = (cfg) => cfg.set(karmaConfig) 75 | -------------------------------------------------------------------------------- /config/project.config.js: -------------------------------------------------------------------------------- 1 | /* eslint key-spacing:0 spaced-comment:0 */ 2 | const path = require('path') 3 | const debug = require('debug')('app:config:project') 4 | const argv = require('yargs').argv 5 | const ip = require('ip') 6 | 7 | debug('Creating default configuration.') 8 | // ======================================================== 9 | // Default Configuration 10 | // ======================================================== 11 | const config = { 12 | env : process.env.NODE_ENV || 'development', 13 | 14 | // ---------------------------------- 15 | // Project Structure 16 | // ---------------------------------- 17 | path_base : path.resolve(__dirname, '..'), 18 | dir_client : 'src', 19 | dir_dist : 'dist', 20 | dir_public : 'public', 21 | dir_server : 'server', 22 | dir_test : 'tests', 23 | 24 | // ---------------------------------- 25 | // Server Configuration 26 | // ---------------------------------- 27 | server_host : ip.address(), // use string 'localhost' to prevent exposure on local network 28 | server_port : process.env.PORT || 3000, 29 | 30 | // ---------------------------------- 31 | // Compiler Configuration 32 | // ---------------------------------- 33 | compiler_babel : { 34 | cacheDirectory : true, 35 | plugins : ['transform-runtime'], 36 | presets : ['es2015', 'react', 'stage-0'] 37 | }, 38 | compiler_devtool : 'source-map', 39 | compiler_hash_type : 'hash', 40 | compiler_fail_on_warning : false, 41 | compiler_quiet : false, 42 | compiler_public_path : '/', 43 | compiler_stats : { 44 | chunks : false, 45 | chunkModules : false, 46 | colors : true 47 | }, 48 | compiler_vendors : [ 49 | 'react', 50 | 'react-redux', 51 | 'react-router', 52 | 'redux' 53 | ], 54 | 55 | // ---------------------------------- 56 | // Test Configuration 57 | // ---------------------------------- 58 | coverage_reporters : [ 59 | { type : 'text-summary' }, 60 | { type : 'lcov', dir : 'coverage' } 61 | ] 62 | } 63 | 64 | /************************************************ 65 | ------------------------------------------------- 66 | 67 | All Internal Configuration Below 68 | Edit at Your Own Risk 69 | 70 | ------------------------------------------------- 71 | ************************************************/ 72 | 73 | // ------------------------------------ 74 | // Environment 75 | // ------------------------------------ 76 | // N.B.: globals added here must _also_ be added to .eslintrc 77 | config.globals = { 78 | 'process.env' : { 79 | 'NODE_ENV' : JSON.stringify(config.env) 80 | }, 81 | 'NODE_ENV' : config.env, 82 | '__DEV__' : config.env === 'development', 83 | '__PROD__' : config.env === 'production', 84 | '__TEST__' : config.env === 'test', 85 | '__COVERAGE__' : !argv.watch && config.env === 'test', 86 | '__BASENAME__' : JSON.stringify(process.env.BASENAME || '') 87 | } 88 | 89 | // ------------------------------------ 90 | // Validate Vendor Dependencies 91 | // ------------------------------------ 92 | const pkg = require('../package.json') 93 | 94 | config.compiler_vendors = config.compiler_vendors 95 | .filter((dep) => { 96 | if (pkg.dependencies[dep]) return true 97 | 98 | debug( 99 | `Package "${dep}" was not found as an npm dependency in package.json; ` + 100 | `it won't be included in the webpack vendor bundle. 101 | Consider removing it from \`compiler_vendors\` in ~/config/index.js` 102 | ) 103 | }) 104 | 105 | // ------------------------------------ 106 | // Utilities 107 | // ------------------------------------ 108 | function base () { 109 | const args = [config.path_base].concat([].slice.call(arguments)) 110 | return path.resolve.apply(path, args) 111 | } 112 | 113 | config.paths = { 114 | base : base, 115 | client : base.bind(null, config.dir_client), 116 | public : base.bind(null, config.dir_public), 117 | dist : base.bind(null, config.dir_dist) 118 | } 119 | 120 | // ======================================================== 121 | // Environment Configuration 122 | // ======================================================== 123 | debug(`Looking for environment overrides for NODE_ENV "${config.env}".`) 124 | const environments = require('./environments.config') 125 | const overrides = environments[config.env] 126 | if (overrides) { 127 | debug('Found overrides, applying to default configuration.') 128 | Object.assign(config, overrides(config)) 129 | } else { 130 | debug('No environment overrides found, defaults will be used.') 131 | } 132 | 133 | module.exports = config 134 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const argv = require('yargs').argv 2 | const webpack = require('webpack') 3 | const cssnano = require('cssnano') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | const project = require('./project.config') 7 | const debug = require('debug')('app:config:webpack') 8 | 9 | const __DEV__ = project.globals.__DEV__ 10 | const __PROD__ = project.globals.__PROD__ 11 | const __TEST__ = project.globals.__TEST__ 12 | 13 | debug('Creating configuration.') 14 | const webpackConfig = { 15 | name : 'client', 16 | target : 'web', 17 | devtool : project.compiler_devtool, 18 | resolve : { 19 | root : project.paths.client(), 20 | extensions : ['', '.js', '.jsx', '.json'] 21 | }, 22 | module : {} 23 | } 24 | // ------------------------------------ 25 | // Entry Points 26 | // ------------------------------------ 27 | const APP_ENTRY = project.paths.client('main.js') 28 | 29 | webpackConfig.entry = { 30 | app : __DEV__ 31 | ? [APP_ENTRY].concat(`webpack-hot-middleware/client?path=${project.compiler_public_path}__webpack_hmr`) 32 | : [APP_ENTRY], 33 | vendor : project.compiler_vendors 34 | } 35 | 36 | // ------------------------------------ 37 | // Bundle Output 38 | // ------------------------------------ 39 | webpackConfig.output = { 40 | filename : `[name].[${project.compiler_hash_type}].js`, 41 | path : project.paths.dist(), 42 | publicPath : project.compiler_public_path 43 | } 44 | 45 | // ------------------------------------ 46 | // Externals 47 | // ------------------------------------ 48 | webpackConfig.externals = {} 49 | webpackConfig.externals['react/lib/ExecutionEnvironment'] = true 50 | webpackConfig.externals['react/lib/ReactContext'] = true 51 | webpackConfig.externals['react/addons'] = true 52 | 53 | // ------------------------------------ 54 | // Plugins 55 | // ------------------------------------ 56 | webpackConfig.plugins = [ 57 | new webpack.DefinePlugin(project.globals), 58 | new HtmlWebpackPlugin({ 59 | template : project.paths.client('index.html'), 60 | hash : false, 61 | favicon : project.paths.public('favicon.ico'), 62 | filename : 'index.html', 63 | inject : 'body', 64 | minify : { 65 | collapseWhitespace : true 66 | } 67 | }) 68 | ] 69 | 70 | // Ensure that the compiler exits on errors during testing so that 71 | // they do not get skipped and misreported. 72 | if (__TEST__ && !argv.watch) { 73 | webpackConfig.plugins.push(function () { 74 | this.plugin('done', function (stats) { 75 | if (stats.compilation.errors.length) { 76 | // Pretend no assets were generated. This prevents the tests 77 | // from running making it clear that there were warnings. 78 | throw new Error( 79 | stats.compilation.errors.map(err => err.message || err) 80 | ) 81 | } 82 | }) 83 | }) 84 | } 85 | 86 | if (__DEV__) { 87 | debug('Enabling plugins for live development (HMR, NoErrors).') 88 | webpackConfig.plugins.push( 89 | new webpack.HotModuleReplacementPlugin(), 90 | new webpack.NoErrorsPlugin() 91 | ) 92 | } else if (__PROD__) { 93 | debug('Enabling plugins for production (OccurenceOrder, Dedupe & UglifyJS).') 94 | webpackConfig.plugins.push( 95 | new webpack.optimize.OccurrenceOrderPlugin(), 96 | new webpack.optimize.DedupePlugin(), 97 | new webpack.optimize.UglifyJsPlugin({ 98 | compress : { 99 | unused : true, 100 | dead_code : true, 101 | warnings : false 102 | } 103 | }) 104 | ) 105 | } 106 | 107 | // Don't split bundles during testing, since we only want import one bundle 108 | if (!__TEST__) { 109 | webpackConfig.plugins.push( 110 | new webpack.optimize.CommonsChunkPlugin({ 111 | names : ['vendor'] 112 | }) 113 | ) 114 | } 115 | 116 | // ------------------------------------ 117 | // Loaders 118 | // ------------------------------------ 119 | // JavaScript / JSON 120 | webpackConfig.module.loaders = [{ 121 | test : /\.(js|jsx)$/, 122 | exclude : /node_modules/, 123 | loader : 'babel', 124 | query : project.compiler_babel 125 | }, { 126 | test : /\.json$/, 127 | loader : 'json' 128 | }] 129 | 130 | // ------------------------------------ 131 | // Style Loaders 132 | // ------------------------------------ 133 | // We use cssnano with the postcss loader, so we tell 134 | // css-loader not to duplicate minimization. 135 | const BASE_CSS_LOADER = 'css?sourceMap&-minimize' 136 | 137 | webpackConfig.module.loaders.push({ 138 | test : /\.scss$/, 139 | exclude : null, 140 | loaders : [ 141 | 'style', 142 | BASE_CSS_LOADER, 143 | 'postcss', 144 | 'sass?sourceMap' 145 | ] 146 | }) 147 | webpackConfig.module.loaders.push({ 148 | test : /\.css$/, 149 | exclude : null, 150 | loaders : [ 151 | 'style', 152 | BASE_CSS_LOADER, 153 | 'postcss' 154 | ] 155 | }) 156 | 157 | webpackConfig.sassLoader = { 158 | includePaths : project.paths.client('styles') 159 | } 160 | 161 | webpackConfig.postcss = [ 162 | cssnano({ 163 | autoprefixer : { 164 | add : true, 165 | remove : true, 166 | browsers : ['last 2 versions'] 167 | }, 168 | discardComments : { 169 | removeAll : true 170 | }, 171 | discardUnused : false, 172 | mergeIdents : false, 173 | reduceIdents : false, 174 | safe : true, 175 | sourcemap : true 176 | }) 177 | ] 178 | 179 | // File loaders 180 | /* eslint-disable */ 181 | webpackConfig.module.loaders.push( 182 | { test: /\.woff(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }, 183 | { test: /\.woff2(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }, 184 | { test: /\.otf(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=font/opentype' }, 185 | { test: /\.ttf(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }, 186 | { test: /\.eot(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]' }, 187 | { test: /\.svg(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }, 188 | { test: /\.(png|jpg)$/, loader: 'url?limit=8192' } 189 | ) 190 | /* eslint-enable */ 191 | 192 | // ------------------------------------ 193 | // Finalize Configuration 194 | // ------------------------------------ 195 | // when we don't know the public path (we know it only when HMR is enabled [in development]) we 196 | // need to use the extractTextPlugin to fix this issue: 197 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 198 | if (!__DEV__) { 199 | debug('Applying ExtractTextPlugin to CSS loaders.') 200 | webpackConfig.module.loaders.filter((loader) => 201 | loader.loaders && loader.loaders.find((name) => /css/.test(name.split('?')[0])) 202 | ).forEach((loader) => { 203 | const first = loader.loaders[0] 204 | const rest = loader.loaders.slice(1) 205 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')) 206 | delete loader.loaders 207 | }) 208 | 209 | webpackConfig.plugins.push( 210 | new ExtractTextPlugin('[name].[contenthash].css', { 211 | allChunks : true 212 | }) 213 | ) 214 | } 215 | 216 | module.exports = webpackConfig 217 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | // allow ES6 imports for the typings that do not yet use ES6 style exports. 5 | "allowSyntheticDefaultImports": true 6 | }, 7 | "files": [ 8 | "src/", 9 | "tests/" 10 | ] 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-starter-kit", 3 | "version": "3.0.0-alpha.2", 4 | "description": "Get started with React, Redux, and React-Router!", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.5.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "compile": "better-npm-run compile", 13 | "lint": "eslint bin build config server src tests", 14 | "lint:fix": "npm run lint -- --fix", 15 | "start": "better-npm-run start", 16 | "dev": "better-npm-run dev", 17 | "test": "better-npm-run test", 18 | "test:dev": "npm run test -- --watch", 19 | "deploy": "better-npm-run deploy", 20 | "deploy:dev": "better-npm-run deploy:dev", 21 | "deploy:prod": "better-npm-run deploy:prod", 22 | "codecov": "cat coverage/*/lcov.info | codecov" 23 | }, 24 | "betterScripts": { 25 | "compile": { 26 | "command": "node bin/compile", 27 | "env": { 28 | "DEBUG": "app:*" 29 | } 30 | }, 31 | "dev": { 32 | "command": "nodemon bin/dev-server --ignore dist --ignore coverage --ignore tests --ignore src", 33 | "env": { 34 | "NODE_ENV": "development", 35 | "DEBUG": "app:*" 36 | } 37 | }, 38 | "deploy": { 39 | "command": "npm run lint && npm run test && npm run clean && npm run compile", 40 | "env": { 41 | "DEBUG": "app:*" 42 | } 43 | }, 44 | "deploy:dev": { 45 | "command": "npm run deploy", 46 | "env": { 47 | "NODE_ENV": "development", 48 | "DEBUG": "app:*" 49 | } 50 | }, 51 | "deploy:prod": { 52 | "command": "npm run deploy", 53 | "env": { 54 | "NODE_ENV": "production", 55 | "DEBUG": "app:*", 56 | "BASENAME": "/react-redux-babylonjs-starter-kit/" 57 | } 58 | }, 59 | "start": { 60 | "command": "node bin/dev-server", 61 | "env": { 62 | "DEBUG": "app:*" 63 | } 64 | }, 65 | "test": { 66 | "command": "node ./node_modules/karma/bin/karma start config/karma.config", 67 | "env": { 68 | "NODE_ENV": "test", 69 | "DEBUG": "app:*" 70 | } 71 | } 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/davezuko/react-redux-starter-kit.git" 76 | }, 77 | "author": "David Zukowski (http://zuko.me)", 78 | "license": "MIT", 79 | "dependencies": { 80 | "babel-core": "^6.17.0", 81 | "babel-loader": "^6.2.5", 82 | "babel-plugin-transform-runtime": "^6.15.0", 83 | "babel-preset-es2015": "^6.14.0", 84 | "babel-preset-react": "^6.11.1", 85 | "babel-preset-stage-0": "^6.3.13", 86 | "babel-runtime": "^6.11.6", 87 | "babylonjs": "^3.2.0-alpha2", 88 | "babylonjs-inspector": "^3.2.0-alpha2", 89 | "better-npm-run": "0.0.13", 90 | "classnames": "^2.2.5", 91 | "compression": "^1.6.2", 92 | "css-loader": "^0.26.0", 93 | "cssnano": "^3.7.4", 94 | "debug": "^2.2.0", 95 | "extract-text-webpack-plugin": "^1.0.0", 96 | "file-loader": "^0.9.0", 97 | "fs-extra": "^1.0.0", 98 | "history": "^3.2.1", 99 | "html-webpack-plugin": "^2.22.0", 100 | "imports-loader": "^0.7.0", 101 | "ip": "^1.1.2", 102 | "json-loader": "^0.5.4", 103 | "node-sass": "^4.0.0", 104 | "normalize.css": "^5.0.0", 105 | "postcss-loader": "^1.1.0", 106 | "react": "^15.0.0", 107 | "react-babylonjs": "^0.2.1", 108 | "react-bootstrap": "^0.30.7", 109 | "react-dom": "^15.0.0", 110 | "react-redux": "^5.0.1", 111 | "react-router": "^3.0.0", 112 | "react-router-bootstrap": "^0.23.1", 113 | "redux": "^3.6.0", 114 | "redux-saga": "^0.14.1", 115 | "redux-thunk": "^2.0.0", 116 | "rimraf": "^2.5.4", 117 | "sass-loader": "^4.0.0", 118 | "style-loader": "^0.13.1", 119 | "url-loader": "^0.5.6", 120 | "webpack": "^1.12.14", 121 | "yargs": "^6.3.0" 122 | }, 123 | "devDependencies": { 124 | "babel-eslint": "^7.1.0", 125 | "babel-plugin-istanbul": "^3.0.0", 126 | "chai": "^3.4.1", 127 | "chai-as-promised": "^6.0.0", 128 | "chai-enzyme": "^0.6.1", 129 | "cheerio": "^0.22.0", 130 | "codecov": "^1.0.1", 131 | "enzyme": "^2.0.0", 132 | "eslint": "^3.0.1", 133 | "eslint-config-standard": "^6.0.0", 134 | "eslint-config-standard-react": "^4.0.0", 135 | "eslint-plugin-babel": "^4.0.0", 136 | "eslint-plugin-promise": "^3.0.0", 137 | "eslint-plugin-react": "^6.0.0", 138 | "eslint-plugin-standard": "^2.0.0", 139 | "express": "^4.14.0", 140 | "karma": "^1.0.0", 141 | "karma-coverage": "^1.0.0", 142 | "karma-mocha": "^1.0.1", 143 | "karma-mocha-reporter": "^2.0.0", 144 | "karma-phantomjs-launcher": "^1.0.2", 145 | "karma-webpack-with-fast-source-maps": "^1.9.2", 146 | "mocha": "^3.0.1", 147 | "nodemon": "^1.10.2", 148 | "phantomjs-prebuilt": "^2.1.12", 149 | "react-addons-test-utils": "^15.0.0", 150 | "redbox-react": "^1.2.10", 151 | "sinon": "^1.17.5", 152 | "sinon-chai": "^2.8.0", 153 | "webpack-dev-middleware": "^1.6.1", 154 | "webpack-hot-middleware": "^2.12.2" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/favicon.ico -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/sfx/boom1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/boom1.wav -------------------------------------------------------------------------------- /public/sfx/boom2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/boom2.wav -------------------------------------------------------------------------------- /public/sfx/boom3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/boom3.wav -------------------------------------------------------------------------------- /public/sfx/boom4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/boom4.wav -------------------------------------------------------------------------------- /public/sfx/boom5.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/boom5.wav -------------------------------------------------------------------------------- /public/sfx/xplode1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/xplode1.wav -------------------------------------------------------------------------------- /public/sfx/xplode2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/xplode2.wav -------------------------------------------------------------------------------- /public/sfx/xplode3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/xplode3.wav -------------------------------------------------------------------------------- /public/sfx/xplode4.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/sfx/xplode4.wav -------------------------------------------------------------------------------- /public/shaders/gradient.fragment.fx: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform mat4 worldView; 4 | 5 | varying vec4 vPosition; 6 | varying vec3 vNormal; 7 | 8 | // Offset position 9 | uniform float offset; 10 | // Colors 11 | uniform vec3 topColor; 12 | uniform vec3 bottomColor; 13 | 14 | void main(void) { 15 | 16 | float h = normalize(vPosition + offset).y; 17 | 18 | gl_FragColor = vec4( mix(bottomColor, topColor, max(pow(max(h, 0.0), 1.2), 0.0)), 1.0 ); 19 | } 20 | -------------------------------------------------------------------------------- /public/shaders/gradient.vertex.fx: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | // Attributes 4 | attribute vec3 position; 5 | attribute vec3 normal; 6 | attribute vec2 uv; 7 | 8 | // Uniforms 9 | uniform mat4 worldViewProjection; 10 | 11 | // Varying 12 | varying vec4 vPosition; 13 | varying vec3 vNormal; 14 | 15 | 16 | 17 | void main() { 18 | 19 | vec4 p = vec4( position, 1. ); 20 | 21 | vPosition = p; 22 | vNormal = normal; 23 | 24 | gl_Position = worldViewProjection * p; 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_nx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_nx.jpg -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_ny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_ny.jpg -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_nz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_nz.jpg -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_px.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_px.jpg -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_py.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_py.jpg -------------------------------------------------------------------------------- /public/skybox/TropicalSunnyDay_pz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/public/skybox/TropicalSunnyDay_pz.jpg -------------------------------------------------------------------------------- /quarto_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/quarto_screenshot.png -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const debug = require('debug')('app:server') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const webpackConfig = require('../config/webpack.config') 6 | const project = require('../config/project.config') 7 | const compress = require('compression') 8 | 9 | const app = express() 10 | 11 | // Apply gzip compression 12 | app.use(compress()) 13 | 14 | // ------------------------------------ 15 | // Apply Webpack HMR Middleware 16 | // ------------------------------------ 17 | if (project.env === 'development') { 18 | const compiler = webpack(webpackConfig) 19 | 20 | debug('Enabling webpack dev and HMR middleware') 21 | app.use(require('webpack-dev-middleware')(compiler, { 22 | publicPath : webpackConfig.output.publicPath, 23 | contentBase : project.paths.client(), 24 | hot : true, 25 | quiet : project.compiler_quiet, 26 | noInfo : project.compiler_quiet, 27 | lazy : false, 28 | stats : project.compiler_stats 29 | })) 30 | app.use(require('webpack-hot-middleware')(compiler, { 31 | path: '/__webpack_hmr' 32 | })) 33 | 34 | // Serve static assets from ~/public since Webpack is unaware of 35 | // these files. This middleware doesn't need to be enabled outside 36 | // of development since this directory will be copied into ~/dist 37 | // when the application is compiled. 38 | app.use(express.static(project.paths.public())) 39 | 40 | // This rewrites all routes requests to the root /index.html file 41 | // (ignoring file requests). If you want to implement universal 42 | // rendering, you'll want to remove this middleware. 43 | app.use('*', function (req, res, next) { 44 | const filename = path.join(compiler.outputPath, 'index.html') 45 | compiler.outputFileSystem.readFile(filename, (err, result) => { 46 | if (err) { 47 | return next(err) 48 | } 49 | res.set('content-type', 'text/html') 50 | res.send(result) 51 | res.end() 52 | }) 53 | }) 54 | } else { 55 | debug( 56 | 'Server is being run outside of live development mode, meaning it will ' + 57 | 'only serve the compiled application bundle in ~/dist. Generally you ' + 58 | 'do not need an application server for this and can instead use a web ' + 59 | 'server such as nginx to serve your static files. See the "deployment" ' + 60 | 'section in the README for more information on deployment strategies.' 61 | ) 62 | 63 | // Serving ~/dist by default. Ideally these files should be served by 64 | // the web server and not the app server, but this helps to demo the 65 | // server in production. 66 | app.use(express.static(project.paths.dist())) 67 | } 68 | 69 | module.exports = app 70 | -------------------------------------------------------------------------------- /src/appHistory.js: -------------------------------------------------------------------------------- 1 | import { useRouterHistory } from 'react-router' 2 | import createBrowserHistory from 'history/lib/createBrowserHistory' 3 | 4 | const historyConfig = { 5 | basename: __BASENAME__ 6 | } 7 | 8 | export const appHistory = useRouterHistory(createBrowserHistory)(historyConfig) 9 | 10 | export default appHistory 11 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IndexLink, Link } from 'react-router' 3 | import './Header.scss' 4 | 5 | export const Header = () => ( 6 |
7 |

Quarto Demo

8 | 9 | Home 10 | 11 | {' · '} 12 | 13 | Quarto 14 | 15 | {' · '} 16 | 17 | Counter 18 | 19 |
20 | ) 21 | 22 | export default Header 23 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .route--active { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } 5 | 6 | .header { 7 | margin-top: 35px; 8 | } -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Router } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | 5 | import appHistory from '../appHistory' 6 | 7 | class AppContainer extends Component { 8 | static propTypes = { 9 | routes : PropTypes.object.isRequired, 10 | store : PropTypes.object.isRequired 11 | } 12 | 13 | shouldComponentUpdate () { 14 | return false 15 | } 16 | 17 | render () { 18 | const { routes, store } = this.props 19 | 20 | return ( 21 | 22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | } 29 | 30 | export default AppContainer 31 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux BabylonJS Starter Kit 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from '../../components/Header' 3 | import './CoreLayout.scss' 4 | import '../../styles/core.scss' 5 | 6 | export const CoreLayout = ({ children }) => ( 7 |
8 |
9 |
10 | {children} 11 |
12 |
13 | ) 14 | 15 | CoreLayout.propTypes = { 16 | children : React.PropTypes.element.isRequired 17 | } 18 | 19 | export default CoreLayout 20 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .core-layout__viewport { 2 | padding-top: 4rem; 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout 4 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import createStore from './store/createStore' 4 | import AppContainer from './containers/AppContainer' 5 | 6 | // ======================================================== 7 | // Store Instantiation 8 | // ======================================================== 9 | const initialState = window.___INITIAL_STATE__ 10 | const store = createStore(initialState) 11 | 12 | // ======================================================== 13 | // Render Setup 14 | // ======================================================== 15 | const MOUNT_NODE = document.getElementById('root') 16 | 17 | let render = () => { 18 | const routes = require('./routes/index').default(store) 19 | 20 | ReactDOM.render( 21 | , 22 | MOUNT_NODE 23 | ) 24 | } 25 | 26 | // This code is excluded from production bundle 27 | if (__DEV__) { 28 | if (module.hot) { 29 | // Development render functions 30 | const renderApp = render 31 | const renderError = (error) => { 32 | const RedBox = require('redbox-react').default 33 | 34 | ReactDOM.render(, MOUNT_NODE) 35 | } 36 | 37 | // Wrap render in try/catch 38 | render = () => { 39 | try { 40 | renderApp() 41 | } catch (error) { 42 | console.error(error) 43 | renderError(error) 44 | } 45 | } 46 | 47 | // Setup hot module replacement 48 | module.hot.accept('./routes/index', () => 49 | setImmediate(() => { 50 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 51 | render() 52 | }) 53 | ) 54 | } 55 | } 56 | 57 | // ======================================================== 58 | // Go! 59 | // ======================================================== 60 | render() 61 | -------------------------------------------------------------------------------- /src/routes/Counter/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Counter = (props) => ( 4 |
5 |

Counter: {props.counter}

6 | 9 | {' '} 10 | 13 |
14 | ) 15 | 16 | Counter.propTypes = { 17 | counter : React.PropTypes.number.isRequired, 18 | doubleAsync : React.PropTypes.func.isRequired, 19 | increment : React.PropTypes.func.isRequired 20 | } 21 | 22 | export default Counter 23 | -------------------------------------------------------------------------------- /src/routes/Counter/containers/CounterContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { increment, doubleAsync } from '../modules/counter' 3 | 4 | /* This is a container component. Notice it does not contain any JSX, 5 | nor does it import React. This component is **only** responsible for 6 | wiring in the actions and state necessary to render a presentational 7 | component - in this case, the counter: */ 8 | 9 | import Counter from '../components/Counter' 10 | 11 | /* Object of action creators (can also be function that returns object). 12 | Keys will be passed as props to presentational components. Here we are 13 | implementing our wrapper around increment; the component doesn't care */ 14 | 15 | const mapDispatchToProps = { 16 | increment : () => increment(1), 17 | doubleAsync 18 | } 19 | 20 | const mapStateToProps = (state) => ({ 21 | counter : state.counter 22 | }) 23 | 24 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 25 | 26 | import { createSelector } from 'reselect' 27 | const counter = (state) => state.counter 28 | const tripleCount = createSelector(counter, (count) => count * 3) 29 | const mapStateToProps = (state) => ({ 30 | counter: tripleCount(state) 31 | }) 32 | 33 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 34 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 35 | Selectors are composable. They can be used as input to other selectors. 36 | https://github.com/reactjs/reselect */ 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(Counter) 39 | -------------------------------------------------------------------------------- /src/routes/Counter/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducers' 2 | 3 | export default (store) => ({ 4 | path : 'counter', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent (nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const Counter = require('./containers/CounterContainer').default 13 | const reducer = require('./modules/counter').default 14 | 15 | /* Add the reducer to the store on key 'counter' */ 16 | injectReducer(store, { key: 'counter', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, Counter) 20 | 21 | /* Webpack named bundle */ 22 | }, 'counter') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/routes/Counter/modules/counter.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT' 5 | export const COUNTER_DOUBLE_ASYNC = 'COUNTER_DOUBLE_ASYNC' 6 | 7 | // ------------------------------------ 8 | // Actions 9 | // ------------------------------------ 10 | export function increment (value = 1) { 11 | return { 12 | type : COUNTER_INCREMENT, 13 | payload : value 14 | } 15 | } 16 | 17 | /* This is a thunk, meaning it is a function that immediately 18 | returns a function for lazy evaluation. It is incredibly useful for 19 | creating async actions, especially when combined with redux-thunk! */ 20 | 21 | export const doubleAsync = () => { 22 | return (dispatch, getState) => { 23 | return new Promise((resolve) => { 24 | setTimeout(() => { 25 | dispatch({ 26 | type : COUNTER_DOUBLE_ASYNC, 27 | payload : getState().counter 28 | }) 29 | resolve() 30 | }, 200) 31 | }) 32 | } 33 | } 34 | 35 | export const actions = { 36 | increment, 37 | doubleAsync 38 | } 39 | 40 | // ------------------------------------ 41 | // Action Handlers 42 | // ------------------------------------ 43 | const ACTION_HANDLERS = { 44 | [COUNTER_INCREMENT] : (state, action) => state + action.payload, 45 | [COUNTER_DOUBLE_ASYNC] : (state, action) => state * 2 46 | } 47 | 48 | // ------------------------------------ 49 | // Reducer 50 | // ------------------------------------ 51 | const initialState = 0 52 | export default function counterReducer (state = initialState, action) { 53 | const handler = ACTION_HANDLERS[action.type] 54 | 55 | return handler ? handler(state, action) : state 56 | } 57 | -------------------------------------------------------------------------------- /src/routes/Home/assets/Duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/src/routes/Home/assets/Duck.jpg -------------------------------------------------------------------------------- /src/routes/Home/components/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DuckImage from '../assets/Duck.jpg' 3 | import './HomeView.scss' 4 | 5 | export const HomeView = () => ( 6 |
7 |

Welcome!

8 | This is a duck, because Redux! 12 |
13 | ) 14 | 15 | export default HomeView 16 | -------------------------------------------------------------------------------- /src/routes/Home/components/HomeView.scss: -------------------------------------------------------------------------------- 1 | .duck { 2 | display: block; 3 | width: 120px; 4 | margin: 1.5rem auto; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import HomeView from './components/HomeView' 2 | 3 | // Sync route definition 4 | export default { 5 | component : HomeView 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/Quarto/assets/pick_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/src/routes/Quarto/assets/pick_icon.png -------------------------------------------------------------------------------- /src/routes/Quarto/assets/put_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/react-redux-babylonjs-starter-kit/946140ee692fc8d08fe9b07e27ffbfbdd90ae575/src/routes/Quarto/assets/put_icon.png -------------------------------------------------------------------------------- /src/routes/Quarto/components/Base.js: -------------------------------------------------------------------------------- 1 | import { Mesh, StandardMaterial, Color3, VertexData, ActionManager, SetValueAction } from 'babylonjs' 2 | 3 | export default class Base extends Mesh { 4 | 5 | constructor (name, size, position, scene, line, col) { 6 | super(name, scene) 7 | 8 | var baseMat = new StandardMaterial('baseMat', scene) 9 | baseMat.diffuseColor = Color3.FromInts(187, 173, 171) 10 | // baseMat.specularColor = Color3.Black(); 11 | 12 | // breaking change in 2.0 CreateCylinder only takes options: 13 | // 1.13 signature: height, diameterTop, diameterBottom, tessellation, subdivisions 14 | // { height: number, diameterTop: number, diameterBottom: number, diameter: number, tessellation: number, subdivisions: number, 15 | // arc: number, faceColors: Color4[], faceUV: Vector4[], hasRings: boolean, enclose: boolean, sideOrientation: number } 16 | var data = VertexData.CreateCylinder({ 17 | height: 2, 18 | diameterTop: size, 19 | diameterBottom: size, 20 | tessellation: 60, 21 | subdivisions: scene 22 | }) 23 | 24 | data.applyToMesh(this, false) 25 | data.receiveShadows = true 26 | this.position = position 27 | this.material = baseMat 28 | this.receiveShadows = true 29 | 30 | this.line = line 31 | this.col = col 32 | 33 | this.actionManager = new ActionManager(scene) 34 | this.actionManager.registerAction(new SetValueAction(ActionManager.OnPointerOutTrigger, this.material, 'emissiveColor', this.material.emissiveColor)) 35 | this.actionManager.registerAction(new SetValueAction(ActionManager.OnPointerOverTrigger, this.material, 'emissiveColor', Color3.FromInts(20, 20, 20))) 36 | 37 | this.piece = null 38 | } 39 | 40 | /** 41 | * Update the base color to the player color 42 | * @param player 43 | */ 44 | setToPlayer = function (player) { 45 | this.material.diffuseColor = player.color 46 | }; 47 | 48 | /** 49 | * Reset the base to its initial state 50 | */ 51 | reset = function () { 52 | this.piece = null 53 | }; 54 | 55 | setPiece = function (p) { 56 | this.piece = p 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Gameboard.js: -------------------------------------------------------------------------------- 1 | import { Mesh, ShadowGenerator, Vector3, Color3, StandardMaterial } from 'babylonjs' 2 | import Base from './Base' 3 | 4 | /** 5 | * The quarto gameboard 6 | * @param size 7 | * @param scene 8 | * @constructor 9 | */ 10 | 11 | export default class Gameboard extends Mesh { 12 | 13 | constructor (size, scene, lights) { 14 | // Mesh.call(this, "ground", scene); 15 | super('ground', scene) 16 | 17 | this.position = Vector3.Zero() 18 | 19 | this.bases = [] 20 | 21 | let shadowGenerator = new ShadowGenerator(1024 /* size of shadow map */, lights.point) 22 | shadowGenerator.usePoissonSampling = true // useBlurVarianceShadowMap 23 | shadowGenerator.setDarkness(0.2) 24 | 25 | this.shadows = shadowGenerator 26 | 27 | var space = 1 28 | var baseSize = size / 4 29 | var baseRadius = baseSize / 2 30 | 31 | // Children 32 | for (var l = 0; l < this.LINE; l++) { 33 | // create a cylinder 34 | var col = [] 35 | for (var c = 0; c < this.COL; c++) { 36 | var position = new Vector3(c * baseSize - size / 2 + baseRadius, 0, l * baseSize + (size / 2) * (1 - l) - baseRadius) 37 | var b = new Base(`base${l}-${c}`, baseSize - space, position, scene, l, c) 38 | b.parent = this 39 | 40 | col.push(b) 41 | } 42 | this.bases.push(col) 43 | } 44 | 45 | // console.log(`bases created ${this.LINE} x ${this.COL}`) 46 | var objSize = 1.5 * size 47 | var obj = Mesh.CreateBox('obj', objSize, scene) 48 | obj.rotation.y = Math.PI / 4 49 | obj.scaling.y = 0.04 50 | obj.position.y = -objSize * 0.04 / 2 51 | obj.receiveShadows = true 52 | obj.parent = this 53 | 54 | var objMat = new StandardMaterial('objmat', scene) 55 | // objMat.diffuseTexture = new BABYLON.Texture("img/board_1024.jpg", scene); 56 | objMat.diffuseColor = Color3.FromInts(100, 100, 100) 57 | objMat.specularColor = Color3.Black() // so not blinded by reflection 58 | obj.material = objMat 59 | 60 | // Create a material for selected pieces 61 | var sp = new StandardMaterial('sp', scene) 62 | sp.diffuseColor = Color3.FromInts(241, 216, 39) 63 | // sp.specularColor = Color3.FromInts(241, 216, 39); 64 | // sp.specularPower = 1.0 65 | this.selectedPieceMaterial = sp 66 | }; 67 | 68 | get LINE () { return 4 }; 69 | get COL () { return 4 }; 70 | 71 | addShadowRender = function (mesh) { 72 | this.shadows.getShadowMap().renderList.push(mesh) 73 | } 74 | 75 | getBasePosition = function (i, j) { 76 | return this.bases[i][j].getAbsolutePosition() 77 | }; 78 | 79 | getBase = function (i, j) { 80 | return this.bases[i][j] 81 | }; 82 | 83 | /** 84 | * Reset the gameboard 85 | */ 86 | reset = function () { 87 | this.bases.forEach(function (array) { 88 | array.forEach(function (b) { 89 | b.reset() 90 | }) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Piece.js: -------------------------------------------------------------------------------- 1 | import { Mesh, Vector3, Color3, CSG, StandardMaterial, Animation } from 'babylonjs' 2 | 3 | import Timer from './Timer' 4 | 5 | export default class Piece extends Mesh { 6 | 7 | constructor (position, isTall, isBlack, isCubic, isSolidTop, scene) { 8 | super('piece', scene) 9 | this.scene = scene 10 | 11 | const size = isTall ? this.TALL_SIZE : this.SMALL_SIZE 12 | const color = isBlack ? this.BLACK_COLOR : this.WHITE_COLOR 13 | let meshTemplate 14 | let mesh 15 | 16 | if (isCubic) { 17 | // Create box 18 | meshTemplate = Mesh.CreateBox('box', 1, scene) 19 | meshTemplate.scaling = new Vector3(this.SCALING, size, this.SCALING) 20 | this.scaling = new Vector3(this.SCALING, size, this.SCALING) 21 | } else { 22 | meshTemplate = Mesh.CreateCylinder('cylinder', size, this.SCALING, this.SCALING, 50, scene) 23 | } 24 | 25 | if (!isSolidTop) { 26 | var toRemoveOnTop = Mesh.CreateSphere('toRemove', 10, this.SCALING / 1.5, scene) 27 | toRemoveOnTop.position.y = size / 2 28 | var toRemove = CSG.FromMesh(toRemoveOnTop) 29 | var piece = CSG.FromMesh(meshTemplate) 30 | var res = piece.subtract(toRemove) 31 | 32 | mesh = res.toMesh('piece', null, scene) 33 | 34 | toRemoveOnTop.dispose() 35 | } else { 36 | mesh = meshTemplate.clone() 37 | } 38 | 39 | var g = mesh._geometry 40 | g.applyToMesh(this) 41 | mesh.dispose() 42 | meshTemplate.dispose() 43 | 44 | var m = new StandardMaterial('m', scene) 45 | m.diffuseColor = color 46 | m.emmissiveColor = color 47 | m.specularColor = color 48 | this.material = m 49 | this.oldMaterial = m 50 | 51 | this.position = position.clone() 52 | this.position.y = size / 2 53 | 54 | this.isTall = isTall 55 | this.isBlack = isBlack 56 | this.isCubic = isCubic 57 | this.isSolidTop = isSolidTop 58 | this.isSelected = false 59 | this.isOnBoard = false 60 | this.initialPosition = null 61 | this.size = size 62 | 63 | // this.actionManager = new ActionManager(scene); 64 | // this.actionManager.registerAction(new SetValueAction(ActionManager.OnPointerOutTrigger, this.material, "emissiveColor", this.material.emissiveColor)); 65 | // this.actionManager.registerAction(new SetValueAction(ActionManager.OnPointerOverTrigger, this.material, "emissiveColor", Color3.FromInts(30, 30, 30))); 66 | this.shake = this.shake.bind(this) // TODO: move this to the actual game, since this logic does not belong here. 67 | this.randomNumber = this.randomNumber.bind(this) 68 | }; 69 | 70 | get TALL_SIZE () { return 20 } 71 | get SMALL_SIZE () { return this.TALL_SIZE / 2 } 72 | get SCALING () { return 10 } 73 | 74 | get BLACK_COLOR () { return Color3.FromInts(72, 73, 74) } 75 | get WHITE_COLOR () { return Color3.FromInts(245, 245, 245) } 76 | 77 | /** 78 | * Select or unselect this piece. 79 | * @param isSelected 80 | * @param material The material when this piece is selected 81 | */ 82 | setSelected (isSelected, material) { 83 | this.isSelected = isSelected 84 | 85 | if (this.isSelected) { 86 | this.oldMaterial = this.material 87 | this.material = material 88 | } else { 89 | this.material = this.oldMaterial 90 | } 91 | }; 92 | 93 | setInitialPosition (pos) { 94 | this.position = pos.clone() 95 | this.initialPosition = pos.clone() 96 | }; 97 | 98 | putOnBoard (base, callback) { 99 | var dst = base.getAbsolutePosition() 100 | 101 | this.animate(dst, callback) 102 | 103 | base.setPiece(this) 104 | 105 | this.isOnBoard = true 106 | } 107 | 108 | // uses bit flags to find rows/cols with for winner. 109 | getCode () { 110 | var code = 0 111 | if (this.isSolidTop) { 112 | code += 1 113 | } 114 | if (this.isCubic) { 115 | code += 2 116 | } 117 | if (this.isBlack) { 118 | code += 4 119 | } 120 | if (this.isTall) { 121 | code += 8 122 | } 123 | return code 124 | } 125 | 126 | randomNumber (min, max) { 127 | if (min === max) { 128 | return (min) 129 | } 130 | var random = Math.random() 131 | return ((random * (max - min)) + min) 132 | } 133 | 134 | animate (dst, callback) { 135 | var oldY = this.position.y 136 | 137 | var _this = this 138 | 139 | // Animation from top to board 140 | var goDown = function () { 141 | var t = new Timer(250, _this.scene, function () { 142 | var translationToBot = new Animation( 143 | 'translationToBot', 144 | 'position', 145 | 60, 146 | Animation.ANIMATIONTYPE_VECTOR3, 147 | Animation.ANIMATIONLOOPMODE_CONSTANT 148 | ) 149 | 150 | var startPos = _this.position.clone() 151 | var endPos = dst.clone() 152 | endPos.y = oldY 153 | // Animation keys 154 | var keys = [ 155 | { 156 | frame:0, 157 | value:startPos 158 | }, 159 | { 160 | frame:100, 161 | value:endPos 162 | } 163 | ] 164 | translationToBot.setKeys(keys) 165 | _this.animations.push(translationToBot) 166 | _this.scene.beginAnimation(_this, 0, 100, false, 20, function () { 167 | _this.shake() 168 | callback() 169 | }) 170 | }) 171 | t.start() 172 | } 173 | 174 | // Animation to top 175 | var translationToTop = new Animation( 176 | 'translationToTop', 177 | 'position', 178 | 60, 179 | Animation.ANIMATIONTYPE_VECTOR3, 180 | Animation.ANIMATIONLOOPMODE_CONSTANT 181 | ) 182 | 183 | var startPos = this.position.clone() 184 | var endPos = dst.clone() 185 | endPos.y = 50 186 | // Animation keys 187 | var keys = [ 188 | { 189 | frame:0, 190 | value:startPos 191 | }, 192 | { 193 | frame:100, 194 | value:endPos 195 | } 196 | ] 197 | translationToTop.setKeys(keys) 198 | this.animations.push(translationToTop) 199 | _this.scene.beginAnimation(this, 0, 100, false, 10, goDown) 200 | } 201 | 202 | shake (value) { 203 | var shakeValue = value || 10 204 | let oldTarget = this.scene.activeCamera.target 205 | let min = -0.5 206 | let max = -min 207 | 208 | let _this = this 209 | 210 | this.scene.registerBeforeRender(function () { 211 | if (shakeValue > 0) { 212 | let dx = _this.randomNumber(min, max) 213 | let dy = _this.randomNumber(min, max) 214 | let dz = _this.randomNumber(min, max) 215 | var target = _this.scene.activeCamera.target 216 | var newTarget = target.add(new Vector3(dx, dy, dz)) 217 | _this.scene.activeCamera.target = newTarget 218 | shakeValue-- 219 | if (shakeValue === 0) { 220 | _this.scene.activeCamera.target = oldTarget 221 | } 222 | } 223 | }) 224 | } 225 | 226 | /** 227 | * Reset the piece to its initial state 228 | */ 229 | reset () { 230 | this.isSelected = false 231 | this.isOnBoard = false 232 | this.position.x = this.initialPosition.x 233 | this.position.y = this.initialPosition.y 234 | this.position.z = this.initialPosition.z 235 | this.resetWinner() 236 | } 237 | 238 | setWinner () { 239 | this.material.diffuseColor = Color3.FromInts(161, 152, 191) 240 | } 241 | 242 | resetWinner () { 243 | if (this.isBlack) { 244 | this.material.diffuseColor = this.BLACK_COLOR 245 | } else { 246 | this.material.diffuseColor = this.WHITE_COLOR 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Player.js: -------------------------------------------------------------------------------- 1 | export default class Player { 2 | constructor (name, color) { 3 | this.name = name 4 | 5 | // The player color 6 | this.color = color 7 | 8 | this.pieces = [] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Quarto.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Button, Panel } from 'react-bootstrap' 3 | import classNames from 'classnames' 4 | import { HemisphericLight, PointLight, Vector3, Color3, PhysicsEngine, OimoJSPlugin, 5 | StandardMaterial, Mesh, CubeTexture, ArcRotateCamera, Texture } from 'babylonjs' 6 | import { Scene, registerHandler, removeHandler } from 'react-babylonjs' 7 | 8 | import classes from './Quarto.scss' 9 | 10 | // import Tutorial from './Tutorial' 11 | import Gameboard from './Gameboard' 12 | import Piece from './Piece' 13 | import Base from './Base' 14 | import Timer from './Timer' 15 | 16 | import PickIconImage from '../assets/pick_icon.png' 17 | import PutIconImage from '../assets/put_icon.png' 18 | 19 | import { START_GAME, PLAYER_PIECE_SELECTED, PLAYER_BASE_SELECTED, GAME_WON } from '../modules/quarto' 20 | 21 | import 'babylonjs-inspector'; 22 | 23 | export default class Quarto extends Component { 24 | constructor (props) { 25 | super(props) 26 | 27 | // this.dispatch = createDispatcher((state) => { 28 | // this.nextState = state 29 | // window.requestAnimationFrame(this.handleNextState) 30 | // }) 31 | // this.dispatch = this.dispatch.bind(this) 32 | 33 | // own methods 34 | this.onPlayersChosen = this.onPlayersChosen.bind(this) 35 | this.onSceneMount = this.onSceneMount.bind(this) 36 | this.onMeshPicked = this.onMeshPicked.bind(this) 37 | this.onWinnerFound = this.onWinnerFound.bind(this) 38 | this.toggleDebug = this.toggleDebug.bind(this) 39 | this.onFocus = this.onFocus.bind(this) 40 | this.onBlur = this.onBlur.bind(this) 41 | 42 | this.initEnvironment = this.initEnvironment.bind(this) 43 | this.setupNewGame = this.setupNewGame.bind(this) 44 | this.selectedMesh = null 45 | 46 | console.log('Quarto props:', props) 47 | 48 | // reducer methods for notifying events in BabylonJs 49 | this.startGame = props.startGame 50 | this.playersChosen = props.playersChosen 51 | this.boardPiecePicked = props.boardPiecePicked 52 | this.boardBasePicked = props.boardBasePicked 53 | 54 | this.debugEnabled = false 55 | } 56 | 57 | toggleDebug () { 58 | if (!this.debugEnabled) { 59 | this.scene.debugLayer.show({ 60 | popup:true 61 | }); 62 | } else { 63 | this.scene.debugLayer.hide(); 64 | } 65 | this.debugEnabled = !this.debugEnabled 66 | } 67 | 68 | // was 'forward' in QUARTO 69 | onPlayersChosen () { 70 | var name1 = this.player1.value || 'Player 1' 71 | var name2 = this.player2.value || 'Player 2' 72 | 73 | // console.log(`triggering redux: playersChosen(${name1}, ${name2})`) 74 | this.playersChosen(name1, name2) 75 | } 76 | 77 | onMeshPicked (mesh, scene) { 78 | // These are purely events happening on the board. The reducer will decide if it is significant 79 | // and change the state accordingly. 80 | if (mesh instanceof Piece) { 81 | this.boardPiecePicked(mesh) 82 | } 83 | 84 | if (mesh instanceof Base) { 85 | this.boardBasePicked(mesh) 86 | } 87 | } 88 | 89 | onWinnerFound (winners) { 90 | let winningCodes = new Set() 91 | winners.forEach(winner => winningCodes.add(winner.code)) 92 | 93 | // console.log('onWinnerFound', winners, winningCodes) 94 | 95 | this.pieces.forEach(piece => { 96 | if (winningCodes.has(piece.getCode())) { 97 | piece.setWinner() 98 | } 99 | }) 100 | 101 | // Activate physics ;) Pieces not on board get gravity. 102 | this.scene.enablePhysics(null, new OimoJSPlugin()) 103 | 104 | this.board.setPhysicsState(PhysicsEngine.BoxImpostor, { 105 | mass: 0 106 | }) 107 | 108 | var time = 0 109 | 110 | // OIMO warning undefined, seems to have no effect on game. 111 | // OIMO.WORLD_SCALE = 10 112 | // OIMO.INV_SCALE = 1 / 10 113 | 114 | this.pieces.forEach(p => { 115 | if (!p.isOnBoard) { 116 | var t = new Timer(time, this.scene, () => { 117 | p.setPhysicsState(PhysicsEngine.BoxImpostor, { mass: 1 }) 118 | }) 119 | t.start() 120 | time += 250 121 | } 122 | }) 123 | } 124 | 125 | setupNewGame () { 126 | this.board.reset() 127 | // Position pieces around board 128 | let alpha = Math.PI * 1 / 16 129 | let r = 120 130 | 131 | this.pieces.forEach(p => { 132 | var x = Math.cos(alpha) * r 133 | var z = Math.sin(alpha) * r 134 | p.setInitialPosition(new Vector3(x, p.size / 2, z)) 135 | alpha += Math.PI * 2 / 16 136 | 137 | // Reset piece 138 | p.reset() 139 | }) 140 | 141 | // want to suspend pieces in mid-air until there is a winner. 142 | this.scene.disablePhysicsEngine() 143 | } 144 | 145 | onSceneMount (e) { 146 | const { canvas, scene, engine } = e 147 | this.scene = scene 148 | this.sound = new BABYLON.Sound('', 'sfx/boom1.wav', scene) 149 | 150 | let lights = this.initEnvironment(canvas, scene) 151 | 152 | // Game 153 | this.board = new Gameboard(100, scene, lights) 154 | this.pieces = [] 155 | 156 | // Create pieces 157 | var count = 0 158 | for (var i = 0; i < this.board.LINE; i++) { 159 | for (var j = 0; j < this.board.COL; j++) { 160 | var isTall, isBlack, isCubic, isSolidTop 161 | isSolidTop = ((count & 1) === 1) 162 | isCubic = ((count & 2) === 2) 163 | isBlack = ((count & 4) === 4) 164 | isTall = ((count & 8) === 8) 165 | count++ 166 | var p = new Piece(this.board.getBasePosition(i, j), isTall, isBlack, isCubic, isSolidTop, scene) 167 | 168 | this.pieces.push(p) 169 | this.board.addShadowRender(p) 170 | } 171 | } 172 | 173 | this.setupNewGame() 174 | 175 | // can't explain this right now, but needed it after 2.5 udpate to 3.2-alpha. Will look at later. 176 | window.setTimeout(() => { 177 | engine.resize() 178 | }, 10) 179 | 180 | engine.runRenderLoop(() => { 181 | if (scene) { 182 | scene.render() 183 | } 184 | }) 185 | } 186 | 187 | initEnvironment (canvas, scene) { 188 | // Update the scene background color 189 | scene.clearColor = new Color3(0.8, 0.8, 0.8) 190 | 191 | // Hemispheric light to light the scene (Hemispheric mimics sunlight) 192 | var light = new HemisphericLight('hemi', new Vector3(0, 1, 0), scene) 193 | this.light = light 194 | // light.intensity = 0.7; 195 | 196 | // this light generates shadows. 197 | var light2 = new PointLight('Omni', new Vector3(200, 400, 400), scene) 198 | light2.intensity = 0.5 199 | 200 | let lights = { 201 | main: light, 202 | point: light2 203 | // directional: dl 204 | } 205 | 206 | // Skydome with no images 207 | // var skybox = Mesh.CreateSphere("skyBox", 20, 2000, scene) 208 | // var shader = new BABYLON.ShaderMaterial("gradient", scene, "gradient", {}) 209 | // shader.setFloat("offset", 200) 210 | // shader.setColor3("topColor", Color3.FromInts(0, 119, 255)) 211 | // shader.setColor3("bottomColor", Color3.FromInts(240, 240, 255)) 212 | // shader.backFaceCulling = false; 213 | // skybox.material = shader; 214 | 215 | // Skybox with images 216 | var skybox = Mesh.CreateBox('skyBox', 1000.0, scene) 217 | var skyboxMaterial = new StandardMaterial('skyBox', scene) 218 | skyboxMaterial.backFaceCulling = false 219 | skyboxMaterial.reflectionTexture = new CubeTexture('skybox/TropicalSunnyDay', scene) 220 | skyboxMaterial.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE 221 | skyboxMaterial.diffuseColor = new Color3(0, 0, 0) 222 | skyboxMaterial.specularColor = new Color3(0, 0, 0) 223 | skybox.material = skyboxMaterial 224 | 225 | // Camera attached to the canvas 226 | // Parameters : name, alpha, beta, radius, target, scene 227 | var camera = new ArcRotateCamera('Camera', 0, 1.05, 280, Vector3.Zero(), scene) 228 | // camera.lowerAlphaLimit = -0.0001; 229 | // camera.upperAlphaLimit = 0.0001; 230 | camera.lowerRadiusLimit = 150 231 | camera.upperRadiusLimit = 350 232 | camera.upperBetaLimit = Math.PI / 2 233 | camera.attachControl(canvas) 234 | camera.maxZ = 2000 // Skydome 235 | 236 | return lights 237 | }; 238 | 239 | componentDidMount () { 240 | let handlers = { 241 | [START_GAME]: (action) => { 242 | // console.log('START_GAME handler called in Quarto', action) 243 | this.setupNewGame() 244 | return true 245 | }, 246 | [PLAYER_PIECE_SELECTED]: (action) => { 247 | // console.log('PLAYER_PIECE_SELECTED handler called in Quarto') 248 | // Unselect all pieces 249 | this.pieces.forEach(p => { 250 | p.setSelected(false) 251 | }) 252 | // Set this Piece as selected (change colour) 253 | action.piece.setSelected(true, this.scene.getMaterialByID('sp')) 254 | return true 255 | }, 256 | [PLAYER_BASE_SELECTED]: (action) => { 257 | let { piece, base } = action 258 | 259 | piece.setSelected(false) 260 | piece.putOnBoard(base, () => { 261 | // after animation completes 262 | this.sound.play() 263 | }) 264 | 265 | return true 266 | }, 267 | [GAME_WON]: (action) => { 268 | let { winResult } = action 269 | 270 | this.onWinnerFound(winResult.winners) 271 | return true 272 | } 273 | } 274 | 275 | this.actionHandler = (action) => { 276 | let handler = handlers[action.type] 277 | if (handler === undefined) { 278 | console.log(`no handler defined in babylonJS scene for ${action.type}`) 279 | } else { 280 | return handler(action) 281 | } 282 | } 283 | 284 | registerHandler(this.actionHandler) 285 | } 286 | 287 | componentWillUnmount () { 288 | this.scene = null 289 | removeHandler(this.actionHandler) 290 | } 291 | 292 | onFocus (e) { 293 | this.light.intensity = 0.7 294 | } 295 | 296 | onBlur (e) { 297 | // let { scene } = e 298 | this.light.intensity = 0.5 299 | } 300 | 301 | render () { 302 | const { quartoState } = this.props 303 | 304 | console.log('quarto gameState', quartoState) 305 | console.log('quarto classes', classes) 306 | 307 | return ( 308 |
309 | 310 |
315 |
316 |

QUARTO

317 | 318 |
319 | { this.player1 = c }} /> 320 | { this.player2 = c }} /> 321 | 322 |
323 |
324 |
325 | 326 |
327 |
334 |
{quartoState.player1Name}
335 |
336 | 340 | 344 |
345 |
346 | 347 |
354 |
{quartoState.player2Name}
355 |
356 | 360 | 364 |
365 |
366 | 367 | {quartoState.playersChosen && 368 |
369 | Player { quartoState.player } choosing {(quartoState.playerPickPiece ? 'piece' : '')}{(quartoState.playerPickBase ? 'base' : '')} 370 |
371 | } 372 | 381 |
382 |
383 | QUARTO 384 |
385 |
386 |
387 | 388 |
393 |

QUARTO

394 | 395 | {quartoState.player === 1 ? quartoState.player1Name : quartoState.player2Name} 396 | 397 | 398 | WON !! 399 | 400 | 401 |
402 | New game ? 403 |
404 |
405 | 406 | { false && 407 |
408 |

QUARTO

409 |

410 | Do you know how to play ? 411 |

412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 |
YESNO
420 |
421 | } 422 | 423 |
424 |

RULES

425 |
426 | 427 |
428 |
429 | In Quarto, there are 16 unique pieces, each of which is either: 430 |
    431 |
  • Tall or short
  • 432 |
  • Black or white
  • 433 |
  • Square or circular
  • 434 |
  • Hollow-top or solid-top
  • 435 |
436 |
437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 |
< PrevNext >
446 |
447 | 448 |
449 |

450 | A player wins by doing a QUARTO.

451 | A QUARTO can be done by placing on the board four 452 | pieces with a common attribute in a line or in a row.

453 | In this example, these 4 pieces have a hollow-top: it's a QUARTO! 454 |

455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 |
< PrevNext >
464 |
465 | 466 |
467 |

468 | At the beginning of his turn, a player places on the board the piece selected by his opponent.

469 | If a QUARTO is done, the current player won. 470 | If not, he selects a piece for his opponent, and his turn is over.

471 | The first player to do a QUARTO wins the game! 472 |

473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 |
< PrevFinish
482 |
483 |
484 | 485 |
486 | ) 487 | } 488 | } 489 | 490 | Quarto.propTypes = { 491 | // TODO: add onBlur, onFocus, on KeyDown, onMeshHover, etc. 492 | startGame: PropTypes.func.isRequired, 493 | playersChosen: PropTypes.func.isRequired, 494 | boardPiecePicked: PropTypes.func.isRequired, 495 | boardBasePicked: PropTypes.func.isRequired, 496 | quartoState: PropTypes.object.isRequired 497 | } 498 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Quarto.scss: -------------------------------------------------------------------------------- 1 | /* The GUI */ 2 | .player { 3 | position: absolute; 4 | width: 350px; 5 | height: 480px; 6 | top: 0; 7 | margin-top: -360px; 8 | background-color: #3f3b60; 9 | -webkit-border-radius: 50%; 10 | border-radius: 50%; 11 | -webkit-transition: -webkit-transform 0.75s ease-in-out; 12 | transition: transform 0.75s ease-in-out; 13 | opacity: 0.5; 14 | } 15 | 16 | .playerShown { 17 | -webkit-transform: translateY(0); 18 | transform: translateY(0); 19 | } 20 | 21 | .playerHidden { 22 | -webkit-transform: translateY(-100%); 23 | transform: translateY(-100%); 24 | } 25 | 26 | .activePlayer { 27 | opacity: 1 !important; 28 | } 29 | 30 | /* The div for the player names */ 31 | .playerName { 32 | position: relative; 33 | height: 40px; 34 | top: 365px; 35 | width: 300px; 36 | margin: auto; 37 | color: white; 38 | text-transform: uppercase; 39 | text-align: center 40 | } 41 | 42 | /* GUI on the left (player 1) */ 43 | .player1 { 44 | left: 0 45 | } 46 | 47 | /* GUI on the right (player 2) */ 48 | .player2 { 49 | right: 0 50 | } 51 | 52 | .currentPlayer { 53 | opacity: 1 54 | } 55 | 56 | :global(.spinner) { 57 | margin: 100px auto; 58 | width: 50px; 59 | height: 30px; 60 | text-align: center; 61 | font-size: 10px; 62 | 63 | &> div { 64 | background-color: #fff; 65 | height: 100%; 66 | width: 6px; 67 | display: inline-block; 68 | 69 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 70 | animation: stretchdelay 1.2s infinite ease-in-out; 71 | } 72 | 73 | &:global(.rect2) { 74 | -webkit-animation-delay: -1.1s; 75 | animation-delay: -1.1s; 76 | } 77 | 78 | &:global(.rect3) { 79 | -webkit-animation-delay: -1.0s; 80 | animation-delay: -1.0s; 81 | } 82 | 83 | &:global(.rect4) { 84 | -webkit-animation-delay: -0.9s; 85 | animation-delay: -0.9s; 86 | } 87 | 88 | &:global(.rect5) { 89 | -webkit-animation-delay: -0.8s; 90 | animation-delay: -0.8s; 91 | } 92 | } 93 | 94 | @-webkit-keyframes stretchdelay { 95 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 96 | 20% { -webkit-transform: scaleY(1.0) } 97 | } 98 | 99 | @keyframes stretchdelay { 100 | 0%, 40%, 100% { 101 | transform: scaleY(0.4); 102 | -webkit-transform: scaleY(0.4); 103 | } 20% { 104 | transform: scaleY(1.0); 105 | -webkit-transform: scaleY(1.0); 106 | } 107 | } 108 | 109 | .loginWrapper { 110 | width:100%; 111 | height:100%; 112 | background: radial-gradient(circle, #5f5991 0%, #3f3b60 85%) no-repeat; 113 | /*background-color: #3f3b60;*/ 114 | transition: transform 0.75s ease-in-out; 115 | } 116 | 117 | .loginWrapperShown { 118 | -webkit-transform: translateY(0); 119 | transform: translateY(0); 120 | display: block; 121 | } 122 | 123 | .loginWrapperHidden { 124 | -webkit-transform: translateY(-200%); 125 | transform: translateY(-200%); 126 | display: none; 127 | } 128 | 129 | .login{ 130 | width:400px; 131 | margin:auto; 132 | padding-top:200px; 133 | margin-bottom:2%; 134 | transition: transform 0.75s ease-in-out; 135 | } 136 | 137 | .login h1{ 138 | background:#7067ab; 139 | padding:20px 0; 140 | font-size:140%; 141 | font-weight:300; 142 | text-align:center; 143 | color:#fff; 144 | } 145 | 146 | .form{ 147 | background:#f0f0f0; 148 | padding:6% 4%; 149 | } 150 | 151 | input[type="text"]{ 152 | width:92%; 153 | background:#fff; 154 | margin-bottom:4%; 155 | border:1px solid #ccc; 156 | padding:4%; 157 | font-size:95%; 158 | color:#555; 159 | } 160 | 161 | input[type="submit"]{ 162 | width:100%; 163 | background:#7067ab; 164 | border:0; 165 | padding:4%; 166 | font-size:100%; 167 | color:#fff; 168 | cursor:pointer; 169 | transition:background .3s; 170 | -webkit-transition:background .3s; 171 | } 172 | #rules { 173 | margin-top:20px; 174 | } 175 | 176 | input[type="submit"]:hover{ 177 | background:#8b80d5; 178 | } 179 | 180 | 181 | :global(.centerv) { 182 | position: relative; 183 | top: 50%; 184 | transform: translateY(-50%); 185 | } 186 | 187 | :global(.wrapper) { 188 | height : 500px; 189 | margin:auto; 190 | background-color: red; 191 | } 192 | 193 | #game { 194 | width: 100%; 195 | height: 100% 196 | } 197 | 198 | #quartoCanvas { 199 | width: 100%; 200 | height: 100%; 201 | margin: 0; 202 | padding: 0 203 | } 204 | 205 | /* TITLE*/ 206 | /* TITLE CIRCLE */ 207 | #title { 208 | position: absolute; 209 | width: 500px; 210 | height: 300px; 211 | margin: -220px auto auto auto; 212 | top: 0; 213 | right: 0; 214 | left: 0; 215 | background-color: #3f3b60; 216 | text-align: center; 217 | -webkit-border-radius: 50%; 218 | border-radius: 50%; 219 | font-size: 1.2em; 220 | -webkit-transition: -webkit-background 0.3s; 221 | transition: background 0.3s 222 | } 223 | 224 | #title:hover { 225 | background: #7067ab 226 | } 227 | 228 | /* QUARTO */ 229 | #titleText { 230 | margin: auto; 231 | margin-top: 225px; 232 | color: white 233 | } 234 | 235 | /* by Temechon */ 236 | #title .author { 237 | font-size: 0.7em 238 | } 239 | 240 | /* website link */ 241 | #title .author a { 242 | text-decoration: underline; 243 | color: white 244 | } 245 | 246 | /* ACTIONS */ 247 | .actions { 248 | width: 200px; 249 | height: 40px; 250 | /*background-color: orange;*/ 251 | position: relative; 252 | top: 350px; 253 | margin: auto; 254 | margin-top: 10px; 255 | text-align: center 256 | } 257 | 258 | .action { 259 | width: 40px; 260 | margin: 0 20px 0 20px; 261 | opacity: 0.5 262 | } 263 | 264 | .actionLeft { 265 | left: 0 266 | } 267 | 268 | .actionRight { 269 | right: 0 270 | } 271 | 272 | .currentAction { 273 | width: 50px; 274 | opacity: 1 275 | } 276 | 277 | /* WIN PANEL */ 278 | .win { 279 | width: 400px; 280 | height: 200px; 281 | position: absolute; 282 | top: 50px; 283 | left: calc(50% - 200px); 284 | text-align: center; 285 | -webkit-transition: -webkit-transform 0.75s ease-in-out; 286 | transition: transform 0.75s ease-in-out; 287 | background: #f0f0f0; 288 | 289 | &> h1 { // QUARTO 290 | background: #7067ab; 291 | padding: 10px 0; 292 | font-size: 140%; 293 | font-weight: 300; 294 | color: #fff 295 | } 296 | } 297 | 298 | .winShown { 299 | -webkit-transform: translateY(0); 300 | transform: translateY(0); 301 | } 302 | 303 | .winHidden { 304 | -webkit-transform: translateY(-300%); 305 | transform: translateY(-300%); 306 | } 307 | 308 | .winnerName { 309 | margin: 5px auto; 310 | /*background-color: green;*/ 311 | font-size: 1.5em; 312 | -webkit-column-rule: #3f3b60; 313 | vertical-align: middle; 314 | } 315 | 316 | .winText { /* WON !! */ 317 | font-size: 4em; 318 | color: white; 319 | vertical-align: middle; 320 | /*background-color: orange;*/ 321 | color: #3f3b60; 322 | } 323 | 324 | .replayButton { 325 | background: #7067ab; 326 | border: 0; 327 | padding: 2.5%; 328 | font-size: 100%; 329 | color: #fff; 330 | cursor: pointer; 331 | margin: 10px auto; 332 | width: 150px; 333 | -webkit-transition: -webkit-background 0.3s; 334 | transition: background 0.3s; 335 | 336 | &:hover { 337 | background: #8b80f6 338 | } 339 | } 340 | 341 | /* TUTORIAL BUBBLE at the bottom */ 342 | #tutorial { 343 | width: 200px; 344 | height: 200px; 345 | background-color: #3f3b60; 346 | opacity: 0.7; 347 | color: white; 348 | -webkit-border-radius: 50%; 349 | border-radius: 50%; 350 | position: fixed; 351 | bottom: -150px; 352 | right: 0; 353 | text-align: center; 354 | -webkit-transition: opacity 0.3s; 355 | -webkit-transition: -webkit-transform 0.3s; 356 | transition: opacity 0.3s; 357 | transition: -webkit-transform 0.3s; 358 | cursor: pointer; 359 | text-transform: uppercase; 360 | transform:translateY(200%); 361 | -webkit-transform: translateY(200%); 362 | } 363 | 364 | #tutorial:hover { 365 | opacity: 1 366 | } 367 | 368 | #tutorial h3 { 369 | padding-top: 15px; 370 | font-weight: 300 371 | } 372 | 373 | /* Tutorial STEPS */ 374 | :global(.tutorial) { 375 | width:calc(100% / 4); 376 | height: 250px; 377 | background-color: #3f3b60; 378 | position: absolute; 379 | -webkit-transition: opacity 0.25s ease-in; 380 | transition: opacity 0.25s ease-in; 381 | top:calc(100% / 4); 382 | right:calc(100% / 15); 383 | visibility: hidden; 384 | opacity: 0; 385 | font-weight: 300 386 | } 387 | 388 | @media screen and (max-width: 1160px) { 389 | :global(.tutorial) { 390 | width:calc(100% / 3); 391 | right: 20px; 392 | } 393 | } 394 | :global(.tutorialBig) { 395 | height: 350px; 396 | } 397 | 398 | :global(.tutorial) p, :global(.tutorial) ul { 399 | margin: 10px; 400 | color: white; 401 | font-size: 1em; 402 | line-height: 1.5em 403 | } 404 | 405 | .tutoButton { 406 | text-align: center; 407 | background: #7067ab; 408 | border: 0; 409 | padding: 10px; 410 | font-size: 100%; 411 | color: #fff; 412 | cursor: pointer; 413 | margin: 25px auto; 414 | width: 100px; 415 | -webkit-transition: -webkit-background 0.3s; 416 | transition: background 0.3s; 417 | 418 | &:hover { 419 | background: #8b80f6 420 | } 421 | } 422 | 423 | 424 | 425 | :global(.deactivate) { 426 | opacity: 0.2; 427 | cursor: auto 428 | } 429 | 430 | /* The quarto text in tutorials */ 431 | :global(.quarto) { 432 | text-transform: uppercase; 433 | font-weight: 300; 434 | color: yellow 435 | } 436 | 437 | /* STARTING TUTORIAL */ 438 | .startingTutorial { 439 | width: 400px; 440 | height: 200px; 441 | position: absolute; 442 | top: calc(50% - 200px); 443 | left: calc(50% - 200px); 444 | text-align: center; 445 | -webkit-transition: -webkit-transform 0.75s ease-in-out; 446 | transition: transform 0.75s ease-in-out; 447 | //-webkit-transform: translateY(-300%); 448 | //transform: translateY(-300%); 449 | background: #f0f0f0; 450 | &> h1 { 451 | /* Quarto */ 452 | background: #7067ab; 453 | padding: 10px 0; 454 | font-size: 140%; 455 | font-weight: 300; 456 | color: #fff; 457 | } 458 | &> p { 459 | /* Do you know how to play ? */ 460 | margin-top:35px; 461 | margin-bottom:-15px; 462 | } 463 | } -------------------------------------------------------------------------------- /src/routes/Quarto/components/Timer.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Timer { 3 | constructor (time, scene, callback) { 4 | this.maxTime = this.currentTime = time 5 | this.isOver = false 6 | this.started = false 7 | this.callback = callback 8 | 9 | this.scene = scene // needed in _update 10 | 11 | this._update = this._update.bind(this) 12 | 13 | var _this = this 14 | scene.registerBeforeRender(function () { 15 | if (_this.started && !_this.isOver) { 16 | _this._update() 17 | } 18 | }) 19 | } 20 | 21 | reset = function () { 22 | this.currentTime = this.maxTime 23 | this.isOver = false 24 | this.started = false 25 | }; 26 | 27 | start = function () { 28 | this.started = true 29 | }; 30 | 31 | _update = function () { 32 | // 2.0 breaking changes: Tools.GetFps() and Tools.GetDeltaTime() are now functions hosted by the engine 33 | this.currentTime -= this.scene.getEngine().getDeltaTime() 34 | if (this.currentTime <= 0) { 35 | this.isOver = true 36 | this.callback() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/Quarto/components/Tutorial.js: -------------------------------------------------------------------------------- 1 | 2 | // export default class Tutorial { 3 | // constructor (scene, showAnimations) { 4 | // this.scene = scene; 5 | 6 | // this.startRadius = 280; 7 | // this.endRadius = 350; 8 | 9 | // this.showAnimations = showAnimations; 10 | 11 | // }; 12 | 13 | // _createAnimationStep1() { 14 | // var s = BABYLON.Mesh.CreateSphere("arrow", 5, 5, this.scene); 15 | // s.material = new BABYLON.StandardMaterial("arrow", this.scene); 16 | // s.material.diffuseColor = BABYLON.Color3.Yellow(); 17 | // s.material.specularColor = BABYLON.Color3.Black(); 18 | // this.sphere = s; 19 | 20 | // var keys = []; 21 | // var f = 0; 22 | // var pos, height = 10; 23 | // var startPos = QUARTO.pieces[0].position.clone(); 24 | // startPos.y = startPos.y*2+height; 25 | 26 | // // TODO: no access to pieces. this will fail. 27 | // QUARTO.pieces.forEach(function(p) { 28 | // pos = p.position.clone(); 29 | // pos.y = pos.y*2+height; 30 | 31 | // var k = {frame:f, value: pos}; 32 | // keys.push(k); 33 | // f+=6; 34 | 35 | // k = {frame:f, value: pos}; 36 | // keys.push(k); 37 | // f+=6; 38 | // }); 39 | // keys.push({frame:f, value:startPos}); 40 | 41 | // var arrow = new BABYLON.Animation( 42 | // "arrow", 43 | // "position", 44 | // 60, 45 | // BABYLON.Animation.ANIMATIONTYPE_VECTOR3, 46 | // BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); 47 | 48 | // arrow.setKeys(keys); 49 | // s.animations.push(arrow); 50 | // this.scene.beginAnimation(s, 0, 200, true, 0.5); 51 | // } 52 | 53 | // _createAnimationStep2() { 54 | 55 | // var pieces = [0,6,8,14]; 56 | // var b = 0; 57 | // var that = this; 58 | // pieces.forEach(function(i) { 59 | // var startPos = QUARTO.pieces[i].position; 60 | // var endPos = QUARTO.board.getBasePosition(0,b++); 61 | // endPos.y = QUARTO.pieces[i].position.y; 62 | // var keys = [ 63 | // { 64 | // frame : 0, 65 | // value: startPos 66 | // }, 67 | // { 68 | // frame:100, 69 | // value:endPos 70 | // } 71 | // ]; 72 | 73 | // var p = new BABYLON.Animation( 74 | // "P"+i, 75 | // "position", 76 | // 60, 77 | // BABYLON.Animation.ANIMATIONTYPE_VECTOR3, 78 | // BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); 79 | 80 | // p.setKeys(keys); 81 | // QUARTO.pieces[i].animations.push(p); 82 | // that.scene.beginAnimation(QUARTO.pieces[i], 0, 100, false, 0.8); 83 | // }); 84 | // } 85 | 86 | // /** 87 | // * Display the step 1 of the tutorial 88 | // */ 89 | // step1(move) { 90 | 91 | // // Put the bobble in pastel red 92 | // console.error('no access to GUI anymore. use events and reducer + state') 93 | // GUI.colorTutorial(); 94 | 95 | // // If it's a back step, dont move the camera 96 | // var camMoving = (move === false)?move: true; 97 | 98 | // // Remove step 2 and 3 99 | // GUI.removeTutorial(2); 100 | // GUI.removeTutorial(3); 101 | 102 | // // Create animation 103 | // if (this.showAnimations) { 104 | // // Remove animations from step 2 105 | // var that = this; 106 | // QUARTO.pieces.forEach(function(p) { 107 | // that.scene.stopAnimation(p); 108 | // p.reset(); 109 | // }); 110 | // this._createAnimationStep1(); 111 | // } 112 | 113 | // // GUI 114 | // var _this = this; 115 | // var callback = function() { 116 | // GUI.displayTutorialStep1(_this.step2, _this); 117 | // }; 118 | 119 | // // Move camera and then call gui draw 120 | // if (camMoving && this.showAnimations) { 121 | // this._moveCamera("right", callback); 122 | // } else { 123 | // callback(); 124 | // } 125 | // } 126 | 127 | // step2() { 128 | // // Remove step 1 and 3 129 | // GUI.removeTutorial(1); 130 | // GUI.removeTutorial(3); 131 | 132 | // if (this.showAnimations) { 133 | // // Remove the animations from step 1 134 | // this.sphere.dispose(); 135 | // // run animation 136 | // this._createAnimationStep2(); 137 | // } 138 | 139 | // var _this = this; 140 | // GUI.displayTutorialStep2( 141 | // _this.step1, 142 | // _this.step3, 143 | // _this 144 | // ); 145 | // } 146 | 147 | // step3() { 148 | // // Remove step 1 and 2 149 | // GUI.removeTutorial(1); 150 | // GUI.removeTutorial(2); 151 | 152 | // if (this.showAnimations) { 153 | // // Remove animations from step 2 154 | // var that = this; 155 | // QUARTO.pieces.forEach(function(p) { 156 | // that.scene.stopAnimation(p); 157 | // p.reset(); 158 | // }); 159 | // } 160 | 161 | // var _this = this; 162 | // GUI.displayTutorialStep3( 163 | // _this.step2, 164 | // _this.exitTutorial, 165 | // _this 166 | // ); 167 | // } 168 | 169 | // exitTutorial() { 170 | // // Revert the bobble background 171 | // GUI.uncolorTutorial(); 172 | // if (this.showAnimations) { 173 | // // Move camera to the left 174 | // this._moveCamera("left"); 175 | // // Remove the animations from step 1 176 | // this.sphere.dispose(); 177 | // // Remove animations from step 2 178 | // var that = this; 179 | // QUARTO.pieces.forEach(function(p) { 180 | // that.scene.stopAnimation(p); 181 | // p.reset(); 182 | // }); 183 | // } 184 | // QUARTO.isTutorialActivated = false; 185 | // GAME_STATES.GAME_STARTED = true; 186 | // } 187 | 188 | // /** 189 | // * Move camera on the left or on the right, according to the given parameter 190 | // * @private 191 | // */ 192 | // _moveCamera(dir, callback) { 193 | // var startPos = this.scene.activeCamera.target.z; 194 | // var startRadius = this.scene.activeCamera.radius; 195 | // var endPos, endRadius; 196 | 197 | // switch (dir) { 198 | // case "left": 199 | // endPos = 0; 200 | // endRadius = this.startRadius; 201 | // break; 202 | // case "right": 203 | // endPos = 100; 204 | // endRadius = this.endRadius; 205 | // break; 206 | // } 207 | // var translate = new BABYLON.Animation( 208 | // "camTranslate", 209 | // "target.z", 210 | // 60, 211 | // BABYLON.Animation.ANIMATIONTYPE_FLOAT, 212 | // BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 213 | // var radius = new BABYLON.Animation( 214 | // "camAlpha", 215 | // "radius", 216 | // 60, 217 | // BABYLON.Animation.ANIMATIONTYPE_FLOAT, 218 | // BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 219 | 220 | // var keys = [{frame:0, value:startPos}, {frame:100, value:endPos}]; 221 | // var keys2 = [{frame:0, value:startRadius}, {frame:100, value:endRadius}]; 222 | // translate.setKeys(keys); 223 | // radius.setKeys(keys2); 224 | // this.scene.activeCamera.animations.push(translate); 225 | // this.scene.activeCamera.animations.push(radius); 226 | // this.scene.beginAnimation(this.scene.activeCamera, 0, 100, false, 5.0, callback); 227 | 228 | // } 229 | 230 | // }; 231 | -------------------------------------------------------------------------------- /src/routes/Quarto/containers/QuartoContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { actions as quartoActions } from '../modules/quarto' 3 | 4 | /* This is a container component. Notice it does not contain any JSX, 5 | nor does it import React. This component is **only** responsible for 6 | wiring in the actions and state necessary to render a presentational 7 | component - in this case, the counter: */ 8 | 9 | import Quarto from '../components/Quarto' 10 | 11 | /* Object of action creators (can also be function that returns object). 12 | Keys will be passed as props to presentational components. Here we are 13 | implementing our wrapper around increment; the component doesn't care */ 14 | 15 | // const mapDispatchToProps = { 16 | // // increment : () => increment(1), 17 | // shiftLeft, 18 | // widen 19 | // } 20 | 21 | const mapStateToProps = (state) => ({ 22 | quartoState : state.quarto 23 | }) 24 | 25 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 26 | 27 | import { createSelector } from 'reselect' 28 | const counter = (state) => state.counter 29 | const tripleCount = createSelector(counter, (count) => count * 3) 30 | const mapStateToProps = (state) => ({ 31 | counter: tripleCount(state) 32 | }) 33 | 34 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 35 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 36 | Selectors are composable. They can be used as input to other selectors. 37 | https://github.com/reactjs/reselect */ 38 | 39 | export default connect(mapStateToProps, { ...quartoActions })(Quarto) 40 | -------------------------------------------------------------------------------- /src/routes/Quarto/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducers' 2 | 3 | export default (store) => ({ 4 | path : 'quarto', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent (nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const Quarto = require('./containers/QuartoContainer').default 13 | const reducer = require('./modules/quarto').default 14 | 15 | /* Add the reducer to the store on key 'counter' */ 16 | injectReducer(store, { key: 'quarto', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, Quarto) 20 | 21 | /* Webpack named bundle */ 22 | }, 'quarto') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/routes/Quarto/modules/quarto.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | 5 | // done when a game starts or is (re)started. 6 | // scene will run init={function}, if it's the first game. 7 | export const START_GAME = 'START_GAME' 8 | // this will probably trigger the first 'start_game' for now... 9 | // todo: possibly change to PLAYER_NAMES_SELECTED and use PLAYERS_CHOSEN as an event? can use as spinner while game is starting.... 10 | export const PLAYERS_CHOSEN = 'PLAYERS_CHOSEN' 11 | 12 | export const BOARD_PIECE_PICKED = 'BOARD_PIECE_PICKED' 13 | export const BOARD_BASE_PICKED = 'BOARD_BASE_PICKED' 14 | 15 | // events that have occured (hence past tense here). 16 | export const GAME_STARTED = 'GAME_STARTED' 17 | export const PLAYER_PIECE_SELECTED = 'PLAYER_PIECE_SELECTED' 18 | export const PLAYER_BASE_SELECTED = 'PLAYER_BASE_SELECTED' 19 | export const GAME_WON = 'GAME_WON' 20 | 21 | // ------------------------------------ 22 | // Actions 23 | // ------------------------------------ 24 | export const startGame = () => ({ type: START_GAME }) 25 | 26 | export const boardPiecePicked = (piece) => ({ 27 | type: BOARD_PIECE_PICKED, 28 | piece 29 | }) 30 | 31 | export const boardBasePicked = (base) => ({ 32 | type: BOARD_BASE_PICKED, 33 | base 34 | }) 35 | 36 | export const playersChosen = (player1Name, player2Name) => ({ 37 | type: PLAYERS_CHOSEN, 38 | player1Name, 39 | player2Name 40 | }) 41 | 42 | export const actions = { 43 | startGame, 44 | playersChosen, 45 | boardPiecePicked, 46 | boardBasePicked 47 | } 48 | 49 | export const events = { 50 | PLAYER_PIECE_SELECTED, 51 | PLAYER_BASE_SELECTED, 52 | GAME_WON 53 | } 54 | // ------------------------------------ 55 | // Action Handlers 56 | // ------------------------------------ 57 | const ACTION_HANDLERS = { 58 | [START_GAME]: (state, action) => { 59 | console.log('game start', action) 60 | 61 | let newState = { ...state, 62 | started: true, 63 | won: false, 64 | player: 1, 65 | playerPickPiece: true, 66 | playerPickBase: false 67 | } 68 | 69 | console.log('game started', newState) 70 | 71 | return newState 72 | }, 73 | [PLAYERS_CHOSEN]: (state, action) => { 74 | let newState = { 75 | ...state, 76 | player1Name: action.player1Name, 77 | player2Name: action.player2Name, 78 | playersChosen: true, 79 | // next 4 mimic a 'start_game' 80 | started: true, 81 | player: 1, 82 | playerPickPiece: true, 83 | playerPickBase: false 84 | } 85 | 86 | console.log('players chosen', newState) 87 | 88 | return newState 89 | }, 90 | [PLAYER_PIECE_SELECTED]: (state, action) => { 91 | const { piece } = action 92 | 93 | // player changes. 94 | let newState = { ...state, 95 | playerPickPiece: false, 96 | playerPickBase: true, 97 | player: ((state.player % 2) + 1) 98 | } 99 | 100 | console.log(`player ${state.player} selected piece`, newState, piece) 101 | 102 | return newState 103 | }, 104 | [PLAYER_BASE_SELECTED]: (state, action) => { 105 | const { boardPieces, winResult } = action 106 | 107 | if (winResult.win) { 108 | // TODO: set winner name, it will be shown in React. 109 | console.log(`player ${state.player} won`, newState, action.base) 110 | return { ...state, 111 | won: true, 112 | boardPieces: boardPieces 113 | } 114 | } 115 | 116 | let newState = { ...state, 117 | playerPickPiece: true, 118 | playerPickBase: false, 119 | boardPieces 120 | } 121 | 122 | console.log(`player ${state.player} selected base`, newState, action.base) 123 | 124 | return newState 125 | } 126 | } 127 | 128 | // ------------------------------------ 129 | // Reducer 130 | // ------------------------------------ 131 | const initialState = { 132 | started: false, 133 | playersChosen: false, 134 | player1Name: undefined, 135 | player2Name: undefined, 136 | won: false 137 | } 138 | 139 | export default function quartoReducer (state = initialState, action) { 140 | const handler = ACTION_HANDLERS[action.type] 141 | 142 | return handler ? handler(state, action) : state 143 | } 144 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | // We only need to import the modules necessary for initial render 2 | import CoreLayout from '../layouts/CoreLayout/CoreLayout' 3 | import Home from './Home' 4 | import CounterRoute from './Counter' 5 | import QuartoRoute from './Quarto' 6 | 7 | /* Note: Instead of using JSX, we recommend using react-router 8 | PlainRoute objects to build route definitions. */ 9 | 10 | export const createRoutes = (store) => ({ 11 | path : '/', 12 | component : CoreLayout, 13 | indexRoute : Home, 14 | childRoutes : [ 15 | CounterRoute(store), 16 | QuartoRoute(store) 17 | ] 18 | }) 19 | 20 | /* Note: childRoutes can be chunked or otherwise loaded programmatically 21 | using getChildRoutes with the following signature: 22 | 23 | getChildRoutes (location, cb) { 24 | require.ensure([], (require) => { 25 | cb(null, [ 26 | // Remove imports! 27 | require('./Counter').default(store) 28 | ]) 29 | }) 30 | } 31 | 32 | However, this is not necessary for code-splitting! It simply provides 33 | an API for async route definitions. Your code splitting should occur 34 | inside the route `getComponent` function, since it is only invoked 35 | when the route exists and matches. 36 | */ 37 | 38 | export default createRoutes 39 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import watchQuarto from './quartoGameLogic' 2 | 3 | import { fork } from 'redux-saga/effects' 4 | 5 | export default function* root () { 6 | yield [ 7 | fork(watchQuarto) 8 | // fork(watchDemoEvents), // ie: hide/show debug layer 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/sagas/quartoGameLogic.js: -------------------------------------------------------------------------------- 1 | // import { delay } from 'redux-saga' 2 | import { takeEvery, take, put, select } from 'redux-saga/effects' 3 | 4 | import { 5 | START_GAME, 6 | PLAYERS_CHOSEN, 7 | BOARD_PIECE_PICKED, 8 | BOARD_BASE_PICKED, 9 | PLAYER_PIECE_SELECTED, 10 | PLAYER_BASE_SELECTED, 11 | GAME_WON 12 | } from '../routes/Quarto/modules/quarto' 13 | 14 | Array.matrix = function (numrows, numcols, initial) { 15 | var arr = [] 16 | for (var i = 0; i < numrows; ++i) { 17 | var columns = [] 18 | for (var j = 0; j < numcols; ++j) { 19 | columns[j] = initial 20 | } 21 | arr[i] = columns 22 | } 23 | return arr 24 | } 25 | 26 | let lastSelectedPiece = null 27 | let boardPieces = Array.matrix(4, 4, null) 28 | 29 | export function* onGameStart (action) { 30 | console.log('saga resetting game state') 31 | lastSelectedPiece = null 32 | boardPieces = Array.matrix(4, 4, null) 33 | 34 | // try { 35 | // console.log(`in saga - game start received:${action.type} putting: 'GAME_STARTED'`) 36 | // yield put({ 37 | // type: 'GAME_STARTED', 38 | // triggerAction: action // not used 39 | // }) 40 | // } catch (error) { 41 | // console.error('error in quartoEventsGenerator', error) 42 | // } 43 | } 44 | 45 | const currentGameState = (state) => { 46 | return state.quarto 47 | } 48 | 49 | export function* onBoardPiecePicked (action) { 50 | const currentState = yield select(currentGameState) 51 | 52 | console.log('piece pick allowed', currentState.started, currentState.playerPickPiece) 53 | 54 | if (currentState.started && currentState.playerPickPiece) { 55 | const { piece } = action 56 | console.log(`pick result name ${piece.name} isOnBoard: ${piece.isOnBoard}`) 57 | 58 | if (piece.isOnBoard) { 59 | console.log('chose a piece already on the board (ignored)') 60 | } else { 61 | lastSelectedPiece = piece 62 | yield put({ 63 | type: PLAYER_PIECE_SELECTED, 64 | piece 65 | }) 66 | } 67 | } 68 | } 69 | 70 | const arrayContainsQuarto = (pieces) => { 71 | var codeAnd = 15 // 1111 72 | var codeNotAnd = 15 // 1111 73 | pieces.forEach(piece => { 74 | if (piece !== null) { 75 | codeAnd &= piece.code 76 | codeNotAnd &= ~piece.code 77 | } else { 78 | codeAnd &= 0 79 | codeNotAnd &= 0 80 | } 81 | }) 82 | 83 | return (codeAnd > 0) ? codeAnd : codeNotAnd 84 | } 85 | 86 | const matrixContainsQuarto = (matrix) => { 87 | // check lines 88 | for (let rowNum of [0, 1, 2, 3]) { 89 | let code = arrayContainsQuarto(matrix[rowNum]) 90 | if (code > 0) { 91 | return { 92 | win: true, 93 | type: 'row', 94 | winners: matrix[rowNum], 95 | number: rowNum 96 | } 97 | } 98 | } 99 | 100 | for (let col of [0, 1, 2, 3]) { 101 | let array = [] 102 | for (let row of matrix) { 103 | array.push(row[col]) 104 | } 105 | let code = arrayContainsQuarto(array) 106 | if (code > 0) { 107 | return { 108 | win: true, 109 | type: 'column', 110 | winners: array, 111 | number: col 112 | } 113 | } 114 | } 115 | 116 | return { 117 | win: false 118 | } 119 | } 120 | 121 | export function* onBoardBasePicked (action) { 122 | const currentState = yield select(currentGameState) 123 | 124 | console.log('piece base allowed', currentState.started, currentState.playerPickBase) 125 | 126 | if (currentState.started && currentState.playerPickBase) { 127 | const { base } = action 128 | console.log(`pick result name ${base.name} piece: ${base.piece}`) 129 | 130 | let pieceToMove = lastSelectedPiece 131 | 132 | if (base.piece) { 133 | console.log('chosen base already has a piece (ignored)') 134 | } else if (pieceToMove === null) { 135 | console.log('race condition on lastPiece (ignored)') 136 | } else { 137 | let pieceData = { 138 | isTall: pieceToMove.isTall, 139 | isBlack: pieceToMove.isBlack, 140 | isCubic: pieceToMove.isCubic, 141 | isSolidTop: pieceToMove.isSolidTop, 142 | code: pieceToMove.getCode() 143 | } 144 | 145 | console.log(`putting piece at ${base.col} x ${base.line}`) 146 | 147 | boardPieces[base.col][base.line] = pieceData 148 | 149 | const winResult = matrixContainsQuarto(boardPieces) 150 | 151 | console.log('win result', winResult) 152 | 153 | yield put({ 154 | type: PLAYER_BASE_SELECTED, 155 | piece : pieceToMove, 156 | base, 157 | boardPieces, 158 | winResult 159 | }) 160 | 161 | lastSelectedPiece = null 162 | 163 | if (winResult.win) { 164 | console.log('putting WON', winResult) 165 | yield put({ 166 | type: GAME_WON, 167 | winResult 168 | }) 169 | // TODO: reset board array - or wait for reset? 170 | } 171 | } 172 | } 173 | } 174 | 175 | export function* everything (action) { 176 | // console.log('quarto Event generator skipping action type:${action.type}') 177 | // TODO: see if game_win state is already set by order of reducers... would be too easy 178 | } 179 | 180 | export default function* watchQuarto () { 181 | while (true) { 182 | console.log('quartoEventsGenerator waiting...') 183 | yield [ 184 | takeEvery(BOARD_PIECE_PICKED, onBoardPiecePicked), 185 | takeEvery(BOARD_BASE_PICKED, onBoardBasePicked), 186 | takeEvery([PLAYERS_CHOSEN, START_GAME], onGameStart), // need to also listen for START_GAME for restarts... 187 | take('*', everything) 188 | ] 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { browserHistory } from 'react-router' 4 | import createSagaMiddleware from 'redux-saga' 5 | 6 | import makeRootReducer from './reducers' 7 | import { updateLocation } from './location' 8 | import rootSaga from '../sagas' 9 | 10 | import { babylonJSMiddleware } from 'react-babylonjs' 11 | 12 | export default (initialState = {}) => { 13 | // ====================================================== 14 | // Middleware Configuration 15 | // ====================================================== 16 | const sagaMiddleware = createSagaMiddleware() 17 | const middleware = [thunk, babylonJSMiddleware, sagaMiddleware] 18 | 19 | // ====================================================== 20 | // Store Enhancers 21 | // ====================================================== 22 | const enhancers = [] 23 | 24 | let composeEnhancers = compose 25 | 26 | if (__DEV__) { 27 | const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 28 | if (typeof composeWithDevToolsExtension === 'function') { 29 | composeEnhancers = composeWithDevToolsExtension 30 | } 31 | } 32 | 33 | // ====================================================== 34 | // Store Instantiation and HMR Setup 35 | // ====================================================== 36 | const store = createStore( 37 | makeRootReducer(), 38 | initialState, 39 | composeEnhancers( 40 | applyMiddleware(...middleware), 41 | ...enhancers 42 | ) 43 | ) 44 | store.asyncReducers = {} 45 | 46 | // then run the sagas (not hot this way...): 47 | // console.log('running saga middleware') 48 | sagaMiddleware.run(rootSaga) 49 | 50 | // store.runSaga = (saga) => { 51 | // sagaMiddleware.run(saga) 52 | // } 53 | 54 | // To unsubscribe, invoke `store.unsubscribeHistory()` anytime 55 | store.unsubscribeHistory = browserHistory.listen(updateLocation(store)) 56 | 57 | if (module.hot) { 58 | module.hot.accept('./reducers', () => { 59 | const reducers = require('./reducers').default 60 | store.replaceReducer(reducers(store.asyncReducers)) 61 | }) 62 | } 63 | 64 | return store 65 | } 66 | -------------------------------------------------------------------------------- /src/store/location.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const LOCATION_CHANGE = 'LOCATION_CHANGE' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function locationChange (location = '/') { 10 | return { 11 | type : LOCATION_CHANGE, 12 | payload : location 13 | } 14 | } 15 | 16 | // ------------------------------------ 17 | // Specialized Action Creator 18 | // ------------------------------------ 19 | export const updateLocation = ({ dispatch }) => { 20 | return (nextLocation) => dispatch(locationChange(nextLocation)) 21 | } 22 | 23 | // ------------------------------------ 24 | // Reducer 25 | // ------------------------------------ 26 | const initialState = null 27 | export default function locationReducer (state = initialState, action) { 28 | return action.type === LOCATION_CHANGE 29 | ? action.payload 30 | : state 31 | } 32 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import locationReducer from './location' 3 | 4 | export const makeRootReducer = (asyncReducers) => { 5 | return combineReducers({ 6 | location: locationReducer, 7 | ...asyncReducers 8 | }) 9 | } 10 | 11 | export const injectReducer = (store, { key, reducer }) => { 12 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return 13 | 14 | store.asyncReducers[key] = reducer 15 | store.replaceReducer(makeRootReducer(store.asyncReducers)) 16 | } 17 | 18 | export default makeRootReducer 19 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Application Settings Go Here 3 | ------------------------------------ 4 | This file acts as a bundler for all variables/mixins/themes, so they 5 | can easily be swapped out without `core.scss` ever having to know. 6 | 7 | For example: 8 | 9 | @import './variables/colors'; 10 | @import './variables/components'; 11 | @import './themes/default'; 12 | */ 13 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import '~normalize.css/normalize'; 3 | 4 | // Some best-practice CSS that's useful for most apps 5 | // Just remove them if they're not what you want 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | html, 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | height: 100%; 15 | } 16 | 17 | *, 18 | *:before, 19 | *:after { 20 | box-sizing: inherit; 21 | } -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/components/Header/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Header } from 'components/Header/Header' 3 | import { IndexLink, Link } from 'react-router' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Header', () => { 7 | let _wrapper 8 | 9 | beforeEach(() => { 10 | _wrapper = shallow(
) 11 | }) 12 | 13 | it('Renders a welcome message', () => { 14 | const welcome = _wrapper.find('h1') 15 | expect(welcome).to.exist 16 | expect(welcome.text()).to.match(/Quarto Demo/) 17 | }) 18 | 19 | describe('Navigation links...', () => { 20 | it('Should render a Link to Home route', () => { 21 | expect(_wrapper.contains( 22 | 23 | Home 24 | 25 | )).to.be.true 26 | }) 27 | 28 | it('Should render a Link to Counter route', () => { 29 | expect(_wrapper.contains( 30 | 31 | Counter 32 | 33 | )).to.be.true 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/layouts/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 4 | 5 | function shallowRender (component) { 6 | const renderer = TestUtils.createRenderer() 7 | 8 | renderer.render(component) 9 | return renderer.getRenderOutput() 10 | } 11 | 12 | function shallowRenderWithProps (props = {}) { 13 | return shallowRender() 14 | } 15 | 16 | describe('(Layout) Core', function () { 17 | let _component 18 | let _props 19 | let _child 20 | 21 | beforeEach(function () { 22 | _child =

Child

23 | _props = { 24 | children : _child 25 | } 26 | 27 | _component = shallowRenderWithProps(_props) 28 | }) 29 | 30 | it('Should render as a
.', function () { 31 | expect(_component.type).to.equal('div') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/routes/Counter/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { Counter } from 'routes/Counter/components/Counter' 4 | import { shallow } from 'enzyme' 5 | 6 | describe('(Component) Counter', () => { 7 | let _props, _spies, _wrapper 8 | 9 | beforeEach(() => { 10 | _spies = {} 11 | _props = { 12 | counter : 5, 13 | ...bindActionCreators({ 14 | doubleAsync : (_spies.doubleAsync = sinon.spy()), 15 | increment : (_spies.increment = sinon.spy()) 16 | }, _spies.dispatch = sinon.spy()) 17 | } 18 | _wrapper = shallow() 19 | }) 20 | 21 | it('Should render as a
.', () => { 22 | expect(_wrapper.is('div')).to.equal(true) 23 | }) 24 | 25 | it('Should render with an

that includes Sample Counter text.', () => { 26 | expect(_wrapper.find('h2').text()).to.match(/Counter:/) 27 | }) 28 | 29 | it('Should render props.counter at the end of the sample counter

.', () => { 30 | expect(_wrapper.find('h2').text()).to.match(/5$/) 31 | _wrapper.setProps({ counter: 8 }) 32 | expect(_wrapper.find('h2').text()).to.match(/8$/) 33 | }) 34 | 35 | it('Should render exactly two buttons.', () => { 36 | expect(_wrapper.find('button')).to.have.length(2) 37 | }) 38 | 39 | describe('An increment button...', () => { 40 | let _button 41 | 42 | beforeEach(() => { 43 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Increment') 44 | }) 45 | 46 | it('has bootstrap classes', () => { 47 | expect(_button.hasClass('btn btn-default')).to.be.true 48 | }) 49 | 50 | it('Should dispatch a `increment` action when clicked', () => { 51 | _spies.dispatch.should.have.not.been.called 52 | 53 | _button.simulate('click') 54 | 55 | _spies.dispatch.should.have.been.called 56 | _spies.increment.should.have.been.called 57 | }) 58 | }) 59 | 60 | describe('A Double (Async) button...', () => { 61 | let _button 62 | 63 | beforeEach(() => { 64 | _button = _wrapper.find('button').filterWhere(a => a.text() === 'Double (Async)') 65 | }) 66 | 67 | it('has bootstrap classes', () => { 68 | expect(_button.hasClass('btn btn-default')).to.be.true 69 | }) 70 | 71 | it('Should dispatch a `doubleAsync` action when clicked', () => { 72 | _spies.dispatch.should.have.not.been.called 73 | 74 | _button.simulate('click') 75 | 76 | _spies.dispatch.should.have.been.called 77 | _spies.doubleAsync.should.have.been.called 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/routes/Counter/index.spec.js: -------------------------------------------------------------------------------- 1 | import CounterRoute from 'routes/Counter' 2 | 3 | describe('(Route) Counter', () => { 4 | let _route 5 | 6 | beforeEach(() => { 7 | _route = CounterRoute({}) 8 | }) 9 | 10 | it('Should return a route configuration object', () => { 11 | expect(typeof _route).to.equal('object') 12 | }) 13 | 14 | it('Configuration should contain path `counter`', () => { 15 | expect(_route.path).to.equal('counter') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/routes/Counter/modules/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | COUNTER_INCREMENT, 3 | increment, 4 | doubleAsync, 5 | default as counterReducer 6 | } from 'routes/Counter/modules/counter' 7 | 8 | describe('(Redux Module) Counter', () => { 9 | it('Should export a constant COUNTER_INCREMENT.', () => { 10 | expect(COUNTER_INCREMENT).to.equal('COUNTER_INCREMENT') 11 | }) 12 | 13 | describe('(Reducer)', () => { 14 | it('Should be a function.', () => { 15 | expect(counterReducer).to.be.a('function') 16 | }) 17 | 18 | it('Should initialize with a state of 0 (Number).', () => { 19 | expect(counterReducer(undefined, {})).to.equal(0) 20 | }) 21 | 22 | it('Should return the previous state if an action was not matched.', () => { 23 | let state = counterReducer(undefined, {}) 24 | expect(state).to.equal(0) 25 | state = counterReducer(state, { type: '@@@@@@@' }) 26 | expect(state).to.equal(0) 27 | state = counterReducer(state, increment(5)) 28 | expect(state).to.equal(5) 29 | state = counterReducer(state, { type: '@@@@@@@' }) 30 | expect(state).to.equal(5) 31 | }) 32 | }) 33 | 34 | describe('(Action Creator) increment', () => { 35 | it('Should be exported as a function.', () => { 36 | expect(increment).to.be.a('function') 37 | }) 38 | 39 | it('Should return an action with type "COUNTER_INCREMENT".', () => { 40 | expect(increment()).to.have.property('type', COUNTER_INCREMENT) 41 | }) 42 | 43 | it('Should assign the first argument to the "payload" property.', () => { 44 | expect(increment(5)).to.have.property('payload', 5) 45 | }) 46 | 47 | it('Should default the "payload" property to 1 if not provided.', () => { 48 | expect(increment()).to.have.property('payload', 1) 49 | }) 50 | }) 51 | 52 | describe('(Action Creator) doubleAsync', () => { 53 | let _globalState 54 | let _dispatchSpy 55 | let _getStateSpy 56 | 57 | beforeEach(() => { 58 | _globalState = { 59 | counter : counterReducer(undefined, {}) 60 | } 61 | _dispatchSpy = sinon.spy((action) => { 62 | _globalState = { 63 | ..._globalState, 64 | counter : counterReducer(_globalState.counter, action) 65 | } 66 | }) 67 | _getStateSpy = sinon.spy(() => { 68 | return _globalState 69 | }) 70 | }) 71 | 72 | it('Should be exported as a function.', () => { 73 | expect(doubleAsync).to.be.a('function') 74 | }) 75 | 76 | it('Should return a function (is a thunk).', () => { 77 | expect(doubleAsync()).to.be.a('function') 78 | }) 79 | 80 | it('Should return a promise from that thunk that gets fulfilled.', () => { 81 | return doubleAsync()(_dispatchSpy, _getStateSpy).should.eventually.be.fulfilled 82 | }) 83 | 84 | it('Should call dispatch and getState exactly once.', () => { 85 | return doubleAsync()(_dispatchSpy, _getStateSpy) 86 | .then(() => { 87 | _dispatchSpy.should.have.been.calledOnce 88 | _getStateSpy.should.have.been.calledOnce 89 | }) 90 | }) 91 | 92 | it('Should produce a state that is double the previous state.', () => { 93 | _globalState = { counter: 2 } 94 | 95 | return doubleAsync()(_dispatchSpy, _getStateSpy) 96 | .then(() => { 97 | _dispatchSpy.should.have.been.calledOnce 98 | _getStateSpy.should.have.been.calledOnce 99 | expect(_globalState.counter).to.equal(4) 100 | return doubleAsync()(_dispatchSpy, _getStateSpy) 101 | }) 102 | .then(() => { 103 | _dispatchSpy.should.have.been.calledTwice 104 | _getStateSpy.should.have.been.calledTwice 105 | expect(_globalState.counter).to.equal(8) 106 | }) 107 | }) 108 | }) 109 | 110 | // NOTE: if you have a more complex state, you will probably want to verify 111 | // that you did not mutate the state. In this case our state is just a number 112 | // (which cannot be mutated). 113 | describe('(Action Handler) COUNTER_INCREMENT', () => { 114 | it('Should increment the state by the action payload\'s "value" property.', () => { 115 | let state = counterReducer(undefined, {}) 116 | expect(state).to.equal(0) 117 | state = counterReducer(state, increment(1)) 118 | expect(state).to.equal(1) 119 | state = counterReducer(state, increment(2)) 120 | expect(state).to.equal(3) 121 | state = counterReducer(state, increment(-3)) 122 | expect(state).to.equal(0) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /tests/routes/Home/components/HomeView.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { HomeView } from 'routes/Home/components/HomeView' 3 | import { render } from 'enzyme' 4 | 5 | describe('(View) Home', () => { 6 | let _component 7 | 8 | beforeEach(() => { 9 | _component = render() 10 | }) 11 | 12 | it('Renders a welcome message', () => { 13 | const welcome = _component.find('h4') 14 | expect(welcome).to.exist 15 | expect(welcome.text()).to.match(/Welcome!/) 16 | }) 17 | 18 | it('Renders an awesome duck image', () => { 19 | const duck = _component.find('img') 20 | expect(duck).to.exist 21 | expect(duck.attr('alt')).to.match(/This is a duck, because Redux!/) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/routes/Home/index.spec.js: -------------------------------------------------------------------------------- 1 | import HomeRoute from 'routes/Home' 2 | 3 | describe('(Route) Home', () => { 4 | let _component 5 | 6 | beforeEach(() => { 7 | _component = HomeRoute.component() 8 | }) 9 | 10 | it('Should return a route configuration object', () => { 11 | expect(typeof HomeRoute).to.equal('object') 12 | }) 13 | 14 | it('Should define a route component', () => { 15 | expect(_component.type).to.equal('div') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/store/createStore.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | default as createStore 3 | } from 'store/createStore' 4 | 5 | describe('(Store) createStore', () => { 6 | let store 7 | 8 | before(() => { 9 | store = createStore() 10 | }) 11 | 12 | it('should have an empty asyncReducers object', () => { 13 | expect(store.asyncReducers).to.be.an('object') 14 | expect(store.asyncReducers).to.be.empty 15 | }) 16 | 17 | describe('(Location)', () => { 18 | it('store should be initialized with Location state', () => { 19 | const location = { 20 | pathname : '/echo' 21 | } 22 | store.dispatch({ 23 | type : 'LOCATION_CHANGE', 24 | payload : location 25 | }) 26 | expect(store.getState().location).to.deep.equal(location) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/store/location.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOCATION_CHANGE, 3 | locationChange, 4 | updateLocation, 5 | default as locationReducer 6 | } from 'store/location' 7 | 8 | describe('(Internal Module) Location', () => { 9 | it('Should export a constant LOCATION_CHANGE.', () => { 10 | expect(LOCATION_CHANGE).to.equal('LOCATION_CHANGE') 11 | }) 12 | 13 | describe('(Reducer)', () => { 14 | it('Should be a function.', () => { 15 | expect(locationReducer).to.be.a('function') 16 | }) 17 | 18 | it('Should initialize with a state of null.', () => { 19 | expect(locationReducer(undefined, {})).to.equal(null) 20 | }) 21 | 22 | it('Should return the previous state if an action was not matched.', () => { 23 | let state = locationReducer(undefined, {}) 24 | expect(state).to.equal(null) 25 | state = locationReducer(state, { type: '@@@@@@@' }) 26 | expect(state).to.equal(null) 27 | 28 | const locationState = { pathname: '/yup' } 29 | state = locationReducer(state, locationChange(locationState)) 30 | expect(state).to.equal(locationState) 31 | state = locationReducer(state, { type: '@@@@@@@' }) 32 | expect(state).to.equal(locationState) 33 | }) 34 | }) 35 | 36 | describe('(Action Creator) locationChange', () => { 37 | it('Should be exported as a function.', () => { 38 | expect(locationChange).to.be.a('function') 39 | }) 40 | 41 | it('Should return an action with type "LOCATION_CHANGE".', () => { 42 | expect(locationChange()).to.have.property('type', LOCATION_CHANGE) 43 | }) 44 | 45 | it('Should assign the first argument to the "payload" property.', () => { 46 | const locationState = { pathname: '/yup' } 47 | expect(locationChange(locationState)).to.have.property('payload', locationState) 48 | }) 49 | 50 | it('Should default the "payload" property to "/" if not provided.', () => { 51 | expect(locationChange()).to.have.property('payload', '/') 52 | }) 53 | }) 54 | 55 | describe('(Specialized Action Creator) updateLocation', () => { 56 | let _globalState 57 | let _dispatchSpy 58 | 59 | beforeEach(() => { 60 | _globalState = { 61 | location : locationReducer(undefined, {}) 62 | } 63 | _dispatchSpy = sinon.spy((action) => { 64 | _globalState = { 65 | ..._globalState, 66 | location : locationReducer(_globalState.location, action) 67 | } 68 | }) 69 | }) 70 | 71 | it('Should be exported as a function.', () => { 72 | expect(updateLocation).to.be.a('function') 73 | }) 74 | 75 | it('Should return a function (is a thunk).', () => { 76 | expect(updateLocation({ dispatch: _dispatchSpy })).to.be.a('function') 77 | }) 78 | 79 | it('Should call dispatch exactly once.', () => { 80 | updateLocation({ dispatch: _dispatchSpy })('/') 81 | expect(_dispatchSpy.should.have.been.calledOnce) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import sinon from 'sinon' 5 | import chai from 'chai' 6 | import sinonChai from 'sinon-chai' 7 | import chaiAsPromised from 'chai-as-promised' 8 | import chaiEnzyme from 'chai-enzyme' 9 | 10 | chai.use(sinonChai) 11 | chai.use(chaiAsPromised) 12 | chai.use(chaiEnzyme()) 13 | 14 | global.chai = chai 15 | global.sinon = sinon 16 | global.expect = chai.expect 17 | global.should = chai.should() 18 | 19 | // --------------------------------------- 20 | // Require Tests 21 | // --------------------------------------- 22 | // for use with karma-webpack-with-fast-source-maps 23 | const __karmaWebpackManifest__ = []; // eslint-disable-line 24 | const inManifest = (path) => ~__karmaWebpackManifest__.indexOf(path) 25 | 26 | // require all `tests/**/*.spec.js` 27 | const testsContext = require.context('./', true, /\.spec\.js$/) 28 | 29 | // only run tests that have changed after the first pass. 30 | const testsToRun = testsContext.keys().filter(inManifest) 31 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext) 32 | 33 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 34 | if (__COVERAGE__) { 35 | const componentsContext = require.context('../src/', true, /^((?!main|reducers).)*\.js$/) 36 | componentsContext.keys().forEach(componentsContext) 37 | } 38 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-babylonjs-starter-kit", 3 | "dependencies": { 4 | // redux and redux-saga already have typings in their source. 5 | "react": "registry:npm/react#15.0.1+20170104200836", 6 | "react-redux": "registry:npm/react-redux#4.4.0+20160614222153" 7 | } 8 | } 9 | --------------------------------------------------------------------------------