├── .eslintrc.json ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.TXT ├── README.md ├── images └── splat-logo.png ├── jsdoc ├── conf.json ├── layout.tmpl └── static │ └── docs.css ├── lib ├── accelerometer.js ├── ads.js ├── assets │ ├── asset-loader.js │ ├── load-asset.js │ ├── load-assets.js │ ├── load-image.js │ └── load-sound.js ├── astar.js ├── binary-heap.js ├── box-pool.js ├── buffer.js ├── callbacks.jsdoc ├── clone.js ├── components │ ├── acceleration.js │ ├── animation.js │ ├── box-collider.js │ ├── collisions.js │ ├── constrain-position.js │ ├── easing.js │ ├── follow.js │ ├── friction.js │ ├── grid.js │ ├── image.js │ ├── index.js │ ├── life-span.js │ ├── match-aspect-ratio.js │ ├── match-center.js │ ├── match.js │ ├── movement-2d.js │ ├── player-controller-2d.js │ ├── position.js │ ├── register.js │ ├── rotation.js │ ├── shake.js │ ├── size.js │ ├── timers.js │ └── velocity.js ├── external.jsdoc ├── font-loader.js ├── game.js ├── iap.js ├── import-from-tiled.js ├── input.js ├── leaderboards.js ├── main.js ├── math.js ├── math2d.js ├── mouse.js ├── ninepatch.js ├── once.js ├── openUrl.js ├── particles.js ├── platform.js ├── prefabs.js ├── random.js ├── save-data.js ├── scene.js ├── set-or-add-component.js ├── sound-manager.js ├── split-filmstrip-animations.js └── systems │ ├── index.js │ ├── renderer │ ├── apply-shake.js │ ├── background-color.js │ ├── clear-screen.js │ ├── draw-frame-rate.js │ ├── draw-image.js │ ├── draw-rectangles.js │ ├── revert-shake.js │ ├── viewport-move-to-camera.js │ └── viewport-reset.js │ └── simulation │ ├── advance-animations.js │ ├── advance-timers.js │ ├── apply-acceleration.js │ ├── apply-easing.js │ ├── apply-friction.js │ ├── apply-movement-2d.js │ ├── apply-velocity.js │ ├── box-collider.js │ ├── box-group-collider.js │ ├── constrain-position.js │ ├── control-player.js │ ├── decay-life-span.js │ ├── follow-mouse.js │ ├── follow-parent.js │ ├── match-aspect-ratio.js │ ├── match-canvas-size.js │ ├── match-center.js │ ├── match-parent.js │ └── set-virtual-buttons.js ├── package.json └── vendor └── FontLoader.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "brace-style": [ 2, "1tbs", { "allowSingleLine": true } ], 4 | "block-spacing": [ 2, "always" ], 5 | "comma-style": [ 2, "last" ], 6 | "eol-last": [ 2, "unix" ], 7 | "indent": [ 2, 2 ], 8 | "key-spacing": [ 2, { "beforeColon": false, "afterColon": true } ], 9 | "keyword-spacing": [ 2 ], 10 | "linebreak-style": [ 2, "unix" ], 11 | "new-cap": [ 2 ], 12 | "new-parens": [ 2 ], 13 | "no-console": [ 0 ], 14 | "no-trailing-spaces": [ 2 ], 15 | "object-curly-spacing": [ 2, "always" ], 16 | "quotes": [ 2, "double" ], 17 | "semi": [ 2, "always" ], 18 | "semi-spacing": [ 2, { "before": false, "after": true } ], 19 | "space-before-blocks": [ 2, "always" ], 20 | "space-before-function-paren": [ 2, "never" ], 21 | "space-in-parens": [ 2, "never" ], 22 | "space-infix-ops": [ 2 ], 23 | "space-unary-ops": [2, { "words": true, "nonwords": false }] 24 | }, 25 | "env": { 26 | "browser": true, 27 | "commonjs": true 28 | }, 29 | "extends": "eslint:recommended" 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [7.6.0] - 2017-01-31 8 | ### Fixed 9 | - Crash in Safari due to missing gamepad suppport 10 | 11 | ### Added 12 | - z-axis support for velocity 13 | 14 | ## [7.5.1] - 2017-01-21 15 | ### Fixed 16 | - Error handling stack traces in `getMultiple` and `setMultiple`. 17 | - Gamepads in Chrome 18 | 19 | ## [7.5.0] - 2016-10-28 20 | ### Fixed 21 | - Bug where sound said it was loaded, but wasn't 22 | - Crash on undefined sound 23 | - Object leak in touch code 24 | - Function leak in draw image system 25 | 26 | ### Added 27 | - Set virtual axes with `game.inputs.setAxis()`. Useful for on-screen joysticks 28 | (coming soon). 29 | 30 | ## [7.4.1] - 2016-10-14 31 | ### Fixed 32 | - Correct the y position of non-tile-size tiles 33 | 34 | ## [7.4.0] - 2016-09-29 35 | ### Fixed 36 | - Filter images before sorting them to improve performance. 37 | 38 | ### Added 39 | - Scene configuration data is now available as `game.sceneConfig`. This can be 40 | useful for configuring systems on a scene-by-scene basis. 41 | - The `box-group-collider` system now supports a list of groups to skip 42 | collision checks against in `game.sceneConfig`. 43 | 44 | ## [7.3.0] - 2016-09-02 45 | ### Added 46 | - New `box-group-collider` system that supports `onEnter`/`onExit`/`script` 47 | event scripts, and allows you to group entities to improve performance. 48 | 49 | ### Deprecated 50 | - `box-collider` system. Upgrade to `box-group-collider`. 51 | 52 | ## [7.2.2] - 2016-08-28 53 | ### Fixed 54 | - Small performance improvement when drawing lots of images. Don't draw anything outside of camera. 55 | 56 | ## [7.2.1] - 2016-08-27 57 | ### Fixed 58 | - Property names of size component 59 | 60 | ## [7.2.0] - 2016-08-27 61 | ### Added 62 | - Support Tiled layer visibility by not importing invisible layers 63 | 64 | ### Changed 65 | - Move some new systems to the correct folder 66 | 67 | ### Fixed 68 | - Use new ECS functions in background-color system 69 | - Use new ECS functions in follow-mouse system 70 | 71 | ## [7.1.0] - 2016-08-23 72 | ### Added 73 | - Support Tiled layer offsets 74 | - Support Tiled zlib & base64 layers 75 | 76 | ## [7.0.0] - 2016-08-23 77 | ### Added 78 | - Multiple scenes can run at the same time. This can be used to draw a UI scene 79 | on top of a game scene. 80 | - Scenes now have a `speed` that effects how fast time passes 81 | 82 | ### Changed 83 | - Systems have been separated into `simulation` and `renderer` folders 84 | - Upgraded to 85 | [`entity-component-system`](https://github.com/ericlathrop/entity-component-system/blob/master/README.md) 86 | v4.x, which is a breaking change that passes through to your game via 87 | `game.entities`. 88 | - Convert the `match-center-x` and `match-center-y` systems to just 89 | `match-center`. 90 | 91 | ## [6.1.0] - 2016-07-02 92 | ### Added 93 | - `importTilemap` can now import "collection of images" tilesets 94 | 95 | ## [6.0.1] - 2016-06-27 96 | ### Fixed 97 | - Handle nonexistant tileset properties on Tiled importer 98 | 99 | ## [6.0.0] - 2016-06-04 100 | ### Changed 101 | - Update renderer systems to only have 2 arguments 102 | ### Removed 103 | - Old touch button support. Not needed since entities can now be buttons 104 | 105 | ## [5.5.1] - 2016-06-04 106 | ### Changed 107 | - Updated `entity-component-system` module. 108 | 109 | ## [5.5.0] - 2016-05-20 110 | ### Added 111 | - Entities can act as virtual buttons now with the `setVirtualButtons` system. 112 | 113 | ## [5.4.0] - 2016-04-23 114 | ### Added 115 | - `applyEasing` system 116 | 117 | ## [5.3.0] - 2016-04-16 118 | ### Fixed 119 | - `matchParent` system now also matches the z property on the `position` component 120 | ### Added 121 | - `decayLifeSpan` system 122 | - `particles` module 123 | 124 | ## [5.2.0] - 2016-04-05 125 | ### Added 126 | - `importTilemap` function for importing [Tiled](http://www.mapeditor.org/) tilemaps. 127 | - `timer` component now has a `loop` flag to make it repeat 128 | - `applyAcceleration` system 129 | - `random.inRange()` & `random.from()` 130 | - `apply-shake` and `revert-shake` systems 131 | - Use `"all"` in `systems.json` for a system to apply to all scenes. This replaces the array. 132 | 133 | ### Changed 134 | - Improved the look of the FPS counter 135 | 136 | ### Fixed 137 | - `game.sounds.setVolume` now works 138 | 139 | ## [5.1.2] - 2016-03-23 140 | ### Fixed 141 | - Updated `html5-gamepad` to fix crash in Safari 142 | 143 | ## [5.1.1] - 2016-03-17 144 | ### Fixed 145 | - Mouse button should default to not pressed 146 | 147 | ## [5.1.0] - 2016-03-13 148 | ### Fixed 149 | - Mouse bug where input sometimes doesn't register 150 | - Make draw ordering stable when entities are on the same Y position 151 | 152 | ### Added 153 | - Add `game.registerPrefab` and `game.registerPrefabs` to create new prefabs at runtime. 154 | - Suppport `alpha` transparency in `image` component. 155 | 156 | ## [5.0.0] - 2016-03-05 157 | ### Removed 158 | - `match-center` system, you should use `match-center-x` and `match-center-y` to achieve the same thing. 159 | 160 | ### Added 161 | - Gamepad support! 162 | 163 | ## [4.1.1] - 2016-02-29 164 | ### Fixed 165 | - Fix `constrainPosition` system 166 | - Fix more places in game.js where `input` needed to be `inputs`. 167 | 168 | ## [4.1.0] - 2016-02-29 169 | ### Fixed 170 | - Fix bug in game.js where `input` needed to be `inputs`. 171 | 172 | ### Added 173 | - Add `matchCenterX` and `matchCenterY` systems 174 | 175 | ## [4.0.0] - 2016-02-28 176 | ### Changed 177 | - Change `contstrain-to-playable-area` to `constrain-position`, and make the system use an entity for the area. 178 | - Renamed `game.input` to `game.inputs`. 179 | - Moved `zindex` component into the `position` component's `z` property. 180 | 181 | ## [3.2.0] - 2015-01-30 182 | ### Added 183 | - Inputs support mouse buttons 184 | 185 | ## [3.1.1] - 2015-01-30 186 | ### Fixed 187 | - allow `game.switchScene()` during scene enter script 188 | 189 | ## [3.1.0] - 2015-01-30 190 | ### Added 191 | - match-center system 192 | 193 | ## [3.0.2] - 2015-12-30 194 | ### Fixed 195 | - Fix soundloader bug. 196 | - Default rotation.x and rotation.y to the center of the entity. 197 | 198 | ## [3.0.1] - 2015-12-30 199 | ### Fixed 200 | - Remove deleted entities from collision lists. 201 | 202 | ## [3.0.0] - 2015-12-30 203 | ### Added 204 | - Add `instantiatePrefab` function to instantiate new entities from prefabs 205 | 206 | ### Changed 207 | - `Game` constructor now loads all the json files by itself. Now it only needs 2 arguments. 208 | 209 | ### Fixed 210 | - animation frame splitting now copies all animation properties, and doesn't lose any 211 | 212 | ## [2.0.0] - 2015-12-28 213 | ### Removed 214 | - remove magical "splatjs:" way of loading systems. 215 | 216 | ## [1.0.0] - 2015-12-28 217 | ### Changed 218 | - automatically size the canvas based on a selectable algorithm. 219 | 220 | ### Added 221 | - matchCanvasSize system to make an entity the same size as the canvas 222 | - matchAspectRatio system to make an entity match the aspect ratio of another entity 223 | 224 | ## [0.7.0] - 2015-12-21 225 | ### Added 226 | - add `Input.buttonPressed()` and `Input.buttonReleased()` 227 | 228 | ## [0.6.2] - 2015-12-21 229 | ### Added 230 | - add warnings about bad image component values and provide defaults for unset values 231 | 232 | ### Fixed 233 | - fix bug where animations wouldn't work 234 | 235 | ## [0.6.1] - 2015-12-20 236 | ### Fixed 237 | - mouse coordinates scale correctly when no css is applied to canvas 238 | 239 | ## [0.6.0] - 2015-12-20 240 | ### Changed 241 | - use box-intersect module for faster collision detection 242 | 243 | ## [0.5.0] - 2015-12-19 244 | ### Added 245 | - window.timeSystems() to log timings of ECS systems 246 | 247 | ### Changed 248 | - Speed up advanceAnimations system 249 | 250 | ## [0.4.2] - 2015-12-19 251 | ### Fixed 252 | - applyMovement2d never found entities 253 | 254 | ## [0.4.1] - 2015-12-17 255 | ### Fixed 256 | - Readme typo 257 | - Format changelog 258 | 259 | ## [0.4.0] - 2015-12-17 260 | ### Changed 261 | - Upgrade to entity-component-system 2.0.0 262 | 263 | ## [0.3.2] 264 | - Add method to reset box collider cache 265 | 266 | ## [0.3.1] 267 | - Un-scale the viewport when it is reset 268 | 269 | ## [0.3.0] 270 | - Support scaling of viewport through camera 271 | - Draw custom buffer for an entity if it is specified 272 | 273 | ## [0.2.0] 274 | - Support rotation when drawing images. 275 | 276 | ## [0.1.1] 277 | - Log more info on no such image error 278 | 279 | ## [0.1.0] 280 | - Add matchParent system. 281 | - Allow sound loop start and end settings 282 | 283 | ## [0.0.1] 284 | - Add a way to remove a deleted entity from the collision detection cache. 285 | 286 | ## [0.0.0] 287 | - Fork from original splatjs project. 288 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at eric@ericlathrop.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Eric Lathrop 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Splat ECS](./images/splat-logo.png) 2 | 3 | A 2d HTML5 Canvas game engine 4 | 5 | Splat ECS is a 2d game engine made for creating multi-platform games entirely in JavaScript. Splat ECS is built around the [Entity Component System](https://github.com/ericlathrop/entity-component-system) pattern, which is flexible and promotes composition of behaviors. 6 | 7 | # Features 8 | 9 | * Rectangles! 10 | * Keyboard, mouse, touch, & gamepad input 11 | * Sounds and music (Web Audio API and HTML5 Audio) 12 | * Sprite animation 13 | * Asset loading, and built-in loading screen 14 | * Games work well on phones, tablets, and desktop browsers. 15 | * A\* Pathfinding 16 | * Particles 17 | * SCREENSHAKE 18 | * Tiled map editor support 19 | * Easing 20 | 21 | # Supported (tested) Platforms 22 | 23 | * Chrome (desktop & mobile) 24 | * Firefox 25 | * Internet Explorer (desktop & mobile) 26 | * Safari (desktop & mobile) 27 | * Mac using [Electron](https://github.com/atom/electron) 28 | * Linux x64 using [Electron](https://github.com/atom/electron) 29 | * Chrome Web Store (currently broken [see issue #69](https://github.com/SplatJS/splat-ecs/issues/69)) 30 | * Android using [Cordova](https://cordova.apache.org/) 31 | 32 | Splat now works in Cordova, and due to updates to recent phone browsers we have seen good framerates on Android in google Chome. We have not tested Cordova builds on iOS yet, please let us know what you find out. 33 | 34 | # Requirements 35 | * Browser (like Firefox or Chrome) 36 | * Text editor 37 | * Terminal 38 | * [Node.js](https://nodejs.org/en/) 39 | 40 | # New to Splat? 41 | If you are new to Splat, it is highly recommended that you try out the [tutorial project](http://splatjs.com/tutorials/splatformer). 42 | 43 | # Create a new Game 44 | 1. [Clone or download a zip of the starter project](https://github.com/SplatJS/splat-ecs-starter-project) 45 | 46 | 2. (skip this step if you are cloning the repo) The zip file should be called splat-ecs-starter-project-master.zip. Unzip this file and you will be left with a folder named splat-ecs-starter-project. 47 | 48 | 3. In your terminal navigate into the splat-ecs-starter-project folder. 49 | 50 | `cd /Path/To/splat-ecs-starter-project` 51 | 52 | 4. Next we will run npm install to install Splat ECS and all of it's modules: 53 | 54 | `npm install` 55 | 5. This will install all of the game and engine dependencies from NPM — it can take a couple of minutes. If you see any warning (denoted by npm WARN) this is okay, this just means that a package Splat-ECS uses is out of date it should not effect your game and newer versions of Splat-ECS Starter Project will take care of this issue. You will know npm install is finished when the terminal returns to your command prompt (you will see your username). 56 | 57 | 6. To run a Splat ECS game all you need to do is navigate to the project folder in your terminal and type `npm start` 58 | This will run webpack, which builds your game and also runs eslint which checks your JavaScript code for errors. 59 | 60 | 7. You should try running your game to make sure it is working before you continue. When the last line in your terminal reads 'webpack: bundle is now VALID.' this means webpack is done and now you can open a browser and go to `localhost:4000`. 61 | 62 | The Splat ECS sample game is just white screen with a black-outlined square you can control with WASD, or arrow keys. Test that this is working and note that if the keys are not working you may need to click inside the browser window to give the game your 'focus'. 63 | 64 | # Games using Splat (ECS) 65 | * [Cluster Junk](https://github.com/TwoScoopGames/Cluster-Junk) 66 | * [Cali Bunga](https://riseshinegames.itch.io/cali-bunga) 67 | * [Flip Flap Pong](https://riseshinegames.itch.io/flip-flap-pong) 68 | * [Polymorphic](http://riseandshinegames.github.io/Polymorphic/build/) 69 | * [Electropolis](https://two-scoop-games.itch.io/electropolis) 70 | * [Morning Ritual](http://twoscoopgames.com/morningritual/game/) 71 | * [Drunken Boss Fight](http://aquisenberry.itch.io/jam-build) 72 | * [Zen Madness](http://aquisenberry.github.io/ggj_meditate/build/) 73 | * [Treatment and Control](http://twoscoopgames.com/ggj15/) 74 | * [The Day the World Changed](https://github.com/TwoScoopGames/ggj15) 75 | * [Uprooted](http://twoscoopgames.com/ld32/) 76 | 77 | See more Splat games at [http://splatjs.com/](http://splatjs.com/) 78 | 79 | Send a pull request to add your game to the list! 80 | 81 | ## Contributing 82 | 83 | If you are interested in participating in this project, please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details on our code of conduct. 84 | 85 | ## Versioning 86 | 87 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/SplatJS/splat-ecs/tags). 88 | 89 | ## Authors 90 | 91 | * **[Eric Lathrop](https://github.com/ericlathrop)** - *Initial work* 92 | 93 | See also the list of [contributors](https://github.com/SplatJS/splat-ecs/contributors) who participated in this project. 94 | 95 | ## License 96 | 97 | This project is licensed under the MIT License - see the [LICENSE.TXT](LICENSE.TXT) file for details 98 | -------------------------------------------------------------------------------- /images/splat-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SplatJS/splat-ecs/c8d4f5f5871c1f02781c6e6adc49a88635124415/images/splat-logo.png -------------------------------------------------------------------------------- /jsdoc/conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "default": { 4 | "layoutFile": "./layout.tmpl", 5 | "staticFiles": { 6 | "include": [ 7 | "./jsdoc/static" 8 | ] 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jsdoc/layout.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: <?js= title ?> 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |

22 | 23 | 24 |
25 | 26 | 29 | 30 |
31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /jsdoc/static/docs.css: -------------------------------------------------------------------------------- 1 | code { 2 | color: #c25; 3 | background-color: #f7f7f9; 4 | border: 1px solid #ccc; 5 | border-radius: 3px; 6 | padding: 1px 3px; 7 | } 8 | 9 | .params .name code { 10 | color: #c25; 11 | } 12 | -------------------------------------------------------------------------------- /lib/accelerometer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Holds the current orientation of the device if the device has an accelerometer. An instance of Accelerometer is available as {@link Splat.Game#accelerometer}. 3 | * @constructor 4 | */ 5 | function Accelerometer() { 6 | /** 7 | * The angle of the device rotated around the z-axis. The z-axis is the axis coming out of the device screen. alpha represents how much the devies is spun around the center of the screen. 8 | * @member {number} 9 | */ 10 | this.alpha = 0; 11 | /** 12 | * The angle of the device rotated around the x-axis. The x-axis is horizontal across the device screen. beta represents how much the device is tilted forward or backward. 13 | * @member {number} 14 | */ 15 | this.beta = 0; 16 | /** 17 | * The angle of the device rotated around the y-axis. The y-axis is vertical across the device screen. gamma represents how much the device is turned left or right. 18 | * @member {number} 19 | */ 20 | this.gamma = 0; 21 | 22 | var self = this; 23 | window.addEventListener("deviceorientation", function(event) { 24 | self.alpha = event.alpha; 25 | self.beta = event.beta; 26 | self.gamma = event.gamma; 27 | }, false); 28 | } 29 | 30 | module.exports = Accelerometer; 31 | -------------------------------------------------------------------------------- /lib/ads.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace Splat.ads 3 | */ 4 | 5 | var platform = require("./platform"); 6 | 7 | if (platform.isEjecta()) { 8 | var adBanner = new window.Ejecta.AdBanner(); 9 | 10 | var isLandscape = window.innerWidth > window.innerHeight; 11 | 12 | var sizes = { 13 | "iPhone": { 14 | "portrait": { 15 | "width": 320, 16 | "height": 50 17 | }, 18 | "landscape": { 19 | "width": 480, 20 | "height": 32 21 | } 22 | }, 23 | "iPad": { 24 | "portrait": { 25 | "width": 768, 26 | "height": 66 27 | }, 28 | "landscape": { 29 | "width": 1024, 30 | "height": 66 31 | } 32 | } 33 | }; 34 | 35 | var device = window.navigator.userAgent.indexOf("iPad") >= 0 ? "iPad" : "iPhone"; 36 | var size = sizes[device][isLandscape ? "landscape" : "portrait"]; 37 | 38 | module.exports = { 39 | /** 40 | * Show an advertisement. 41 | * @alias Splat.ads.show 42 | * @param {boolean} isAtBottom true if the ad should be shown at the bottom of the screen. false if it should be shown at the top. 43 | */ 44 | "show": function(isAtBottom) { 45 | adBanner.isAtBottom = isAtBottom; 46 | adBanner.show(); 47 | }, 48 | /** 49 | * Hide the current advertisement. 50 | * @alias Splat.ads.hide 51 | */ 52 | "hide": function() { 53 | adBanner.hide(); 54 | }, 55 | /** 56 | * The width of the ad that will show. 57 | * @alias Splat.ads#width 58 | */ 59 | "width": size.width, 60 | /** 61 | * The height of the ad that will show. 62 | * @alias Splat.ads#height 63 | */ 64 | "height": size.height 65 | }; 66 | } else { 67 | module.exports = { 68 | "show": function() {}, 69 | "hide": function() {}, 70 | "width": 0, 71 | "height": 0 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /lib/assets/asset-loader.js: -------------------------------------------------------------------------------- 1 | var loadAssets = require("./load-assets"); 2 | 3 | /** 4 | * Loads external assets, lets you track their progress, and lets you access the loaded data. 5 | * @constructor 6 | */ 7 | function AssetLoader(manifest, loader) { 8 | this.assets = loadAssets(manifest, loader, function(err) { 9 | if (err) { 10 | console.error(err); 11 | } 12 | }); 13 | } 14 | AssetLoader.prototype.bytesLoaded = function() { 15 | return Object.keys(this.assets).reduce(function(accum, key) { 16 | var asset = this.assets[key]; 17 | return accum + asset.loaded; 18 | }.bind(this), 0); 19 | }; 20 | AssetLoader.prototype.totalBytes = function() { 21 | return Object.keys(this.assets).reduce(function(accum, key) { 22 | var asset = this.assets[key]; 23 | return accum + asset.total; 24 | }.bind(this), 0); 25 | }; 26 | /** 27 | * Retrieve a loaded asset. 28 | * @param {string} name The name given to the asset in the manifest. 29 | * @returns {object} 30 | */ 31 | AssetLoader.prototype.get = function(name) { 32 | var asset = this.assets[name]; 33 | if (asset === undefined) { 34 | console.error("No such asset:", name); 35 | return undefined; 36 | } 37 | return asset.data; 38 | }; 39 | 40 | module.exports = AssetLoader; 41 | -------------------------------------------------------------------------------- /lib/assets/load-asset.js: -------------------------------------------------------------------------------- 1 | var once = require("../once"); 2 | 3 | module.exports = function(url, responseType, callback) { 4 | callback = once(callback); 5 | var asset = { 6 | loaded: 0, 7 | total: 1, 8 | data: undefined 9 | }; 10 | 11 | var request = new XMLHttpRequest(); 12 | request.open("GET", url, true); 13 | request.responseType = responseType; 14 | request.addEventListener("progress", function(e) { 15 | if (e.lengthComputable) { 16 | asset.loaded = e.loaded; 17 | asset.total = e.total; 18 | } 19 | }); 20 | request.addEventListener("load", function() { 21 | asset.data = request.response; 22 | asset.loaded = asset.total; 23 | callback(undefined, asset); 24 | }); 25 | request.addEventListener("abort", function(e) { 26 | asset.error = e; 27 | callback(e); 28 | }); 29 | request.addEventListener("error", function(e) { 30 | asset.error = e; 31 | callback(e); 32 | }); 33 | try { 34 | request.send(); 35 | } catch (e) { 36 | callback(e); 37 | } 38 | return asset; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/assets/load-assets.js: -------------------------------------------------------------------------------- 1 | var once = require("../once"); 2 | 3 | module.exports = function(manifest, loader, callback) { 4 | callback = once(callback); 5 | 6 | var finished = 0; 7 | var keys = Object.keys(manifest); 8 | return keys.reduce(function(assets, key) { 9 | var url = manifest[key]; 10 | assets[key] = loader(url, function(err) { 11 | if (err) { 12 | callback(err); 13 | return; 14 | } 15 | finished++; 16 | if (finished === keys.length) { 17 | callback(undefined, assets); 18 | } 19 | }); 20 | return assets; 21 | }, {}); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/assets/load-image.js: -------------------------------------------------------------------------------- 1 | var loadAsset = require("./load-asset"); 2 | 3 | function blobToImage(blob) { 4 | var image = new Image(); 5 | image.src = window.URL.createObjectURL(blob); 6 | return image; 7 | } 8 | 9 | module.exports = function(url, callback) { 10 | return loadAsset(url, "blob", function(err, asset) { 11 | if (err) { 12 | callback(err); 13 | return; 14 | } 15 | asset.data = blobToImage(asset.data); 16 | callback(undefined, asset); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/assets/load-sound.js: -------------------------------------------------------------------------------- 1 | var loadAsset = require("./load-asset"); 2 | 3 | module.exports = function(audioContext, url, callback) { 4 | return loadAsset(url, "arraybuffer", function(err, asset) { 5 | if (err) { 6 | callback(err); 7 | return; 8 | } 9 | 10 | // FIXME: this is a hack, we shouldn't be rewriting asset.data in place like this 11 | var data = asset.data; 12 | asset.data = undefined; 13 | 14 | // FIXME: this may not work with a Blob(), might have to convert to array buffer http://stackoverflow.com/a/15981017 15 | audioContext.decodeAudioData(data, function(buffer) { 16 | asset.data = buffer; 17 | callback(undefined, asset); 18 | }, callback); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/astar.js: -------------------------------------------------------------------------------- 1 | var BinaryHeap = require("./binary-heap"); 2 | 3 | /** 4 | * Implements the [A* pathfinding algorithm]{@link http://en.wikipedia.org/wiki/A*_search_algorithm} on a 2-dimensional grid. You can use this to find a path between a source and destination coordinate while avoiding obstacles. 5 | * @constructor 6 | * @alias Splat.AStar 7 | * @param {isWalkable} isWalkable A function to test if a coordinate is walkable by the entity you're performing the pathfinding for. 8 | */ 9 | function AStar(isWalkable) { 10 | this.destX = 0; 11 | this.destY = 0; 12 | this.scaleX = 1; 13 | this.scaleY = 1; 14 | this.openNodes = {}; 15 | this.closedNodes = {}; 16 | this.openHeap = new BinaryHeap(function(a, b) { 17 | return a.f - b.f; 18 | }); 19 | this.isWalkable = isWalkable; 20 | } 21 | /** 22 | * The [A* heuristic]{@link http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html}, commonly referred to as h(x), that estimates how far a location is from the destination. This implementation is the [Manhattan method]{@link http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#manhattan-distance}, which is good for situations when the entity can travel in four directions. Feel free to replace this with a different heuristic implementation. 23 | * @param {number} x The x coordinate to estimate the distance to the destination. 24 | * @param {number} y The y coordinate to estimate the distance to the destination. 25 | */ 26 | AStar.prototype.heuristic = function(x, y) { 27 | // manhattan method 28 | var dx = Math.abs(x - this.destX) / this.scaleX; 29 | var dy = Math.abs(y - this.destY) / this.scaleY; 30 | return dx + dy; 31 | }; 32 | /** 33 | * Make a node to track a given coordinate 34 | * @param {number} x The x coordinate of the node 35 | * @param {number} y The y coordinate of the node 36 | * @param {object} parent The parent node for the current node. This chain of parents eventually points back at the starting node. 37 | * @param {number} g The g(x) travel cost from the parent node to this node. 38 | * @private 39 | */ 40 | AStar.prototype.makeNode = function(x, y, parent, g) { 41 | g += parent.g; 42 | var h = this.heuristic(x, y); 43 | 44 | return { 45 | x: x, 46 | y: y, 47 | parent: parent, 48 | f: g + h, 49 | g: parent.g + g, 50 | h: h 51 | }; 52 | }; 53 | /** 54 | * Update the g(x) travel cost to a node if a new lower-cost path is found. 55 | * @param {string} key The key of the node on the open list. 56 | * @param {object} parent A parent node that may have a shorter path for the node specified in key. 57 | * @param {number} g The g(x) travel cost from parent to the node specified in key. 58 | * @private 59 | */ 60 | AStar.prototype.updateOpenNode = function(key, parent, g) { 61 | var node = this.openNodes[key]; 62 | if (!node) { 63 | return false; 64 | } 65 | 66 | var newG = parent.g + g; 67 | 68 | if (newG >= node.g) { 69 | return true; 70 | } 71 | 72 | node.parent = parent; 73 | node.g = newG; 74 | node.f = node.g + node.h; 75 | 76 | var pos = this.openHeap.indexOf(node); 77 | this.openHeap.bubbleUp(pos); 78 | 79 | return true; 80 | }; 81 | /** 82 | * Create a neighbor node to a parent node, and add it to the open list for consideration. 83 | * @param {string} key The key of the new neighbor node. 84 | * @param {number} x The x coordinate of the new neighbor node. 85 | * @param {number} y The y coordinate of the new neighbor node. 86 | * @param {object} parent The parent node of the new neighbor node. 87 | * @param {number} g The travel cost from the parent to the new parent node. 88 | * @private 89 | */ 90 | AStar.prototype.insertNeighbor = function(key, x, y, parent, g) { 91 | var node = this.makeNode(x, y, parent, g); 92 | this.openNodes[key] = node; 93 | this.openHeap.insert(node); 94 | }; 95 | AStar.prototype.tryNeighbor = function(x, y, parent, g) { 96 | var key = makeKey(x, y); 97 | if (this.closedNodes[key]) { 98 | return; 99 | } 100 | if (!this.isWalkable(x, y)) { 101 | return; 102 | } 103 | if (!this.updateOpenNode(key, parent, g)) { 104 | this.insertNeighbor(key, x, y, parent, g); 105 | } 106 | }; 107 | AStar.prototype.getNeighbors = function getNeighbors(parent) { 108 | var diagonalCost = 1.4; 109 | var straightCost = 1; 110 | this.tryNeighbor(parent.x - this.scaleX, parent.y - this.scaleY, parent, diagonalCost); 111 | this.tryNeighbor(parent.x, parent.y - this.scaleY, parent, straightCost); 112 | this.tryNeighbor(parent.x + this.scaleX, parent.y - this.scaleY, parent, diagonalCost); 113 | 114 | this.tryNeighbor(parent.x - this.scaleX, parent.y, parent, straightCost); 115 | this.tryNeighbor(parent.x + this.scaleX, parent.y, parent, straightCost); 116 | 117 | this.tryNeighbor(parent.x - this.scaleX, parent.y + this.scaleY, parent, diagonalCost); 118 | this.tryNeighbor(parent.x, parent.y + this.scaleY, parent, straightCost); 119 | this.tryNeighbor(parent.x + this.scaleX, parent.y + this.scaleY, parent, diagonalCost); 120 | }; 121 | 122 | function generatePath(node) { 123 | var path = []; 124 | while (node.parent) { 125 | var ix = node.x; 126 | var iy = node.y; 127 | while (ix !== node.parent.x || iy !== node.parent.y) { 128 | path.unshift({ x: ix, y: iy }); 129 | 130 | var dx = node.parent.x - ix; 131 | if (dx > 0) { 132 | ix++; 133 | } else if (dx < 0) { 134 | ix--; 135 | } 136 | var dy = node.parent.y - iy; 137 | if (dy > 0) { 138 | iy++; 139 | } else if (dy < 0) { 140 | iy--; 141 | } 142 | } 143 | node = node.parent; 144 | } 145 | return path; 146 | } 147 | 148 | function makeKey(x, y) { 149 | return x + "," + y; 150 | } 151 | 152 | /** 153 | * Search for an optimal path between srcX, srcY and destX, destY, while avoiding obstacles. 154 | * @param {number} srcX The starting x coordinate 155 | * @param {number} srcY The starting y coordinate 156 | * @param {number} destX The destination x coordinate 157 | * @param {number} destY The destination y coordinate 158 | * @returns {Array} The optimal path, in the form of an array of objects that each have an x and y property. 159 | */ 160 | AStar.prototype.search = function aStar(srcX, srcY, destX, destY) { 161 | function scale(c, s) { 162 | var downscaled = Math.floor(c / s); 163 | return downscaled * s; 164 | } 165 | srcX = scale(srcX, this.scaleX); 166 | srcY = scale(srcY, this.scaleY); 167 | this.destX = scale(destX, this.scaleX); 168 | this.destY = scale(destY, this.scaleY); 169 | 170 | if (!this.isWalkable(this.destX, this.destY)) { 171 | return []; 172 | } 173 | 174 | var srcKey = makeKey(srcX, srcY); 175 | var srcNode = { 176 | x: srcX, 177 | y: srcY, 178 | g: 0, 179 | h: this.heuristic(srcX, srcY) 180 | }; 181 | srcNode.f = srcNode.h; 182 | this.openNodes = {}; 183 | this.openNodes[srcKey] = srcNode; 184 | this.openHeap = new BinaryHeap(function(a, b) { 185 | return a.f - b.f; 186 | }); 187 | this.openHeap.insert(srcNode); 188 | this.closedNodes = {}; 189 | 190 | var node = this.openHeap.deleteRoot(); 191 | while (node) { 192 | var key = makeKey(node.x, node.y); 193 | delete this.openNodes[key]; 194 | this.closedNodes[key] = node; 195 | if (node.x === this.destX && node.y === this.destY) { 196 | return generatePath(node); 197 | } 198 | this.getNeighbors(node); 199 | node = this.openHeap.deleteRoot(); 200 | } 201 | return []; 202 | }; 203 | 204 | module.exports = AStar; 205 | -------------------------------------------------------------------------------- /lib/binary-heap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An implementation of the [Binary Heap]{@link https://en.wikipedia.org/wiki/Binary_heap} data structure suitable for priority queues. 3 | * @constructor 4 | * @alias Splat.BinaryHeap 5 | * @param {compareFunction} cmp A comparison function that determines how the heap is sorted. 6 | */ 7 | function BinaryHeap(cmp) { 8 | /** 9 | * The comparison function for sorting the heap. 10 | * @member {compareFunction} 11 | * @private 12 | */ 13 | this.cmp = cmp; 14 | /** 15 | * The list of elements in the heap. 16 | * @member {Array} 17 | * @private 18 | */ 19 | this.array = []; 20 | /** 21 | * The number of elements in the heap. 22 | * @member {number} 23 | * @readonly 24 | */ 25 | this.length = 0; 26 | } 27 | /** 28 | * Calculate the index of a node's parent. 29 | * @param {number} i The index of the child node 30 | * @returns {number} 31 | * @private 32 | */ 33 | BinaryHeap.prototype.parentIndex = function(i) { 34 | return Math.floor((i - 1) / 2); 35 | }; 36 | /** 37 | * Calculate the index of a parent's first child node. 38 | * @param {number} i The index of the parent node 39 | * @returns {number} 40 | * @private 41 | */ 42 | BinaryHeap.prototype.firstChildIndex = function(i) { 43 | return (2 * i) + 1; 44 | }; 45 | /** 46 | * Bubble a node up the heap, stopping when it's value should not be sorted before its parent's value. 47 | * @param {number} pos The index of the node to bubble up. 48 | * @private 49 | */ 50 | BinaryHeap.prototype.bubbleUp = function(pos) { 51 | if (pos === 0) { 52 | return; 53 | } 54 | 55 | var data = this.array[pos]; 56 | var parentIndex = this.parentIndex(pos); 57 | var parent = this.array[parentIndex]; 58 | if (this.cmp(data, parent) < 0) { 59 | this.array[parentIndex] = data; 60 | this.array[pos] = parent; 61 | this.bubbleUp(parentIndex); 62 | } 63 | }; 64 | /** 65 | * Store a new node in the heap. 66 | * @param {object} data The data to store 67 | */ 68 | BinaryHeap.prototype.insert = function(data) { 69 | this.array.push(data); 70 | this.length = this.array.length; 71 | var pos = this.array.length - 1; 72 | this.bubbleUp(pos); 73 | }; 74 | /** 75 | * Bubble a node down the heap, stopping when it's value should not be sorted after its parent's value. 76 | * @param {number} pos The index of the node to bubble down. 77 | * @private 78 | */ 79 | BinaryHeap.prototype.bubbleDown = function(pos) { 80 | var left = this.firstChildIndex(pos); 81 | var right = left + 1; 82 | var largest = pos; 83 | if (left < this.array.length && this.cmp(this.array[left], this.array[largest]) < 0) { 84 | largest = left; 85 | } 86 | if (right < this.array.length && this.cmp(this.array[right], this.array[largest]) < 0) { 87 | largest = right; 88 | } 89 | if (largest !== pos) { 90 | var tmp = this.array[pos]; 91 | this.array[pos] = this.array[largest]; 92 | this.array[largest] = tmp; 93 | this.bubbleDown(largest); 94 | } 95 | }; 96 | /** 97 | * Remove the heap's root node, and return it. The root node is whatever comes first as determined by the {@link compareFunction}. 98 | * @returns {data} The root node's data. 99 | */ 100 | BinaryHeap.prototype.deleteRoot = function() { 101 | var root = this.array[0]; 102 | if (this.array.length <= 1) { 103 | this.array = []; 104 | this.length = 0; 105 | return root; 106 | } 107 | this.array[0] = this.array.pop(); 108 | this.length = this.array.length; 109 | this.bubbleDown(0); 110 | return root; 111 | }; 112 | /** 113 | * Search for a node in the heap. 114 | * @param {object} data The data to search for. 115 | * @returns {number} The index of the data in the heap, or -1 if it is not found. 116 | */ 117 | BinaryHeap.prototype.indexOf = function(data) { 118 | for (var i = 0; i < this.array.length; i++) { 119 | if (this.array[i] === data) { 120 | return i; 121 | } 122 | } 123 | return -1; 124 | }; 125 | 126 | module.exports = BinaryHeap; 127 | -------------------------------------------------------------------------------- /lib/box-pool.js: -------------------------------------------------------------------------------- 1 | var boxIntersect = require("box-intersect"); 2 | var ObjectPool = require("entity-component-system/lib/object-pool"); 3 | 4 | function boxFactory() { 5 | return [0, 0, 0, 0]; 6 | } 7 | 8 | var boxPool = new ObjectPool(boxFactory, 64); 9 | 10 | function BoxPool() { 11 | this.ids = []; 12 | this.boxes = []; 13 | this.length = 0; 14 | this.handleCollisionSelf = this.handleCollisionSelf.bind(this); 15 | this.handleCollisionOther = this.handleCollisionOther.bind(this); 16 | } 17 | BoxPool.prototype.add = function(id, position, size) { 18 | this.ids.push(id); 19 | 20 | var box = boxPool.alloc(); 21 | box[0] = position.x; 22 | box[1] = position.y; 23 | box[2] = position.x + size.width; 24 | box[3] = position.y + size.height; 25 | this.boxes.push(box); 26 | }; 27 | BoxPool.prototype.reset = function() { 28 | this.ids.length = 0; 29 | for (var i = 0; i < this.boxes.length; i++) { 30 | boxPool.free(this.boxes[i]); 31 | } 32 | this.boxes.length = 0; 33 | }; 34 | BoxPool.prototype.collideOther = function(otherBoxPool, handler) { 35 | this.otherBoxPool = otherBoxPool; 36 | this.handler = handler; 37 | boxIntersect(this.boxes, otherBoxPool.boxes, this.handleCollisionOther); 38 | }; 39 | BoxPool.prototype.handleCollisionOther = function(a, b) { 40 | var idA = this.ids[a]; 41 | var idB = this.otherBoxPool.ids[b]; 42 | return this.handler(idA, idB); 43 | }; 44 | BoxPool.prototype.collideSelf = function(handler) { 45 | this.handler = handler; 46 | boxIntersect(this.boxes, this.handleCollisionSelf); 47 | }; 48 | BoxPool.prototype.handleCollisionSelf = function(a, b) { 49 | var idA = this.ids[a]; 50 | var idB = this.ids[b]; 51 | return this.handler(idA, idB); 52 | }; 53 | 54 | module.exports = BoxPool; 55 | -------------------------------------------------------------------------------- /lib/buffer.js: -------------------------------------------------------------------------------- 1 | /** @module splat-ecs/lib/buffer */ 2 | 3 | var platform = require("./platform"); 4 | 5 | /** 6 | * Make an invisible {@link canvas}. 7 | * @param {number} width The width of the canvas 8 | * @param {number} height The height of the canvas 9 | * @returns {external:canvas} A canvas DOM element 10 | * @private 11 | */ 12 | function makeCanvas(width, height) { 13 | var c = document.createElement("canvas"); 14 | c.width = width; 15 | c.height = height; 16 | // when retina support is enabled, context.getImageData() reads from the wrong pixel causing NinePatch to break 17 | if (platform.isEjecta()) { 18 | c.retinaResolutionEnabled = false; 19 | } 20 | return c; 21 | } 22 | 23 | /** 24 | * Make an invisible canvas buffer, and draw on it. 25 | * @param {number} width The width of the buffer 26 | * @param {number} height The height of the buffer 27 | * @param {drawCallback} drawFun The callback that draws on the buffer 28 | * @returns {external:canvas} The drawn buffer 29 | */ 30 | function makeBuffer(width, height, drawFun) { 31 | var canvas = makeCanvas(width, height); 32 | var ctx = canvas.getContext("2d"); 33 | // when image smoothing is enabled, the image gets blurred and the pixel data isn't correct even when the image shouldn't be scaled which breaks NinePatch 34 | if (platform.isEjecta()) { 35 | ctx.imageSmoothingEnabled = false; 36 | } 37 | drawFun(ctx); 38 | return canvas; 39 | } 40 | 41 | /** 42 | * Make a horizonally-flipped copy of a buffer or image. 43 | * @param {external:canvas|external:image} buffer The original image 44 | * @return {external:canvas} The flipped buffer 45 | */ 46 | function flipBufferHorizontally(buffer) { 47 | return makeBuffer(buffer.width, buffer.height, function(context) { 48 | context.scale(-1, 1); 49 | context.drawImage(buffer, -buffer.width, 0); 50 | }); 51 | } 52 | 53 | /** 54 | * Make a vertically-flipped copy of a buffer or image. 55 | * @param {external:canvas|external:image} buffer The original image 56 | * @return {external:canvas} The flipped buffer 57 | */ 58 | function flipBufferVertically(buffer) { 59 | return makeBuffer(buffer.width, buffer.height, function(context) { 60 | context.scale(1, -1); 61 | context.drawImage(buffer, 0, -buffer.height); 62 | }); 63 | } 64 | /** 65 | * Make a copy of a buffer that is rotated 90 degrees clockwise. 66 | * @param {external:canvas|external:image} buffer The original image 67 | * @return {external:canvas} The rotated buffer 68 | */ 69 | function rotateClockwise(buffer) { 70 | var w = buffer.height; 71 | var h = buffer.width; 72 | var w2 = Math.floor(w / 2); 73 | var h2 = Math.floor(h / 2); 74 | return makeBuffer(w, h, function(context) { 75 | context.translate(w2, h2); 76 | context.rotate(Math.PI / 2); 77 | context.drawImage(buffer, -h2, -w2); 78 | }); 79 | } 80 | /** 81 | * Make a copy of a buffer that is rotated 90 degrees counterclockwise. 82 | * @param {external:canvas|external:image} buffer The original image 83 | * @return {external:canvas} The rotated buffer 84 | */ 85 | function rotateCounterclockwise(buffer) { 86 | var w = buffer.height; 87 | var h = buffer.width; 88 | var w2 = Math.floor(w / 2); 89 | var h2 = Math.floor(h / 2); 90 | return makeBuffer(w, h, function(context) { 91 | context.translate(w2, h2); 92 | context.rotate(-Math.PI / 2); 93 | context.drawImage(buffer, -h2, -w2); 94 | }); 95 | } 96 | 97 | module.exports = { 98 | makeBuffer: makeBuffer, 99 | flipBufferHorizontally: flipBufferHorizontally, 100 | flipBufferVertically: flipBufferVertically, 101 | rotateClockwise: rotateClockwise, 102 | rotateCounterclockwise: rotateCounterclockwise 103 | }; 104 | -------------------------------------------------------------------------------- /lib/callbacks.jsdoc: -------------------------------------------------------------------------------- 1 | /** 2 | * A callback that takes no parameters, and returns nothing. 3 | * See the user for the description of what it should do. 4 | * @callback emptyCallback 5 | */ 6 | /** 7 | * A callback to perform a single step in a simulation. 8 | * @callback simulationCallback 9 | * @param {number} elapsedMillis The number of milliseconds to advance the simulation by 10 | */ 11 | /** 12 | * A callback to perform drawing operations on a canvas 13 | * @callback drawCallback 14 | * @param {external:CanvasRenderingContext2D} context The context to use for drawing 15 | */ 16 | /** 17 | * A function that compares two elements and returns a number that determines how they should be sorted. 18 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort Array.prototype.sort() at Mozilla Developer Network} 19 | * @callback compareFunction 20 | * @param {object} a 21 | * @param {object} b 22 | * @returns {number} 23 | * If compareFunction(a, b) returns less than 0, then a comes first. 24 | * If compareFunction(a, b) returns 0, then don't change the order of a relative to b. 25 | * If compareFunction(a, b) returns greater than 0, then b comes first. 26 | */ 27 | /** 28 | * A function that tests if a given coordinate can be travelled across. 29 | * @callback isWalkable 30 | * @param {number} x The position along the x-axis on a grid 31 | * @param {number} y The position along the y-axis on a grid 32 | * @returns {boolean} 33 | */ 34 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | module.exports = function clone(obj) { 2 | if (obj === undefined) { 3 | return undefined; 4 | } 5 | return JSON.parse(JSON.stringify(obj)); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/components/acceleration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | x: 0, 5 | y: 0 6 | }; 7 | }, 8 | reset: function(acceleration) { 9 | acceleration.x = 0; 10 | acceleration.y = 0; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/components/animation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function animation() { 3 | return { 4 | time: 0, 5 | frame: 0, 6 | loop: true, 7 | speed: 1 8 | }; 9 | }, 10 | reset: function(animation) { 11 | delete animation.name; 12 | animation.time = 0; 13 | animation.frame = 0; 14 | animation.loop = true; 15 | animation.speed = 1; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/components/box-collider.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | entities: [], 5 | last: [] 6 | }; 7 | }, 8 | reset: function(boxCollider) { 9 | boxCollider.entities.length = 0; 10 | boxCollider.last.length = 0; 11 | delete boxCollider.group; 12 | delete boxCollider.script; 13 | delete boxCollider.onEnter; 14 | delete boxCollider.onExit; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/components/collisions.js: -------------------------------------------------------------------------------- 1 | // This component is deprecated and will be removed in the next major version. 2 | // Use box-collider instead. 3 | module.exports = { 4 | factory: function() { 5 | return []; 6 | }, 7 | reset: function(collisions) { 8 | collisions.length = 0; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/components/constrain-position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An entity to keep this entity inside of another 3 | * @typedef {Object} constrainPosition 4 | * @memberof Components 5 | * @property {float} id - The id of a target entity to keep this entity inside of 6 | */ 7 | module.exports = { 8 | factory: function() { 9 | return {}; 10 | }, 11 | reset: function(constrainPosition) { 12 | delete constrainPosition.id; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/components/easing.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return {}; 4 | }, 5 | reset: function(easing) { 6 | var names = Object.keys(easing); 7 | for (var i = 0; i < names.length; i++) { 8 | delete easing[name[i]]; 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/components/follow.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | distance: 0 5 | }; 6 | }, 7 | reset: function(follow) { 8 | delete follow.id; 9 | follow.distance = 0; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/components/friction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The speed modifier of an entity in the game world. Each frame the speed is multiplied by the friction. 3 | * @typedef {Object} friction 4 | * @memberof Components 5 | * @property {float} x - The amount to modify the velocity of this entity along the x-axis. 6 | * @property {float} y - The amount to modify the velocity of this entity along the y-axis. 7 | */ 8 | module.exports = { 9 | factory: function() { 10 | return { 11 | x: 1, 12 | y: 1 13 | }; 14 | }, 15 | reset: function(friction) { 16 | friction.x = 1; 17 | friction.y = 1; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/components/grid.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | x: 0, 5 | y: 0, 6 | z: 0 7 | }; 8 | }, 9 | reset: function(grid) { 10 | grid.x = 0; 11 | grid.y = 0; 12 | grid.z = 0; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/components/image.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return {}; 4 | }, 5 | reset: function(image) { 6 | delete image.name; 7 | delete image.alpha; 8 | delete image.sourceX; 9 | delete image.sourceY; 10 | delete image.sourceWidth; 11 | delete image.sourceHeight; 12 | delete image.destinationX; 13 | delete image.destinationY; 14 | delete image.destinationWidth; 15 | delete image.destinationHeight; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The components used by {@link Systems} provided by Splat ECS. 3 | * @namespace Components 4 | */ 5 | 6 | module.exports = { 7 | acceleration: require("./acceleration"), 8 | animation: require("./animation"), 9 | boxCollider: require("./box-collider"), 10 | collisions: require("./collisions"), 11 | constrainPosition: require("./constrain-position"), 12 | easing: require("./easing"), 13 | follow: require("./follow"), 14 | friction: require("./friction"), 15 | grid: require("./grid"), 16 | image: require("./image"), 17 | lifeSpan: require("./life-span"), 18 | match: require("./match"), 19 | matchAspectRatio: require("./match-aspect-ratio"), 20 | matchCenter: require("./match-center"), 21 | movement2d: require("./movement-2d"), 22 | playerController2d: require("./player-controller-2d"), 23 | position: require("./position"), 24 | rotation: require("./rotation"), 25 | shake: require("./shake"), 26 | size: require("./size"), 27 | timers: require("./timers"), 28 | velocity: require("./velocity") 29 | }; 30 | -------------------------------------------------------------------------------- /lib/components/life-span.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | current: 0, 5 | max: 1000 6 | }; 7 | }, 8 | reset: function(lifeSpan) { 9 | lifeSpan.current = 0; 10 | lifeSpan.max = 1000; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/components/match-aspect-ratio.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return {}; 4 | }, 5 | reset: function(matchAspectRatio) { 6 | delete matchAspectRatio.id; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/components/match-center.js: -------------------------------------------------------------------------------- 1 | /** Align the center of this entity with the center of another entity. 2 | * @typedef {object} matchCenter 3 | * @memberof Components 4 | * @property {int} id - The id of the entity to align to on both the x and y axes. 5 | * @property {int} x - The id of the entity to align to on the x axis. 6 | * @property {int} y - The id of the entity to align to on the y axis. 7 | */ 8 | 9 | module.exports = { 10 | factory: function() { 11 | return {}; 12 | }, 13 | reset: function(matchCenter) { 14 | delete matchCenter.id; 15 | delete matchCenter.x; 16 | delete matchCenter.y; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/components/match.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | matchX: 0, 5 | matchY: 0, 6 | matchZ: 0 7 | }; 8 | }, 9 | reset: function(match) { 10 | delete match.id; 11 | match.matchX = 0; 12 | match.matchY = 0; 13 | match.matchZ = 0; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/components/movement-2d.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | up: false, 5 | down: false, 6 | left: false, 7 | right: false, 8 | upAccel: -0.1, 9 | downAccel: 0.1, 10 | leftAccel: -0.1, 11 | rightAccel: 0.1, 12 | upMax: -1.0, 13 | downMax: 1.0, 14 | leftMax: -1.0, 15 | rightMax: 1.0 16 | }; 17 | }, 18 | reset: function(movement2d) { 19 | movement2d.up = false; 20 | movement2d.down = false; 21 | movement2d.left = false; 22 | movement2d.right = false; 23 | movement2d.upAccel = -0.1; 24 | movement2d.downAccel = 0.1; 25 | movement2d.leftAccel = -0.1; 26 | movement2d.rightAccel = 0.1; 27 | movement2d.upMax = -1.0; 28 | movement2d.downMax = 1.0; 29 | movement2d.leftMax = -1.0; 30 | movement2d.rightMax = 1.0; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /lib/components/player-controller-2d.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | up: "up", 5 | down: "down", 6 | left: "left", 7 | right: "right" 8 | }; 9 | }, 10 | reset: function(playerController2d) { 11 | playerController2d.up = "up"; 12 | playerController2d.down = "down"; 13 | playerController2d.left = "left"; 14 | playerController2d.right = "right"; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/components/position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The coordinates of an entity in the game world. 3 | * @typedef {Object} position 4 | * @memberof Components 5 | * @property {float} x - The position of this entity along the x-axis. 6 | * @property {float} y - The position of this entity along the y-axis. 7 | * @property {float} z - The position of this entity along the z-axis. 8 | * Since Splat is 2D this is mainly for creating layers when drawing sprites similar to z-index in CSS. 9 | */ 10 | 11 | module.exports = { 12 | factory: function() { 13 | return { 14 | x: 0, 15 | y: 0, 16 | z: 0 17 | }; 18 | }, 19 | reset: function(position) { 20 | position.x = 0; 21 | position.y = 0; 22 | position.z = 0; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/components/register.js: -------------------------------------------------------------------------------- 1 | module.exports = function registerAll(entities, componentSpecs) { 2 | var names = Object.keys(componentSpecs); 3 | for (var i = 0; i < names.length; i++) { 4 | var name = names[i]; 5 | var componentSpec = componentSpecs[name]; 6 | entities.registerComponent(name, componentSpec.factory, componentSpec.reset, componentSpec.poolSize); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/components/rotation.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | angle: 0 5 | }; 6 | }, 7 | reset: function(rotation) { 8 | rotation.angle = 0; 9 | delete rotation.x; 10 | delete rotation.y; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/components/shake.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | factory: function() { 3 | return { 4 | duration: 0 5 | // magnitude: 1, 6 | // magnitudeX: 1, 7 | // magnitudeY: 1 8 | }; 9 | }, 10 | reset: function(shake) { 11 | shake.duration = 0; 12 | delete shake.magnitude; 13 | delete shake.magnitudeX; 14 | delete shake.magnitudeY; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lib/components/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The size of an entity in the game world. 3 | * @typedef {Object} size 4 | * @memberof Components 5 | * @property {float} width - The width of this entity rightward from {@link Components.position} along the x-axis. 6 | * @property {float} height - The height of this entity downward from {@link Components.position} along the y-axis. 7 | */ 8 | 9 | module.exports = { 10 | factory: function() { 11 | return { 12 | width: 0, 13 | height: 0 14 | }; 15 | }, 16 | reset: function(size) { 17 | size.width = 0; 18 | size.height = 0; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/components/timers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A named group of timers. Each key is the name of a timer, and the value is a {@link Components.timer}. 3 | * @typedef {Object} timers 4 | * @memberof Components 5 | */ 6 | /** 7 | * Measure time or run code after a duration. 8 | * @typedef {Object} timer 9 | * @memberof Components 10 | * @property {bool} loop - true if the timer should repeat after it reaches max. 11 | * @property {float} max - The maximum amount of time to accumulate. If time reaches max, then time will be set to 0 and running will be set to false. 12 | * @property {bool} running - Determines if the timer is accumulating time. 13 | * @property {string} script - The require path to a script to run when the timer is reset. The path is relative to your game's src folder. For example ./scripts/next-scene might execute the code in /src/scripts/next-scene.js. 14 | * @property {float} time - The amount of time, in milliseconds, the timer has accumulated. 15 | */ 16 | 17 | module.exports = { 18 | factory: function() { 19 | return {}; 20 | }, 21 | reset: function(timers) { 22 | var names = Object.keys(timers); 23 | for (var i = 0; i < names.length; i++) { 24 | delete timers[name[i]]; 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/components/velocity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The speed of an entity in the game world in pixels-per-millisecond. 3 | * @typedef {Object} velocity 4 | * @memberof Components 5 | * @property {float} x - The velocity of this entity along the x-axis. 6 | * @property {float} y - The velocity of this entity along the y-axis. 7 | * @property {float} z - The velocity of this entity along the z-axis. 8 | */ 9 | 10 | module.exports = { 11 | factory: function() { 12 | return { 13 | x: 0, 14 | y: 0, 15 | z: 0 16 | }; 17 | }, 18 | reset: function(velocity) { 19 | velocity.x = 0; 20 | velocity.y = 0; 21 | velocity.z = 0; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/external.jsdoc: -------------------------------------------------------------------------------- 1 | /** 2 | * The built-in image DOM element. 3 | * @typedef image 4 | * @external image 5 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement Image at the Mozilla Developer Network} 6 | */ 7 | 8 | /** 9 | * The built-in canvas DOM element. 10 | * @typedef canvas 11 | * @external canvas 12 | * @see {@link https://developer.mozilla.org/en-US/docs/HTML/Canvas Canvas at the Mozilla Developer Network} 13 | */ 14 | 15 | /** 16 | * The built-in 2D rendering context for the drawing surface of a {@link external:canvas}. 17 | * @typedef CanvasRenderingContext2D 18 | * @external CanvasRenderingContext2D 19 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D CanvasRenderingContext2D at the Mozilla Developer Network} 20 | */ 21 | 22 | /** 23 | * The built-in Web Audio API audio context. 24 | * @typedef AudioContext 25 | * @external AudioContext 26 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext at the Mozilla Developer Network} 27 | */ 28 | 29 | /** 30 | * A container for all entities in a scene. 31 | * @typedef EntityPool 32 | * @external EntityPool 33 | * @see {@link https://github.com/ericlathrop/entity-component-system#entitypool EntityPool documentation on Github} 34 | */ 35 | -------------------------------------------------------------------------------- /lib/font-loader.js: -------------------------------------------------------------------------------- 1 | require("../vendor/FontLoader.js"); 2 | var platform = require("./platform"); 3 | 4 | function buildFontFaceRule(family, urls) { 5 | var eot = urls["embedded-opentype"]; 6 | var woff = urls.woff; 7 | var ttf = urls.truetype; 8 | var svg = urls.svg; 9 | 10 | var css = "\n"; 11 | css += "@font-face {\n"; 12 | css += " font-family: \"" + family + "\";\n"; 13 | css += " src: url(\"" + eot + "\");\n"; 14 | css += " src: url(\"" + eot + "?iefix\") format(\"embedded-opentype\"),\n"; 15 | css += " url(\"" + woff + "\") format(\"woff\"),\n"; 16 | css += " url(\"" + ttf + "\") format(\"ttf\"),\n"; 17 | css += " url(\"" + svg + "\") format(\"svg\");\n"; 18 | css += "}\n"; 19 | return css; 20 | } 21 | 22 | function createCssFontFaces(fontFamilies) { 23 | var style = document.createElement("style"); 24 | style.setAttribute("type", "text/css"); 25 | var css = ""; 26 | for (var family in fontFamilies) { 27 | if (fontFamilies.hasOwnProperty(family)) { 28 | css += buildFontFaceRule(family, fontFamilies[family]); 29 | } 30 | } 31 | style.appendChild(document.createTextNode(css)); 32 | document.head.appendChild(style); 33 | } 34 | 35 | /** 36 | * Load fonts and lets you know when they're all available. An instance of FontLoader is available as {@link Splat.Game#fonts}. 37 | * @constructor 38 | */ 39 | function FontLoader() { 40 | /** 41 | * The total number of fonts to be loaded. 42 | * @member {number} 43 | * @private 44 | */ 45 | this.totalFonts = 0; 46 | /** 47 | * The number of fonts that have loaded completely. 48 | * @member {number} 49 | * @private 50 | */ 51 | this.loadedFonts = 0; 52 | } 53 | /** 54 | * Load a font. 55 | * @param {object} fontFamilies A key-value object that maps css font-family names to another object that holds paths to the various font files in different formats. 56 | * @example 57 | game.fonts.load({ 58 | "pixelade": { 59 | "embedded-opentype": "pixelade/pixelade-webfont.eot", 60 | "woff": "pixelade/pixelade-webfont.woff", 61 | "truetype": "pixelade/pixelade-webfont.ttf", 62 | "svg": "pixelade/pixelade-webfont.svg#pixeladeregular" 63 | } 64 | }); 65 | */ 66 | FontLoader.prototype.load = function(fontFamilies) { 67 | createCssFontFaces(fontFamilies); 68 | 69 | var families = []; 70 | for (var family in fontFamilies) { 71 | if (families.hasOwnProperty(family)) { 72 | families.push(family); 73 | } 74 | } 75 | this.totalFonts += families.length; 76 | 77 | var self = this; 78 | var loader = new window.FontLoader(families, { 79 | "fontLoaded": function() { 80 | self.loadedFonts++; 81 | } 82 | }); 83 | loader.loadFonts(); 84 | }; 85 | /** 86 | * Test if all font fonts have loaded. 87 | * @returns {boolean} 88 | */ 89 | FontLoader.prototype.allLoaded = function() { 90 | return this.totalFonts === this.loadedFonts; 91 | }; 92 | 93 | /** 94 | * An alternate {@link FontLoader} when the game is running inside [Ejecta]{@link http://impactjs.com/ejecta}. You shouldn't need to worry about this. 95 | * @constructor 96 | * @private 97 | */ 98 | function EjectaFontLoader() { 99 | this.totalFonts = 0; 100 | this.loadedFonts = 0; 101 | } 102 | /** 103 | * See {@link FontLoader#load}. 104 | */ 105 | EjectaFontLoader.prototype.load = function(fontFamilies) { 106 | for (var family in fontFamilies) { 107 | if (fontFamilies.hasOwnProperty(family)) { 108 | var fontPath = fontFamilies[family].truetype; 109 | if (fontPath) { 110 | window.ejecta.loadFont(fontPath); 111 | } 112 | } 113 | } 114 | }; 115 | /** 116 | * See {@link FontLoader#allLoaded}. 117 | */ 118 | EjectaFontLoader.prototype.allLoaded = function() { 119 | return true; 120 | }; 121 | 122 | if (platform.isEjecta()) { 123 | module.exports = EjectaFontLoader; 124 | } else { 125 | module.exports = FontLoader; 126 | } 127 | -------------------------------------------------------------------------------- /lib/game.js: -------------------------------------------------------------------------------- 1 | var AssetLoader = require("./assets/asset-loader"); 2 | var loadImage = require("./assets/load-image"); 3 | var Input = require("./input"); 4 | var Prefabs = require("./prefabs"); 5 | var Scene = require("./scene"); 6 | var SoundManager = require("./sound-manager"); 7 | var splitFilmStripAnimations = require("./split-filmstrip-animations"); 8 | 9 | function Game(canvas, customRequire) { 10 | this.animations = customRequire("./data/animations"); 11 | splitFilmStripAnimations(this.animations); 12 | this.canvas = canvas; 13 | this.context = canvas.getContext("2d"); 14 | this.images = new AssetLoader(customRequire("./data/images"), loadImage); 15 | this.inputs = new Input(customRequire("./data/inputs"), canvas); 16 | this.require = customRequire; 17 | this.sounds = new SoundManager(customRequire("./data/sounds")); 18 | this.prefabs = new Prefabs(customRequire("./data/prefabs")); 19 | this.lastTime = -1; 20 | this.remainingDebugTime = undefined; 21 | 22 | this.scaleCanvasToCssSize(); 23 | window.addEventListener("resize", this.onCanvasResize.bind(this)); 24 | 25 | this.scenes = this.makeScenes(customRequire("./data/scenes")); 26 | this.run = this.run.bind(this); 27 | 28 | this.timings = []; 29 | window.timingIdx = -1; 30 | for (var i = 0; i < 100; i++) { 31 | this.timings.push({ 32 | elapsed: 0, 33 | totalTime: 0, 34 | simulationTime: 0, 35 | rendererTime: 0 }); 36 | } 37 | } 38 | Game.prototype.makeScenes = function(sceneList) { 39 | var names = Object.keys(sceneList); 40 | var scenes = {}; 41 | for (var i = 0; i < names.length; i++) { 42 | var name = names[i]; 43 | scenes[name] = new Scene(name, { 44 | animations: this.animations, 45 | canvas: this.canvas, 46 | context: this.context, 47 | images: this.images, 48 | inputs: this.inputs, 49 | prefabs: this.prefabs, 50 | require: this.require, 51 | scaleCanvasToCssSize: this.scaleCanvasToCssSize.bind(this), 52 | scaleCanvasToFitRectangle: this.scaleCanvasToFitRectangle.bind(this), 53 | scenes: scenes, 54 | sounds: this.sounds 55 | }); 56 | if (sceneList[name].first) { 57 | scenes[name].start(); 58 | } 59 | } 60 | return scenes; 61 | }; 62 | Game.prototype.start = function() { 63 | if (this.running) { 64 | return; 65 | } 66 | this.running = true; 67 | this.lastTime = -1; 68 | window.requestAnimationFrame(this.run); 69 | }; 70 | Game.prototype.stop = function() { 71 | this.running = false; 72 | }; 73 | Game.prototype.run = function(time) { 74 | var scenes = Object.keys(this.scenes); 75 | 76 | if (this.lastTime === -1) { 77 | this.lastTime = time; 78 | } 79 | var elapsed = time - this.lastTime; 80 | this.lastTime = time; 81 | 82 | var simulationStart = window.performance.now(); 83 | for (var i = 0; i < scenes.length; i++) { 84 | var name = scenes[i]; 85 | var scene = this.scenes[name]; 86 | scene.simulate(elapsed); 87 | } 88 | var simulationEnd = window.performance.now(); 89 | 90 | for (i = 0; i < scenes.length; i++) { 91 | name = scenes[i]; 92 | scene = this.scenes[name]; 93 | this.context.save(); 94 | scene.render(elapsed); 95 | this.context.restore(); 96 | } 97 | var renderEnd = window.performance.now(); 98 | 99 | if (window.timingIdx >= 0) { 100 | this.timings[window.timingIdx].elapsed = elapsed; 101 | this.timings[window.timingIdx].simulationTime = simulationEnd - simulationStart; 102 | this.timings[window.timingIdx].rendererTime = renderEnd - simulationEnd; 103 | this.timings[window.timingIdx].totalTime = renderEnd - simulationStart; 104 | window.timingIdx++; 105 | } 106 | if (window.timingIdx >= this.timings.length) { 107 | window.timingIdx = -1; 108 | console.table(this.timings); 109 | } 110 | 111 | if (this.remainingDebugTime !== undefined) { 112 | this.remainingDebugTime -= elapsed; 113 | if (this.remainingDebugTime <= 0) { 114 | this.remainingDebugTime = undefined; 115 | this.logDebugTimes(); 116 | } 117 | } 118 | 119 | if (this.running) { 120 | window.requestAnimationFrame(this.run); 121 | } 122 | }; 123 | Game.prototype.timeSystems = function(total) { 124 | var scenes = Object.keys(this.scenes); 125 | for (var i = 0; i < scenes.length; i++) { 126 | var name = scenes[i]; 127 | var scene = this.scenes[name]; 128 | scene.simulation.resetTimings(); 129 | scene.renderer.resetTimings(); 130 | } 131 | this.remainingDebugTime = total; 132 | }; 133 | Game.prototype.logDebugTimes = function() { 134 | var scenes = Object.keys(this.scenes); 135 | var timings = []; 136 | for (var i = 0; i < scenes.length; i++) { 137 | var name = scenes[i]; 138 | var scene = this.scenes[name]; 139 | timings = timings.concat(scene.simulation.timings()); 140 | timings = timings.concat(scene.renderer.timings()); 141 | } 142 | console.table(groupTimings(timings)); 143 | }; 144 | function groupTimings(timings) { 145 | var total = timings.map(function(timing) { 146 | return timing.time; 147 | }).reduce(function(a, b) { 148 | return a + b; 149 | }); 150 | timings.sort(function(a, b) { 151 | return b.time - a.time; 152 | }).forEach(function(timing) { 153 | timing.percent = timing.time / total; 154 | }); 155 | return timings; 156 | } 157 | Game.prototype.onCanvasResize = function() { 158 | this.resizer(); 159 | }; 160 | Game.prototype.scaleCanvasToCssSize = function() { 161 | this.resizer = function() { 162 | var canvasStyle = window.getComputedStyle(this.canvas); 163 | var width = parseInt(canvasStyle.width); 164 | var height = parseInt(canvasStyle.height); 165 | this.canvas.width = width; 166 | this.canvas.height = height; 167 | }.bind(this); 168 | this.resizer(); 169 | }; 170 | Game.prototype.scaleCanvasToFitRectangle = function(width, height) { 171 | this.resizer = function() { 172 | var canvasStyle = window.getComputedStyle(this.canvas); 173 | var cssWidth = parseInt(canvasStyle.width); 174 | var cssHeight = parseInt(canvasStyle.height); 175 | var cssAspectRatio = cssWidth / cssHeight; 176 | 177 | var desiredWidth = width; 178 | var desiredHeight = height; 179 | var desiredAspectRatio = width / height; 180 | if (desiredAspectRatio > cssAspectRatio) { 181 | desiredHeight = Math.floor(width / cssAspectRatio); 182 | } else if (desiredAspectRatio < cssAspectRatio) { 183 | desiredWidth = Math.floor(height * cssAspectRatio); 184 | } 185 | 186 | this.canvas.width = desiredWidth; 187 | this.canvas.height = desiredHeight; 188 | }.bind(this); 189 | this.resizer(); 190 | }; 191 | 192 | module.exports = Game; 193 | -------------------------------------------------------------------------------- /lib/iap.js: -------------------------------------------------------------------------------- 1 | var platform = require("./platform"); 2 | 3 | if (platform.isEjecta()) { 4 | var iap = new window.Ejecta.IAPManager(); 5 | 6 | module.exports = { 7 | "get": function(sku, callback) { 8 | iap.getProducts([sku], function(err, products) { 9 | if (err) { 10 | callback(err); 11 | return; 12 | } 13 | callback(undefined, products[0]); 14 | }); 15 | }, 16 | "buy": function(product, quantity, callback) { 17 | product.purchase(quantity, callback); 18 | }, 19 | "restore": function(callback) { 20 | iap.restoreTransactions(function(err, transactions) { 21 | if (err) { 22 | callback(err); 23 | return; 24 | } 25 | callback(undefined, transactions.map(function(transaction) { 26 | return transaction.productId; 27 | })); 28 | }); 29 | } 30 | }; 31 | } else if (platform.isChromeApp()) { 32 | // FIXME: needs google's buy.js included 33 | // https://developer.chrome.com/webstore/payments-iap 34 | module.exports = { 35 | "get": function(sku, callback) { 36 | window.google.payments.inapp.getSkuDetails({ 37 | "parameters": { 38 | "env": "prod" 39 | }, 40 | "sku": sku, 41 | "success": function(response) { 42 | callback(undefined, response.response.details.inAppProducts[0]); 43 | }, 44 | "failure": function(response) { 45 | callback(response); 46 | } 47 | }); 48 | }, 49 | "buy": function(product, quantity, callback) { 50 | window.google.payments.inapp.buy({ 51 | "parameters": { 52 | "env": "prod" 53 | }, 54 | "sku": product.sku, 55 | "success": function(response) { 56 | callback(undefined, response); 57 | }, 58 | "failure": function(response) { 59 | callback(response); 60 | } 61 | }); 62 | }, 63 | "restore": function(callback) { 64 | window.google.payments.inapp.getPurchases({ 65 | "success": function(response) { 66 | callback(undefined, response.response.details.map(function(detail) { 67 | return detail.sku; 68 | })); 69 | }, 70 | "failure": function(response) { 71 | callback(response); 72 | } 73 | }); 74 | } 75 | }; 76 | } else { 77 | module.exports = { 78 | "get": function(sku, callback) { 79 | callback(undefined, undefined); 80 | }, 81 | "buy": function(product, quantity, callback) { 82 | callback(undefined); 83 | }, 84 | "restore": function(callback) { 85 | callback(undefined, []); 86 | } 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /lib/import-from-tiled.js: -------------------------------------------------------------------------------- 1 | /** @module splat-ecs/lib/import-from-tiled */ 2 | 3 | var clone = require("./clone"); 4 | var pako = require("pako"); 5 | var path = require("path"); 6 | var setOrAddComponent = require("./set-or-add-component"); 7 | 8 | /** 9 | *

Import an orthogonal tilemap from the Tiled map editor.

10 | *

This will create entities in the current scene representing all the tiles & objects in the tilemap. Each tile gets a tile component tag so you can find them later. Each tile also gets a grid component with the x/y/z grid coordinates in tile space.

11 | *

All of the properties that are in the tilemap are set as components in each tile entity, and the property values are treated as JSON. More specific properties override less specific properties. The order of precedence for tile properties is:

12 | *
    13 | *
  1. tile layer
  2. 14 | *
  3. tile in tileset
  4. 15 | *
  5. tileset
  6. 16 | *
17 | *

The order of precendence for object properties is:

18 | *
    19 | *
  1. object
  2. 20 | *
  3. object layer
  4. 21 | *
  5. tile in tileset
  6. 22 | *
  7. tileset
  8. 23 | *
24 | *

A special "container" entity is also created with a container component tag that has the dimensions of the map in its size component.

25 | * @function importTilemap 26 | * @param {Object} file JSON file exported from Tiled. This should be required in a scene enter 27 | * script and passed to the function. 28 | * @param {external:EntityPool} entities EntityPool from game.entities 29 | * @param {ImageLoader} images ImageLoader from game.images 30 | * @see {@link http://www.mapeditor.org/ Tiled Map Editor} 31 | */ 32 | module.exports = function importTilemap(file, entities, images) { 33 | 34 | var imageComponents = tilesetsToImageComponents(file.tilesets, images); 35 | 36 | for (var z = 0; z < file.layers.length; z++) { 37 | var layer = file.layers[z]; 38 | if (!layer.visible) { 39 | continue; 40 | } 41 | if (layer.type == "tilelayer") { 42 | makeTiles(file, z, entities, imageComponents); 43 | } else if (layer.type == "objectgroup") { 44 | makeObjects(file, z, entities, imageComponents); 45 | } 46 | } 47 | 48 | // create a "container" entity so we can find the bounds of the map, and maybe constrain the player to it 49 | var container = entities.create(); 50 | entities.setComponent(container, "name", "container"); 51 | entities.setComponent(container, "container", true); 52 | entities.addComponent(container, "position"); 53 | var size = entities.addComponent(container, "size"); 54 | size.width = file.width * file.tilewidth; 55 | size.height = file.height * file.tileheight; 56 | }; 57 | 58 | function tilesetsToImageComponents(tilesets, images) { 59 | var imageComponents = []; 60 | for (var i = 0; i < tilesets.length; i++) { 61 | if (tilesets[i].image) { 62 | tilesetToImage(tilesets[i], imageComponents); 63 | } else { 64 | collectionOfImagesToImage(tilesets[i], imageComponents, images); 65 | } 66 | } 67 | return imageComponents; 68 | } 69 | 70 | function tilesetToImage(tileset, imageComponents) { 71 | var i = tileset.firstgid; 72 | var j = 0; 73 | for (var y = tileset.margin; y < tileset.imageheight - tileset.margin; y += tileset.tileheight + tileset.spacing) { 74 | for (var x = tileset.margin; x < tileset.imagewidth - tileset.margin; x += tileset.tilewidth + tileset.spacing) { 75 | var tileProps = (tileset.tileproperties || {})[j]; 76 | var props = merge(clone(tileset.properties || {}), tileProps); 77 | imageComponents[i] = { 78 | image: { 79 | name: path.basename(tileset.image), 80 | sourceX: x, 81 | sourceY: y, 82 | sourceWidth: tileset.tilewidth, 83 | sourceHeight: tileset.tileheight, 84 | destinationWidth: tileset.tilewidth, 85 | destinationHeight: tileset.tileheight 86 | }, 87 | properties: props 88 | }; 89 | i++; 90 | j++; 91 | } 92 | } 93 | } 94 | 95 | function collectionOfImagesToImage(tileset, imageComponents, images) { 96 | var keys = Object.keys(tileset.tiles).map(function(id) { return parseInt(id); }); 97 | for (var k = 0; k < keys.length; k++) { 98 | var key = keys[k]; 99 | var tileProps = (tileset.tileproperties || {})[key]; 100 | var props = merge(clone(tileset.properties || {}), tileProps); 101 | var name = path.basename(tileset.tiles[key].image); 102 | var image = images.get(name); 103 | imageComponents[tileset.firstgid + key] = { 104 | image: { 105 | name: name, 106 | sourceX: 0, 107 | sourceY: 0, 108 | sourceWidth: image.width, 109 | sourceHeight: image.height, 110 | destinationWidth: image.width, 111 | destinationHeight: image.height 112 | }, 113 | properties: props 114 | }; 115 | } 116 | } 117 | 118 | function merge(dest, src) { 119 | if (src === undefined) { 120 | return dest; 121 | } 122 | var keys = Object.keys(src); 123 | for (var i = 0; i < keys.length; i++) { 124 | dest[keys[i]] = src[keys[i]]; 125 | } 126 | return dest; 127 | } 128 | 129 | function makeTiles(file, z, entities, imageComponents) { 130 | var layer = file.layers[z]; 131 | var data = layer.data; 132 | if (layer.encoding === "base64") { 133 | data = window.atob(data); 134 | } 135 | if (layer.compression === "zlib") { 136 | var inflated = pako.inflate(data); 137 | data = new window.Uint32Array(inflated.buffer); 138 | } 139 | for (var i = 0; i < data.length; i++) { 140 | var tile = data[i]; 141 | if (tile === 0) { 142 | continue; 143 | } 144 | var image = clone(imageComponents[tile].image); 145 | var gridX = i % file.width; 146 | var gridY = Math.floor(i / file.width); 147 | var x = (gridX * file.tilewidth) + (layer.offsetx || 0); 148 | var y = ((gridY + 1) * file.tileheight) - image.sourceHeight + (layer.offsety || 0); 149 | var entity = makeTile({ x: x, y: y, z: z }, { x: gridX, y: gridY, z: z }, image, entities); 150 | setComponentsFromProperties(entity, imageComponents[tile].properties, entities); 151 | setComponentsFromProperties(entity, layer.properties, entities); 152 | } 153 | } 154 | 155 | function makeTile(position, grid, image, entities) { 156 | var tile = entities.create(); 157 | entities.setComponent(tile, "name", "tile"); 158 | entities.setComponent(tile, "tile", true); 159 | 160 | var pos = entities.addComponent(tile, "position"); 161 | pos.x = position.x; 162 | pos.y = position.y; 163 | pos.z = position.z; 164 | 165 | var g = entities.addComponent(tile, "grid"); 166 | g.x = grid.x; 167 | g.y = grid.y; 168 | g.z = grid.z; 169 | 170 | addImage(entities, tile, image); 171 | 172 | var size = entities.addComponent(tile, "size"); 173 | size.width = image.sourceWidth; 174 | size.height = image.sourceHeight; 175 | 176 | return tile; 177 | } 178 | 179 | function addImage(entities, id, image) { 180 | var img = entities.addComponent(id, "image"); 181 | img.name = image.name; 182 | img.sourceX = image.sourceX; 183 | img.sourceY = image.sourceY; 184 | img.sourceWidth = image.sourceWidth; 185 | img.sourceHeight = image.sourceHeight; 186 | img.destinationWidth = image.destinationWidth; 187 | img.destinationHeight = image.destinationHeight; 188 | } 189 | 190 | function makeObjects(file, z, entities, imageComponents) { 191 | var layer = file.layers[z]; 192 | for (var i = 0; i < layer.objects.length; i++) { 193 | var object = layer.objects[i]; 194 | makeObject(layer, object, z, entities, imageComponents); 195 | } 196 | } 197 | 198 | function makeObject(layer, object, z, entities, imageComponents) { 199 | var entity = entities.create(); 200 | entities.setComponent(entity, "name", object.name); 201 | entities.setComponent(entity, "type", object.type); 202 | 203 | var position = entities.addComponent(entity, "position"); 204 | position.x = object.x + (layer.offsetx || 0); 205 | position.y = object.y + (layer.offsety || 0); 206 | position.z = z; 207 | 208 | var size = entities.addComponent(entity, "size"); 209 | size.width = object.width; 210 | size.height = object.height; 211 | 212 | if (object.gid !== undefined) { 213 | addImage(entities, entity, imageComponents[object.gid].image); 214 | setComponentsFromProperties(entity, imageComponents[object.gid].properties, entities); 215 | } 216 | setComponentsFromProperties(entity, layer.properties, entities); 217 | setComponentsFromProperties(entity, object.properties, entities); 218 | } 219 | 220 | function setComponentsFromProperties(entity, properties, entities) { 221 | if (!properties) { 222 | return; 223 | } 224 | Object.keys(properties).forEach(function(key) { 225 | var value = parsePropertyValue(properties[key]); 226 | setOrAddComponent(entities, entity, key, value); 227 | }); 228 | } 229 | 230 | function parsePropertyValue(val) { 231 | try { 232 | return JSON.parse(val); 233 | } catch (e) { 234 | if (e instanceof SyntaxError) { 235 | return val; 236 | } else { 237 | throw e; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /lib/input.js: -------------------------------------------------------------------------------- 1 | var Keyboard = require("game-keyboard"); 2 | var keyMap = require("game-keyboard/key_map").US; 3 | var keyboard = new Keyboard(keyMap); 4 | var Mouse = require("./mouse"); 5 | 6 | function Input(config, canvas) { 7 | this.config = config; 8 | this.gamepads = require("html5-gamepad"); 9 | this.mouse = new Mouse(canvas); 10 | this.lastButtonState = {}; 11 | this.delayedButtonUpdates = {}; 12 | this.virtualAxes = {}; 13 | this.virtualButtons = {}; 14 | } 15 | Input.prototype.axis = function(name) { 16 | var input = this.config.axes[name]; 17 | if (input === undefined) { 18 | console.error("No such axis: " + name); 19 | return 0; 20 | } 21 | var val = 0; 22 | for (var i = 0; i < input.length; i++) { 23 | var physicalInput = input[i]; 24 | var device = physicalInput.device; 25 | if (device === "mouse") { 26 | if (physicalInput.axis === "x") { 27 | val = this.mouse.x; 28 | } 29 | if (physicalInput.axis === "y") { 30 | val = this.mouse.y; 31 | } 32 | } 33 | if (device === "gamepad" && this.gamepads[0]) { 34 | val = this.gamepads[0].axis(physicalInput.axis); 35 | } 36 | if (device === "virtual") { 37 | val = physicalInput.state; 38 | } 39 | if (val !== 0) { 40 | break; 41 | } 42 | } 43 | return val; 44 | }; 45 | Input.prototype.button = function(name) { 46 | var input = this.config.buttons[name]; 47 | if (input === undefined) { 48 | console.error("No such button: " + name); 49 | return false; 50 | } 51 | for (var i = 0; i < input.length; i++) { 52 | var physicalInput = input[i]; 53 | var device = physicalInput.device; 54 | if (device === "keyboard") { 55 | if (keyboard.isPressed(physicalInput.button)) { 56 | return true; 57 | } 58 | } 59 | if (device === "mouse") { 60 | if (this.mouse.isPressed(physicalInput.button)) { 61 | return true; 62 | } 63 | } 64 | if (device === "gamepad" && this.gamepads[0]) { 65 | if (this.gamepads[0].button(physicalInput.button)) { 66 | return true; 67 | } 68 | } 69 | if (device === "virtual") { 70 | if (physicalInput.state) { 71 | return true; 72 | } 73 | } 74 | } 75 | return false; 76 | }; 77 | Input.prototype.buttonPressed = function(name) { 78 | var current = this.button(name); 79 | var last = this.lastButtonState[name]; 80 | if (last === undefined) { 81 | last = true; 82 | } 83 | this.delayedButtonUpdates[name] = current; 84 | return current && !last; 85 | }; 86 | Input.prototype.buttonReleased = function(name) { 87 | var current = this.button(name); 88 | var last = this.lastButtonState[name]; 89 | if (last === undefined) { 90 | last = false; 91 | } 92 | this.delayedButtonUpdates[name] = current; 93 | return !current && last; 94 | }; 95 | Input.prototype.setAxis = function(name, instance, state) { 96 | var virtualName = name + "|" + instance; 97 | var virtual = this.virtualAxes[virtualName]; 98 | if (virtual) { 99 | virtual.state = state; 100 | } else { 101 | virtual = { 102 | device: "virtual", 103 | state: state 104 | }; 105 | this.virtualAxes[virtualName] = virtual; 106 | var inputs = this.config.axes[name]; 107 | if (inputs) { 108 | inputs.push(virtual); 109 | } else { 110 | this.config.axes[name] = [virtual]; 111 | } 112 | } 113 | }; 114 | Input.prototype.setButton = function(name, instance, state) { 115 | var virtualName = name + "|" + instance; 116 | var virtual = this.virtualButtons[virtualName]; 117 | if (virtual) { 118 | virtual.state = state; 119 | } else { 120 | virtual = { 121 | device: "virtual", 122 | state: state 123 | }; 124 | this.virtualButtons[virtualName] = virtual; 125 | var inputs = this.config.buttons[name]; 126 | if (inputs) { 127 | inputs.push(virtual); 128 | } else { 129 | this.config.buttons[name] = [virtual]; 130 | } 131 | } 132 | }; 133 | Input.prototype.processUpdates = function() { 134 | if (typeof window.navigator.getGamepads === "function") { 135 | window.navigator.getGamepads(); 136 | } 137 | Object.keys(this.delayedButtonUpdates).forEach(function(name) { 138 | this.lastButtonState[name] = this.delayedButtonUpdates[name]; 139 | delete this.delayedButtonUpdates[name]; 140 | }.bind(this)); 141 | }; 142 | 143 | module.exports = Input; 144 | -------------------------------------------------------------------------------- /lib/leaderboards.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace Splat.leaderboards 3 | */ 4 | 5 | var platform = require("./platform"); 6 | 7 | if (platform.isEjecta()) { 8 | var gameCenter = new window.Ejecta.GameCenter(); 9 | gameCenter.softAuthenticate(); 10 | 11 | var authFirst = function(action) { 12 | if (gameCenter.authed) { 13 | action(); 14 | } else { 15 | gameCenter.authenticate(function(err) { 16 | if (err) { 17 | return; 18 | } 19 | action(); 20 | }); 21 | } 22 | }; 23 | 24 | module.exports = { 25 | /** 26 | * Report that an achievement was achieved. 27 | * @alias Splat.leaderboards.reportAchievement 28 | * @param {string} id The name of the achievement. 29 | * @param {int} percent The percentage of the achievement that is completed in the range of 0-100. 30 | */ 31 | "reportAchievement": function(id, percent) { 32 | authFirst(function() { 33 | gameCenter.reportAchievement(id, percent); 34 | }); 35 | }, 36 | /** 37 | * Report that a score was achieved on a leaderboard. 38 | * @alias Splat.leaderboards.reportScore 39 | * @param {string} leaderboard The name of the leaderboard the score is on. 40 | * @param {int} score The score that was achieved. 41 | */ 42 | "reportScore": function(leaderboard, score) { 43 | authFirst(function() { 44 | gameCenter.reportScore(leaderboard, score); 45 | }); 46 | }, 47 | /** 48 | * Show the achievements screen. 49 | * @alias Splat.leaderboards.showAchievements 50 | */ 51 | "showAchievements": function() { 52 | authFirst(function() { 53 | gameCenter.showAchievements(); 54 | }); 55 | }, 56 | /** 57 | * Show a leaderboard screen. 58 | * @alias Splat.leaderboards.showLeaderboard 59 | * @param {string} name The name of the leaderboard to show. 60 | */ 61 | "showLeaderboard": function(name) { 62 | authFirst(function() { 63 | gameCenter.showLeaderboard(name); 64 | }); 65 | } 66 | }; 67 | } else { 68 | module.exports = { 69 | "reportAchievement": function() {}, 70 | "reportScore": function() {}, 71 | "showAchievements": function() {}, 72 | "showLeaderboard": function() {} 73 | }; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | var buffer = require("./buffer"); 2 | 3 | /** 4 | * @namespace Splat 5 | */ 6 | module.exports = { 7 | makeBuffer: buffer.makeBuffer, 8 | flipBufferHorizontally: buffer.flipBufferHorizontally, 9 | flipBufferVertically: buffer.flipBufferVertically, 10 | 11 | ads: require("./ads"), 12 | AStar: require("./astar"), 13 | BinaryHeap: require("./binary-heap"), 14 | Game: require("./game"), 15 | iap: require("./iap"), 16 | Input: require("./input"), 17 | leaderboards: require("./leaderboards"), 18 | math: require("./math"), 19 | openUrl: require("./openUrl"), 20 | NinePatch: require("./ninepatch"), 21 | Particles: require("./particles"), 22 | saveData: require("./save-data"), 23 | Scene: require("./scene") 24 | }; 25 | -------------------------------------------------------------------------------- /lib/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Oscillate between -1 and 1 given a value and a period. This is basically a simplification on using Math.sin(). 3 | * @alias Splat.math.oscillate 4 | * @param {number} current The current value of the number you want to oscillate. 5 | * @param {number} period The period, or how often the number oscillates. The return value will oscillate between -1 and 1, depending on how close current is to a multiple of period. 6 | * @returns {number} A number between -1 and 1. 7 | * @example 8 | Splat.math.oscillate(0, 100); // returns 0 9 | Splat.math.oscillate(100, 100); // returns 0-ish 10 | Splat.math.oscillate(50, 100); // returns 1 11 | Splat.math.oscillate(150, 100); // returns -1 12 | Splat.math.oscillate(200, 100); // returns 0-ish 13 | */ 14 | function oscillate(current, period) { 15 | return Math.sin(current / period * Math.PI); 16 | } 17 | 18 | /** 19 | * @namespace Splat.math 20 | */ 21 | module.exports = { 22 | oscillate: oscillate, 23 | /** 24 | * A seedable pseudo-random number generator. Currently a Mersenne Twister PRNG. 25 | * @constructor 26 | * @alias Splat.math.Random 27 | * @param {number} [seed] The seed for the PRNG. 28 | * @see [mersenne-twister package at github]{@link https://github.com/boo1ean/mersenne-twister} 29 | * @example 30 | var rand = new Splat.math.Random(123); 31 | var val = rand.random(); 32 | */ 33 | Random: require("mersenne-twister") 34 | }; 35 | -------------------------------------------------------------------------------- /lib/math2d.js: -------------------------------------------------------------------------------- 1 | module.exports.distanceSquared = function distanceSquared(x1, y1, x2, y2) { 2 | return ((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)); 3 | }; 4 | -------------------------------------------------------------------------------- /lib/mouse.js: -------------------------------------------------------------------------------- 1 | var platform = require("./platform"); 2 | 3 | // prevent springy scrolling on ios 4 | document.ontouchmove = function(e) { 5 | e.preventDefault(); 6 | }; 7 | 8 | // prevent right-click on desktop 9 | window.oncontextmenu = function() { 10 | return false; 11 | }; 12 | 13 | var relMouseCoords = function(canvas, event, outCoords) { 14 | var x = event.pageX - canvas.offsetLeft + document.body.scrollLeft; 15 | var y = event.pageY - canvas.offsetTop + document.body.scrollTop; 16 | 17 | // scale based on ratio of canvas internal dimentions to css dimensions 18 | var style = window.getComputedStyle(canvas); 19 | var cw = parseInt(style.width); 20 | var ch = parseInt(style.height); 21 | 22 | x *= canvas.width / cw; 23 | y *= canvas.height / ch; 24 | 25 | outCoords.x = Math.floor(x); 26 | outCoords.y = Math.floor(y); 27 | }; 28 | 29 | function relMouseCoordsEjecta(canvas, event, outCoords) { 30 | var ratioX = canvas.width / window.innerWidth; 31 | var ratioY = canvas.height / window.innerHeight; 32 | outCoords.x = event.pageX * ratioX; 33 | outCoords.y = event.pageY * ratioY; 34 | } 35 | 36 | if (platform.isEjecta()) { 37 | relMouseCoords = relMouseCoordsEjecta; 38 | } 39 | 40 | /** 41 | * Mouse and touch input handling. An instance of Mouse is available as {@link Splat.Game#mouse}. 42 | * 43 | * The first touch will emulates a mouse press with button 0. 44 | * This means you can use the mouse ({@link Mouse#isPressed}) APIs and your game will work on touch screens (as long as you only need the left button). 45 | * 46 | * A mouse press will emulate a touch if the device does not support touch. 47 | * This means you can use {@link Mouse#touches}, and your game will still work on a PC with a mouse. 48 | * 49 | * @constructor 50 | * @param {external:canvas} canvas The canvas to listen for events on. 51 | */ 52 | function Mouse(canvas) { 53 | /** 54 | * The x coordinate of the cursor relative to the left side of the canvas. 55 | * @member {number} 56 | */ 57 | this.x = 0; 58 | /** 59 | * The y coordinate of the cursor relative to the top of the canvas. 60 | * @member {number} 61 | */ 62 | this.y = 0; 63 | /** 64 | * The current button states. 65 | * @member {Array} 66 | * @private 67 | */ 68 | this.buttons = [false, false, false]; 69 | 70 | /** 71 | * An array of the current touches on a touch screen device. Each touch has a `x`, `y`, and `id` field. 72 | * @member {Array} 73 | */ 74 | this.touches = []; 75 | 76 | /** 77 | * A function that is called when a mouse button or touch is released. 78 | * @callback onmouseupHandler 79 | * @param {number} x The x coordinate of the mouse or touch that was released. 80 | * @param {number} y The y coordinate of the mouse or touch that was released. 81 | */ 82 | /** 83 | * A function that will be called when a mouse button is released, or a touch has stopped. 84 | * This is useful for opening a URL with {@link Splat.openUrl} to avoid popup blockers. 85 | * @member {onmouseupHandler} 86 | */ 87 | this.onmouseup = undefined; 88 | 89 | var self = this; 90 | canvas.addEventListener("mousedown", function(event) { 91 | relMouseCoords(canvas, event, self); 92 | self.buttons[event.button] = true; 93 | updateTouchFromMouse(); 94 | }); 95 | canvas.addEventListener("mouseup", function(event) { 96 | relMouseCoords(canvas, event, self); 97 | self.buttons[event.button] = false; 98 | updateTouchFromMouse(); 99 | if (self.onmouseup) { 100 | self.onmouseup(self.x, self.y); 101 | } 102 | }); 103 | canvas.addEventListener("mousemove", function(event) { 104 | relMouseCoords(canvas, event, self); 105 | updateTouchFromMouse(); 106 | }); 107 | 108 | function updateTouchFromMouse() { 109 | if (self.supportsTouch()) { 110 | return; 111 | } 112 | var idx = touchIndexById("mouse"); 113 | if (self.isPressed(0)) { 114 | if (idx !== undefined) { 115 | var touch = self.touches[idx]; 116 | touch.x = self.x; 117 | touch.y = self.y; 118 | } else { 119 | self.touches.push({ 120 | id: "mouse", 121 | x: self.x, 122 | y: self.y 123 | }); 124 | } 125 | } else if (idx !== undefined) { 126 | self.touches.splice(idx, 1); 127 | } 128 | } 129 | function updateMouseFromTouch(touch) { 130 | self.x = touch.x; 131 | self.y = touch.y; 132 | if (self.buttons[0] === false) { 133 | self.buttons[0] = true; 134 | } 135 | } 136 | function touchIndexById(id) { 137 | for (var i = 0; i < self.touches.length; i++) { 138 | if (self.touches[i].id === id) { 139 | return i; 140 | } 141 | } 142 | return undefined; 143 | } 144 | function eachChangedTouch(event, onChangeFunc) { 145 | var touches = event.changedTouches; 146 | for (var i = 0; i < touches.length; i++) { 147 | onChangeFunc(touches[i]); 148 | } 149 | } 150 | canvas.addEventListener("touchstart", function(event) { 151 | eachChangedTouch(event, function(touch) { 152 | var t = { 153 | id: touch.identifier 154 | }; 155 | relMouseCoords(canvas, touch, t); 156 | if (self.touches.length === 0) { 157 | t.isMouse = true; 158 | updateMouseFromTouch(t); 159 | } 160 | self.touches.push(t); 161 | }); 162 | }); 163 | canvas.addEventListener("touchmove", function(event) { 164 | eachChangedTouch(event, function(touch) { 165 | var idx = touchIndexById(touch.identifier); 166 | var t = self.touches[idx]; 167 | relMouseCoords(canvas, touch, t); 168 | if (t.isMouse) { 169 | updateMouseFromTouch(t); 170 | } 171 | }); 172 | }); 173 | canvas.addEventListener("touchend", function(event) { 174 | eachChangedTouch(event, function(touch) { 175 | var idx = touchIndexById(touch.identifier); 176 | var t = self.touches.splice(idx, 1)[0]; 177 | if (t.isMouse) { 178 | if (self.touches.length === 0) { 179 | self.buttons[0] = false; 180 | } else { 181 | self.touches[0].isMouse = true; 182 | updateMouseFromTouch(self.touches[0]); 183 | } 184 | } 185 | if (self.onmouseup) { 186 | self.onmouseup(t.x, t.y); 187 | } 188 | }); 189 | }); 190 | } 191 | /** 192 | * Test whether the device supports touch events. This is useful to customize messages to say either "click" or "tap". 193 | * @returns {boolean} 194 | */ 195 | Mouse.prototype.supportsTouch = function() { 196 | return "ontouchstart" in window || navigator.msMaxTouchPoints; 197 | }; 198 | /** 199 | * Test if a mouse button is currently pressed. 200 | * @param {number} button The button number to test. Button 0 is typically the left mouse button, as well as the first touch location. 201 | * @returns {boolean} 202 | */ 203 | Mouse.prototype.isPressed = function(button) { 204 | return this.buttons[button]; 205 | }; 206 | 207 | module.exports = Mouse; 208 | -------------------------------------------------------------------------------- /lib/ninepatch.js: -------------------------------------------------------------------------------- 1 | var buffer = require("./buffer"); 2 | 3 | function getContextForImage(image) { 4 | var ctx; 5 | buffer.makeBuffer(image.width, image.height, function(context) { 6 | context.drawImage(image, 0, 0, image.width, image.height); 7 | ctx = context; 8 | }); 9 | return ctx; 10 | } 11 | 12 | /** 13 | * A stretchable image that has borders. 14 | * Similar to the [Android NinePatch]{@link https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch}, but it only has the lines on the bottom and right edges to denote the stretchable area. 15 | * A NinePatch is a normal picture, but has an extra 1-pixel wide column on the right edge and bottom edge. The extra column contains a black line that denotes the tileable center portion of the image. The lines are used to divide the image into nine tiles that can be automatically repeated to stretch the picture to any size without distortion. 16 | * @constructor 17 | * @alias Splat.NinePatch 18 | * @param {external:image} image The source image to make stretchable. 19 | */ 20 | function NinePatch(image) { 21 | this.img = image; 22 | var imgw = image.width - 1; 23 | var imgh = image.height - 1; 24 | 25 | var context = getContextForImage(image); 26 | var firstDiv = imgw; 27 | var secondDiv = imgw; 28 | var pixel; 29 | var alpha; 30 | for (var x = 0; x < imgw; x++) { 31 | pixel = context.getImageData(x, imgh, 1, 1).data; 32 | alpha = pixel[3]; 33 | if (firstDiv === imgw && alpha > 0) { 34 | firstDiv = x; 35 | } 36 | if (firstDiv < imgw && alpha === 0) { 37 | secondDiv = x; 38 | break; 39 | } 40 | } 41 | this.w1 = firstDiv; 42 | this.w2 = secondDiv - firstDiv; 43 | this.w3 = imgw - secondDiv; 44 | 45 | firstDiv = secondDiv = imgh; 46 | for (var y = 0; y < imgh; y++) { 47 | pixel = context.getImageData(imgw, y, 1, 1).data; 48 | alpha = pixel[3]; 49 | if (firstDiv === imgh && alpha > 0) { 50 | firstDiv = y; 51 | } 52 | if (firstDiv < imgh && alpha === 0) { 53 | secondDiv = y; 54 | break; 55 | } 56 | } 57 | this.h1 = firstDiv; 58 | this.h2 = secondDiv - firstDiv; 59 | this.h3 = imgh - secondDiv; 60 | } 61 | /** 62 | * Draw the image stretched to a given rectangle. 63 | * @param {external:CanvasRenderingContext2D} context The drawing context. 64 | * @param {number} x The left side of the rectangle. 65 | * @param {number} y The top of the rectangle. 66 | * @param {number} width The width of the rectangle. 67 | * @param {number} height The height of the rectangle. 68 | */ 69 | NinePatch.prototype.draw = function(context, x, y, width, height) { 70 | x = Math.floor(x); 71 | y = Math.floor(y); 72 | width = Math.floor(width); 73 | height = Math.floor(height); 74 | var cx, cy, w, h; 75 | 76 | for (cy = y + this.h1; cy < y + height - this.h3; cy += this.h2) { 77 | for (cx = x + this.w1; cx < x + width - this.w3; cx += this.w2) { 78 | w = Math.min(this.w2, x + width - this.w3 - cx); 79 | h = Math.min(this.h2, y + height - this.h3 - cy); 80 | context.drawImage(this.img, this.w1, this.h1, w, h, cx, cy, w, h); 81 | } 82 | } 83 | for (cy = y + this.h1; cy < y + height - this.h3; cy += this.h2) { 84 | h = Math.min(this.h2, y + height - this.h3 - cy); 85 | if (this.w1 > 0) { 86 | context.drawImage(this.img, 0, this.h1, this.w1, h, x, cy, this.w1, h); 87 | } 88 | if (this.w3 > 0) { 89 | context.drawImage(this.img, this.w1 + this.w2, this.h1, this.w3, h, x + width - this.w3, cy, this.w3, h); 90 | } 91 | } 92 | for (cx = x + this.w1; cx < x + width - this.w3; cx += this.w2) { 93 | w = Math.min(this.w2, x + width - this.w3 - cx); 94 | if (this.h1 > 0) { 95 | context.drawImage(this.img, this.w1, 0, w, this.h1, cx, y, w, this.h1); 96 | } 97 | if (this.h3 > 0) { 98 | context.drawImage(this.img, this.w1, this.w1 + this.w2, w, this.h3, cx, y + height - this.h3, w, this.h3); 99 | } 100 | } 101 | if (this.w1 > 0 && this.h1 > 0) { 102 | context.drawImage(this.img, 0, 0, this.w1, this.h1, x, y, this.w1, this.h1); 103 | } 104 | if (this.w3 > 0 && this.h1 > 0) { 105 | context.drawImage(this.img, this.w1 + this.w2, 0, this.w3, this.h1, x + width - this.w3, y, this.w3, this.h1); 106 | } 107 | if (this.w1 > 0 && this.h3 > 0) { 108 | context.drawImage(this.img, 0, this.h1 + this.h2, this.w1, this.h3, x, y + height - this.h3, this.w1, this.h3); 109 | } 110 | if (this.w3 > 0 && this.h3 > 0) { 111 | context.drawImage(this.img, this.w1 + this.w2, this.h1 + this.h2, this.w3, this.h3, x + width - this.w3, y + height - this.h3, this.w3, this.h3); 112 | } 113 | }; 114 | 115 | module.exports = NinePatch; 116 | -------------------------------------------------------------------------------- /lib/once.js: -------------------------------------------------------------------------------- 1 | module.exports = function(fn) { 2 | var called = false; 3 | return function() { 4 | if (!called) { 5 | called = true; 6 | return fn.apply(this, arguments); 7 | } 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/openUrl.js: -------------------------------------------------------------------------------- 1 | var platform = require("./platform"); 2 | 3 | /** 4 | * Open a url in a new window. 5 | * @alias Splat.openUrl 6 | * @param {string} url The url to open in a new window. 7 | */ 8 | module.exports = function(url) { 9 | window.open(url); 10 | }; 11 | 12 | if (platform.isEjecta()) { 13 | module.exports = function(url) { 14 | window.ejecta.openURL(url); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/particles.js: -------------------------------------------------------------------------------- 1 | /** @module splat-ecs/lib/particles */ 2 | 3 | var random = require("splat-ecs/lib/random"); 4 | 5 | module.exports = { 6 | /** 7 | * Create between {@link module:splat-ecs/lib/particles.Config#qtyMin qtyMin} and {@link module:splat-ecs/lib/particles.Config#qtyMax qtyMax} particles, and randomize their properties according to config. 8 | * @param {object} game The game object that you get in systems and scripts. 9 | * @param {module:splat-ecs/lib/particles.Config} config The settings to use to create the particles. 10 | */ 11 | "create": function(game, config) { 12 | var particleCount = Math.floor(random.inRange(config.qtyMin, config.qtyMax)); 13 | for (var i = 0; i < particleCount; i++) { 14 | var particle = game.prefabs.instantiate(game.entities, config.prefab); 15 | // check if origin is an entity 16 | var origin = config.origin; 17 | if (typeof config.origin === "number") { 18 | origin = choosePointInEntity(game, origin); 19 | } 20 | 21 | var randomSize = random.inRange(config.sizeMin, config.sizeMax); 22 | scaleEntityRect(game, particle, randomSize); 23 | 24 | centerEntityOnPoint(game, particle, origin); 25 | 26 | var velocity = random.inRange(config.velocityMin, config.velocityMax); 27 | 28 | var angle = pickAngle(config, i, particleCount); 29 | var velocityComponent = game.entities.addComponent(particle, "velocity"); 30 | var direction = pointOnCircle(angle, velocity); 31 | velocityComponent.x = direction.x; 32 | velocityComponent.y = direction.y; 33 | 34 | if (config.accelerationX || config.accelerationY) { 35 | var accel = game.entities.addComponent(particle, "acceleration"); 36 | accel.x = config.accelerationX; 37 | accel.y = config.accelerationY; 38 | } 39 | var lifeSpan = game.entities.addComponent(particle, "lifeSpan"); 40 | lifeSpan.max = random.inRange(config.lifeSpanMin, config.lifeSpanMax); 41 | } 42 | }, 43 | 44 | /** 45 | * The settings for a type of particle. 46 | * @constructor 47 | * @param {string} prefab The name of a prefab to instantiate for the particle, as defined in prefabs.json. 48 | */ 49 | "Config": function(prefab) { 50 | /** 51 | * The name of a prefab to instantiate for the particle, as defined in prefabs.json. 52 | * @member {string} 53 | */ 54 | this.prefab = prefab; 55 | /** 56 | * The origin point in which to create particles. 57 | * 58 | * If the origin is a number it represents an entity and a random point inside the entity will be used. 59 | * If origin is a point like {"x": 50, "y": 50} particles will spawn at that position. 60 | * @member {object | number} 61 | */ 62 | this.origin = { "x": 0, "y": 0 }; 63 | /** 64 | * How to distribute particles along the {@link module:splat-ecs/lib/particles.Config#arcWidth arcWidth}. 65 | * 66 | * Possible values: 67 | *
68 | *
"even"
69 | *
Distribute the particles evenly along the arc.
70 | *
"random"
71 | *
Scatter the particles on random points of the arc.
72 | *
73 | * @member {string} 74 | */ 75 | this.spreadType = "random"; 76 | /** 77 | * The direction (an angle in radians) that the particles should move. 78 | * @member {number} 79 | */ 80 | this.angle = 0; 81 | /** 82 | * The width of an arc (represented by an angle in radians) to spread the particles. The arc is centered around {@link module:splat-ecs/lib/particles.Config#angle angle}. 83 | * @member {number} 84 | */ 85 | this.arcWidth = Math.PI / 2; 86 | /** 87 | * The minimum number of particles to create. 88 | * @member {number} 89 | */ 90 | this.qtyMin = 1; 91 | /** 92 | * The maximum number of particles to create. 93 | * @member {number} 94 | */ 95 | this.qtyMax = 1; 96 | /** 97 | * The minimum percentage to scale each particle. 98 | * 103 | * @member {number} 104 | */ 105 | this.sizeMin = 1; 106 | /** 107 | * The maximum percentage to scale each particle. 108 | * 113 | * @member {number} 114 | */ 115 | this.sizeMax = 1; 116 | /** 117 | * The minimum velocity to apply to each particle. 118 | * @member {number} 119 | */ 120 | this.velocityMin = 0.5; 121 | /** 122 | * The maximum velocity to apply to each particle. 123 | * @member {number} 124 | */ 125 | this.velocityMax = 0.5; 126 | /** 127 | * The acceleration on the x-axis to apply to each particle. 128 | * @member {number} 129 | */ 130 | this.accelerationX = 0; 131 | /** 132 | * The acceleration on the y-axis to apply to each particle. 133 | * @member {number} 134 | */ 135 | this.accelerationY = 0; 136 | /** 137 | * The minimum life span to apply to each particle. 138 | * @member {number} 139 | */ 140 | this.lifeSpanMin = 0; 141 | /** 142 | * The maximum life span to apply to each particle. 143 | * @member {number} 144 | */ 145 | this.lifeSpanMax = 500; 146 | } 147 | }; 148 | 149 | function pickAngle(config, particleNumber, particleCount) { 150 | var startAngle = config.angle - (config.arcWidth / 2); 151 | if (config.spreadType === "even") { 152 | return (particleNumber * (config.arcWidth / (particleCount - 1))) + startAngle; 153 | } else { 154 | var endAngle = startAngle + config.arcWidth; 155 | return random.inRange(startAngle, endAngle); 156 | } 157 | } 158 | 159 | function scaleEntityRect(game, entity, scaleFactor) { 160 | var size = game.entities.getComponent(entity, "size"); 161 | size.width = size.width * scaleFactor; 162 | size.height = size.height * scaleFactor; 163 | } 164 | 165 | function pointOnCircle(angle, radius) { 166 | return { 167 | "x": (radius * Math.cos(angle)), 168 | "y": (radius * Math.sin(angle)) 169 | }; 170 | } 171 | 172 | /** 173 | * Center an entity on a given point. 174 | * @private 175 | * @param {object} game Required for game.entities.get(). 176 | * @param {integer} entity The id of entity to center. 177 | * @param {object} point A point object {"x": 50, "y": 50} on which to center the entity. 178 | */ 179 | function centerEntityOnPoint(game, entity, point) { 180 | var size = game.entities.getComponent(entity, "size"); 181 | var position = game.entities.addComponent(entity, "position"); 182 | position.x = point.x - (size.width / 2); 183 | position.y = point.y - (size.height / 2); 184 | } 185 | 186 | /** 187 | * Choose a random point inside the bounding rectangle of an entity. 188 | * @private 189 | * @param {object} game Required for game.entities.get(). 190 | * @param {integer} entity The id of entity to pick a point within. 191 | * @returns {object} an point object {"x": 50, "y": 50}. 192 | */ 193 | function choosePointInEntity(game, entity) { 194 | var position = game.entities.getComponent(entity, "position"); 195 | var size = game.entities.getComponent(entity, "size"); 196 | if (size === undefined) { 197 | return { 198 | "x": position.x, 199 | "y": position.y 200 | }; 201 | } 202 | return { 203 | "x": random.inRange(position.x, (position.x + size.width)), 204 | "y": random.inRange(position.y, (position.y + size.height)) 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /lib/platform.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | isChromeApp: function() { 3 | return window.chrome && window.chrome.app && window.chrome.app.runtime; 4 | }, 5 | isEjecta: function() { 6 | return window.ejecta; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/prefabs.js: -------------------------------------------------------------------------------- 1 | var clone = require("./clone"); 2 | var setOrAddComponent = require("./set-or-add-component"); 3 | 4 | function Prefabs(prefabs) { 5 | this.prefabs = prefabs; 6 | } 7 | Prefabs.prototype.instantiate = function(entities, name) { 8 | var id = entities.create(); 9 | var prefab = this.prefabs[name]; 10 | Object.keys(prefab).forEach(function(key) { 11 | if (key === "id") { 12 | return; 13 | } 14 | setOrAddComponent(entities, id, key, clone(prefab[key])); 15 | }); 16 | return id; 17 | }; 18 | Prefabs.prototype.register = function(name, components) { 19 | this.prefabs[name] = components; 20 | }; 21 | Prefabs.prototype.registerMultiple = function(prefabs) { 22 | Object.keys(prefabs).forEach(function(key) { 23 | this.registerPrefab(key, prefabs[key]); 24 | }.bind(this)); 25 | }; 26 | 27 | module.exports = Prefabs; 28 | -------------------------------------------------------------------------------- /lib/random.js: -------------------------------------------------------------------------------- 1 | /** @module splat-ecs/lib/random */ 2 | 3 | module.exports = { 4 | /** 5 | * Get a pseudo-random number between the minimum (inclusive) and maximum (exclusive) parameters. 6 | * @function inRange 7 | * @param {number} min Inclusive minimum value for the random number 8 | * @param {number} max Exclusive maximum value for the random number 9 | * @returns {number} A number between min and max 10 | * @see [Bracket Notation: Inclusion and Exclusion]{@link https://en.wikipedia.org/wiki/Bracket_%28mathematics%29#Intervals} 11 | * @example 12 | var random = require("splat-ecs/lib/random"); 13 | random.inRange(0, 1) // Returns 0.345822917402371 14 | random.inRange(10, 100) // Returns 42.4823819274931274 15 | */ 16 | "inRange": function(min, max) { 17 | return min + Math.random() * (max - min); 18 | }, 19 | 20 | /** 21 | * Get a random element in an array 22 | * @function from 23 | * @param {array} array Array of elements to choose from 24 | * @returns {Object} A random element from the given array 25 | * @example 26 | var random = require("splat-ecs/lib/random"); 27 | var fruit = ["Apple", "Banana", "Orange", "Peach"]; 28 | random.from(fruit); // Could return "Orange" 29 | random.from(fruit); // Could return "Apple" 30 | random.from(fruit); // Could return "Peach" 31 | random.from(fruit); // Could return "Banana" 32 | */ 33 | "from": function(array) { 34 | return array[Math.floor(Math.random() * array.length)]; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/save-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @namespace Splat.saveData 3 | */ 4 | 5 | var platform = require("./platform"); 6 | 7 | function cookieGet(name) { 8 | var value = "; " + document.cookie; 9 | var parts = value.split("; " + name + "="); 10 | if (parts.length === 2) { 11 | return parts.pop().split(";").shift(); 12 | } else { 13 | throw "cookie " + name + " was not found"; 14 | } 15 | } 16 | 17 | function cookieSet(name, value) { 18 | var expire = new Date(); 19 | expire.setTime(expire.getTime() + 1000 * 60 * 60 * 24 * 365); 20 | var cookie = name + "=" + value + "; expires=" + expire.toUTCString() + ";"; 21 | document.cookie = cookie; 22 | } 23 | 24 | function getMultiple(getSingleFunc, keys, callback) { 25 | if (typeof keys === "string") { 26 | keys = [keys]; 27 | } 28 | 29 | var data; 30 | try { 31 | data = keys.map(function(key) { 32 | return [key, getSingleFunc(key)]; 33 | }).reduce(function(accum, pair) { 34 | accum[pair[0]] = pair[1]; 35 | return accum; 36 | }, {}); 37 | } catch (e) { 38 | return callback(e); 39 | } 40 | callback(undefined, data); 41 | } 42 | 43 | function setMultiple(setSingleFunc, data, callback) { 44 | try { 45 | for (var key in data) { 46 | if (data.hasOwnProperty(key)) { 47 | setSingleFunc(key, data[key]); 48 | } 49 | } 50 | } catch (e) { 51 | return callback(e); 52 | } 53 | callback(); 54 | } 55 | 56 | var cookieSaveData = { 57 | "get": getMultiple.bind(undefined, cookieGet), 58 | "set": setMultiple.bind(undefined, cookieSet) 59 | }; 60 | 61 | function localStorageGet(name) { 62 | return window.localStorage.getItem(name); 63 | } 64 | 65 | function localStorageSet(name, value) { 66 | window.localStorage.setItem(name, value.toString()); 67 | } 68 | 69 | var localStorageSaveData = { 70 | "get": getMultiple.bind(undefined, localStorageGet), 71 | "set": setMultiple.bind(undefined, localStorageSet) 72 | }; 73 | 74 | /** 75 | * A function that is called when save data has finished being retrieved. 76 | * @callback saveDataGetFinished 77 | * @param {error} err If defined, err is the error that occurred when retrieving the data. 78 | * @param {object} data The key-value pairs of data that were previously saved. 79 | */ 80 | /** 81 | * Retrieve data previously stored with {@link Splat.saveData.set}. 82 | * @alias Splat.saveData.get 83 | * @param {string | Array} keys A single key or array of key names of data items to retrieve. 84 | * @param {saveDataGetFinished} callback A callback that is called with the data when it has been retrieved. 85 | */ 86 | function chromeStorageGet(keys, callback) { 87 | window.chrome.storage.sync.get(keys, function(data) { 88 | if (window.chrome.runtime.lastError) { 89 | callback(window.chrome.runtime.lastError); 90 | } else { 91 | callback(undefined, data); 92 | } 93 | }); 94 | } 95 | 96 | /** 97 | * A function that is called when save data has finished being stored. 98 | * @callback saveDataSetFinished 99 | * @param {error} err If defined, err is the error that occurred when saving the data. 100 | */ 101 | /** 102 | * Store data for later. 103 | * @alias Splat.saveData.set 104 | * @param {object} data An object containing key-value pairs of data to save. 105 | * @param {saveDataSetFinished} callback A callback that is called when the data has finished saving. 106 | */ 107 | function chromeStorageSet(data, callback) { 108 | window.chrome.storage.sync.set(data, function() { 109 | callback(window.chrome.runtime.lastError); 110 | }); 111 | } 112 | 113 | var chromeStorageSaveData = { 114 | "get": chromeStorageGet, 115 | "set": chromeStorageSet 116 | }; 117 | 118 | if (platform.isChromeApp()) { 119 | module.exports = chromeStorageSaveData; 120 | } else if (window.localStorage) { 121 | module.exports = localStorageSaveData; 122 | } else { 123 | module.exports = cookieSaveData; 124 | } 125 | -------------------------------------------------------------------------------- /lib/scene.js: -------------------------------------------------------------------------------- 1 | var clone = require("./clone"); 2 | var components = require("./components"); 3 | var ECS = require("entity-component-system").EntityComponentSystem; 4 | var EntityPool = require("entity-component-system").EntityPool; 5 | var registerComponents = require("./components/register"); 6 | 7 | function Scene(name, globals) { 8 | this.data = {}; 9 | this.entities = new EntityPool(); 10 | this.globals = globals; 11 | this.name = name; 12 | this.onEnter = function() {}; 13 | this.onExit = function() {}; 14 | this.renderer = new ECS(); 15 | this.state = "stopped"; 16 | this.speed = 1.0; 17 | this.simulation = new ECS(); 18 | this.simulationStepTime = 5; 19 | 20 | this.firstTime = true; 21 | this.accumTime = 0; 22 | 23 | this.sceneConfig = globals.require("./data/scenes")[name]; 24 | if (typeof this.sceneConfig.onEnter === "string") { 25 | this.onEnter = globals.require(this.sceneConfig.onEnter); 26 | } 27 | if (typeof this.sceneConfig.onExit === "string") { 28 | this.onExit = globals.require(this.sceneConfig.onExit); 29 | } 30 | } 31 | Scene.prototype.start = function(sceneArgs) { 32 | if (this.state !== "stopped") { 33 | return; 34 | } 35 | this.state = "starting"; 36 | this.tempArguments = sceneArgs; 37 | }; 38 | Scene.prototype._initialize = function() { 39 | this.entities = new EntityPool(); 40 | this.firstTime = true; 41 | this.accumTime = 0; 42 | 43 | this.data = { 44 | animations: this.globals.animations, 45 | arguments: this.tempArguments || {}, 46 | canvas: this.globals.canvas, 47 | context: this.globals.context, 48 | entities: this.entities, 49 | images: this.globals.images, 50 | inputs: this.globals.inputs, 51 | prefabs: this.globals.prefabs, 52 | require: this.globals.require, 53 | scaleCanvasToCssSize: this.globals.scaleCanvasToCssSize, 54 | scaleCanvasToFitRectangle: this.globals.scaleCanvasToFitRectangle, 55 | sceneConfig: this.sceneConfig, 56 | scenes: this.globals.scenes, 57 | sounds: this.globals.sounds, 58 | switchScene: this.switchScene.bind(this) 59 | }; 60 | 61 | this.simulation = new ECS(); 62 | this.renderer = new ECS(); 63 | this.simulation.add(function processInputUpdates() { 64 | this.globals.inputs.processUpdates(); 65 | }.bind(this)); 66 | 67 | var systems = this.globals.require("./data/systems"); 68 | this.installSystems(systems.simulation, this.simulation, this.data); 69 | this.installSystems(systems.renderer, this.renderer, this.data); 70 | 71 | registerComponents(this.entities, components); 72 | registerComponents(this.entities, this.globals.require("./data/components")); 73 | var entities = this.globals.require("./data/entities"); 74 | this.entities.load(clone(entities[this.name]) || []); 75 | 76 | this.onEnter(this.data); 77 | }; 78 | Scene.prototype.stop = function() { 79 | if (this.state === "stopped") { 80 | return; 81 | } 82 | this.state = "stopped"; 83 | this.onExit(this.data); 84 | }; 85 | Scene.prototype.switchScene = function(scene, sceneArgs) { 86 | this.stop(); 87 | this.data.scenes[scene].start(sceneArgs); 88 | }; 89 | Scene.prototype.installSystems = function(systems, ecs, data) { 90 | for (var i = 0; i < systems.length; i++) { 91 | var system = systems[i]; 92 | 93 | if (system.scenes.indexOf(this.name) === -1 && system.scenes !== "all") { 94 | continue; 95 | } 96 | var script = this.globals.require(system.name); 97 | if (script === undefined) { 98 | console.error("failed to load script", system.name); 99 | } 100 | script(ecs, data); 101 | } 102 | }; 103 | Scene.prototype.simulate = function(elapsed) { 104 | if (this.state === "stopped") { 105 | return; 106 | } 107 | if (this.state === "starting") { 108 | var start = window.performance.now(); 109 | this._initialize(); 110 | var end = window.performance.now(); 111 | this.accumTime = start - end; // negative so a long initialize doesn't make the first few frames slow 112 | this.state = "started"; 113 | } 114 | 115 | if (this.firstTime) { 116 | this.firstTime = false; 117 | // run simulation the first time, because not enough time will have elapsed 118 | this.simulation.run(this.entities, 0); 119 | elapsed = 0; 120 | } 121 | 122 | elapsed *= this.speed; 123 | 124 | this.accumTime += elapsed; 125 | while (this.accumTime >= this.simulationStepTime) { 126 | this.accumTime -= this.simulationStepTime; 127 | this.simulation.run(this.entities, this.simulationStepTime); 128 | } 129 | }; 130 | Scene.prototype.render = function(elapsed) { 131 | if (this.state !== "started") { 132 | return; 133 | } 134 | this.renderer.run(this.entities, elapsed); 135 | }; 136 | 137 | module.exports = Scene; 138 | -------------------------------------------------------------------------------- /lib/set-or-add-component.js: -------------------------------------------------------------------------------- 1 | module.exports = function setOrAddComponent(entities, entity, component, value) { 2 | if (typeof value !== "object") { 3 | entities.setComponent(entity, component, value); 4 | } else { 5 | var data = entities.addComponent(entity, component); 6 | Object.keys(value).forEach(function(valKey) { 7 | data[valKey] = value[valKey]; 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/sound-manager.js: -------------------------------------------------------------------------------- 1 | var AssetLoader = require("./assets/asset-loader"); 2 | var loadSound = require("./assets/load-sound"); 3 | 4 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 5 | 6 | /** 7 | * Plays audio, tracks looping sounds, and manages volume. 8 | * This implementation uses the Web Audio API. 9 | * @constructor 10 | * @param {Object} manifest A hash where the key is the name of a sound, and the value is the URL of a sound file. 11 | */ 12 | function SoundManager(manifest) { 13 | /** 14 | * A flag signifying if sounds have been muted through {@link SoundManager#mute}. 15 | * @member {boolean} 16 | * @private 17 | */ 18 | this.muted = false; 19 | /** 20 | * A key-value object that stores named looping sounds. 21 | * @member {object} 22 | * @private 23 | */ 24 | this.looping = {}; 25 | 26 | /** 27 | * The Web Audio API AudioContext 28 | * @member {external:AudioContext} 29 | * @private 30 | */ 31 | this.context = new window.AudioContext(); 32 | 33 | this.gainNode = this.context.createGain(); 34 | this.gainNode.connect(this.context.destination); 35 | this.volume = this.gainNode.gain.value; 36 | this.installSafariWorkaround(); 37 | this.assets = new AssetLoader(manifest, loadSound.bind(undefined, this.context)); 38 | } 39 | SoundManager.prototype.installSafariWorkaround = function() { 40 | // safari on iOS mutes sounds until they're played in response to user input 41 | // play a dummy sound on first touch 42 | var firstTouchHandler = function() { 43 | window.removeEventListener("click", firstTouchHandler); 44 | window.removeEventListener("keydown", firstTouchHandler); 45 | window.removeEventListener("touchstart", firstTouchHandler); 46 | 47 | var source = this.context.createOscillator(); 48 | source.connect(this.gainNode); 49 | source.start(0); 50 | source.stop(0); 51 | 52 | if (this.firstPlay) { 53 | this.play(this.firstPlay, this.firstPlayLoop); 54 | } else { 55 | this.firstPlay = "workaround"; 56 | } 57 | }.bind(this); 58 | window.addEventListener("click", firstTouchHandler); 59 | window.addEventListener("keydown", firstTouchHandler); 60 | window.addEventListener("touchstart", firstTouchHandler); 61 | }; 62 | /** 63 | * Play a sound. 64 | * @param {string} name The name of the sound to play. 65 | * @param {Object} [loop=undefined] A hash containing loopStart and loopEnd options. To stop a looped sound use {@link SoundManager#stop}. 66 | */ 67 | SoundManager.prototype.play = function(name, loop) { 68 | if (loop && this.looping[name]) { 69 | return; 70 | } 71 | if (!this.firstPlay) { 72 | // let the iOS user input workaround handle it 73 | this.firstPlay = name; 74 | this.firstPlayLoop = loop; 75 | return; 76 | } 77 | var snd = this.assets.get(name); 78 | if (snd === undefined) { 79 | console.error("Unknown sound: " + name); 80 | return; 81 | } 82 | var source = this.context.createBufferSource(); 83 | source.buffer = snd; 84 | source.connect(this.gainNode); 85 | if (loop) { 86 | source.loop = true; 87 | source.loopStart = loop.loopStart || 0; 88 | source.loopEnd = loop.loopEnd || 0; 89 | this.looping[name] = source; 90 | } 91 | source.start(0); 92 | }; 93 | /** 94 | * Stop playing a sound. This currently only stops playing a sound that was looped earlier, and doesn't stop a sound mid-play. Patches welcome. 95 | * @param {string} name The name of the sound to stop looping. 96 | */ 97 | SoundManager.prototype.stop = function(name) { 98 | if (!this.looping[name]) { 99 | return; 100 | } 101 | this.looping[name].stop(0); 102 | delete this.looping[name]; 103 | }; 104 | /** 105 | * Silence all sounds. Sounds keep playing, but at zero volume. Call {@link SoundManager#unmute} to restore the previous volume level. 106 | */ 107 | SoundManager.prototype.mute = function() { 108 | this.gainNode.gain.value = 0; 109 | this.muted = true; 110 | }; 111 | /** 112 | * Restore volume to whatever value it was before {@link SoundManager#mute} was called. 113 | */ 114 | SoundManager.prototype.unmute = function() { 115 | this.gainNode.gain.value = this.volume; 116 | this.muted = false; 117 | }; 118 | /** 119 | * Set the volume of all sounds. 120 | * @param {number} gain The desired volume level. A number between 0.0 and 1.0, with 0.0 being silent, and 1.0 being maximum volume. 121 | */ 122 | SoundManager.prototype.setVolume = function(gain) { 123 | this.volume = gain; 124 | this.gainNode.gain.value = gain; 125 | this.muted = false; 126 | }; 127 | /** 128 | * Test if the volume is currently muted. 129 | * @return {boolean} True if the volume is currently muted. 130 | */ 131 | SoundManager.prototype.isMuted = function() { 132 | return this.muted; 133 | }; 134 | 135 | 136 | function FakeSoundManager() {} 137 | FakeSoundManager.prototype.play = function() {}; 138 | FakeSoundManager.prototype.stop = function() {}; 139 | FakeSoundManager.prototype.mute = function() {}; 140 | FakeSoundManager.prototype.unmute = function() {}; 141 | FakeSoundManager.prototype.setVolume = function() {}; 142 | FakeSoundManager.prototype.isMuted = function() { 143 | return true; 144 | }; 145 | 146 | if (window.AudioContext) { 147 | module.exports = SoundManager; 148 | } else { 149 | console.warn("This browser doesn't support the Web Audio API."); 150 | module.exports = FakeSoundManager; 151 | } 152 | -------------------------------------------------------------------------------- /lib/split-filmstrip-animations.js: -------------------------------------------------------------------------------- 1 | var clone = require("./clone"); 2 | 3 | module.exports = function splitFilmStripAnimations(animations) { 4 | Object.keys(animations).forEach(function(key) { 5 | var firstFrame = animations[key][0]; 6 | if (firstFrame.filmstripFrames) { 7 | splitFilmStripAnimation(animations, key); 8 | } 9 | }); 10 | }; 11 | 12 | function splitFilmStripAnimation(animations, key) { 13 | var firstFrame = animations[key][0]; 14 | if (firstFrame.properties.image.sourceWidth % firstFrame.filmstripFrames != 0) { 15 | console.warn("The \"" + key + "\" animation is " + firstFrame.properties.image.sourceWidth + " pixels wide and that is is not evenly divisible by " + firstFrame.filmstripFrames + " frames."); 16 | } 17 | for (var i = 0; i < firstFrame.filmstripFrames; i++) { 18 | var frameWidth = firstFrame.properties.image.sourceWidth / firstFrame.filmstripFrames; 19 | var newFrame = clone(firstFrame); 20 | newFrame.properties.image.sourceX = frameWidth * i; 21 | newFrame.properties.image.sourceWidth = frameWidth; 22 | animations[key].push(newFrame); 23 | } 24 | animations[key].splice(0,1); 25 | } 26 | -------------------------------------------------------------------------------- /lib/systems/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | *

A "system" is a function that runs on all entities with specific {@link Components}.

3 | *

The systems listed here are built-in to splat and should not be called directly in your game.

4 | *

Instead these systems are included in [systems.json]{@link https://github.com/SplatJS/splat-ecs-starter-project/blob/master/src/data/systems.json} in your project.

5 | *

When you write your own systems they will also be included in your project's systems.json file. 6 | * @namespace Systems 7 | */ 8 | 9 | -------------------------------------------------------------------------------- /lib/systems/renderer/apply-shake.js: -------------------------------------------------------------------------------- 1 | var random = require("../../random"); 2 | 3 | /** 4 | * System that looks for an entity with the {@link Components.shake} and {@link Components.position} components. 5 | * Every frame the apply shake system will move the entity's position by a pseudo-random number of pixels between half the magnitude (positive and negative). 6 | * @memberof Systems 7 | * @alias applyShake 8 | * @requires Systems.revertShake 9 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 10 | * @see [random]{@link splat-ecs/lib/random} 11 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 12 | * @see [revertShake]{@link Systems.revertShake} 13 | */ 14 | module.exports = function(ecs, game) { 15 | game.entities.registerSearch("applyShakeSearch", ["shake", "position"]); 16 | ecs.addEach(function applyShake(entity, elapsed) { 17 | var shake = game.entities.getComponent(entity, "shake"); 18 | if (shake.duration !== undefined) { 19 | shake.duration -= elapsed; 20 | if (shake.duration <= 0) { 21 | game.entities.removeComponent(entity, "shake"); 22 | return; 23 | } 24 | } 25 | var position = game.entities.getComponent(entity, "position"); 26 | shake.lastPositionX = position.x; 27 | shake.lastPositionY = position.y; 28 | 29 | var mx = shake.magnitudeX; 30 | if (mx === undefined) { 31 | mx = shake.magnitude || 0; 32 | } 33 | mx /= 2; 34 | position.x += random.inRange(-mx, mx); 35 | 36 | var my = shake.magnitudeY; 37 | if (my === undefined) { 38 | my = shake.magnitude || 0; 39 | } 40 | my /= 2; 41 | position.y += random.inRange(-my, my); 42 | }, "applyShakeSearch"); 43 | }; 44 | -------------------------------------------------------------------------------- /lib/systems/renderer/background-color.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(ecs, game) { // eslint-disable-line no-unused-vars 4 | game.entities.registerSearch("backgroundColorSearch", ["backgroundColor", "size", "position"]); 5 | ecs.addEach(function(entity, elapsed) { // eslint-disable-line no-unused-vars 6 | var color = game.entities.getComponent(entity, "backgroundColor"); 7 | var position = game.entities.getComponent(entity, "position"); 8 | var size = game.entities.getComponent(entity, "size"); 9 | game.context.fillStyle = color; 10 | game.context.fillRect(position.x, position.y, size.width, size.height); 11 | }, "backgroundColorSearch"); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/systems/renderer/clear-screen.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | ecs.add(function clearScreen() { 3 | game.context.clearRect(0, 0, game.canvas.width, game.canvas.height); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/systems/renderer/draw-frame-rate.js: -------------------------------------------------------------------------------- 1 | function roundRect(context, x, y, width, height, radius, stroke) { 2 | if (typeof stroke == "undefined") { 3 | stroke = true; 4 | } 5 | if (typeof radius === "undefined") { 6 | radius = 5; 7 | } 8 | context.beginPath(); 9 | context.moveTo(x + radius, y); 10 | context.lineTo(x + width - radius, y); 11 | context.quadraticCurveTo(x + width, y, x + width, y + radius); 12 | context.lineTo(x + width, y + height - radius); 13 | context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); 14 | context.lineTo(x + radius, y + height); 15 | context.quadraticCurveTo(x, y + height, x, y + height - radius); 16 | context.lineTo(x, y + radius); 17 | context.quadraticCurveTo(x, y, x + radius, y); 18 | context.closePath(); 19 | if (stroke) { 20 | context.stroke(); 21 | } 22 | context.fill(); 23 | } 24 | 25 | module.exports = function(ecs, game) { 26 | ecs.add(function drawFrameRate(entities, elapsed) { 27 | var fps = Math.floor(1000 / elapsed); 28 | 29 | var msg = fps + " FPS"; 30 | game.context.font = "24px monospace"; 31 | var w = game.context.measureText(msg).width; 32 | 33 | game.context.fillStyle = "rgba(0,0,0,0.8)"; 34 | game.context.strokeStyle = "rgba(0,0,0,0.9)"; 35 | roundRect(game.context, game.canvas.width - 130, -5, 120, 45, 5); 36 | 37 | if (fps < 30) { 38 | game.context.fillStyle = "#FE4848"; //red 39 | } else if (fps < 50) { 40 | game.context.fillStyle = "#FDFA3C"; //yellow 41 | } else { 42 | game.context.fillStyle = "#38F82A"; //green 43 | } 44 | 45 | if (fps < 10) { 46 | fps = " " + fps; 47 | } 48 | 49 | game.context.fillText(msg, game.canvas.width - w - 26, 25); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /lib/systems/renderer/draw-image.js: -------------------------------------------------------------------------------- 1 | var defaultSize = { "width": 0, "height": 0 }; 2 | 3 | function drawEntity(game, entity, context) { 4 | var imageComponent = game.entities.getComponent(entity, "image"); 5 | 6 | var image = imageComponent.buffer; 7 | if (!image) { 8 | image = game.images.get(imageComponent.name); 9 | } 10 | if (!image) { 11 | console.error("No such image", imageComponent.name, "for entity", entity, game.entities.getComponent(entity, "name")); 12 | return; 13 | } 14 | 15 | // FIXME: disable these checks/warnings in production version 16 | 17 | var sx = imageComponent.sourceX || 0; 18 | var sy = imageComponent.sourceY || 0; 19 | 20 | var dx = imageComponent.destinationX || 0; 21 | var dy = imageComponent.destinationY || 0; 22 | 23 | var size = game.entities.getComponent(entity, "size") || defaultSize; 24 | 25 | var sw = imageComponent.sourceWidth || image.width; 26 | if (sw === 0) { 27 | console.warn("sourceWidth is 0, image would be invisible for entity", entity, game.entities.getComponent(entity, "name")); 28 | } 29 | var sh = imageComponent.sourceHeight || image.height; 30 | if (sh === 0) { 31 | console.warn("sourceHeight is 0, image would be invisible for entity", entity, game.entities.getComponent(entity, "name")); 32 | } 33 | 34 | var dw = imageComponent.destinationWidth || size.width || image.width; 35 | var dh = imageComponent.destinationHeight || size.height || image.height; 36 | 37 | var position = game.entities.getComponent(entity, "position"); 38 | 39 | var dx2 = dx + position.x; 40 | var dy2 = dy + position.y; 41 | 42 | var rotation = game.entities.getComponent(entity, "rotation"); 43 | if (rotation !== undefined) { 44 | context.save(); 45 | var rx = rotation.x || size.width / 2 || 0; 46 | var ry = rotation.y || size.height / 2 || 0; 47 | var x = position.x + rx; 48 | var y = position.y + ry; 49 | context.translate(x, y); 50 | context.rotate(rotation.angle); 51 | 52 | dx2 = dx - rx; 53 | dy2 = dy - ry; 54 | } 55 | 56 | var alpha = 1; 57 | if (imageComponent.alpha !== undefined) { 58 | alpha = imageComponent.alpha; 59 | } 60 | context.globalAlpha = alpha; 61 | context.drawImage(image, sx, sy, sw, sh, dx2, dy2, dw, dh); 62 | 63 | if (rotation !== undefined) { 64 | context.restore(); 65 | } 66 | } 67 | 68 | var defaultCameraPosition = { x: 0, y: 0 }; 69 | var defaultCameraSize = { width: 0, height: 0 }; 70 | 71 | module.exports = function(ecs, game) { 72 | 73 | var toDraw = []; 74 | 75 | function comparePositions(a, b) { 76 | if (a === -1) { 77 | return 1; 78 | } 79 | if (b === -1) { 80 | return -1; 81 | } 82 | var pa = game.entities.getComponent(a, "position"); 83 | var pb = game.entities.getComponent(b, "position"); 84 | var za = pa.z || 0; 85 | var zb = pb.z || 0; 86 | var ya = pa.y || 0; 87 | var yb = pb.y || 0; 88 | return za - zb || ya - yb || a - b; 89 | } 90 | 91 | game.entities.registerSearch("drawImage", ["image", "position"]); 92 | ecs.add(function drawImage(entities) { 93 | var camera = game.entities.find("camera")[0]; 94 | var cameraPosition; 95 | var cameraSize; 96 | if (camera) { 97 | cameraPosition = game.entities.getComponent(camera, "position"); 98 | cameraSize = game.entities.getComponent(camera, "size"); 99 | } else { 100 | cameraPosition = defaultCameraPosition; 101 | cameraSize = defaultCameraSize; 102 | cameraSize.width = game.canvas.width; 103 | cameraSize.height = game.canvas.height; 104 | } 105 | 106 | toDraw.length = 0; 107 | 108 | var ids = entities.find("drawImage"); 109 | for (var i = 0; i < ids.length; i++) { 110 | if (isOnScreen(game, ids[i], cameraPosition, cameraSize)) { 111 | toDraw.push(ids[i]); 112 | } 113 | } 114 | 115 | toDraw.sort(comparePositions); 116 | 117 | for (i = 0; i < toDraw.length; i++) { 118 | drawEntity(game, toDraw[i], game.context); 119 | } 120 | }); 121 | }; 122 | 123 | function isOnScreen(game, entity, cameraPosition, cameraSize) { 124 | var imageComponent = game.entities.getComponent(entity, "image"); 125 | 126 | var image = imageComponent.buffer; 127 | if (!image) { 128 | image = game.images.get(imageComponent.name); 129 | } 130 | if (!image) { 131 | console.error("No such image", imageComponent.name, "for entity", entity, game.entities.getComponent(entity, "name")); 132 | return false; 133 | } 134 | 135 | // FIXME: disable these checks/warnings in production version 136 | 137 | var dx = imageComponent.destinationX || 0; 138 | var dy = imageComponent.destinationY || 0; 139 | 140 | var size = game.entities.getComponent(entity, "size") || defaultSize; 141 | 142 | var dw = imageComponent.destinationWidth || size.width || image.width; 143 | if (dw === 0) { 144 | console.warn("destinationWidth is 0, image would be invisible for entity", entity, game.entities.getComponent(entity, "name")); 145 | } 146 | var dh = imageComponent.destinationHeight || size.height || image.height; 147 | if (dh === 0) { 148 | console.warn("destinationHeight is 0, image would be invisible for entity", entity, game.entities.getComponent(entity, "name")); 149 | } 150 | 151 | var position = game.entities.getComponent(entity, "position"); 152 | 153 | var dx2 = dx + position.x; 154 | var dy2 = dy + position.y; 155 | 156 | return (dx2 + dw >= cameraPosition.x && 157 | dy2 + dh >= cameraPosition.y && 158 | dx2 < cameraPosition.x + cameraSize.width && 159 | dy2 < cameraPosition.y + cameraSize.height 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /lib/systems/renderer/draw-rectangles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("drawRectangles", ["position", "size"]); 3 | ecs.addEach(function drawRectangles(entity) { 4 | var strokeStyle = game.entities.getComponent(entity, "strokeStyle"); 5 | if (strokeStyle) { 6 | game.context.strokeStyle = strokeStyle; 7 | } 8 | var position = game.entities.getComponent(entity, "position"); 9 | var size = game.entities.getComponent(entity, "size"); 10 | game.context.strokeRect(Math.floor(position.x), Math.floor(position.y), size.width, size.height); 11 | }, "drawRectangles"); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/systems/renderer/revert-shake.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System that looks for an entity with the {@link Components.shake} and {@link Components.position} components. 3 | * After each iteration of the applyShake system, the revertShake system will move the entity back to where it started 4 | * @memberof Systems 5 | * @alias revertShake 6 | * @requires Systems.applyShake 7 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 8 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 9 | * @see [applyShake]{@link Systems.applyShake} 10 | */ 11 | module.exports = function(ecs, game) { 12 | game.entities.registerSearch("revertShakeSearch",["shake", "position"]); 13 | ecs.addEach(function revertShake(entity) { 14 | var shake = game.entities.getComponent(entity, "shake"); 15 | var position = game.entities.getComponent(entity, "position"); 16 | position.x = shake.lastPositionX; 17 | position.y = shake.lastPositionY; 18 | }, "revertShakeSearch"); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/systems/renderer/viewport-move-to-camera.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("viewport", ["camera", "position", "size"]); 3 | ecs.addEach(function viewportMoveToCamera(entity) { 4 | var position = game.entities.getComponent(entity, "position"); 5 | var size = game.entities.getComponent(entity, "size"); 6 | 7 | game.context.save(); 8 | game.context.scale(game.canvas.width / size.width, game.canvas.height / size.height); 9 | game.context.translate(-Math.floor(position.x), -Math.floor(position.y)); 10 | }, "viewport"); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/systems/renderer/viewport-reset.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | ecs.add(function viewportReset() { 3 | game.context.restore(); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/systems/simulation/advance-animations.js: -------------------------------------------------------------------------------- 1 | function setOwnPropertiesDeep(src, dest) { 2 | var props = Object.keys(src); 3 | for (var i = 0; i < props.length; i++) { 4 | var prop = props[i]; 5 | var val = src[prop]; 6 | if (typeof val === "object") { 7 | if (!dest[prop]) { 8 | dest[prop] = {}; 9 | } 10 | setOwnPropertiesDeep(val, dest[prop]); 11 | } else { 12 | dest[prop] = val; 13 | } 14 | } 15 | } 16 | 17 | function applyAnimation(entity, a, animation, entities) { 18 | a.lastName = a.name; // track the old name so we can see if it changes 19 | Object.keys(animation[a.frame].properties).forEach(function(property) { 20 | var dest = entities.getComponent(entity, property); 21 | if (!dest) { 22 | dest = entities.addComponent(entity, property); 23 | } 24 | setOwnPropertiesDeep(animation[a.frame].properties[property], dest); 25 | }); 26 | } 27 | 28 | module.exports = function(ecs, game) { 29 | game.entities.onAddComponent("animation", function(entity, component, a) { 30 | var animation = game.animations[a.name]; 31 | if (!animation) { 32 | return; 33 | } 34 | applyAnimation(entity, a, animation, game.entities); 35 | }); 36 | ecs.addEach(function advanceAnimations(entity, elapsed) { 37 | var a = game.entities.getComponent(entity, "animation"); 38 | var animation = game.animations[a.name]; 39 | if (!animation) { 40 | return; 41 | } 42 | if (a.name != a.lastName) { 43 | a.frame = 0; 44 | a.time = 0; 45 | } 46 | a.time += elapsed * a.speed; 47 | var lastFrame = a.frame; 48 | while (a.time > animation[a.frame].time) { 49 | a.time -= animation[a.frame].time; 50 | a.frame++; 51 | if (a.frame >= animation.length) { 52 | if (a.loop) { 53 | a.frame = 0; 54 | } else { 55 | a.frame--; 56 | } 57 | } 58 | } 59 | if (lastFrame != a.frame || a.name != a.lastName) { 60 | applyAnimation(entity, a, animation, game.entities); 61 | } 62 | }, "animation"); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/systems/simulation/advance-timers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System that looks for an entity with the {@link Components.timers} components. 3 | * Every frame the advanceTimers system will loop through an entity's timers component and increment the "time" property by the elapsed time since the last frame. If the timer is set to loop it will restart the time when it hits max. 4 | * @memberof Systems 5 | * @alias advanceTimers 6 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 7 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 8 | */ 9 | module.exports = function(ecs, game) { 10 | ecs.addEach(function advanceTimers(entity, elapsed) { 11 | var timers = game.entities.getComponent(entity, "timers"); 12 | var names = Object.keys(timers); 13 | 14 | names.forEach(function(name) { 15 | var timer = timers[name]; 16 | if (!timer.running) { 17 | return; 18 | } 19 | 20 | timer.time += elapsed; 21 | 22 | while (timer.time > timer.max) { 23 | if (timer.loop) { 24 | timer.time -= timer.max; 25 | } else { 26 | timer.running = false; 27 | timer.time = 0; 28 | } 29 | if (timer.script !== undefined) { 30 | var script = game.require(timer.script); 31 | script(entity, game); 32 | } 33 | } 34 | }); 35 | }, "timers"); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/systems/simulation/apply-acceleration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System that looks for an entity with the {@link Components.acceleration} and {@link Components.velocity} components. 3 | * Every frame the apply acceleration system will modify the entity's velocity by the acceleration per elapsed millisecond. 4 | * @memberof Systems 5 | * @alias applyAcceleration 6 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 7 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 8 | */ 9 | module.exports = function(ecs, game) { 10 | game.entities.registerSearch("applyAcceleration", ["acceleration", "velocity"]); 11 | ecs.addEach(function applyAcceleration(entity, elapsed) { 12 | var velocity = game.entities.getComponent(entity, "velocity"); 13 | var acceleration = game.entities.getComponent(entity, "acceleration"); 14 | velocity.x += acceleration.x * elapsed; 15 | velocity.y += acceleration.y * elapsed; 16 | }, "applyAcceleration"); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/systems/simulation/apply-easing.js: -------------------------------------------------------------------------------- 1 | var easingJS = require("easing-js"); 2 | 3 | module.exports = function(ecs, game) { 4 | ecs.addEach(function applyEasing(entity, elapsed) { 5 | var easing = game.entities.getComponent(entity, "easing"); 6 | 7 | var properties = Object.keys(easing); 8 | for (var i = 0; i < properties.length; i++) { 9 | var current = easing[properties[i]]; 10 | current.time += elapsed; 11 | easeProperty(game, entity, properties[i], current); 12 | if (current.time > current.max) { 13 | delete easing[properties[i]]; 14 | } 15 | } 16 | }, "easing"); 17 | }; 18 | 19 | function easeProperty(game, entity, property, easing) { 20 | var parts = property.split("."); 21 | var componentName = parts[0]; 22 | var component = game.entities.getComponent(entity, componentName); 23 | var partNames = parts.slice(1, parts.length, parts - 1); 24 | for (var i = 0; i < partNames.length - 1; i++) { 25 | component = component[partNames[i]]; 26 | } 27 | var last = parts[parts.length - 1]; 28 | component[last] = easingJS[easing.type](easing.time, easing.start, easing.end - easing.start, easing.max); 29 | } 30 | -------------------------------------------------------------------------------- /lib/systems/simulation/apply-friction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System that looks for an entity with the {@link Components.friction} and {@link Components.velocity} components. 3 | * Every frame the apply friction system will modify the entity's velocity by the friction. 4 | * @memberof Systems 5 | * @alias applyFriction 6 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 7 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 8 | */ 9 | module.exports = function(ecs, game) { 10 | game.entities.registerSearch("applyFriction", ["velocity", "friction"]); 11 | ecs.addEach(function applyFriction(entity) { 12 | var velocity = game.entities.getComponent(entity, "velocity"); 13 | var friction = game.entities.getComponent(entity, "friction"); 14 | velocity.x *= friction.x; 15 | velocity.y *= friction.y; 16 | }, "applyFriction"); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/systems/simulation/apply-movement-2d.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("applyMovement2d", ["velocity", "movement2d"]); 3 | ecs.addEach(function applyMovement2d(entity) { 4 | var velocity = game.entities.getComponent(entity, "velocity"); 5 | var movement2d = game.entities.getComponent(entity, "movement2d"); 6 | if (movement2d.up && velocity.y > movement2d.upMax) { 7 | velocity.y += movement2d.upAccel; 8 | } 9 | if (movement2d.down && velocity.y < movement2d.downMax) { 10 | velocity.y += movement2d.downAccel; 11 | } 12 | if (movement2d.left && velocity.x > movement2d.leftMax) { 13 | velocity.x += movement2d.leftAccel; 14 | } 15 | if (movement2d.right && velocity.x < movement2d.rightMax) { 16 | velocity.x += movement2d.rightAccel; 17 | } 18 | }, "applyMovement2d"); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/systems/simulation/apply-velocity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System that looks for an entity with the {@link Components.position} and {@link Components.velocity} components. 3 | * Every frame the apply velocity system will move the entity's position by the velocity per elapsed millisecond. 4 | * @memberof Systems 5 | * @alias applyVelocity 6 | * @see [addEach]{@link https://github.com/ericlathrop/entity-component-system#addeachsystem-search} 7 | * @see [registerSearch]{@link https://github.com/ericlathrop/entity-component-system#registersearchsearch-components} 8 | */ 9 | module.exports = function(ecs, game) { 10 | game.entities.registerSearch("applyVelocity", ["position", "velocity"]); 11 | ecs.addEach(function applyVelocity(entity, elapsed) { 12 | var position = game.entities.getComponent(entity, "position"); 13 | var velocity = game.entities.getComponent(entity, "velocity"); 14 | position.x += velocity.x * elapsed; 15 | position.y += velocity.y * elapsed; 16 | position.z += velocity.z * elapsed; 17 | }, "applyVelocity"); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/systems/simulation/box-collider.js: -------------------------------------------------------------------------------- 1 | var boxIntersect = require("box-intersect"); 2 | 3 | // This system is deprecated and will be removed in the next major version. 4 | // Use box-group-collider instead. 5 | module.exports = function(ecs, game) { 6 | 7 | game.entities.registerSearch("boxCollider", ["position", "size", "collisions"]); 8 | 9 | game.entities.onRemoveComponent("collisions", function(entity, component, collisions) { 10 | for (var i = 0; i < collisions.length; i++) { 11 | var otherCollisions = game.entities.getComponent(collisions[i], "collisions"); 12 | var idx = otherCollisions.indexOf(entity); 13 | if (idx !== -1) { 14 | otherCollisions.splice(idx, 1); 15 | } 16 | } 17 | }); 18 | 19 | var idPool = []; 20 | var boxPool = []; 21 | var boxPoolLength = 0; 22 | function growBoxPool(size) { 23 | boxPoolLength = size; 24 | while (boxPool.length < size) { 25 | for (var i = 0; i < 50; i++) { 26 | idPool.push(0); 27 | boxPool.push([0, 0, 0, 0]); 28 | } 29 | } 30 | } 31 | 32 | function handleCollision(a, b) { 33 | if (a >= boxPoolLength || b >= boxPoolLength) { 34 | return; 35 | } 36 | var idA = idPool[a]; 37 | var idB = idPool[b]; 38 | game.entities.getComponent(idA, "collisions").push(idB); 39 | game.entities.getComponent(idB, "collisions").push(idA); 40 | } 41 | 42 | ecs.add(function boxCollider() { 43 | var ids = game.entities.find("boxCollider"); 44 | 45 | growBoxPool(ids.length); 46 | 47 | for (var i = 0; i < ids.length; i++) { 48 | var entity = ids[i]; 49 | game.entities.getComponent(entity, "collisions").length = 0; 50 | var position = game.entities.getComponent(entity, "position"); 51 | var size = game.entities.getComponent(entity, "size"); 52 | idPool[i] = entity; 53 | boxPool[i][0] = position.x; 54 | boxPool[i][1] = position.y; 55 | boxPool[i][2] = position.x + size.width; 56 | boxPool[i][3] = position.y + size.height; 57 | } 58 | boxIntersect(boxPool, handleCollision); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/systems/simulation/box-group-collider.js: -------------------------------------------------------------------------------- 1 | var BoxPool = require("../../box-pool"); 2 | 3 | module.exports = function(ecs, game) { 4 | 5 | game.entities.registerSearch("boxColliderSearch", ["position", "size", "boxCollider"]); 6 | 7 | game.entities.onRemoveComponent("collisions", function(entity, component, collisions) { 8 | for (var i = 0; i < collisions.entities.length; i++) { 9 | var otherCollisions = game.entities.getComponent(collisions.entities[i], "boxCollider"); 10 | var idx = otherCollisions.entities.indexOf(entity); 11 | if (idx !== -1) { 12 | otherCollisions.entities.splice(idx, 1); 13 | } 14 | } 15 | }); 16 | 17 | var boxPoolNames = [ "unnamed" ]; 18 | var boxPools = { 19 | unnamed: new BoxPool() 20 | }; 21 | 22 | function handleCollision(a, b) { 23 | handleCollision2(game, a, b); 24 | handleCollision2(game, b, a); 25 | } 26 | 27 | ecs.add(function boxGroupCollider() { 28 | var ids = game.entities.find("boxColliderSearch"); 29 | 30 | for (var i = 0; i < boxPoolNames.length; i++) { 31 | boxPools[boxPoolNames[i]].reset(); 32 | } 33 | 34 | for (i = 0; i < ids.length; i++) { 35 | var entity = ids[i]; 36 | var collisions = game.entities.getComponent(entity, "boxCollider"); 37 | collisions.entities.length = 0; 38 | var position = game.entities.getComponent(entity, "position"); 39 | var size = game.entities.getComponent(entity, "size"); 40 | 41 | var name = collisions.group || "unnamed"; 42 | if (!boxPools[name]) { 43 | boxPoolNames.push(name); 44 | boxPools[name] = new BoxPool(); 45 | } 46 | boxPools[name].add(entity, position, size); 47 | } 48 | for (i = 0; i < boxPoolNames.length; i++) { 49 | for (var j = i + 1; j < boxPoolNames.length; j++) { 50 | var aName = boxPoolNames[i]; 51 | var bName = boxPoolNames[j]; 52 | if (areGroupsExcluded(game, aName, bName)) { 53 | continue; 54 | } 55 | 56 | var a = boxPools[aName]; 57 | var b = boxPools[bName]; 58 | if (a && b) { 59 | a.collideOther(b, handleCollision); 60 | } 61 | } 62 | } 63 | boxPools["unnamed"].collideSelf(handleCollision); 64 | for (i = 0; i < ids.length; i++) { 65 | updateLastCollisions(game, ids[i]); 66 | } 67 | }); 68 | }; 69 | 70 | function areGroupsExcluded(game, a, b) { 71 | var config = game.sceneConfig["box-group-collider"]; 72 | if (!config) { 73 | return false; 74 | } 75 | var skip = config.skip; 76 | if (!skip) { 77 | return false; 78 | } 79 | for (var i = 0; i < skip.length; i++) { 80 | if ( 81 | (skip[i][0] === a && skip[i][1] === b) || 82 | (skip[i][0] === b && skip[i][1] === a) 83 | ) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | function updateLastCollisions(game, entity) { 91 | var collisions = game.entities.getComponent(entity, "boxCollider"); 92 | 93 | if (collisions.onExit) { 94 | var currentCollisions = {}; 95 | for (var j = 0; j < collisions.entities.length; j++) { 96 | currentCollisions[collisions.entities[j]] = true; 97 | } 98 | for (var k = 0; k < collisions.last.length; k++) { 99 | if (!currentCollisions[collisions.last[k]]) { 100 | var onExit = game.require(collisions.onExit); 101 | onExit(entity, collisions.last[k], game); 102 | } 103 | } 104 | } 105 | 106 | if (collisions.last) { 107 | collisions.last.length = 0; 108 | } else { 109 | collisions.last = []; 110 | } 111 | for (var i = 0; i < collisions.entities.length; i++) { 112 | collisions.last.push(collisions.entities[i]); 113 | } 114 | } 115 | 116 | function handleCollision2(game, entity, other) { 117 | var collisions = game.entities.getComponent(entity, "boxCollider"); 118 | collisions.entities.push(other); 119 | if (collisions.onEnter && collisions.last.indexOf(other) === -1) { 120 | var onEnter = game.require(collisions.onEnter); 121 | onEnter(entity, other, game); 122 | } 123 | if (collisions.script) { 124 | var script = game.require(collisions.script); 125 | script(entity, other, game); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/systems/simulation/constrain-position.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("constrainPositionSearch", ["position", "size", "constrainPosition"]); 3 | ecs.addEach(function constrainTocontrainPosition(entity) { 4 | var position = game.entities.getComponent(entity, "position"); 5 | var size = game.entities.getComponent(entity, "size"); 6 | 7 | var constrainPosition = game.entities.getComponent(entity, "constrainPosition"); 8 | var other = constrainPosition.id; 9 | var otherPosition = game.entities.getComponent(other, "position"); 10 | var otherSize = game.entities.getComponent(other, "size"); 11 | 12 | if (position.x < otherPosition.x) { 13 | position.x = otherPosition.x; 14 | } 15 | if (position.x + size.width > otherPosition.x + otherSize.width) { 16 | position.x = otherPosition.x + otherSize.width - size.width; 17 | } 18 | if (position.y < otherPosition.y) { 19 | position.y = otherPosition.y; 20 | } 21 | if (position.y + size.height > otherPosition.y + otherSize.height) { 22 | position.y = otherPosition.y + otherSize.height - size.height; 23 | } 24 | }, "constrainPositionSearch"); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/systems/simulation/control-player.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("controlPlayer", ["movement2d", "playerController2d"]); 3 | ecs.addEach(function controlPlayer(entity) { 4 | var movement2d = game.entities.getComponent(entity, "movement2d"); 5 | var playerController2d = game.entities.getComponent(entity, "playerController2d"); 6 | movement2d.up = game.inputs.button(playerController2d.up); 7 | movement2d.down = game.inputs.button(playerController2d.down); 8 | movement2d.left = game.inputs.button(playerController2d.left); 9 | movement2d.right = game.inputs.button(playerController2d.right); 10 | }, "controlPlayer"); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/systems/simulation/decay-life-span.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | ecs.addEach(function decayLifeSpan(entity, elapsed) { 3 | var lifeSpan = game.entities.getComponent(entity, "lifeSpan"); 4 | lifeSpan.current += elapsed; 5 | if (lifeSpan.current >= lifeSpan.max) { 6 | game.entities.destroy(entity); 7 | } 8 | }, "lifeSpan"); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/systems/simulation/follow-mouse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(ecs, game) { // eslint-disable-line no-unused-vars 4 | ecs.addEach(function(entity, elapsed) { // eslint-disable-line no-unused-vars 5 | var position = game.entities.getComponent(entity, "position"); 6 | var camera = game.entities.find("camera")[0]; 7 | var cameraPosition = game.entities.getComponent(camera, "position"); 8 | position.x = cameraPosition.x + game.inputs.mouse.x; 9 | position.y = cameraPosition.y + game.inputs.mouse.y; 10 | }, "followMouse"); 11 | }; 12 | -------------------------------------------------------------------------------- /lib/systems/simulation/follow-parent.js: -------------------------------------------------------------------------------- 1 | var distanceSquared = require("../../math2d").distanceSquared; 2 | 3 | module.exports = function(ecs, game) { 4 | game.entities.registerSearch("followParent", ["position", "size", "follow"]); 5 | ecs.addEach(function followParent(entity) { 6 | var position = game.entities.getComponent(entity, "position"); 7 | var follow = game.entities.getComponent(entity, "follow"); 8 | var size = game.entities.getComponent(entity, "size"); 9 | 10 | var x1 = position.x + (size.width / 2); 11 | var y1 = position.y + (size.height / 2); 12 | 13 | var parent = follow.id; 14 | if (game.entities.getComponent(parent, "id") === undefined) { 15 | return; 16 | } 17 | var parentPosition = game.entities.getComponent(parent, "position"); 18 | var parentSize = game.entities.getComponent(parent, "size"); 19 | 20 | var x2 = parentPosition.x + (parentSize.width / 2); 21 | var y2 = parentPosition.y + (parentSize.height / 2); 22 | 23 | var angle = Math.atan2(y2 - y1, x2 - x1); 24 | var rotation = game.entities.getComponent(entity, "rotation"); 25 | if (rotation !== undefined) { 26 | rotation.angle = angle - (Math.PI / 2); 27 | } 28 | 29 | var distSquared = distanceSquared(x1, y1, x2, y2); 30 | if (distSquared < follow.distance * follow.distance) { 31 | return; 32 | } 33 | 34 | var toMove = Math.sqrt(distSquared) - follow.distance; 35 | 36 | position.x += toMove * Math.cos(angle); 37 | position.y += toMove * Math.sin(angle); 38 | }, "followParent"); 39 | }; 40 | -------------------------------------------------------------------------------- /lib/systems/simulation/match-aspect-ratio.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("matchAspectRatioSearch", ["matchAspectRatio", "size"]); 3 | ecs.addEach(function matchCanvasSize(entity) { 4 | var size = game.entities.getComponent(entity, "size"); 5 | 6 | var match = game.entities.getComponent(entity, "matchAspectRatio").id; 7 | var matchSize = game.entities.getComponent(match, "size"); 8 | if (matchSize === undefined) { 9 | return; 10 | } 11 | 12 | var matchAspectRatio = matchSize.width / matchSize.height; 13 | 14 | var currentAspectRatio = size.width / size.height; 15 | if (currentAspectRatio > matchAspectRatio) { 16 | size.height = Math.floor(size.width / matchAspectRatio); 17 | } else if (currentAspectRatio < matchAspectRatio) { 18 | size.width = Math.floor(size.height * matchAspectRatio); 19 | } 20 | }, "matchAspectRatioSearch"); 21 | }; 22 | -------------------------------------------------------------------------------- /lib/systems/simulation/match-canvas-size.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | ecs.addEach(function matchCanvasSize(entity) { 3 | var size = game.entities.addComponent(entity, "size"); 4 | size.width = game.canvas.width; 5 | size.height = game.canvas.height; 6 | }, "matchCanvasSize"); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/systems/simulation/match-center.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("matchCenterXSearch", ["matchCenter", "size", "position"]); 3 | ecs.addEach(function matchCenterX(entity) { 4 | var position = game.entities.getComponent(entity, "position"); 5 | var size = game.entities.getComponent(entity, "size"); 6 | 7 | var matchCenter = game.entities.getComponent(entity, "matchCenter"); 8 | 9 | var idX = matchCenter.x; 10 | if (idX === undefined) { 11 | idX = matchCenter.id; 12 | } 13 | if (idX !== undefined) { 14 | verifyTarget(game, idX, adjustX, position, size); 15 | } 16 | 17 | var idY = matchCenter.y; 18 | if (idY === undefined) { 19 | idY = matchCenter.id; 20 | } 21 | if (idY !== undefined) { 22 | verifyTarget(game, idY, adjustY, position, size); 23 | } 24 | }, "matchCenterXSearch"); 25 | }; 26 | 27 | function verifyTarget(game, target, fn, position, size) { 28 | var matchPosition = game.entities.getComponent(target, "position"); 29 | if (matchPosition === undefined) { 30 | return; 31 | } 32 | var matchSize = game.entities.getComponent(target, "size"); 33 | if (matchSize === undefined) { 34 | return; 35 | } 36 | 37 | fn(position, size, matchPosition, matchSize); 38 | } 39 | 40 | function adjustX(position, size, matchPosition, matchSize) { 41 | position.x = matchPosition.x + (matchSize.width / 2) - (size.width / 2); 42 | } 43 | 44 | function adjustY(position, size, matchPosition, matchSize) { 45 | position.y = matchPosition.y + (matchSize.height / 2) - (size.height / 2); 46 | } 47 | -------------------------------------------------------------------------------- /lib/systems/simulation/match-parent.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("matchParent", ["position", "match"]); 3 | ecs.addEach(function matchParent(entity) { 4 | var match = game.entities.getComponent(entity, "match"); 5 | 6 | var parentPosition = game.entities.getComponent(match.id, "position"); 7 | if (parentPosition === undefined) { 8 | return; 9 | } 10 | 11 | var position = game.entities.addComponent(entity, "position"); 12 | position.x = parentPosition.x + match.offsetX; 13 | position.y = parentPosition.y + match.offsetY; 14 | position.z = parentPosition.z + match.offsetZ; 15 | }, "matchParent"); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/systems/simulation/set-virtual-buttons.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ecs, game) { 2 | game.entities.registerSearch("setVirtualButtons", ["virtualButton", "position", "size"]); 3 | ecs.addEach(function setVirtualButtons(entity) { 4 | var virtualButton = game.entities.getComponent(entity, "virtualButton"); 5 | var position = game.entities.getComponent(entity, "position"); 6 | var size = game.entities.getComponent(entity, "size"); 7 | 8 | var camera = game.entities.find("camera")[0]; 9 | var cameraPosition = { x: 0, y: 0 }; 10 | if (camera !== undefined) { 11 | cameraPosition = game.entities.getComponent(camera, "position"); 12 | } 13 | 14 | for (var i = 0; i < game.inputs.mouse.touches.length; i++) { 15 | var t = game.inputs.mouse.touches[i]; 16 | var tx = t.x + cameraPosition.x; 17 | var ty = t.y + cameraPosition.y; 18 | if (tx >= position.x && tx < position.x + size.width && ty >= position.y && ty < position.y + size.height) { 19 | game.inputs.setButton(virtualButton, entity, true); 20 | return true; 21 | } 22 | } 23 | game.inputs.setButton(virtualButton, entity, false); 24 | }, "setVirtualButtons"); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splat-ecs", 3 | "version": "7.6.0", 4 | "description": "A 2D HTML5 Canvas game engine", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "lint-js": "eslint lib", 8 | "docs": "jsdoc lib -c jsdoc/conf.json -d docs -r README.md", 9 | "build": "npm run lint-js && npm run docs" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/SplatJS/splat-ecs.git" 14 | }, 15 | "keywords": [ 16 | "html5", 17 | "canvas", 18 | "game", 19 | "browser" 20 | ], 21 | "author": "Eric Lathrop (http://ericlathrop.com)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/SplatJS/splat-ecs/issues" 25 | }, 26 | "homepage": "https://github.com/SplatJS/splat-ecs", 27 | "dependencies": { 28 | "box-intersect": "^1.0.1", 29 | "easing-js": "^1.0.1", 30 | "entity-component-system": "^4.0.4", 31 | "game-keyboard": "0.1.0", 32 | "html5-gamepad": "^1.0.0", 33 | "mersenne-twister": "^1.1.0", 34 | "pako": "^1.0.3", 35 | "time-accumulator": "0.0.0" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^3.3.1", 39 | "jsdoc": "^3.4.0" 40 | } 41 | } 42 | --------------------------------------------------------------------------------