├── .eslintrc.json ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── dist ├── GridPhysics.js └── GridPhysics.min.js ├── example ├── assets │ ├── images │ │ ├── basictiles.png │ │ └── hero.png │ ├── maps │ │ └── demo.json │ ├── spriteatlas │ │ ├── sprites.json │ │ └── sprites.png │ └── styles │ │ └── main.css ├── index.html └── js │ ├── dat.gui.controls.js │ ├── dat.gui.min.js │ ├── debugGUI.js │ ├── main.js │ └── scenes │ └── game.js ├── package-lock.json ├── package.json ├── src ├── gridBody.js ├── main.js ├── pathfinding │ ├── astar.js │ └── binaryHeap.js ├── tilemap.js └── world.js ├── stuffedAway └── stuff.js ├── webpack.config.js └── webpack.demo.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "rules": { 10 | "semi": 2 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | .cache 16 | .project 17 | .settings 18 | .tmproj 19 | nbproject 20 | Thumbs.db 21 | 22 | # NPM packages folder. 23 | node_modules/ 24 | 25 | # Brunch folder for temporary files. 26 | tmp/ 27 | 28 | # Brunch output folder. 29 | public/ 30 | 31 | # Bower stuff. 32 | bower_components/ 33 | 34 | # You'll have to build the demo yourself 35 | dev/ 36 | 37 | analytics.js 38 | 39 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 20 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 21 | "plusplus" : false, // true: Prohibit use of `++` and `--` 22 | "quotmark" : false, // Quotation mark consistency: 23 | // false : do nothing (default) 24 | // true : ensure whatever is used is consistent 25 | // "single" : require single quotes 26 | // "double" : require double quotes 27 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 28 | "unused" : true, // Unused variables: 29 | // true : all variables, last function parameter 30 | // "vars" : all variables only 31 | // "strict" : all variables, all function parameters 32 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 33 | "maxparams" : false, // {int} Max number of formal params allowed per function 34 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 35 | "maxstatements" : false, // {int} Max number statements per function 36 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 37 | "maxlen" : false, // {int} Max number of characters per line 38 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 39 | 40 | // Relaxing 41 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 42 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 43 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 44 | "eqnull" : false, // true: Tolerate use of `== null` 45 | "esversion" : 6, // {int} Specify the ECMAScript version to which the code must adhere. 46 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 47 | // (ex: `for each`, multiple try/catch, function expression…) 48 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 49 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 50 | "funcscope" : false, // true: Tolerate defining variables inside control statements 51 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 52 | "iterator" : false, // true: Tolerate using the `__iterator__` property 53 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 54 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 55 | "laxcomma" : false, // true: Tolerate comma-first style coding 56 | "loopfunc" : false, // true: Tolerate functions being defined in loops 57 | "multistr" : false, // true: Tolerate multi-line strings 58 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 59 | "notypeof" : false, // true: Tolerate invalid typeof operator values 60 | "proto" : false, // true: Tolerate using the `__proto__` property 61 | "scripturl" : false, // true: Tolerate script-targeted URLs 62 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 63 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 64 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 65 | "validthis" : false, // true: Tolerate using this in a non-constructor function 66 | 67 | // Environments 68 | "browser" : true, // Web Browser (window, document, etc) 69 | "browserify" : false, // Browserify (node.js code in the browser) 70 | "couch" : false, // CouchDB 71 | "devel" : true, // Development/debugging (alert, confirm, etc) 72 | "dojo" : false, // Dojo Toolkit 73 | "jasmine" : false, // Jasmine 74 | "jquery" : false, // jQuery 75 | "mocha" : true, // Mocha 76 | "mootools" : false, // MooTools 77 | "node" : false, // Node.js 78 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 79 | "phantom" : false, // PhantomJS 80 | "prototypejs" : false, // Prototype and Scriptaculous 81 | "qunit" : false, // QUnit 82 | "rhino" : false, // Rhino 83 | "shelljs" : false, // ShellJS 84 | "typed" : false, // Globals for typed array constructions 85 | "worker" : false, // Web Workers 86 | "wsh" : false, // Windows Scripting Host 87 | "yui" : false, // Yahoo User Interface 88 | 89 | // Custom Globals 90 | "globals" : { "phaser": true, "Phaser": true } // additional predefined global variables 91 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Richard Davey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grid Physics for Phaser 2 | 3 | The Grid Physics plugin for Phaser is to Arcade physics what Arcade physics is to Matter.js. :-) The plugin adds support for grid/tile based movement along with some other stuff to make your life easier doing games with grid restricted movement. My aim is to build the API as close as possible to Arcade physics. 4 | 5 | The plugin is being developed and there are both bugs and missing features. I'm making this to use it in a RPG project. That means that I'll prioritize those parts I need and that bugs that isn't an issue for my own game isn't attended. One example is that objects migth fly away if they are pushed by severeral bodies at once. If you run into issues that you would like addressed, please open an issue here. 6 | 7 | ![Walking on bridge](http://metroid.niklasberg.se/P3GridPhysics/gifs/bridge.gif) 8 | 9 | Walking on a bridge and colliding with tiles with collidable borders. 10 | 11 | ![One way collision](http://metroid.niklasberg.se/P3GridPhysics/gifs/oneway.gif) 12 | 13 | Tested one-way collision, and got stuck. 14 | 15 | ![Push and turn-based](http://metroid.niklasberg.se/P3GridPhysics/gifs/pushturnbased.gif) 16 | 17 | Turn-based mode with smooth animations plus pushing multiple bodies. 18 | 19 | **WARNING: Current state of the plugin** I've done the most basic port from Phaser 2 to Phaser 3. The source is a bit messy but it's a start and enough to run my example demo. Parts of the plugin isn't prepared for general use and contain shortcuts to get the demo running. These shortcuts is all related to the tilemap implementation: A 8x8 pixel grid with 16x16 pixel tiles is required to get all features working, probably also a maximum height of two levels. My next focus will be pathfinding and then cleaning up stuff to make it better prepared for general use. 20 | 21 | **Setup instructions** is in the end of this file. 22 | 23 | **Demo** Current version is hosted here: http://metroid.niklasberg.se/P3GridPhysics/ 24 | (The Phaser 2 demo with pathfinding support not yet added to the Phaser 3 version: http://metroid.niklasberg.se/gridPhysics/) 25 | 26 | **Discuss** Questions, feedback, requests or and other stuff related to the plugin: http://www.html5gamedevs.com/topic/37156-grid-physics-plugin-for-phaser-3/ 27 | 28 | ## Credits 29 | 30 | - Tileset and sprites: Dawnlike by DragonDePlatino, DawnBringer. http://opengameart.org/content/dawnlike-16x16-universal-rogue-like-tileset-v181 31 | - Hero sprite by Gazer: http://opengameart.org/content/overhead-action-rpg-characters 32 | - I don't remember the source for the box graphics. 33 | 34 | ## Installation / Serve / build 35 | 36 | Run `npm i` to install dependencies. Then you can run `npm run serve` to start the demo or `npm run build` to build the plugin. 37 | 38 | ## This plugin vs a quick custom solution 39 | 40 | Grid based movement isn't very complicated (Move your sprite one step to the right: "sprite.x+=gridUnitWidth"). The purpose with this plugin is to do this in a stable way, allowing animations without resorting to tweens, collision detection, one-way-tiles, movable objects, turn-based time, path finding and loads of methods and properties tied to the grid physics body. It will be an asset for anyone doing an 8-bit style RPG, puzzle games (like "The adventures of Lolo" on NES or Sokoban), retro action rpg (like "Zelda" on NES), rouge like games, retro platformers (like Castlequest on MSX (minus gravity for now)) strategy and board games. 41 | 42 | ## Features 43 | 44 | - Any grid size (not necessarily squares, i.e. 8x16 is possible, and not restricted to sprite or tile sizes). 45 | - _Tilemap:_ 46 | - Tile dimensions may differ from grid dimensions (but needs to be multiple of the grid dimensions, like 16x16 tiles on an 8x8 grid). 47 | - Tile collisions: normal, on specified directions only and against leaving tile in any direction. 48 | - _Bodies (sprites):_ 49 | - Body size is not restricted to grid-size, and different body sizes may co-exist (but must be equal or a multiple of grid dimensions. The sprite graphics may differ from body size, like an 18x18 sprite with an 16x16 pixel body.) 50 | - Moveable objects (can be chained, i.e. the player push one crate against another crate that will also move). 51 | - Mass (and strength that limit total mass that can be pushed by the power of one sprite) 52 | - Velocity, ("struggle" property that can slow down a body based on mass pushed) 53 | - Populated properties like isMoving.x (boolean) or isBlocked.top (boolean). 54 | 55 | ## Upcoming features 56 | 57 | - Turn-based or real-time. 58 | - Visual debugging 59 | - Collision callbacks. 60 | - Path finding (hopefully without external dependencies) 61 | 62 | ## Future features 63 | 64 | Depending on the interest I may add extra features. Some of the features is quick-fixes that I just typed down to remember them, others will be a bit more challenging. Possible ideas in no particular order: 65 | 66 | - _Tilemap:_ 67 | - Ground types: Slippery, slowdown (factor), moving 68 | - getPath(x,y,x2,y2) 69 | - noExitTop (etc) - As collide but prevent from leaving a tile in a direction. 70 | - _Body:_ 71 | - setGridPosition 72 | - Gestures: Jump, Shake etc. 73 | - movement.slide - Slide around corners to prevent getting stuck (pressing up with a character with the top right part of the body blocked will result in sliding to the left if it would be possible to walk up in the next step) 74 | - movement.ZeldaNESMovement - "Free movement" that adjusts the body to a the grid. 75 | - movement.turn - Allow to turn back in the opposite direction before finishing the ongoing move. 76 | - movement.margin - Allow new turn when the margin in pixels is left of the last one (default = 0) 77 | - Callback on collision to bodies and tiles, possibility to cancel collision. 78 | - standingOn - An array of tiles the body is standing on 79 | - blocked.dir 80 | - pushque.dir (oavsett om orkar) 81 | - Particle collision 82 | - Arcade-like movements (bullets, random vector) 83 | - Moving platforms 84 | - Drag 85 | - Magnetism 86 | - Conjoined bodies 87 | - Gravity.x/y 88 | - Pause 89 | - MoveCountToXY - numbers of moves necessarily (using pathfinding) 90 | - JumpToXY - Move to XY without collision detection 91 | - NextPossible - Move to next free space in given direction. 92 | - Body.onCollide, stopVelocityOnCollide 93 | - Support for non-linear velocities (like tweens) 94 | - Properties: blocked.left/right/up/down, isMoving, isPushing, massPushed 95 | - Trying to go x,y simulanously - If x wont work, try y. 96 | - forcedMovement (being pushed overrides the the desired velocioty of the object) 97 | - collidingBodies - List of colliding bodies, for a complete list, make sure collectAllCollidingBodies is set to true 98 | - collectAllCollidingBodies - If set to false the collision check will stop as soon as it's established that the body is blocked. If set to true it will check for all possible bodies for collision which require more resources. 99 | -------------------------------------------------------------------------------- /dist/GridPhysics.min.js: -------------------------------------------------------------------------------- 1 | !function(i,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("GridPhysics",[],t):"object"==typeof exports?exports.GridPhysics=t():i.GridPhysics=t()}(window,function(){return function(i){var t={};function e(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return i[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}return e.m=i,e.c=t,e.d=function(i,t,o){e.o(i,t)||Object.defineProperty(i,t,{configurable:!1,enumerable:!0,get:o})},e.r=function(i){Object.defineProperty(i,"__esModule",{value:!0})},e.n=function(i){var t=i&&i.__esModule?function(){return i.default}:function(){return i};return e.d(t,"a",t),t},e.o=function(i,t){return Object.prototype.hasOwnProperty.call(i,t)},e.p="",e(e.s=3)}([function(i,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function i(i,t){for(var e=0;e1&&void 0!==arguments[1]?arguments[1]:0,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=(arguments.length>3&&void 0!==arguments[3]?arguments[3]:this.world.tilemaplayers,arguments.length>4&&void 0!==arguments[4]&&arguments[4]),r=void 0,s=void 0,l=void 0,n=void 0,h=void 0,a=void 0,d=void 0;if(i.hasOwnProperty("body")?(r={x:i.body.gridPosition.x,y:i.body.gridPosition.y},s=i.body.width,l=i.body.height,n=i.body.collideWorldBounds,a=i.body.level,d=i.body.collisionCallback.tile):(r={x:i.x,y:i.y},s=i.width?i.width:1,l=i.height?i.height:1,n=!!i.hasOwnProperty("collideWorldBounds")&&i.collideWorldBounds,h=!0,a=i.level?i.level:0,d=i.collisionCallback?i.body.collisionCallback.tile:null),n&&(r.x+t<0||r.y+e<0||r.x+t+s>this.world.tilemaplayers[0].width/this.world.gridSize.x||r.y+e+l>this.world.tilemaplayers[0].height/this.world.gridSize.y))return!0;r.x+=t,r.y+=e,0!==t?(t>0&&(r.x+=s-1),s=1):0!==e&&(e>0&&(r.y+=l-1),l=1);for(var c=r.x;cw.level)){var b=w.layer.baseTileHeight,P=w.layer.baseTileHeight,m=Math.floor(y*this.world.gridSize.y/b),S=void 0;if(m<0||m>w.layer.data.length-1){if(this.collideWorldBounds)return!0}else if((S=Math.floor(c*this.world.gridSize.x/P))<0||m>w.layer.data[m].length-1){if(this.collideWorldBounds)return!0}else{var k=w.layer.data[m][S]||null;if(k&&-1===k.index&&w.level>0&&a>0)return console.log(w.level),!0;if(h&&console.log(k,S,m),null!==k&&(-1!==k.index||k.gotBorder)){var T=d?d(k):k;if(T.collideRight&&T.collideLeft&&T.collideDown&&T.collideUp){u=!0;break}if(t<0&&T.collideRight){u=!0;break}if(t>0&&T.collideLeft){u=!0;break}if(e<0&&T.collideDown){u=!0;break}if(e>0&&T.collideUp){u=!0;break}}}}}}catch(i){f=!0,v=i}finally{try{!g&&x.return&&x.return()}finally{if(f)throw v}}if(u){if(o){if(0!==t){if(!this.collide(i,t,e-1))return{dx:t,dy:e-1};if(!this.collide(i,t,e+1))return{dx:t,dy:e+1}}if(0!==e){if(!this.collide(i,t-1,e))return{dx:t-1,dy:e};if(!this.collide(i,t+1,e))return{dx:t+1,dy:e}}}return{dx:0,dy:0}}}return!1}},{key:"getTilesUnderBody",value:function(i){return console.log("Check",i.gridPosition.x,i.gridPosition.y,i.width,i.height),this.getTilesAt(i.gridPosition.x,i.gridPosition.y,i.width,i.height)}},{key:"getTilesAt",value:function(i,t,e,o){var r=[],s=Math.floor(i/2);s=s<0?0:s;var l=Math.floor(t/2);l=l<0?0:l;var n=Math.floor((i+e)/2);n=n>this.world.tilemaplayers[0].tilemap.width?this.world.tilemaplayers[0].tilemap.width:n;var h=Math.floor(t+o)/2;return h=h>this.world.tilemaplayers[0].tilemap.height?this.world.tilemaplayers[0].tilemap.height:h,console.log("scan x="+s+" to "+n+" amd y="+l+" to "+h),this.world.tilemaplayers.forEach(function(i,t){r[t]=[];for(var e=s;e0?o.x=o.x+r:e>0&&(o.y=o.y+s);for(var n=o.x;n0&&g.level>l&&(l=g.level)}}catch(i){d=!0,c=i}finally{try{!a&&u.return&&u.return()}finally{if(d)throw c}}}return console.log("LEVEL",l),l}}]),i}();t.default=r},function(i,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function i(i,t){for(var e=0;e0&&void 0!==arguments[0]?arguments[0]:0,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;this.gridPosition.x=i,this.gridPosition.y=t,this.sprite.x=this.world.gridSize.x*this.gridPosition.x,this.sprite.y=this.world.gridSize.y*this.gridPosition.y,this.isLocked.x=!1,this.isLocked.y=!1}},{key:"setVelocity",value:function(i){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;t=null!==t?t:i,0===i&&0!==t?this._longpress=1:0===t&&0!==i&&(this._longpress=0),this._desiredVelocity={x:i,y:t}}},{key:"_intersectRect",value:function(i,t){return!(t.x>=i.x+i.width||t.x+t.width<=i.x||t.y>=i.y+i.height||t.y+t.height<=i.y)}},{key:"getTilesUnderBody",value:function(){return console.log("check",this),this.tilemap.getTilesUnderBody(this)}},{key:"testMove",value:function(i,t){var e=!0;if(!this.collidable)return!0;if(this.onStairs&&(this.level=this.tilemap.checkLevel(this.sprite,i,t)),this.tilemap.collide(this.sprite,i,t)&&this.solid)return!1;if(!this.collectCollidingBodies&&!e)return!1;var o=!0,r=!1,s=void 0;try{for(var l,n=this.world.bodies[Symbol.iterator]();!(o=(l=n.next()).done);o=!0){var h=l.value;if(this!==h&&h.sprite.active&&(h.collidable&&(h.level===this.level||h.onStairs))){if(!e&&!this.collectCollidingBodies&&!h.collectCollidingBodies)continue;if(this._intersectRect({x:this.gridPosition.x+i,y:this.gridPosition.y+t,width:this.width,height:this.height},{x:h.gridPosition.x,y:h.gridPosition.y,width:h.width,height:h.height})){this.collectCollidingBodies&&-1===this.collidingBodies.indexOf(h)&&this.collidingBodies.push(h),h.collectCollidingBodies&&-1===h.collidingBodies.indexOf(this)&&h.collidingBodies.push(this);var a=this.collisionCallback.body?this.collisionCallback.body(h,this):h.solid;if(!this.solid||!a)continue;h.immovable?e=!1:-1===this.world._pushChain.indexOf(h)&&(this.world._pushChain.push(h),h.testMove(i,t)||(e=!1))}}}}catch(i){r=!0,s=i}finally{try{!o&&n.return&&n.return()}finally{if(r)throw s}}return e}},{key:"postUpdate",value:function(){if(this.collidingBodies=[],this.justMoved=!1,!this.isLocked.x&&!this.isLocked.y){if(this.moveTo.active){this._desiredVelocity={x:0,y:0};var i=this.moveTo.path[this.moveTo.next];i.xthis.gridPosition.x?this._desiredVelocity.x=100:i.ythis.gridPosition.y&&(this._desiredVelocity.y=100),this.moveTo.next++,this.moveTo.next>this.moveTo.path.length-1&&(this.moveTo.active=!1)}if(this.isMoving={x:!1,y:!1,any:!1},0===this._desiredVelocity.x&&0===this._desiredVelocity.y)return this.velocity.x=0,this.velocity.y=0,void(this.struggling=!1);this.world._pushChain=[];for(var t=!0,e={},o=["x","y"],r=0;r0?1:-1}for(var l=[0,1],n=0;n-1||this.struggle>1){var h=0,a=!0,d=!1,c=void 0;try{for(var y,u=this.world._pushChain[Symbol.iterator]();!(a=(y=u.next()).done);a=!0){h+=y.value.mass}}catch(i){d=!0,c=i}finally{try{!a&&u.return&&u.return()}finally{if(d)throw c}}this.strength>-1&&h>this.strength&&(t=!1),this.struggle>1&&h>0&&(this._desiredVelocity.x=this._desiredVelocity.x/(h*this.struggle),this._desiredVelocity.y=this._desiredVelocity.y/(h*this.struggle))}if(this.pushLimit>-1&&this.world._pushChain.length>this.pushLimit&&(t=!1),!t)return 0===this._desiredVelocity.x&&0===this._desiredVelocity.y||(this.struggling=!0),this._desiredVelocity.x>0?this.facing=Phaser.RIGHT:this._desiredVelocity.x<0?this.facing=Phaser.LEFT:this._desiredVelocity.y<0?this.facing=Phaser.UP:this._desiredVelocity.y>0&&(this.facing=Phaser.DOWN),this.velocity.x=0,this.velocity.y=0,this._shadow.x=0,void(this._shadow.y=0);for(var g=["x","y"],f=0;f0?1:-1,this.isLocked[v]=!0,this._shadow[v]=this.velocity[v]<0?1:-1;var p=!0,x=!1,w=void 0;try{for(var b,P=this.world._pushChain[Symbol.iterator]();!(p=(b=P.next()).done);p=!0){var m=b.value;m.passiveSteps++,m.snapToGrid(),m.velocity[v]=this._desiredVelocity[v],m.gridPosition[v]+=m.velocity[v]>0?1:-1,m.isLocked[v]=!0,m.onStairs=this.checkStairs(m)}}catch(i){x=!0,w=i}finally{try{!p&&P.return&&P.return()}finally{if(x)throw w}}}}this.velocity.x>0?this.facing=Phaser.RIGHT:this.velocity.x<0?this.facing=Phaser.LEFT:this.velocity.y<0?this.facing=Phaser.UP:this.velocity.y>0&&(this.facing=Phaser.DOWN),0!==this.velocity.x&&(this.isMoving.x=!0),0!==this.velocity.y&&(this.isMoving.y=!0),this.isMoving.any=!0,this.struggling=!1,this.baseVelocity=75,this.activeSteps++,this.justMoved=!0;var S=this.onStairs;this.onStairs=this.checkStairs(this),S&&!this.onStairs&&(1===this.level?this.sprite.setDepth(11):this.sprite.setDepth(1))}}},{key:"checkStairs",value:function(i){var t=i.gridPosition,e=!0,o=!1,r=void 0;try{for(var s,l=this.world.stairs[Symbol.iterator]();!(e=(s=l.next()).done);e=!0){var n=s.value;if(t.xn.x&&t.yn.y)return i.sprite.setDepth(1e3),!0}}catch(i){o=!0,r=i}finally{try{!e&&l.return&&l.return()}finally{if(o)throw r}}return!1}},{key:"drawDebug",value:function(i){var t=this.gridPosition.x*this.world.gridSize.x,e=this.gridPosition.y*this.world.gridSize.y,o=this.width*this.world.gridSize.x,r=this.height*this.world.gridSize.y;i.lineStyle(1,"0xFFFFFF"),this.sprite.active||i.lineStyle(1,"0xFF0000"),i.strokeRect(t,e,o,r)}}]),i}();t.default=r},function(i,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o,r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},s=function(){function i(i,t){for(var e=0;e1&&void 0!==arguments[1]?arguments[1]:0;if(!i.hasOwnProperty("myTurn"))if(i.hasOwnProperty("body"))i=i.body;else{if("object"===(void 0===i?"undefined":r(i))&&i.length>0)return void i.forEach(function(i){t.addToQue(i,e)});console.error("You need to pass a sprite with gridBody, gridBody or an array to addToQue.")}if(this.firstInLine=this.firstInLine?this.firstInLine:i.sprite,this.firstInLine.body.myTurn=!0,0===e&&(e=i.reload>0?i.reload:1),this.que.length<2*e)for(var o=0;o<2*e;o++)this.que.push(null);for(var s=2*e;null!=this.que[s];)s++;for(this.que.splice(s,0,i);null===this.que[0]&&this.que.length>0;)this.que.shift(0)}},{key:"nextTurn",value:function(){var i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=null;for(this.addToQue(this.firstInLine,i),this.firstInLine.body.myTurn=!1,this.firstInLine.body.turn++,this.turn++,this.que.shift(0);null===this.que[0]&&this.que.length>0;)this.que.shift(0);0!==this.que.length?((t=this.que[0]).myTurn=!0,this.firstInLine=t.sprite):console.error("EMPTY QUE!")}},{key:"update",value:function(i,t){this.turnMade=!1;var e=t/1e3,o=!0,r=!1,s=void 0;try{for(var l,n=this.bodies[Symbol.iterator]();!(o=(l=n.next()).done);o=!0){var h=l.value;if(h.sprite.active)for(var a={x:h.sprite.x,y:h.sprite.y},d=["x","y"],c=0;c0&&a[y]>h.gridPosition[y]*h.world.gridSize[y]&&(h.isLocked[y]=!1),h.velocity[y]<0&&a[y] 2 | 3 | 4 | 5 | 6 | Grid Physics Example 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/js/dat.gui.controls.js: -------------------------------------------------------------------------------- 1 | 2 | function startGui(plugin) { 3 | var settings = { 4 | active: true, 5 | rate: 1, 6 | waterRate: 1, 7 | lavaRate: 1, 8 | resetRates: function () { 9 | plugin.resetRates(); 10 | settings.rate = 1; 11 | settings.waterRate = 1; 12 | settings.lavaRate = 1; 13 | settings.leftMap.rate = 1; 14 | settings.leftMap.waterRate = 1; 15 | settings.leftMap.lavaRate = 1; 16 | settings.rightMap.rate = 1; 17 | settings.rightMap.waterRate = 1; 18 | fixGuiValues(); 19 | }, 20 | leftMap: { 21 | active: true, 22 | active0: true, 23 | active1: true, 24 | rate: 1, 25 | waterRate: 1, 26 | lavaRate: 1, 27 | resetRates: function () { 28 | plugin.resetRates(0); 29 | settings.leftMap.rate = 1; 30 | settings.leftMap.waterRate = 1; 31 | settings.leftMap.lavaRate = 1; 32 | fixGuiValues(); 33 | }, 34 | }, 35 | rightMap: { 36 | active: true, 37 | waterRate: 1, 38 | } 39 | }; 40 | 41 | var fixGuiValues = function(){ 42 | gui.__folders["Global"].__controllers[1].updateDisplay(); 43 | gui.__folders["Global"].__controllers[2].updateDisplay(); 44 | gui.__folders["Global"].__controllers[3].updateDisplay(); 45 | gui.__folders["Left map"].__controllers[3].updateDisplay(); 46 | gui.__folders["Left map"].__controllers[4].updateDisplay(); 47 | gui.__folders["Left map"].__controllers[5].updateDisplay(); 48 | gui.__folders["Right map"].__controllers[1].updateDisplay(); 49 | 50 | } 51 | 52 | 53 | var gui = new dat.GUI(); 54 | 55 | /// GLOBAL 56 | var folder = gui.addFolder('Global'); 57 | f = folder.add(settings, 'active'); 58 | f.onChange(function (value) { 59 | if (value) { 60 | plugin.resume(); 61 | } 62 | else { 63 | plugin.pause(); 64 | } 65 | }); 66 | f = folder.add(settings, 'rate', 0, 5); 67 | f.onChange(function (value) { 68 | plugin.setRate(value); 69 | }); 70 | f = folder.add(settings, 'waterRate', 0, 5); 71 | f.onChange(function (value) { 72 | plugin.setRate(value, 1384); 73 | }); 74 | f = folder.add(settings, 'lavaRate', 0, 5); 75 | f.onChange(function (value) { 76 | plugin.setRate(value, 1412); 77 | }); 78 | folder.add(settings, 'resetRates'); 79 | 80 | folder.open(); 81 | 82 | /// LEFT MAP 83 | var leftMap = gui.addFolder('Left map'); 84 | f = leftMap.add(settings.leftMap, 'active'); 85 | f.onChange(function (value) { 86 | if (value) { 87 | plugin.resume(null,0); 88 | } 89 | else { 90 | plugin.pause(null,0); 91 | } 92 | }); 93 | f = leftMap.add(settings.leftMap, 'active0').name('Bottom layer'); 94 | f.onChange(function (value) { 95 | if (value) { 96 | plugin.resume(0,0); 97 | } 98 | else { 99 | plugin.pause(0,0); 100 | } 101 | }); 102 | f = leftMap.add(settings.leftMap, 'active1').name('Top layer'); 103 | f.onChange(function (value) { 104 | if (value) { 105 | plugin.resume(1,0); 106 | } 107 | else { 108 | plugin.pause(1,0); 109 | } 110 | }); 111 | f = leftMap.add(settings.leftMap, 'rate', 0, 5); 112 | f.onChange(function (value) { 113 | plugin.setRate(value, null, 0); 114 | }); 115 | f = leftMap.add(settings.leftMap, 'waterRate', 0, 5); 116 | f.onChange(function (value) { 117 | plugin.setRate(value, 1384, 0); 118 | }); 119 | f = leftMap.add(settings.leftMap, 'lavaRate', 0, 5); 120 | f.onChange(function (value) { 121 | plugin.setRate(value, 1412, 0); 122 | }); 123 | leftMap.add(settings.leftMap, 'resetRates'); 124 | /*leftMap.add(settings.leftMap, 'resetRates'); 125 | f = leftMap.add(settings.leftMap, 'active0').name('Bottom layer'); 126 | f.onChange(function (value) { 127 | if (value) { 128 | plugin.resume(0); 129 | } 130 | else { 131 | plugin.pause(0); 132 | } 133 | }); 134 | f = leftMap.add(settings.leftMap, 'active1').name('Top layer'); 135 | f.onChange(function (value) { 136 | if (value) { 137 | plugin.resume(1); 138 | } 139 | else { 140 | plugin.pause(1); 141 | } 142 | });*/ 143 | leftMap.open(); 144 | 145 | var rightMap = gui.addFolder('Right map'); 146 | f = rightMap.add(settings.rightMap, 'active'); 147 | f.onChange(function (value) { 148 | if (value) { 149 | plugin.resume(null,1); 150 | } 151 | else { 152 | plugin.pause(null,1); 153 | } 154 | }); 155 | 156 | f = rightMap.add(settings.rightMap, 'waterRate', 0, 5); 157 | f.onChange(function (value) { 158 | plugin.setRate(value, 1384, 1); 159 | }); 160 | rightMap.open(); 161 | 162 | 163 | } -------------------------------------------------------------------------------- /example/js/debugGUI.js: -------------------------------------------------------------------------------- 1 | class debugGUI extends dat.GUI { 2 | constructor() { 3 | super(); 4 | this.value = 0; 5 | this.listen = 0; 6 | 7 | } 8 | setupGUI(that) { 9 | 10 | this.heroFolder = this.addFolder('Hero'); 11 | this.worldFolder = this.addFolder('World'); 12 | //this.debugFolder = this.addFolder('Debug'); 13 | 14 | this.heroFolder.add(that.player.body, 'strength', -1, 10).step(1).name('Strength'); 15 | this.heroFolder.add(that.player.body, 'pushLimit', -1, 10).step(1).name('MaxCue'); 16 | //this.heroFolder.add(that.player.body, 'struggle', 0, 10).step(1).name('Struggle'); 17 | this.heroFolder.add(that, 'velocity', 1, 480).step(1).name('Velocity'); 18 | //this.heroFolder.add(that.player.body, 'zIndex', 0, 3).step(1).name('z-index'); 19 | 20 | //this.heroFolder.add(that.player, 'debugger').name('Debug body'); 21 | this.heroFolder.add(that.player.body, 'collidable').name('Collidable'); 22 | this.heroFolder.add(that.player.body, 'activeSteps').name('Steps').listen(); 23 | this.heroFolder.add(that.player.body, 'onStairs').name('onStairs').listen(); 24 | this.heroFolder.open(); 25 | // this.debugFolder.add(that.debugGfx.grid, 'active').name('Grid'); 26 | // this.debugFolder.add(that.debugGfx.collision, 'active').name('Collision'); 27 | // this.debugFolder.add(that.debugGfx.path, 'active').name('Path'); 28 | // this.debugFolder.add(that.debugGfx.pathCollision, 'active').name('Path collision'); 29 | 30 | // this.worldFolder.add(that, 'renderGrid').name('Render grid'); 31 | 32 | this.worldFolder.add(that.gridPhysics.world, 'turnbased').name('Turn based'); 33 | this.worldFolder.open(); 34 | } 35 | } 36 | 37 | export default debugGUI; 38 | -------------------------------------------------------------------------------- /example/js/main.js: -------------------------------------------------------------------------------- 1 | //import "phaser"; 2 | import { ENOMEM } from "constants"; 3 | // States 4 | import Game from './scenes/game'; 5 | 6 | 7 | //import GridPhysics from "./gridPhysics";s 8 | let config = { 9 | type: Phaser.WEBGL, 10 | width: 25 * 16, 11 | height: 15 * 16, 12 | scene: [ 13 | Game 14 | ], 15 | physics: { 16 | grid: { 17 | debug: true, 18 | gridSize: 8 19 | } 20 | } 21 | }; 22 | 23 | let game = new Phaser.Game(config); 24 | -------------------------------------------------------------------------------- /example/js/scenes/game.js: -------------------------------------------------------------------------------- 1 | //import "phaser"; 2 | import dat from "dat.gui"; 3 | import debugGUI from "../debugGUI"; 4 | class Game extends Phaser.Scene { 5 | constructor() { 6 | super({ 7 | key: "Game" 8 | }); 9 | } 10 | preload() { 11 | this.load.image("basictiles", "assets/images/basictiles.png"); 12 | this.load.tilemapTiledJSON("map", "assets/maps/demo.json"); 13 | this.load.atlas( 14 | "sprites", 15 | "assets/spriteatlas/sprites.png", 16 | "assets/spriteatlas/sprites.json" 17 | ); 18 | this.load.scenePlugin( 19 | "GridPhysics", 20 | "GridPhysics.js", 21 | "gridPhysics", 22 | "gridPhysics" 23 | ); 24 | } 25 | create() { 26 | //this.sys.install('GridPhysics'); 27 | var map = this.make.tilemap({ 28 | key: "map" 29 | }); 30 | var tiles = map.addTilesetImage("basictiles", "basictiles"); 31 | let layer = map.createStaticLayer("ground", tiles, 0, 0); 32 | 33 | layer = map.createStaticLayer("onGround", tiles, 0, 0); 34 | layer.layer.data.forEach(row => 35 | row.forEach(tile => { 36 | if (!map.tilesets[0].tileProperties.hasOwnProperty(tile.index - 1)) { 37 | return; 38 | } 39 | let p = map.tilesets[0].tileProperties[tile.index - 1]; 40 | if (p.collide) { 41 | return; 42 | } 43 | tile.collideUp = p.collideUp ? true : false; 44 | tile.collideRight = p.collideRight ? true : false; 45 | tile.collideDown = p.collideDown ? true : false; 46 | tile.collideLeft = p.collideLeft ? true : false; 47 | tile.borderUp = p.borderUp ? true : false; 48 | tile.borderRight = p.borderRight ? true : false; 49 | tile.borderDown = p.borderDown ? true : false; 50 | tile.borderLeft = p.borderLeft ? true : false; 51 | }) 52 | ); 53 | layer.setCollisionByProperty({ 54 | collide: true 55 | }); 56 | this.gridPhysics.world.enable(layer); 57 | 58 | this.player = this.add.sprite(0, 0); 59 | this.velocity = 50; 60 | 61 | [ 62 | "up", 63 | "right", 64 | "down", 65 | "left", 66 | "hit-up", 67 | "hit-right", 68 | "hit-down", 69 | "hit-left" 70 | ].forEach(key => { 71 | this.anims.create({ 72 | key: "hero/" + key, 73 | frames: this.anims.generateFrameNames("sprites", { 74 | prefix: "hero/hero-" + key + "-", 75 | end: 1 76 | }), 77 | frameRate: 5, 78 | repeat: -1, 79 | repeatDelay: 0 80 | }); 81 | }); 82 | 83 | this.anims.create({ 84 | key: "crate", 85 | frames: [ 86 | { 87 | key: "sprites", 88 | frame: "box" 89 | } 90 | ], 91 | frameRate: 5, 92 | repeat: -1, 93 | repeatDelay: 0 94 | }); 95 | 96 | let layer2 = map.createStaticLayer("above", tiles, 0, 0); 97 | this.gridPhysics.world.setLayerLevel(layer2, 1); 98 | this.gridPhysics.world.enable(layer2); 99 | layer2.setDepth(10); 100 | 101 | this.player.play("hero/right"); 102 | window.player = this.player; 103 | this.gridPhysics.world.enable(this.player); 104 | this.player.body.setPosition(48, 14); 105 | 106 | // this.player.body.gridPosition.x = 48; 107 | // this.player.body.gridPosition.y = 14; 108 | // this.player.body.snapToGrid(); 109 | //this.player.body.gridPosition.x = 25; 110 | //this.player.body.gridPosition.y = 16; 111 | 112 | this.player.body.immovable = true; 113 | this.player.body.baseVelocity = 50; 114 | /*this.player.body.collisionCallback.tile = tile => { 115 | console.log("DOING"); 116 | const modifiedTile = tile; 117 | modifiedTile.collideUp = false; 118 | modifiedTile.collideDown = false; 119 | modifiedTile.collideLeft = false; 120 | modifiedTile.collideRight = false; 121 | 122 | return modifiedTile; 123 | }; 124 | 125 | this.player.body.collisionCallback.body = (collidingBody, myBody) => { 126 | return false; 127 | };*/ 128 | this.player.body.strength = -1; 129 | this.player.body.collideWorldBounds = true; 130 | this.player.id = "player"; 131 | this.debugGraphics = this.add.graphics(); 132 | //this.player.setDepth(1); 133 | 134 | this.enemies = []; 135 | let enemy = this.add.sprite(0, 48); 136 | //enemy.originX = 0; 137 | //enemy.originY = 0; 138 | enemy.tint = 0xff00ff; 139 | enemy.id = "enemy1"; 140 | enemy.play("hero/right"); 141 | 142 | this.gridPhysics.world.enable(enemy); 143 | enemy.body.collideWorldBounds = true; 144 | enemy.body.immovable = true; 145 | 146 | this.enemies.push(enemy); 147 | enemy = this.add.sprite(48, 48 + 32); 148 | enemy.id = "enemy2"; 149 | //enemy.originX = 0; 150 | //enemy.originY = 0; 151 | enemy.tint = 0xff00ff; 152 | enemy.play("hero/right"); 153 | this.gridPhysics.world.enable(enemy); 154 | enemy.body.collideWorldBounds = true; 155 | enemy.body.immovable = true; 156 | this.enemies.push(enemy); 157 | for (let crateObj of map.objects[0].objects) { 158 | if (crateObj.properties && crateObj.properties.level) { 159 | //layer.layer.data[crateObj.y/16][crateObj.x/16].level = true; 160 | this.gridPhysics.world.addStairs(crateObj); 161 | } else { 162 | let anim = "box"; 163 | if (crateObj.properties && crateObj.properties.box) { 164 | anim += crateObj.properties.box; 165 | } 166 | let crate = this.add.sprite(crateObj.x, crateObj.y - 16); 167 | //crate.originX = 0; 168 | //crate.originY = 0; 169 | crate.play("crate"); 170 | if (crateObj.properties && crateObj.properties.scale) { 171 | crate.width = 16 * crateObj.properties.scale; 172 | crate.height = 16 * crateObj.properties.scale; 173 | crate.setScale(crateObj.properties.scale); 174 | } else { 175 | crate.width = 16; 176 | crate.height = 16; 177 | } 178 | this.gridPhysics.world.enable(crate); 179 | crate.body.collideWorldBounds = true; 180 | if (crateObj.properties && crateObj.properties.mass) { 181 | crate.body.mass = crateObj.properties.mass; 182 | } 183 | } 184 | } 185 | 186 | this.keys = { 187 | up: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.UP), 188 | x: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.X), 189 | left: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.LEFT), 190 | right: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.RIGHT), 191 | down: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.DOWN) 192 | }; 193 | 194 | if (!this.debugGUI) { 195 | this.debugGUI = new debugGUI(); 196 | } 197 | this.debugGUI.setupGUI(this); 198 | 199 | this.cameras.main.startFollow(this.player); 200 | this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels); 201 | window.player = this.player; 202 | 203 | this.gridPhysics.world.addToQue(player); 204 | this.gridPhysics.world.addToQue(this.enemies); 205 | 206 | //debugger; 207 | // this.gridPhysics.world.turnbased = true; 208 | } 209 | 210 | update(time, delta) { 211 | //console.log(this.gridPhysics.world.firstInLine.id); 212 | if (this.gridPhysics.world.firstInLine.body.justMoved) { 213 | this.gridPhysics.world.nextTurn(); 214 | } 215 | 216 | if (this.player.body.myTurn || !this.gridPhysics.world.turnbased) { 217 | let animDir = ""; 218 | let madeAMove = false; 219 | if (this.keys.up.isDown) { 220 | this.player.body.setVelocity(0, -this.velocity); 221 | madeAMove = true; 222 | animDir = "up"; 223 | } else if (this.keys.right.isDown) { 224 | this.player.body.setVelocity(this.velocity, 0); 225 | madeAMove = true; 226 | animDir = "right"; 227 | } else if (this.keys.down.isDown) { 228 | this.player.body.setVelocity(0, this.velocity); 229 | madeAMove = true; 230 | animDir = "down"; 231 | } else if (this.keys.left.isDown) { 232 | this.player.body.setVelocity(-this.velocity, 0); 233 | madeAMove = true; 234 | animDir = "left"; 235 | } else { 236 | this.player.body.setVelocity(0, 0); 237 | } 238 | let anim = "hero/" + animDir; 239 | if (anim !== this.player.anims.currentAnim.key) { 240 | this.player.play(anim); 241 | } 242 | if (!this.gridPhysics.world.turnbased) { 243 | this.enemies.forEach(enemy => { 244 | if (enemy.body.activeSteps < enemy.steps && enemy.body.isMoving.any) { 245 | return; 246 | } 247 | enemy.steps = enemy.body.activeSteps + Phaser.Math.Between(1, 4); 248 | enemy.dir = Phaser.Math.Between(1, 4); 249 | if (enemy.dir < 3) { 250 | enemy.body.setVelocity( 251 | enemy.dir === 1 ? this.velocity : -this.velocity, 252 | 0 253 | ); 254 | } else { 255 | enemy.body.setVelocity( 256 | 0, 257 | enemy.dir === 3 ? this.velocity : -this.velocity 258 | ); 259 | } 260 | }); 261 | } 262 | } else { 263 | let enemy = this.gridPhysics.world.firstInLine; 264 | if (enemy.body.activeSteps > enemy.steps || !enemy.body.isMoving.any) { 265 | enemy.steps = enemy.body.activeSteps + Phaser.Math.Between(1, 4); 266 | enemy.dir = Phaser.Math.Between(1, 4); 267 | } 268 | if (enemy.dir < 3) { 269 | enemy.body.setVelocity( 270 | enemy.dir === 1 ? this.velocity : -this.velocity, 271 | 0 272 | ); 273 | } else { 274 | enemy.body.setVelocity( 275 | 0, 276 | enemy.dir === 3 ? this.velocity : -this.velocity 277 | ); 278 | } 279 | } 280 | //this.debug(); 281 | } 282 | debug() { 283 | this.debugGraphics.clear(); 284 | var color = 0xffff00; 285 | var thickness = 2; 286 | var alpha = 1; 287 | 288 | this.debugGraphics.lineStyle(thickness, color, alpha); 289 | 290 | this.debugGraphics.strokeRect( 291 | player.body.gridPosition.x * 8, 292 | player.body.gridPosition.y * 8, 293 | 16, 294 | 16 295 | ); 296 | //console.log(layer.getTileAt(player.body.gridPosition.x * 8,player.body.gridPosition.x * 8)); 297 | } 298 | } 299 | 300 | export default Game; 301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phaser-grid-physics", 3 | "version": "2.1.0", 4 | "description": "Phaser 3 Grid Physics Plugin", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "dev": "webpack --config webpack.demo.config.js", 9 | "serve": "webpack --config webpack.demo.config.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nkholski/phaser-grid-physics.git" 14 | }, 15 | "author": "Niklas Berg", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/nkholski/phaser-grid-physics/issues" 19 | }, 20 | "homepage": "https://github.com/nkholski/phaser-grid-physics#readme", 21 | "devDependencies": { 22 | "babel-core": "^6.26.3", 23 | "babel-eslint": "^8.2.3", 24 | "babel-loader": "^7.1.4", 25 | "babel-polyfill": "^6.26.0", 26 | "babel-preset-es2015": "^6.24.1", 27 | "browser-sync": "^2.24.4", 28 | "browser-sync-webpack-plugin": "^2.2.2", 29 | "copy-webpack-plugin": "^4.5.1", 30 | "dat.gui": "^0.7.2", 31 | "html-webpack-plugin": "^3.2.0", 32 | "phaser": "3.8.0", 33 | "raw-loader": "^0.5.1", 34 | "webpack": "^4.8.3", 35 | "webpack-cli": "^2.1.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/gridBody.js: -------------------------------------------------------------------------------- 1 | class GridBody { 2 | constructor(sprite) { 3 | /** 4 | * @property {Phaser.Sprite} sprite - Reference to the parent Sprite. 5 | */ 6 | this.sprite = sprite; 7 | 8 | /** 9 | * @property {Phaser.Game} world - Local reference to the grid physics world. 10 | */ 11 | this.world = Phaser.Physics.GridPhysics.world; 12 | 13 | this.tilemap = Phaser.Physics.GridPhysics.tilemap; 14 | 15 | this.solid = true; 16 | 17 | // UNDECIDED: 18 | /** 19 | * @property {boolean} enable - A disabled body won't be checked for any form of collision or overlap or have its pre/post updates run. 20 | * @default 21 | */ 22 | //this.enable = true; 23 | 24 | // TODO: 25 | /** 26 | * @property {Phaser.Point} offset - The offset of the Physics Body from the Sprite x/y this.gridPosition. 27 | */ 28 | // this.offset = new Phaser.Point(); 29 | 30 | /** 31 | * @property {Phaser.Point} position - The position of the physics body. 32 | * @readonly 33 | */ 34 | this.gridPosition = new Phaser.Geom.Point(0, 0); 35 | 36 | this.zIndex = 0; 37 | this.zHeight = 1; 38 | 39 | /** 40 | * @property {number} width - The calculated width of the physics body in grid units. Default match sprite size. 41 | * @readonly 42 | */ 43 | this.width = Math.round(sprite.width / this.world.gridSize.x); 44 | 45 | /** 46 | * @property {number} height - The calculated height of the physics body in grid units. Default match sprite size. 47 | * @readonly 48 | */ 49 | this.height = Math.round(sprite.height / this.world.gridSize.y); 50 | 51 | /** 52 | * @property {Phaser.Point} velocity - The velocity, or rate of change in speed of the Body. Measured in pixels per second. 53 | */ 54 | this.velocity = new Phaser.Geom.Point(0, 0); 55 | 56 | /** 57 | * @property {Phaser.Point} _desiredVelocity - Velocity the entity strives to get. 58 | * @readonly 59 | */ 60 | this._desiredVelocity = new Phaser.Geom.Point(0, 0); 61 | 62 | // TODO: 63 | /** 64 | * @property {Phaser.Point} _forcedVelocity - Velocity the entity is being pushed or otherwise forced. 65 | * @readonly 66 | */ 67 | //this._forcedVelocity = new Phaser.Point(); 68 | 69 | // TODO: 70 | /** 71 | * A Signal that is dispatched when this Body collides with another Body. 72 | */ 73 | //this.onCollide = null; 74 | 75 | /** 76 | * @property {number} mass - The mass of the body. A body with strength => this.mass will be able to push this body. 77 | * @default 78 | */ 79 | this.mass = 1; 80 | 81 | /** 82 | * @property {number} strength - Total mass that this body can move. -1 = unlimited 83 | * @default 84 | */ 85 | this.strength = -1; 86 | 87 | /** 88 | * @property {number} pushLimit - Max number of objects that can be pushed, -1 = unlimited 89 | * @default 90 | */ 91 | this.pushLimit = -1; 92 | 93 | /** 94 | * @property {number} struggle - Speed decrese ( velocity /= struggle * pushed mass), 0 = no decrese 95 | * NOTE: This will probably change to allow more flexible calculations (strugglePower = struggle.c + struggle.k * mass). 96 | * @default 97 | */ 98 | this.struggle = 0; 99 | 100 | /** 101 | * @property {boolean} struggling - If the object is trying to move but not able, it will struggle :-) 102 | * @readonly 103 | */ 104 | this.struggling = false; 105 | 106 | /** 107 | * @property {number} facing - A const reference to the direction the Body is traveling or facing. 108 | * @default 109 | */ 110 | this.facing = Phaser.NONE; 111 | 112 | /** 113 | * @property {boolean} immovable - An immovable Body will not receive any impacts from other bodies. 114 | * @default 115 | */ 116 | this.immovable = false; 117 | 118 | /** 119 | * @property {boolean} collidable - The body will check collision only if collidible is set to true. 120 | * @default 121 | */ 122 | this.collidable = true; 123 | 124 | /** 125 | * A Body can be set to collide against the World bounds automatically and rebound back into the World if this is set to true. Otherwise it will leave the World. 126 | * @property {boolean} collideWorldBounds - Should the Body collide with the World bounds? 127 | */ 128 | this.collideWorldBounds = false; 129 | 130 | /** 131 | * @property {object} isMoving - Is populated with information if the body is actually moving. 132 | * @readonly 133 | */ 134 | this.isMoving = { 135 | x: false, 136 | y: false, 137 | any: false 138 | }; 139 | 140 | this.level = 0; 141 | 142 | // MAYBE: 143 | /** 144 | * @property {Phaser.Signal} onMoveComplete - Listen for the completion of `moveTo` or `moveFrom` events. 145 | */ 146 | //this.onMoveComplete = new Phaser.Signal(); 147 | 148 | this.moveTo = { 149 | active: false, 150 | path: [], 151 | next: null, 152 | recalc: 0 // 0 - never, 1 - if collision, 2 - each step (not impleneted yet) 153 | }; 154 | 155 | this.magnetism = { 156 | dragItself: false, // If the body's weight is less than the object it's trying to pull and it's movable=true it will be pulled to the object instead of the opposite 157 | weakenByFactor: 1, //A power of 10 with weakenByFactor of 0.5 will have a power of 10*0.5 = 5 on one gridunit distance, and 10*0.5*0.5 = 2.5 on two. 158 | maxRange: 1, // Stop magnetism after this amount of squares regardless of power or weakenByFactor 159 | top: { 160 | active: false, 161 | power: 0, // 0 = Infinited, If dragged this magnetism could pull this mass without break seal. 162 | pole: null, // any pole-id (could be NORTH/SOUTH), null == any 163 | attractedTo: null, // any pole-id of other body 164 | attractedToAll: false // Like a black hole. 165 | }, 166 | right: { 167 | active: false, 168 | power: 0, 169 | pole: null, 170 | attractedTo: null, 171 | attractedToAll: false 172 | }, 173 | bottom: { 174 | active: false, 175 | power: 0, 176 | pole: null, 177 | attractedTo: null, 178 | attractedToAll: false 179 | }, 180 | left: { 181 | active: false, 182 | power: 0, 183 | pole: null, 184 | attractedTo: null, 185 | attractedToAll: false 186 | } 187 | }; 188 | 189 | this.cojoinedBodies = []; // Array of bodies 190 | 191 | this.collidingBodies = []; // [{active: Body, passive: Body}] 192 | this.collectCollidingBodies = true; 193 | 194 | /** 195 | * @property {Phaser.Point} shadow - The shadow reserves tiles this object moves from while animating a move to prevent overlaps from other sprites. 196 | * NOTE: Not yet entirely implemented 197 | * @readonly 198 | */ 199 | this._shadow = new Phaser.Geom.Point(0, 0); 200 | 201 | /** 202 | * @property {object} isLocked - Used to prevent body from doing new moves before the last is finished. 203 | * NOTE: Not yet entirely implemented: May allow to turn in same direction and other stuff to make responsive. 204 | * @readonly 205 | */ 206 | this.isLocked = { 207 | x: false, 208 | y: false, 209 | any: false 210 | }; 211 | 212 | // MAY BE REMOVED: 213 | this._longPress = 0; // 0 - X, 1-Y (längsta nedtryckt) 214 | 215 | /** 216 | * @property {Number} activeSteps - Number of steps the body has taken. 217 | * NOTE: Not yet entirely implemented 218 | * @readonly 219 | */ 220 | this.activeSteps = 0; 221 | 222 | /** 223 | * @property {Number} passiveSteps - Number of steps the body has been forced to take (being pushed). 224 | * NOTE: Not yet entirely implemented 225 | * @readonly 226 | */ 227 | this.passiveSteps = 0; 228 | 229 | /** 230 | * Callbacks can override collisions. This is just for blocking behaviour. Set collectCollidingBodies to populate colliding bodies and getTilesUnderBody() to get all tiles a body i positioned over. 231 | * @property {Function} tile - Takes a tile and return new collision up, right, down and left values. 232 | * @property {Function} body - Takes a two bodies and return boolean weather the first should be considered solid 233 | */ 234 | this.collisionCallback = { 235 | tile: null, 236 | body: null 237 | }; 238 | 239 | this.myTurn = false; 240 | this.turns = 0; 241 | this.reload = 1; 242 | 243 | this.onStairs = false; 244 | 245 | this.sprite.setOrigin(0, 0); 246 | 247 | this.snapToGrid(); 248 | } 249 | 250 | snapToGrid() { 251 | this.gridPosition = { 252 | x: Math.round(this.sprite.x / this.world.gridSize.x), 253 | y: Math.round(this.sprite.y / this.world.gridSize.y) 254 | }; 255 | this.sprite.x = this.world.gridSize.x * this.gridPosition.x; 256 | this.sprite.y = this.world.gridSize.y * this.gridPosition.y; 257 | } 258 | 259 | setPosition(x = 0, y = 0) { 260 | this.gridPosition.x = x; 261 | this.gridPosition.y = y; 262 | this.sprite.x = this.world.gridSize.x * this.gridPosition.x; 263 | this.sprite.y = this.world.gridSize.y * this.gridPosition.y; 264 | this.isLocked.x = false; 265 | this.isLocked.y = false; 266 | } 267 | 268 | setVelocity(x, y = null) { 269 | // longpress is not yet implemented 270 | y = y !== null ? y : x; 271 | 272 | if (x === 0 && y !== 0) { 273 | this._longpress = 1; 274 | } else if (y === 0 && x !== 0) { 275 | this._longpress = 0; 276 | } 277 | this._desiredVelocity = { 278 | x, 279 | y 280 | }; 281 | } 282 | 283 | _intersectRect(r1, r2) { 284 | return !( 285 | r2.x >= r1.x + r1.width || 286 | r2.x + r2.width <= r1.x || 287 | r2.y >= r1.y + r1.height || 288 | r2.y + r2.height <= r1.y 289 | ); 290 | } 291 | 292 | getTilesUnderBody() { 293 | console.log("check", this); 294 | return this.tilemap.getTilesUnderBody(this); 295 | } 296 | 297 | testMove(dx, dy) { 298 | let freeToGo = true; 299 | 300 | if (!this.collidable) { 301 | return true; 302 | } 303 | 304 | if (this.onStairs) { 305 | this.level = this.tilemap.checkLevel(this.sprite, dx, dy); 306 | } 307 | 308 | if (this.tilemap.collide(this.sprite, dx, dy) && this.solid) { 309 | return false; 310 | } 311 | 312 | if (!this.collectCollidingBodies && !freeToGo) { 313 | return false; 314 | } 315 | 316 | for (let body of this.world.bodies) { 317 | if (this === body || !body.sprite.active) { 318 | continue; 319 | } 320 | 321 | if (body.collidable && (body.level === this.level || body.onStairs)) { 322 | // If not able to move and neither body is collecting colliding bodies, skip further checks 323 | if ( 324 | !freeToGo && 325 | !this.collectCollidingBodies && 326 | !body.collectCollidingBodies 327 | ) { 328 | continue; 329 | } 330 | 331 | if ( 332 | this._intersectRect( 333 | { 334 | x: this.gridPosition.x + dx, 335 | y: this.gridPosition.y + dy, 336 | width: this.width, 337 | height: this.height 338 | }, 339 | { 340 | x: body.gridPosition.x, 341 | y: body.gridPosition.y, 342 | width: body.width, 343 | height: body.height 344 | } 345 | ) 346 | ) { 347 | // Collect bodies if needed 348 | if ( 349 | this.collectCollidingBodies && 350 | this.collidingBodies.indexOf(body) === -1 351 | ) { 352 | this.collidingBodies.push(body); 353 | } 354 | if ( 355 | body.collectCollidingBodies && 356 | body.collidingBodies.indexOf(this) === -1 357 | ) { 358 | body.collidingBodies.push(this); 359 | } 360 | // Collision callback 361 | const bodyIsSolid = this.collisionCallback.body 362 | ? this.collisionCallback.body(body, this) 363 | : body.solid; 364 | 365 | // Check how the other body might affect this one 366 | if (!this.solid || !bodyIsSolid) { 367 | continue; 368 | } 369 | 370 | if (body.immovable) { 371 | freeToGo = false; 372 | } else { 373 | if (this.world._pushChain.indexOf(body) === -1) { 374 | this.world._pushChain.push(body); // Tryck på denna 375 | if (!body.testMove(dx, dy)) { 376 | // kolla upp kroppen 377 | freeToGo = false; 378 | } 379 | } 380 | } 381 | } 382 | } 383 | } 384 | return freeToGo; 385 | } 386 | 387 | postUpdate() { 388 | this.collidingBodies = []; 389 | this.justMoved = false; 390 | if (this.isLocked.x || this.isLocked.y) { 391 | return; 392 | } 393 | 394 | if (this.moveTo.active) { 395 | this._desiredVelocity = { 396 | x: 0, 397 | y: 0 398 | }; 399 | let dest = this.moveTo.path[this.moveTo.next]; 400 | if (dest.x < this.gridPosition.x) { 401 | this._desiredVelocity.x = -100; 402 | } else if (dest.x > this.gridPosition.x) { 403 | this._desiredVelocity.x = 100; 404 | } else if (dest.y < this.gridPosition.y) { 405 | this._desiredVelocity.y = -100; 406 | } else if (dest.y > this.gridPosition.y) { 407 | this._desiredVelocity.y = 100; 408 | } 409 | this.moveTo.next++; 410 | if (this.moveTo.next > this.moveTo.path.length - 1) { 411 | this.moveTo.active = false; 412 | } 413 | } 414 | 415 | this.isMoving = { 416 | x: false, 417 | y: false, 418 | any: false 419 | }; 420 | 421 | if (this._desiredVelocity.x === 0 && this._desiredVelocity.y === 0) { 422 | this.velocity.x = 0; 423 | this.velocity.y = 0; 424 | this.struggling = false; 425 | return; 426 | } 427 | 428 | this.world._pushChain = []; 429 | let moveOk = true; 430 | let _d = {}; 431 | for (let dim of ["x", "y"]) { 432 | _d[dim] = 433 | this._desiredVelocity[dim] === 0 434 | ? 0 435 | : this._desiredVelocity[dim] > 0 436 | ? 1 437 | : -1; 438 | } 439 | 440 | for (let dim of [0, 1]) { 441 | // Array här ändrar ordning efter prio 442 | if (dim === 0) { 443 | if (_d.x === 0) { 444 | continue; 445 | } 446 | if (!this.testMove(_d.x, 0)) { 447 | moveOk = false; 448 | break; 449 | } else { 450 | moveOk = true; 451 | } 452 | } else { 453 | if (_d.y === 0) { 454 | continue; 455 | } 456 | if (!this.testMove(0, _d.y)) { 457 | moveOk = false; 458 | break; 459 | } else { 460 | moveOk = true; 461 | } 462 | } 463 | } 464 | 465 | // Check if strong enough 466 | if (this.strength > -1 || this.struggle > 1) { 467 | let totalMass = 0; 468 | for (let body of this.world._pushChain) { 469 | totalMass += body.mass; 470 | } 471 | if (this.strength > -1 && totalMass > this.strength) { 472 | moveOk = false; 473 | } 474 | if (this.struggle > 1 && totalMass > 0) { 475 | this._desiredVelocity.x = 476 | this._desiredVelocity.x / (totalMass * this.struggle); 477 | this._desiredVelocity.y = 478 | this._desiredVelocity.y / (totalMass * this.struggle); 479 | } 480 | } 481 | if (this.pushLimit > -1 && this.world._pushChain.length > this.pushLimit) { 482 | moveOk = false; 483 | } 484 | 485 | if (!moveOk) { 486 | if (this._desiredVelocity.x !== 0 || this._desiredVelocity.y !== 0) { 487 | this.struggling = true; 488 | } 489 | if (this._desiredVelocity.x > 0) { 490 | this.facing = Phaser.RIGHT; 491 | } else if (this._desiredVelocity.x < 0) { 492 | this.facing = Phaser.LEFT; 493 | } else if (this._desiredVelocity.y < 0) { 494 | this.facing = Phaser.UP; 495 | } else if (this._desiredVelocity.y > 0) { 496 | this.facing = Phaser.DOWN; 497 | } 498 | this.velocity.x = 0; 499 | this.velocity.y = 0; 500 | this._shadow.x = 0; 501 | this._shadow.y = 0; 502 | 503 | return; 504 | } 505 | 506 | for (let dim of ["x", "y"]) { 507 | this.velocity[dim] = this._desiredVelocity[dim]; 508 | this._shadow[dim] = 0; 509 | if (this.velocity[dim] != 0) { 510 | this.gridPosition[dim] += this.velocity[dim] > 0 ? 1 : -1; 511 | this.isLocked[dim] = true; 512 | this._shadow[dim] = this.velocity[dim] < 0 ? 1 : -1; 513 | 514 | for (let body of this.world._pushChain) { 515 | body.passiveSteps++; 516 | body.snapToGrid(); 517 | body.velocity[dim] = this._desiredVelocity[dim]; 518 | body.gridPosition[dim] += body.velocity[dim] > 0 ? 1 : -1; 519 | body.isLocked[dim] = true; 520 | body.onStairs = this.checkStairs(body); 521 | } 522 | } 523 | } 524 | 525 | if (this.velocity.x > 0) { 526 | this.facing = Phaser.RIGHT; 527 | } else if (this.velocity.x < 0) { 528 | this.facing = Phaser.LEFT; 529 | } else if (this.velocity.y < 0) { 530 | this.facing = Phaser.UP; 531 | } else if (this.velocity.y > 0) { 532 | this.facing = Phaser.DOWN; 533 | } 534 | 535 | if (this.velocity.x !== 0) { 536 | this.isMoving.x = true; 537 | } 538 | if (this.velocity.y !== 0) { 539 | this.isMoving.y = true; 540 | } 541 | this.isMoving.any = true; 542 | this.struggling = false; 543 | 544 | this.baseVelocity = 75; 545 | this.activeSteps++; 546 | this.justMoved = true; 547 | 548 | let wasOnStairs = this.onStairs; 549 | 550 | this.onStairs = this.checkStairs(this); 551 | 552 | if (wasOnStairs && !this.onStairs) { 553 | // HACK 554 | if (this.level === 1) { 555 | this.sprite.setDepth(11); 556 | } else { 557 | this.sprite.setDepth(1); 558 | } 559 | } 560 | } 561 | 562 | checkStairs(body) { 563 | let pos = body.gridPosition; 564 | for (let stairs of this.world.stairs) { 565 | if ( 566 | pos.x < stairs.x + stairs.width && 567 | pos.x + body.width > stairs.x && 568 | pos.y < stairs.y + stairs.height && 569 | body.height + pos.y > stairs.y 570 | ) { 571 | body.sprite.setDepth(1000); 572 | return true; 573 | } 574 | } 575 | return false; 576 | } 577 | 578 | /** 579 | * Draws this Body's boundary and velocity, if enabled. 580 | * 581 | * @method Phaser.Physics.Arcade.Body#drawDebug 582 | * @since 3.0.0 583 | * 584 | * @param {Phaser.GameObjects.Graphics} graphic - The Graphics object to draw on. 585 | */ 586 | 587 | drawDebug(graphic) { 588 | let x = this.gridPosition.x * this.world.gridSize.x; 589 | let y = this.gridPosition.y * this.world.gridSize.y; 590 | let w = this.width * this.world.gridSize.x; 591 | let h = this.height * this.world.gridSize.y; 592 | 593 | if (true || this.debugShowBody) { 594 | graphic.lineStyle(1, "0xFFFFFF"); //this.debugBodyColor 595 | if (!this.sprite.active) { 596 | graphic.lineStyle(1, "0xFF0000"); //this.debugBodyColor 597 | } 598 | graphic.strokeRect(x, y, w, h); 599 | } 600 | /*if (this.debugShowVelocity) 601 | { 602 | graphic.lineStyle(1, this.world.defaults.velocityDebugColor, 1); 603 | graphic.lineBetween(x, y, x + this.velocity.x / 2, y + this.velocity.y / 2); 604 | }*/ 605 | } 606 | 607 | /*renderDebugBody() { 608 | if (!this.debugBody) { 609 | this.debugBody = new Phaser.Rectangle(sprite.x, sprite.y, sprite.width, sprite.height); 610 | this.debugShadow = new Phaser.Rectangle(sprite.x, sprite.y, sprite.width, sprite.height); 611 | } 612 | 613 | this.debugBody.x = this.gridPosition.x * this.world.gridSize.x; 614 | this.debugBody.y = this.gridPosition.y * this.world.gridSize.y; 615 | this.debugShadow.x = this.gridPosition.x * this.world.gridSize.x + this.world.gridSize.x * this._shadow.x; 616 | this.debugShadow.y = this.gridPosition.y * this.world.gridSize.y + this.world.gridSize.y * this._shadow.y; 617 | if (this._shadow.x == 0) { 618 | this.debugShadow.width = this.sprite.width; 619 | this.debugShadow.height = this.world.gridSize.y; 620 | } else { 621 | this.debugShadow.width = this.world.gridSize.x; 622 | this.debugShadow.height = this.sprite.height; 623 | 624 | } 625 | game.debug.geom(this.debugBody, 'rgba(0,255,0,0.4)'); 626 | }*/ 627 | 628 | /*renderBodyInfo(debug, body) { 629 | debug.line('x: ' + body.gridPosition.x, 'y: ' + body.gridPosition.y, 'width: ' + body.width, 'height: ' + body.height); 630 | /*debug.line('x: ' + body.x.toFixed(2), 'y: ' + body.y.toFixed(2), 'width: ' + body.width, 'height: ' + body.height); 631 | debug.line('velocity x: ' + body.velocity.x.toFixed(2), 'y: ' + body.velocity.y.toFixed(2), 'deltaX: ' + body._dx.toFixed(2), 'deltaY: ' + body._dy.toFixed(2)); 632 | debug.line('acceleration x: ' + body.acceleration.x.toFixed(2), 'y: ' + body.acceleration.y.toFixed(2), 'speed: ' + body.speed.toFixed(2), 'angle: ' + body.angle.toFixed(2)); 633 | debug.line('gravity x: ' + body.gravity.x, 'y: ' + body.gravity.y, 'bounce x: ' + body.bounce.x.toFixed(2), 'y: ' + body.bounce.y.toFixed(2)); 634 | debug.line('touching left: ' + body.touching.left, 'right: ' + body.touching.right, 'up: ' + body.touching.up, 'down: ' + body.touching.down); 635 | debug.line('blocked left: ' + body.blocked.left, 'right: ' + body.blocked.right, 'up: ' + body.blocked.up, 'down: ' + body.blocked.down);* / 636 | 637 | } 638 | */ 639 | 640 | /* moveToPixelXY(x, y, speed = 60, maxTime = 0, active = true) { 641 | x = Math.round(x / this.world.gridSize.x); 642 | y = Math.round(y / this.world.gridSize.y); 643 | this.moveToXY(x, y, speed, maxTime, active); 644 | }*/ 645 | 646 | /* moveToXY(x, y, speed = 60, maxTime = 0, active = true) { 647 | /* 648 | Yes, I know that this is ridiculously inefficient rebuilding the grid from the tilemap 649 | on each call without any cache, and trying to find the path of the full current map 650 | even if it's not necessarily in most cases. 651 | 652 | It's also still buggy. Different bodysizes and stuff is a challenge, and now 653 | the body size is locked to 2x2 of the grid size. 654 | 655 | And, also it just checks the first tilemap layer for collision even if you added more. 656 | 657 | * / 658 | 659 | 660 | if (typeof(EasyStar) === 'undefined') { 661 | console.error("Grid Physics error: Easystar.js must be enabled!"); 662 | return; 663 | } 664 | if (this.physics.render.path || this.physics.render.pathCollision) { 665 | this.physics.resetDebugRenderer(); 666 | } 667 | 668 | let easystar = new EasyStar.js(); 669 | 670 | // Generate array 671 | // Respond 672 | let grid = []; 673 | let i = 0; 674 | let tileRatio = { 675 | x: this.physics.tilemaplayers[0].layer.data[0][0].width / game.physics.gridPhysics.gridSize.x, 676 | y: this.physics.tilemaplayers[0].layer.data[0][0].height / game.physics.gridPhysics.gridSize.y 677 | }; 678 | for (let row of this.physics.tilemaplayers[0].layer.data) { 679 | grid[i] = []; 680 | grid[i + 1] = []; 681 | for (let tile of row) { 682 | if (tile.collideUp && tile.collideDown && tile.collideLeft && tile.collideRight) { // ¦¦ --> && när conditional 683 | for (let dy = 0; dy < tileRatio.y; dy++) { 684 | for (let dx = 0; dx < tileRatio.x; dx++) { 685 | grid[i + dx].push(1); 686 | } 687 | } 688 | } else if (tile.collideUp || tile.collideDown || tile.collideLeft || tile.collideRight) { 689 | for (let dy = 0; dy < tileRatio.y; dy++) { 690 | for (let dx = 0; dx < tileRatio.x; dx++) { 691 | grid[i + dx].push(4); 692 | } 693 | } 694 | } else { 695 | for (let dy = 0; dy < tileRatio.y; dy++) { 696 | for (let dx = 0; dx < tileRatio.x; dx++) { 697 | grid[i + dx].push(0); 698 | } 699 | } 700 | } 701 | } 702 | //i++; 703 | i += tileRatio.y; 704 | } 705 | 706 | // Bodies 707 | for (let body of this.physics.bodies) { 708 | if (body === this) { 709 | continue; 710 | } 711 | for (let x = 0; x < body.width; x++) { 712 | for (let y = 0; y < body.height; y++) { 713 | grid[body.gridPosition.y + y][body.gridPosition.x + x] = 3; 714 | 715 | } 716 | } 717 | } 718 | 719 | 720 | 721 | 722 | // Smalare korridorer 723 | for (let y = 0; y < grid.length; y++) { 724 | 725 | for (let x = 0; x < grid[0].length; x++) { 726 | if (x > 0 && grid[y][x] > 0 && grid[y][x] < 4) { 727 | if (grid[y][x - 1] == 0 || grid[y][x - 1] == 4) { 728 | grid[y][x - 1] = 2; 729 | } 730 | if (y > 0 && grid[y - 1][x] == 0 || grid[y][x - 1] == 4) { 731 | grid[y - 1][x] = 2; 732 | } 733 | if (x > 0 && y > 0 && grid[y - 1][x - 1] == 0 || grid[y][x - 1] == 4) { 734 | grid[y - 1][x - 1] = 2; 735 | } 736 | } 737 | } 738 | 739 | } 740 | 741 | 742 | //debugger; 743 | // console.warn(grid); 744 | 745 | this.physics.renderPathCollision(grid); 746 | 747 | 748 | 749 | easystar.setGrid(grid); 750 | 751 | 752 | // Directional condition 753 | for (let y = 0; y < grid.length; y++) { 754 | for (let x = 0; x < grid[0].length; x++) { 755 | 756 | if (grid[y][x] === 4) { 757 | let tile = this.physics.tilemaplayers[0].layer.data[Math.round(y / 2)][Math.round(x / 2)]; 758 | let paths = []; //; 759 | if (!tile.collideUp) { 760 | paths.push(EasyStar.BOTTOM); 761 | } 762 | if (!tile.collideRight) { 763 | paths.push(EasyStar.LEFT); 764 | } 765 | if (!tile.collideDown) { 766 | paths.push(EasyStar.TOP); 767 | } 768 | if (!tile.collideLeft) { 769 | paths.push(EasyStar.RIGHT); 770 | } 771 | grid[y][x] = 0; 772 | easystar.setDirectionalCondition(x, y, paths); 773 | } 774 | } 775 | } 776 | 777 | easystar.setAcceptableTiles([0, 4]); 778 | 779 | easystar.findPath(this.gridPosition.x, this.gridPosition.y, x, y, (path) => { 780 | if (path === null) { 781 | //console.log("Path was not found."); 782 | } else if (path.length > 0) { 783 | // console.log("Path was found. The first Point is " + path[1].x + " " + path[1].y, this); 784 | this.moveTo = { 785 | active: true, 786 | path, 787 | next: 1, 788 | velocity: speed, 789 | recalc: 0 790 | }; 791 | this.physics.renderPath(this.moveTo.path); 792 | 793 | /*this.moveTo.x = 794 | this.moveTo.y = path[1].y*2;*/ 795 | /* if (x < this.gridPosition.x) { 796 | // this.setVelocity(-speed, 0); 797 | this.moveTo.x = -1; 798 | } 799 | else if (x > this.gridPosition.x) { 800 | // this.setVelocity(speed, 0); 801 | this.moveTo.x = 1; 802 | } 803 | else if (y < this.gridPosition.y) { 804 | // this.setVelocity(0, -speed); 805 | this.moveTo.y = -1; 806 | } else if (y > this.gridPosition.y) { 807 | // this.setVelocity(0, speed); 808 | this.moveTo.y = 1; 809 | }* / 810 | } 811 | }); 812 | easystar.setIterationsPerCalculation(1000); 813 | easystar.calculate(); 814 | 815 | // This.isMovingToXY = {active: true, x,y,speed) <-- Kör på automatiskt medan detta finns. Testa ny väg vid varje stopp 816 | }*/ 817 | } 818 | 819 | export default GridBody; 820 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Niklas Berg 3 | * @copyright 2018 Niklas Berg 4 | * @license {@link https://github.com/nkholski/phaser3-animated-tiles/blob/master/LICENSE|MIT License} 5 | */ 6 | 7 | // 8 | // This plugin is based on Photonstorms Phaser 3 plugin template with added support for ES6. 9 | // 10 | 11 | import World from "./world"; 12 | import Tilemap from "./tilemap"; 13 | 14 | class GridPhysics extends Phaser.Plugins.ScenePlugin { 15 | constructor(scene, pluginManager) { 16 | super(scene, pluginManager); 17 | 18 | let config = scene.registry.parent.config.physics.grid; 19 | if (scene.registry.parent.config.physics.grid) { 20 | let gridSize = config.gridSize; 21 | if (!gridSize) { 22 | gridSize = { x: 8, y: 8 }; 23 | } 24 | if (typeof gridSize === "number") { 25 | gridSize = { x: gridSize, y: gridSize }; 26 | } else if ( 27 | !gridSize.hasOwnProperty("x") || 28 | !gridSize.hasOwnProperty("y") 29 | ) { 30 | gridSize = { x: 8, y: 8 }; 31 | } 32 | config = { 33 | debug: config.debug ? true : false, 34 | gridSize 35 | }; 36 | } 37 | 38 | Phaser.Physics.GridPhysics = this; 39 | // The Scene that owns this plugin 40 | //this.scene = scene; 41 | this.world = new World(scene, config); 42 | this.tilemap = new Tilemap(); 43 | scene.gridPhysics = this; 44 | //this.systems = scene.sys; 45 | 46 | if (!scene.sys.settings.isBooted) { 47 | scene.sys.events.once("boot", this.boot, this); 48 | } 49 | } 50 | 51 | // Called when the Plugin is booted by the PluginManager. 52 | // If you need to reference other systems in the Scene (like the Loader or DisplayList) then set-up those references now, not in the constructor. 53 | boot() { 54 | var eventEmitter = this.systems.events; 55 | eventEmitter.on("update", this.update, this); 56 | eventEmitter.on("postupdate", this.postUpdate, this); 57 | eventEmitter.on("shutdown", this.shutdown, this); 58 | eventEmitter.on("destroy", this.destroy, this); 59 | } 60 | 61 | postUpdate(time, delta) { 62 | this.world.bodies.forEach(body => { 63 | body.sprite.active ? body.postUpdate() : null; 64 | }); 65 | } 66 | 67 | update(time, delta) { 68 | this.world.update(time, delta); 69 | } 70 | // Called when a Scene shuts down, it may then come back again later (which will invoke the 'start' event) but should be considered dormant. 71 | shutdown() {} 72 | 73 | // Called when a Scene is destroyed by the Scene Manager. There is no coming back from a destroyed Scene, so clear up all resources here. 74 | destroy() { 75 | this.shutdown(); 76 | this.scene = undefined; 77 | } 78 | } 79 | 80 | // Static function called by the PluginFile Loader. 81 | GridPhysics.register = function(PluginManager) { 82 | // Register this plugin with the PluginManager, so it can be added to Scenes. 83 | PluginManager.register("GridPhysics", GridPhysics, "GridPhysics"); 84 | }; 85 | 86 | module.exports = GridPhysics; 87 | -------------------------------------------------------------------------------- /src/pathfinding/astar.js: -------------------------------------------------------------------------------- 1 | // javascript-astar 0.4.1 2 | // http://github.com/bgrins/javascript-astar 3 | // Freely distributable under the MIT License. 4 | // Implements the astar search algorithm in javascript using a Binary Heap. 5 | // Includes Binary Heap (with modifications) from Marijn Haverbeke. 6 | // http://eloquentjavascript.net/appendix2.html 7 | (function(definition) { 8 | /* global module, define */ 9 | if (typeof module === 'object' && typeof module.exports === 'object') { 10 | module.exports = definition(); 11 | } else if (typeof define === 'function' && define.amd) { 12 | define([], definition); 13 | } else { 14 | var exports = definition(); 15 | window.astar = exports.astar; 16 | window.Graph = exports.Graph; 17 | } 18 | })(function() { 19 | 20 | function pathTo(node) { 21 | var curr = node; 22 | var path = []; 23 | while (curr.parent) { 24 | path.unshift(curr); 25 | curr = curr.parent; 26 | } 27 | return path; 28 | } 29 | 30 | function getHeap() { 31 | return new BinaryHeap(function(node) { 32 | return node.f; 33 | }); 34 | } 35 | 36 | var astar = { 37 | /** 38 | * Perform an A* Search on a graph given a start and end node. 39 | * @param {Graph} graph 40 | * @param {GridNode} start 41 | * @param {GridNode} end 42 | * @param {Object} [options] 43 | * @param {bool} [options.closest] Specifies whether to return the 44 | path to the closest node if the target is unreachable. 45 | * @param {Function} [options.heuristic] Heuristic function (see 46 | * astar.heuristics). 47 | */ 48 | search: function(graph, start, end, options) { 49 | graph.cleanDirty(); 50 | options = options || {}; 51 | var heuristic = options.heuristic || astar.heuristics.manhattan; 52 | var closest = options.closest || false; 53 | 54 | var openHeap = getHeap(); 55 | var closestNode = start; // set the start node to be the closest if required 56 | 57 | start.h = heuristic(start, end); 58 | graph.markDirty(start); 59 | 60 | openHeap.push(start); 61 | 62 | while (openHeap.size() > 0) { 63 | 64 | // Grab the lowest f(x) to process next. Heap keeps this sorted for us. 65 | var currentNode = openHeap.pop(); 66 | 67 | // End case -- result has been found, return the traced path. 68 | if (currentNode === end) { 69 | return pathTo(currentNode); 70 | } 71 | 72 | // Normal case -- move currentNode from open to closed, process each of its neighbors. 73 | currentNode.closed = true; 74 | 75 | // Find all neighbors for the current node. 76 | var neighbors = graph.neighbors(currentNode); 77 | 78 | for (var i = 0, il = neighbors.length; i < il; ++i) { 79 | var neighbor = neighbors[i]; 80 | 81 | if (neighbor.closed || neighbor.isWall()) { 82 | // Not a valid node to process, skip to next neighbor. 83 | continue; 84 | } 85 | 86 | // The g score is the shortest distance from start to current node. 87 | // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. 88 | var gScore = currentNode.g + neighbor.getCost(currentNode); 89 | var beenVisited = neighbor.visited; 90 | 91 | if (!beenVisited || gScore < neighbor.g) { 92 | 93 | // Found an optimal (so far) path to this node. Take score for node to see how good it is. 94 | neighbor.visited = true; 95 | neighbor.parent = currentNode; 96 | neighbor.h = neighbor.h || heuristic(neighbor, end); 97 | neighbor.g = gScore; 98 | neighbor.f = neighbor.g + neighbor.h; 99 | graph.markDirty(neighbor); 100 | if (closest) { 101 | // If the neighbour is closer than the current closestNode or if it's equally close but has 102 | // a cheaper path than the current closest node then it becomes the closest node 103 | if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) { 104 | closestNode = neighbor; 105 | } 106 | } 107 | 108 | if (!beenVisited) { 109 | // Pushing to heap will put it in proper place based on the 'f' value. 110 | openHeap.push(neighbor); 111 | } else { 112 | // Already seen the node, but since it has been rescored we need to reorder it in the heap 113 | openHeap.rescoreElement(neighbor); 114 | } 115 | } 116 | } 117 | } 118 | 119 | if (closest) { 120 | return pathTo(closestNode); 121 | } 122 | 123 | // No result was found - empty array signifies failure to find path. 124 | return []; 125 | }, 126 | // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html 127 | heuristics: { 128 | manhattan: function(pos0, pos1) { 129 | var d1 = Math.abs(pos1.x - pos0.x); 130 | var d2 = Math.abs(pos1.y - pos0.y); 131 | return d1 + d2; 132 | }, 133 | diagonal: function(pos0, pos1) { 134 | var D = 1; 135 | var D2 = Math.sqrt(2); 136 | var d1 = Math.abs(pos1.x - pos0.x); 137 | var d2 = Math.abs(pos1.y - pos0.y); 138 | return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2)); 139 | } 140 | }, 141 | cleanNode: function(node) { 142 | node.f = 0; 143 | node.g = 0; 144 | node.h = 0; 145 | node.visited = false; 146 | node.closed = false; 147 | node.parent = null; 148 | } 149 | }; 150 | 151 | /** 152 | * A graph memory structure 153 | * @param {Array} gridIn 2D array of input weights 154 | * @param {Object} [options] 155 | * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed 156 | */ 157 | function Graph(gridIn, options) { 158 | options = options || {}; 159 | this.nodes = []; 160 | this.diagonal = !!options.diagonal; 161 | this.grid = []; 162 | for (var x = 0; x < gridIn.length; x++) { 163 | this.grid[x] = []; 164 | 165 | for (var y = 0, row = gridIn[x]; y < row.length; y++) { 166 | var node = new GridNode(x, y, row[y]); 167 | this.grid[x][y] = node; 168 | this.nodes.push(node); 169 | } 170 | } 171 | this.init(); 172 | } 173 | 174 | Graph.prototype.init = function() { 175 | this.dirtyNodes = []; 176 | for (var i = 0; i < this.nodes.length; i++) { 177 | astar.cleanNode(this.nodes[i]); 178 | } 179 | }; 180 | 181 | Graph.prototype.cleanDirty = function() { 182 | for (var i = 0; i < this.dirtyNodes.length; i++) { 183 | astar.cleanNode(this.dirtyNodes[i]); 184 | } 185 | this.dirtyNodes = []; 186 | }; 187 | 188 | Graph.prototype.markDirty = function(node) { 189 | this.dirtyNodes.push(node); 190 | }; 191 | 192 | Graph.prototype.neighbors = function(node) { 193 | var ret = []; 194 | var x = node.x; 195 | var y = node.y; 196 | var grid = this.grid; 197 | 198 | // West 199 | if (grid[x - 1] && grid[x - 1][y]) { 200 | ret.push(grid[x - 1][y]); 201 | } 202 | 203 | // East 204 | if (grid[x + 1] && grid[x + 1][y]) { 205 | ret.push(grid[x + 1][y]); 206 | } 207 | 208 | // South 209 | if (grid[x] && grid[x][y - 1]) { 210 | ret.push(grid[x][y - 1]); 211 | } 212 | 213 | // North 214 | if (grid[x] && grid[x][y + 1]) { 215 | ret.push(grid[x][y + 1]); 216 | } 217 | 218 | if (this.diagonal) { 219 | // Southwest 220 | if (grid[x - 1] && grid[x - 1][y - 1]) { 221 | ret.push(grid[x - 1][y - 1]); 222 | } 223 | 224 | // Southeast 225 | if (grid[x + 1] && grid[x + 1][y - 1]) { 226 | ret.push(grid[x + 1][y - 1]); 227 | } 228 | 229 | // Northwest 230 | if (grid[x - 1] && grid[x - 1][y + 1]) { 231 | ret.push(grid[x - 1][y + 1]); 232 | } 233 | 234 | // Northeast 235 | if (grid[x + 1] && grid[x + 1][y + 1]) { 236 | ret.push(grid[x + 1][y + 1]); 237 | } 238 | } 239 | 240 | return ret; 241 | }; 242 | 243 | Graph.prototype.toString = function() { 244 | var graphString = []; 245 | var nodes = this.grid; 246 | for (var x = 0; x < nodes.length; x++) { 247 | var rowDebug = []; 248 | var row = nodes[x]; 249 | for (var y = 0; y < row.length; y++) { 250 | rowDebug.push(row[y].weight); 251 | } 252 | graphString.push(rowDebug.join(" ")); 253 | } 254 | return graphString.join("\n"); 255 | }; 256 | 257 | function GridNode(x, y, weight) { 258 | this.x = x; 259 | this.y = y; 260 | this.weight = weight; 261 | } 262 | 263 | GridNode.prototype.toString = function() { 264 | return "[" + this.x + " " + this.y + "]"; 265 | }; 266 | 267 | GridNode.prototype.getCost = function(fromNeighbor) { 268 | // Take diagonal weight into consideration. 269 | if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) { 270 | return this.weight * 1.41421; 271 | } 272 | return this.weight; 273 | }; 274 | 275 | GridNode.prototype.isWall = function() { 276 | return this.weight === 0; 277 | }; 278 | 279 | function BinaryHeap(scoreFunction) { 280 | this.content = []; 281 | this.scoreFunction = scoreFunction; 282 | } 283 | 284 | BinaryHeap.prototype = { 285 | push: function(element) { 286 | // Add the new element to the end of the array. 287 | this.content.push(element); 288 | 289 | // Allow it to sink down. 290 | this.sinkDown(this.content.length - 1); 291 | }, 292 | pop: function() { 293 | // Store the first element so we can return it later. 294 | var result = this.content[0]; 295 | // Get the element at the end of the array. 296 | var end = this.content.pop(); 297 | // If there are any elements left, put the end element at the 298 | // start, and let it bubble up. 299 | if (this.content.length > 0) { 300 | this.content[0] = end; 301 | this.bubbleUp(0); 302 | } 303 | return result; 304 | }, 305 | remove: function(node) { 306 | var i = this.content.indexOf(node); 307 | 308 | // When it is found, the process seen in 'pop' is repeated 309 | // to fill up the hole. 310 | var end = this.content.pop(); 311 | 312 | if (i !== this.content.length - 1) { 313 | this.content[i] = end; 314 | 315 | if (this.scoreFunction(end) < this.scoreFunction(node)) { 316 | this.sinkDown(i); 317 | } else { 318 | this.bubbleUp(i); 319 | } 320 | } 321 | }, 322 | size: function() { 323 | return this.content.length; 324 | }, 325 | rescoreElement: function(node) { 326 | this.sinkDown(this.content.indexOf(node)); 327 | }, 328 | sinkDown: function(n) { 329 | // Fetch the element that has to be sunk. 330 | var element = this.content[n]; 331 | 332 | // When at 0, an element can not sink any further. 333 | while (n > 0) { 334 | 335 | // Compute the parent element's index, and fetch it. 336 | var parentN = ((n + 1) >> 1) - 1; 337 | var parent = this.content[parentN]; 338 | // Swap the elements if the parent is greater. 339 | if (this.scoreFunction(element) < this.scoreFunction(parent)) { 340 | this.content[parentN] = element; 341 | this.content[n] = parent; 342 | // Update 'n' to continue at the new position. 343 | n = parentN; 344 | } 345 | // Found a parent that is less, no need to sink any further. 346 | else { 347 | break; 348 | } 349 | } 350 | }, 351 | bubbleUp: function(n) { 352 | // Look up the target element and its score. 353 | var length = this.content.length; 354 | var element = this.content[n]; 355 | var elemScore = this.scoreFunction(element); 356 | 357 | while (true) { 358 | // Compute the indices of the child elements. 359 | var child2N = (n + 1) << 1; 360 | var child1N = child2N - 1; 361 | // This is used to store the new position of the element, if any. 362 | var swap = null; 363 | var child1Score; 364 | // If the first child exists (is inside the array)... 365 | if (child1N < length) { 366 | // Look it up and compute its score. 367 | var child1 = this.content[child1N]; 368 | child1Score = this.scoreFunction(child1); 369 | 370 | // If the score is less than our element's, we need to swap. 371 | if (child1Score < elemScore) { 372 | swap = child1N; 373 | } 374 | } 375 | 376 | // Do the same checks for the other child. 377 | if (child2N < length) { 378 | var child2 = this.content[child2N]; 379 | var child2Score = this.scoreFunction(child2); 380 | if (child2Score < (swap === null ? elemScore : child1Score)) { 381 | swap = child2N; 382 | } 383 | } 384 | 385 | // If the element needs to be moved, swap it, and continue. 386 | if (swap !== null) { 387 | this.content[n] = this.content[swap]; 388 | this.content[swap] = element; 389 | n = swap; 390 | } 391 | // Otherwise, we are done. 392 | else { 393 | break; 394 | } 395 | } 396 | } 397 | }; 398 | 399 | return { 400 | astar: astar, 401 | Graph: Graph 402 | }; 403 | 404 | }); -------------------------------------------------------------------------------- /src/pathfinding/binaryHeap.js: -------------------------------------------------------------------------------- 1 | // Borrowed from http://eloquentjavascript.net/1st_edition/appendix2.html 2 | 3 | function BinaryHeap(scoreFunction) { 4 | this.content = []; 5 | this.scoreFunction = scoreFunction; 6 | } 7 | 8 | BinaryHeap.prototype = { 9 | push: function (element) { 10 | // Add the new element to the end of the array. 11 | this.content.push(element); 12 | // Allow it to bubble up. 13 | this.bubbleUp(this.content.length - 1); 14 | }, 15 | 16 | pop: function () { 17 | // Store the first element so we can return it later. 18 | var result = this.content[0]; 19 | // Get the element at the end of the array. 20 | var end = this.content.pop(); 21 | // If there are any elements left, put the end element at the 22 | // start, and let it sink down. 23 | if (this.content.length > 0) { 24 | this.content[0] = end; 25 | this.sinkDown(0); 26 | } 27 | return result; 28 | }, 29 | 30 | remove: function (node) { 31 | var length = this.content.length; 32 | // To remove a value, we must search through the array to find 33 | // it. 34 | for (var i = 0; i < length; i++) { 35 | if (this.content[i] != node) continue; 36 | // When it is found, the process seen in 'pop' is repeated 37 | // to fill up the hole. 38 | var end = this.content.pop(); 39 | // If the element we popped was the one we needed to remove, 40 | // we're done. 41 | if (i == length - 1) break; 42 | // Otherwise, we replace the removed element with the popped 43 | // one, and allow it to float up or sink down as appropriate. 44 | this.content[i] = end; 45 | this.bubbleUp(i); 46 | this.sinkDown(i); 47 | break; 48 | } 49 | }, 50 | 51 | size: function () { 52 | return this.content.length; 53 | }, 54 | 55 | bubbleUp: function (n) { 56 | // Fetch the element that has to be moved. 57 | var element = this.content[n], 58 | score = this.scoreFunction(element); 59 | // When at 0, an element can not go up any further. 60 | while (n > 0) { 61 | // Compute the parent element's index, and fetch it. 62 | var parentN = Math.floor((n + 1) / 2) - 1, 63 | parent = this.content[parentN]; 64 | // If the parent has a lesser score, things are in order and we 65 | // are done. 66 | if (score >= this.scoreFunction(parent)) 67 | break; 68 | 69 | // Otherwise, swap the parent with the current element and 70 | // continue. 71 | this.content[parentN] = element; 72 | this.content[n] = parent; 73 | n = parentN; 74 | } 75 | }, 76 | 77 | sinkDown: function (n) { 78 | // Look up the target element and its score. 79 | var length = this.content.length, 80 | element = this.content[n], 81 | elemScore = this.scoreFunction(element); 82 | 83 | while (true) { 84 | // Compute the indices of the child elements. 85 | var child2N = (n + 1) * 2, 86 | child1N = child2N - 1; 87 | // This is used to store the new position of the element, 88 | // if any. 89 | var swap = null; 90 | // If the first child exists (is inside the array)... 91 | if (child1N < length) { 92 | // Look it up and compute its score. 93 | var child1 = this.content[child1N], 94 | child1Score = this.scoreFunction(child1); 95 | // If the score is less than our element's, we need to swap. 96 | if (child1Score < elemScore) 97 | swap = child1N; 98 | } 99 | // Do the same checks for the other child. 100 | if (child2N < length) { 101 | var child2 = this.content[child2N], 102 | child2Score = this.scoreFunction(child2); 103 | if (child2Score < (swap == null ? elemScore : child1Score)) 104 | swap = child2N; 105 | } 106 | 107 | // No need to swap further, we are done. 108 | if (swap == null) break; 109 | 110 | // Otherwise, swap and continue. 111 | this.content[n] = this.content[swap]; 112 | this.content[swap] = element; 113 | n = swap; 114 | } 115 | } 116 | }; -------------------------------------------------------------------------------- /src/tilemap.js: -------------------------------------------------------------------------------- 1 | class Tilemap { 2 | constructor() { 3 | this.tilemaps = []; 4 | this.world = Phaser.Physics.GridPhysics.world; 5 | } 6 | 7 | collide( 8 | source, 9 | dx = 0, 10 | dy = 0, 11 | layers = this.world.tilemaplayers, 12 | slide = false 13 | ) { 14 | let position, 15 | width, 16 | height, 17 | collideWorldBounds, 18 | returnTile, 19 | level, 20 | callback; 21 | 22 | // Sort out variables to work with, either from a sprite with a body or just an object 23 | if (source.hasOwnProperty("body")) { 24 | position = { 25 | x: source.body.gridPosition.x, 26 | y: source.body.gridPosition.y 27 | }; 28 | width = source.body.width; 29 | height = source.body.height; 30 | collideWorldBounds = source.body.collideWorldBounds; 31 | level = source.body.level; 32 | callback = source.body.collisionCallback.tile; 33 | } else { 34 | position = { 35 | x: source.x, 36 | y: source.y 37 | }; 38 | width = source.width ? source.width : 1; 39 | height = source.height ? source.height : 1; 40 | collideWorldBounds = source.hasOwnProperty("collideWorldBounds") 41 | ? source.collideWorldBounds 42 | : false; 43 | returnTile = true; 44 | level = source.level ? source.level : 0; 45 | callback = source.collisionCallback 46 | ? source.body.collisionCallback.tile 47 | : null; 48 | } 49 | // Prevent going outside the tilemap? 50 | 51 | if ( 52 | collideWorldBounds && 53 | (position.x + dx < 0 || 54 | position.y + dy < 0 || 55 | position.x + dx + width > 56 | this.world.tilemaplayers[0].width / this.world.gridSize.x || 57 | position.y + dy + height > 58 | this.world.tilemaplayers[0].height / this.world.gridSize.y) 59 | ) { 60 | return true; 61 | } 62 | // Update the position to the attempted movement 63 | position.x += dx; 64 | position.y += dy; 65 | 66 | // Slim the body to prevent unnecessary collision checks (not that the physics are particulary demanding but anyway) 67 | if (dx !== 0) { 68 | if (dx > 0) { 69 | position.x += width - 1; 70 | } 71 | width = 1; 72 | } else if (dy !== 0) { 73 | if (dy > 0) { 74 | position.y += height - 1; 75 | } 76 | height = 1; 77 | } 78 | 79 | for (let x = position.x; x < position.x + width; x++) { 80 | for (let y = position.y; y < position.y + height; y++) { 81 | let collide = false; 82 | for (let layer of this.world.tilemaplayers) { 83 | if (level > layer.level) { 84 | continue; 85 | } 86 | 87 | //let tile = this.world.map.getTileAt(Math.floor(x * this.world.gridSize.x / layer.collisionWidth), Math.floor(y * this.world.gridSize.y / layer.collisionHeight), layer, true); 88 | let collisionHeight = layer.layer.baseTileHeight; 89 | let collisionWidth = layer.layer.baseTileHeight; 90 | 91 | //layer.collisionWidth = 16; 92 | //let tile = this.world.getTileAt(Math.floor(x * this.world.gridSize.x / layer.collisionWidth), Math.floor(y * this.world.gridSize.y / layer.collisionHeight), layer, true); 93 | //debugger; 94 | 95 | let checkY = Math.floor( 96 | (y * this.world.gridSize.y) / collisionHeight 97 | ); 98 | let checkX; 99 | if (checkY < 0 || checkY > layer.layer.data.length - 1) { 100 | if (this.collideWorldBounds) { 101 | return true; 102 | } else { 103 | continue; 104 | } 105 | } else { 106 | checkX = Math.floor((x * this.world.gridSize.x) / collisionWidth); 107 | if (checkX < 0 || checkY > layer.layer.data[checkY].length - 1) { 108 | if (this.collideWorldBounds) { 109 | return true; 110 | } else { 111 | continue; 112 | } 113 | } 114 | } 115 | 116 | const tile = layer.layer.data[checkY][checkX] || null; 117 | 118 | if (tile && tile.index === -1 && layer.level > 0 && level > 0) { 119 | // HACK 120 | console.log(layer.level); 121 | return true; 122 | } 123 | 124 | if (returnTile) { 125 | console.log(tile, checkX, checkY); 126 | } 127 | 128 | if (tile === null || (tile.index === -1 && !tile.gotBorder)) { 129 | // No tile, or empty - OK 130 | continue; 131 | } 132 | 133 | const tileCollider = callback ? callback(tile) : tile; 134 | 135 | if ( 136 | tileCollider.collideRight && 137 | tileCollider.collideLeft && 138 | tileCollider.collideDown && 139 | tileCollider.collideUp 140 | ) { 141 | // tile collides whatever direction the body enter 142 | collide = true; 143 | break; 144 | } else if (dx < 0 && tileCollider.collideRight) { 145 | // moving left and the tile collides from the right 146 | //console.log("Collide RIGHT", tile) 147 | collide = true; 148 | break; 149 | } else if (dx > 0 && tileCollider.collideLeft) { 150 | //console.log("Collide KEFT", tile) 151 | collide = true; 152 | break; 153 | } 154 | if (dy < 0 && tileCollider.collideDown) { 155 | //console.log("Collide DOWN", tile) 156 | collide = true; 157 | break; 158 | } else if (dy > 0 && tileCollider.collideUp) { 159 | //console.log("Collide UP", tile) 160 | collide = true; 161 | break; 162 | } 163 | 164 | // Prevents bodies to walk with path of body outside of blocked tile side 165 | /* if (dx != 0) { 166 | if (tile.borderUp && position.y < tile.y * tileRatio.y) { 167 | collide = true; 168 | break; 169 | } else if (tile.borderDown && position.y + height > tile.y * tileRatio.y) { 170 | collide = true; 171 | break; 172 | } 173 | } 174 | if (dy != 0) { 175 | if (tile.borderLeft && position.x < tile.x * tileRatio.x) { 176 | collide = true; 177 | break; 178 | } else if (tile.borderRight && position.x + width > tile.x * tileRatio.x) { 179 | collide = true; 180 | break; 181 | } 182 | }*/ 183 | } 184 | if (collide) { 185 | if (slide) { 186 | // Left-over from previous working version, needs review... 187 | if (dx !== 0) { 188 | if (!this.collide(source, dx, dy - 1)) { 189 | return { 190 | dx, 191 | dy: dy - 1 192 | }; 193 | } else if (!this.collide(source, dx, dy + 1)) { 194 | return { 195 | dx, 196 | dy: dy + 1 197 | }; 198 | } 199 | } 200 | if (dy !== 0) { 201 | if (!this.collide(source, dx - 1, dy)) { 202 | return { 203 | dx: dx - 1, 204 | dy 205 | }; 206 | } else if (!this.collide(source, dx + 1, dy)) { 207 | return { 208 | dx: dx + 1, 209 | dy 210 | }; 211 | } 212 | } 213 | } 214 | return { 215 | dx: 0, 216 | dy: 0 217 | }; 218 | } 219 | } 220 | } 221 | return false; 222 | } 223 | 224 | getTilesUnderBody(body) { 225 | console.log( 226 | "Check", 227 | body.gridPosition.x, 228 | body.gridPosition.y, 229 | body.width, 230 | body.height 231 | ); 232 | 233 | return this.getTilesAt( 234 | body.gridPosition.x, 235 | body.gridPosition.y, 236 | body.width, 237 | body.height 238 | ); 239 | } 240 | 241 | getTilesAt(x, y, width, height) { 242 | const tileScaleX = 2; 243 | const tileScaleY = 2; 244 | const tiles = []; 245 | 246 | let startX = Math.floor(x / tileScaleX); 247 | startX = startX < 0 ? 0 : startX; 248 | 249 | let startY = Math.floor(y / tileScaleY); 250 | startY = startY < 0 ? 0 : startY; 251 | 252 | let stopX = Math.floor((x + width) / tileScaleX); 253 | stopX = 254 | stopX > this.world.tilemaplayers[0].tilemap.width 255 | ? this.world.tilemaplayers[0].tilemap.width 256 | : stopX; 257 | 258 | let stopY = Math.floor(y + height) / tileScaleY; 259 | stopY = 260 | stopY > this.world.tilemaplayers[0].tilemap.height 261 | ? this.world.tilemaplayers[0].tilemap.height 262 | : stopY; 263 | 264 | console.log(`scan x=${startX} to ${stopX} amd y=${startY} to ${stopY}`); 265 | 266 | this.world.tilemaplayers.forEach((layer, layerIndex) => { 267 | tiles[layerIndex] = []; 268 | 269 | for (let checkX = startX; checkX < stopX; checkX += tileScaleX) { 270 | for (let checkY = startY; checkY < stopY; checkY += tileScaleY) { 271 | const tile = layer.layer.data[checkY][checkX] || null; 272 | if (tile) { 273 | tiles[layerIndex].push(tile); 274 | } 275 | } 276 | } 277 | }); 278 | return tiles; 279 | } 280 | 281 | checkLevel(source, dx, dy) { 282 | let position, width, height; 283 | let level = 0; 284 | 285 | // DRY FAIL: Slightly modified copy from collision 286 | if (source.hasOwnProperty("body")) { 287 | position = { 288 | x: source.body.gridPosition.x, 289 | y: source.body.gridPosition.y 290 | }; 291 | width = source.body.width; 292 | height = source.body.height; 293 | } else { 294 | position = { 295 | x: source.x, 296 | y: source.y 297 | }; 298 | width = source.width ? source.width : 1; 299 | height = source.height ? source.height : 1; 300 | } 301 | 302 | position.x += dx; 303 | position.y += dy; 304 | 305 | if (dx !== 0) { 306 | width = 1; 307 | } else { 308 | height = 1; 309 | } 310 | 311 | if (dx > 0) { 312 | position.x = position.x + width; 313 | } else if (dy > 0) { 314 | position.y = position.y + height; 315 | } 316 | 317 | // Return level a sprite move to, higher level is prioritized 318 | for (let x = position.x; x < position.x + width; x++) { 319 | for (let y = position.y; y < position.y + height; y++) { 320 | for (let layer of this.world.tilemaplayers) { 321 | let tile = layer.layer.data[Math.floor(y / 2)][Math.floor(x / 2)]; 322 | 323 | console.log(layer, layer.level, x, y, tile); 324 | 325 | if (tile && tile.index > 0 && layer.level > level) { 326 | level = layer.level; 327 | } 328 | } 329 | } 330 | } 331 | console.log("LEVEL", level); 332 | return level; 333 | } 334 | } 335 | export default Tilemap; 336 | -------------------------------------------------------------------------------- /src/world.js: -------------------------------------------------------------------------------- 1 | import GridBody from "./gridBody"; 2 | 3 | export default class World { 4 | constructor(scene, config) { 5 | this.cnt = 0; 6 | 7 | this.scene = scene; 8 | 9 | // Size of Grid in pixels 10 | this.gridSize = new Phaser.Geom.Point(config.gridSize.x, config.gridSize.y); 11 | 12 | // Shadow size in pixels. 13 | this.shadowSize = 0; 14 | 15 | // Locked while pushed 16 | this.lockBodies = false; 17 | 18 | // Sprites and stuff with gridPhysics enable 19 | this.bodies = []; 20 | 21 | // Collidable tilemap layers 22 | this.tilemaplayers = []; 23 | 24 | this.tileGridRatio = new Phaser.Geom.Point(-1, -1); 25 | 26 | this._pushChain = []; 27 | 28 | this.stairs = []; 29 | 30 | this.map = null; 31 | 32 | this.debugGfx = { 33 | graphics: null, 34 | update: true, 35 | grid: { 36 | active: false, 37 | wasActive: false, 38 | data: null 39 | }, 40 | path: { 41 | active: false, 42 | wasActive: false, 43 | data: null 44 | }, 45 | pathCollision: { 46 | active: false, 47 | wasActive: false 48 | }, 49 | collision: { 50 | active: false, 51 | wasActive: false, 52 | data: null 53 | } 54 | }; 55 | 56 | window.debugGfx = this.debugGfx; 57 | 58 | this.turnbased = false; 59 | this.turn = 0; 60 | this.que = []; 61 | this.firstInLine = null; 62 | this.collisionMap = null; 63 | 64 | this.showWarnings = true; 65 | 66 | this.drawDebug = config.debug ? this.createDebugGraphic() : false; 67 | } 68 | 69 | enable(entity) { 70 | switch (entity.type) { 71 | case "Sprite": 72 | entity.body = new GridBody(entity); 73 | this.bodies.push(entity.body); 74 | break; 75 | case "DynamicTilemapLayer": 76 | case "StaticTilemapLayer": 77 | if ( 78 | (this.showWarnings && 79 | entity.collisionWidth % this.gridSize.x !== 0) || 80 | entity.collisionWidth % this.gridSize.x !== 0 81 | ) { 82 | console.warn( 83 | "Tile size isn't an even multiplier of the grid size. This will probably break your game." 84 | ); 85 | } 86 | this.tilemaplayers.push(entity); 87 | entity.level = entity.level ? entity.level : 0; 88 | entity.setOrigin(0, 0); 89 | if (this.tileGridRatio.x === -1) { 90 | this.tileGridRatio.setTo( 91 | entity.collisionWidth / this.gridSize.x, 92 | entity.collisionHeight / this.gridSize.y 93 | ); 94 | } 95 | this.updateBorders(entity); 96 | return; 97 | default: 98 | // Phaser.TILEMAP???? 99 | if (entity.hasOwnProperty) { 100 | this.map = entity; 101 | } 102 | // debugger; 103 | break; 104 | } 105 | } 106 | updateBorders(layer) { 107 | let data = layer.layer.data; 108 | for (let y = 0; y < data.length; y++) { 109 | for (let x = 0; x < data[y].length; x++) { 110 | let tile = data[y][x]; 111 | if (tile.borderUp) { 112 | tile.collideUp = true; 113 | data[y - 1][x].collideDown = true; 114 | data[y - 1][x].gotBorder = true; 115 | } 116 | if (tile.borderDown) { 117 | tile.collideDown = true; 118 | data[y + 1][x].collideUp = true; 119 | data[y + 1][x].gotBorder = true; 120 | } 121 | if (tile.borderLeft) { 122 | tile.collideLeft = true; 123 | data[y][x - 1].collideRight = true; 124 | data[y][x - 1].gotBorder = true; 125 | } 126 | if (tile.borderRight) { 127 | tile.collideRight = true; 128 | data[y][x + 1].collideLeft = true; 129 | data[y][x + 1].gotBorder = true; 130 | } 131 | } 132 | } 133 | } 134 | 135 | addToQue(body, reload = 0) { 136 | if (!body.hasOwnProperty("myTurn")) { 137 | if (body.hasOwnProperty("body")) { 138 | body = body.body; 139 | } else if (typeof body === "object" && body.length > 0) { 140 | body.forEach(b => { 141 | this.addToQue(b, reload); 142 | }); 143 | return; 144 | } else { 145 | console.error( 146 | "You need to pass a sprite with gridBody, gridBody or an array to addToQue." 147 | ); 148 | } 149 | } 150 | 151 | this.firstInLine = this.firstInLine ? this.firstInLine : body.sprite; 152 | this.firstInLine.body.myTurn = true; 153 | 154 | if (reload === 0) { 155 | reload = body.reload > 0 ? body.reload : 1; 156 | } 157 | // Fill the que with nulls to make room for quicker or faster units. 158 | if (this.que.length < reload * 2) { 159 | for (let s = 0; s < reload * 2; s++) { 160 | this.que.push(null); 161 | } 162 | } 163 | let pos = reload * 2; 164 | while (this.que[pos] != null) { 165 | pos++; 166 | } 167 | this.que.splice(pos, 0, body); 168 | 169 | // Remove unnecessary nulls in the beginning of the array from the first added body 170 | while (this.que[0] === null && this.que.length > 0) { 171 | this.que.shift(0); 172 | } 173 | } 174 | 175 | nextTurn(reload = 0) { 176 | let body = null; 177 | 178 | // Add current body to the end of the que 179 | this.addToQue(this.firstInLine, reload); 180 | this.firstInLine.body.myTurn = false; 181 | this.firstInLine.body.turn++; 182 | 183 | // Keep track of total global moves 184 | this.turn++; 185 | this.que.shift(0); 186 | 187 | while (this.que[0] === null && this.que.length > 0) { 188 | this.que.shift(0); 189 | } 190 | 191 | if (this.que.length === 0) { 192 | console.error("EMPTY QUE!"); 193 | return; 194 | } 195 | 196 | body = this.que[0]; 197 | body.myTurn = true; 198 | this.firstInLine = body.sprite; 199 | } 200 | 201 | update(time, delta) { 202 | this.turnMade = false; 203 | let elapsedTime = delta / 1000; //body.game.time.physicsElapsed 204 | 205 | for (const body of this.bodies) { 206 | if (!body.sprite.active) { 207 | continue; 208 | } 209 | let next = { 210 | x: body.sprite.x, 211 | y: body.sprite.y 212 | }; 213 | for (let dim of ["x", "y"]) { 214 | if (body.velocity[dim] === 0) { 215 | // Stannat, fixa positionen exakt. 216 | body.sprite[dim] = body.gridPosition[dim] * body.world.gridSize[dim]; 217 | continue; 218 | } 219 | if ( 220 | body.gridPosition[dim] * body.world.gridSize[dim] != 221 | body.sprite[dim] 222 | ) { 223 | body.sprite[dim] += body.velocity[dim] * elapsedTime; 224 | next[dim] = body.sprite[dim] + body.velocity[dim] * elapsedTime; 225 | } 226 | if ( 227 | body.velocity[dim] > 0 && 228 | next[dim] > body.gridPosition[dim] * body.world.gridSize[dim] 229 | ) { 230 | // Nästa steg är klart! 231 | body.isLocked[dim] = false; // Kan sätta ny gridPosition och velocity! 232 | } 233 | if ( 234 | body.velocity[dim] < 0 && 235 | next[dim] < body.gridPosition[dim] * body.world.gridSize[dim] 236 | ) { 237 | // Nästa steg är klart! 238 | body.isLocked[dim] = false; // Kan sätta ny gridPosition och velocity! 239 | } 240 | if (!body.isLocked.x && !body.isLocked.y && this.turnbased) { 241 | body.setVelocity(0); 242 | } 243 | } 244 | } 245 | 246 | if (this.drawDebug) { 247 | var graphics = this.debugGraphic; 248 | 249 | graphics.clear(); 250 | 251 | this.bodies.forEach(body => { 252 | //if (body && body.willDrawDebug()) 253 | //{ 254 | // if (!body.sprite.active) { 255 | // console.log(body); 256 | // debugger; 257 | // return; 258 | // } 259 | body.drawDebug(graphics); 260 | //} 261 | }); 262 | } 263 | } 264 | 265 | renderDebug() { 266 | let gfx = this.debugGfx; 267 | if ( 268 | gfx.grid.active != gfx.grid.wasActive || 269 | gfx.path.active != gfx.path.wasActive || 270 | gfx.pathCollision.active != gfx.pathCollision.wasActive || 271 | gfx.collision.active != gfx.collision.wasActive 272 | ) { 273 | if (gfx.graphics) { 274 | gfx.graphics.clear(); 275 | } else { 276 | gfx.graphics = game.add.graphics(0, 0); 277 | } 278 | 279 | if (gfx.grid.active) { 280 | this.renderGrid(); 281 | } 282 | 283 | if (gfx.path.active) { 284 | this.renderPath(); 285 | } 286 | 287 | if (gfx.pathCollision.active) { 288 | this.renderPathCollision(); 289 | } 290 | 291 | if (gfx.collision.active) { 292 | this.renderCollision(); 293 | } 294 | 295 | for (let type of ["grid", "collision", "path", "pathCollision"]) { 296 | gfx[type].wasActive = gfx[type].active; 297 | } 298 | 299 | if ( 300 | gfx.grid.active || 301 | gfx.path.active || 302 | gfx.pathCollision.active || 303 | gfx.collision.active 304 | ) { 305 | gfx.graphics.alpha = 1; 306 | } else { 307 | gfx.graphics.alpha = 0; 308 | } 309 | } 310 | } 311 | 312 | renderGrid() { 313 | let graphics = this.debugGfx.graphics; 314 | graphics.lineStyle(1, 0x000000, 0.2); 315 | for (let x = 0; x < this.game.width / this.gridSize.x; x++) { 316 | graphics.moveTo(x * this.gridSize.x, 0); 317 | graphics.lineTo(x * this.gridSize.x, this.game.height); 318 | } 319 | for (let y = 0; y < this.game.height / this.gridSize.y; y++) { 320 | graphics.moveTo(0, y * this.gridSize.y); 321 | graphics.lineTo(this.game.width, y * this.gridSize.y); 322 | } 323 | } 324 | 325 | renderCollision() { 326 | let graphics = this.debugGfx.graphics; 327 | graphics.lineStyle(0, 0x00ff00, 0.0); 328 | 329 | for (let layer of this.tilemaplayers) { 330 | for (let y in layer.layer.data) { 331 | for (let x in layer.layer.data[y]) { 332 | let tile = layer.layer.data[y][x]; 333 | if (tile.collides) { 334 | graphics.beginFill(0xff0000, 0.5); 335 | graphics.drawRect( 336 | x * tile.width + 1, 337 | y * tile.height + 1, 338 | tile.width - 2, 339 | tile.height - 2 340 | ); 341 | graphics.endFill(); 342 | } 343 | } 344 | } 345 | } 346 | } 347 | 348 | renderPathCollision(data) { 349 | if (data) { 350 | this.debugGfx.pathCollision.data = data; 351 | this.debugGfx.pathCollision.wasActive = false; 352 | return; 353 | } 354 | if ( 355 | !this.debugGfx.pathCollision.data || 356 | this.debugGfx.pathCollision.data.length === 0 357 | ) { 358 | return; 359 | } 360 | 361 | let graphics = this.debugGfx.graphics; 362 | let grid = this.debugGfx.pathCollision.data; 363 | 364 | for (let y = 0; y < grid.length; y++) { 365 | graphics.lineStyle(1, 0xff0000, 0.3); 366 | for (let x = 0; x < grid[0].length; x++) { 367 | if (grid[y][x] != 0) { 368 | if (grid[y][x] < 2) { 369 | graphics.beginFill(0xff3300, 0.4); 370 | } else if (grid[y][x] == 4) { 371 | graphics.beginFill(0x0000ff, 0.4); 372 | } else { 373 | graphics.beginFill(0xff33ff, 0.4); 374 | } 375 | graphics.drawRect( 376 | x * this.gridSize.x, 377 | y * this.gridSize.y, 378 | this.gridSize.x, 379 | this.gridSize.y 380 | ); 381 | graphics.endFill(); 382 | } 383 | } 384 | } 385 | } 386 | 387 | renderPath(data) { 388 | if (data) { 389 | this.debugGfx.path.data = data; 390 | this.debugGfx.path.wasActive = false; 391 | return; 392 | } 393 | if (!this.debugGfx.path.data || this.debugGfx.path.data.length === 0) { 394 | return; 395 | } 396 | 397 | let graphics = this.debugGfx.graphics; 398 | let path = this.debugGfx.path.data; 399 | graphics.lineStyle(1, 0x00ff00, 0.5); 400 | for (let point of path) { 401 | graphics.beginFill(0x00ff00, 0.4); 402 | 403 | graphics.drawRect( 404 | point.x * this.gridSize.x, 405 | point.y * this.gridSize.y, 406 | this.gridSize.x, 407 | this.gridSize.y 408 | ); 409 | } 410 | let lastPoint = path[path.length - 1]; 411 | graphics.beginFill(0xff3300, 0.7); 412 | 413 | graphics.drawRect( 414 | lastPoint.x * this.gridSize.x, 415 | lastPoint.y * this.gridSize.y, 416 | this.gridSize.x, 417 | this.gridSize.y 418 | ); 419 | graphics.endFill(); 420 | } 421 | 422 | resetCollisionLayer() { 423 | let map = this.map; 424 | let colLayerIndex = map.getLayerIndex("gridPhysicsCollision"); 425 | let colTile, tile; 426 | for (let x = 0; x < map.layers[colLayerIndex].width; x++) { 427 | for (let y = 0; y < map.layers[colLayerIndex].height; y++) { 428 | //console.log(collisionLayerName); 429 | map.putTileAt(1, x, y, "gridPhysicsCollision"); 430 | colTile = map.getTileAt(x, y, "gridPhysicsCollision"); 431 | colTile.collideUp = false; 432 | colTile.collideRight = false; 433 | colTile.collideDown = false; 434 | colTile.collideLeft = false; 435 | //colTile.collides = false; 436 | } 437 | } 438 | } 439 | 440 | addToLayerToCollision(layer) { 441 | //console.log("lay",layer); 442 | //let map = this.map; 443 | console.log("WHAT"); 444 | let map = layer.tilemap; 445 | layer.collisionWidth = map.tileWidth; 446 | layer.collisionHeight = map.tileHeight; 447 | console.log(layer.collisionHeigh, map, layer); 448 | // console.log(layer, map, "what") 449 | let colLayerIndex = map.getLayerIndex("gridPhysicsCollision"); 450 | console.log("COL", layer); 451 | 452 | //let collisionLayer = null; 453 | let colTile, tile; 454 | tile = map.getTileAt(1, 0, layer); 455 | console.log("tile", tile.index); 456 | if (!colLayerIndex) { 457 | let collisionLayer = map.createBlankDynamicLayer( 458 | "gridPhysicsCollision", 459 | map.width, 460 | map.height, 461 | map.tileWidth, 462 | map.tileHeight 463 | ); 464 | colLayerIndex = map.getLayerIndex("gridPhysicsCollision"); 465 | collisionLayer.visible = false; 466 | this.resetCollisionLayer(); 467 | } 468 | console.log(map); 469 | tile = map.getTileAt(1, 0, layer); 470 | console.log("tile", tile.index); 471 | // Prepare the collision layer 472 | 473 | // Loop tiles for collision and add to collision layer 474 | console.log("layerindex", layer.layerIndex); 475 | console.log("asa", layer); 476 | 477 | for (let x = 0; x < layer.width; x++) { 478 | console.log(x); 479 | for (let y = 0; y < layer.height; y++) { 480 | //console.log(x+" "+y) 481 | let tile = map.getTileAt(x, y, layer); 482 | 483 | if (tile === null) { 484 | continue; 485 | } 486 | 487 | if (tile.index !== 21) { 488 | console.log(tile); 489 | } else { 490 | console.log("."); 491 | } 492 | //console.log(tile); 493 | 494 | colTile = map.getTileAt(x, y, "gridPhysicsCollision"); 495 | colTile.collideUp = tile.collideUp ? true : colTile.collideUp; 496 | colTile.collideRight = tile.collideRight ? true : colTile.collideRight; 497 | colTile.collideDown = tile.collideDown ? true : colTile.collideDown; 498 | colTile.collideLeft = tile.collideLeft ? true : colTile.collideLeft; 499 | //colTile.collides = tile.collides ? true : colTile.collides;*/ 500 | 501 | // console.log("TILE", colTile); 502 | } 503 | } 504 | 505 | // Set all non-collision tiles to null (save some ram and probably perfomance) 506 | /*for (var x = 0; x < map.layers[colLayerIndex].width; x++) { 507 | for (var y = 0; y < map.layers[colLayerIndex].height; y++) { 508 | colTile = map.getTile(x, y, collisionLayerName); 509 | if (!colTile.collideUp && !colTile.collideDown && !colTile.collideLeft && !colTile.collideRight) { 510 | map.putTile(null, x, y, collisionLayerName); 511 | } 512 | } 513 | }*/ 514 | 515 | // Build collision grid for pathfinding! 516 | 517 | // Fix faces - Not used by GridPhysics ATM, but could be nice for visual debugging 518 | // map.calculateFaces(colLayerIndex); 519 | 520 | //return collisionLayer ? collisionLayer : null; 521 | console.log(map.layers[colLayerIndex]); 522 | } 523 | 524 | setLayerLevel(layer, level) { 525 | layer.level = level; 526 | } 527 | 528 | addStairs(obj) { 529 | this.stairs.push({ 530 | x: obj.x / this.gridSize.x, 531 | y: obj.y / this.gridSize.y - 2, // Don't remember why -2 is there. Remove? 532 | width: obj.width / this.gridSize.x, 533 | height: obj.height / this.gridSize.y 534 | }); 535 | } 536 | 537 | /** 538 | * Creates the graphics object responsible for debug display. 539 | * 540 | * @method Phaser.Physics.Arcade.World#createDebugGraphic 541 | * @since 3.0.0 542 | * 543 | * @return {Phaser.GameObjects.Graphics} [description] 544 | */ 545 | createDebugGraphic() { 546 | var graphic = this.scene.sys.add.graphics({ x: 0, y: 0 }); 547 | 548 | graphic.setDepth(Number.MAX_VALUE); 549 | 550 | this.debugGraphic = graphic; 551 | 552 | return true; 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /stuffedAway/stuff.js: -------------------------------------------------------------------------------- 1 | /* I have had four ideas on how to animate tiles. Currently I use a method that probably fit most developers. 2 | For very large tilemaps with high number of animated tiles, an approach inspired by whats was going on here 3 | might be better. We'll se. I just stuff it away here until possibly forever.*/ 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | // First postUpdate went through all tiles in the tilemap and updated them. Might not be a bad idea if you have high 16 | // number of animated tiles anyway, so it could return as an alternative to trade with memory usage. 17 | postUpdateOLD: function (time, delta) { 18 | if (!this.active) { 19 | return; 20 | } 21 | this.animatedTiles.forEach( 22 | (animatedTile) => { 23 | //let animatedTile = this.animatedTiles[tilkey]; 24 | animatedTile.next -= delta * this.rate; 25 | if (animatedTile.next < 0) { 26 | let currentIndex = animatedTile.currentFrame; 27 | let newIndex = currentIndex + 1; 28 | if (newIndex > (animatedTile.frames.length - 1)) { 29 | newIndex = 0; 30 | } 31 | animatedTile.next = animatedTile.frames[newIndex].duration; 32 | animatedTile.currentFrame = newIndex; 33 | /** 34 | * 35 | * TODO: 1. Gå på AnimationIndex, 36 | * 2. ändra bara inom vyn: MEN då måste räkna ut nya tiles som inte syntes nyss. Kom ihåg förra området! 37 | * 38 | */ 39 | this.map.replaceByIndex(animatedTile.frames[currentIndex].tileid, animatedTile.frames[newIndex].tileid); 40 | } 41 | else { 42 | // TODO: Uppdatera sådana som inte synts i förra uppdateringen 43 | 44 | } 45 | } 46 | ); 47 | }, 48 | 49 | // Never finished. Was planned to update only culledTiles, however then I need to check all culled tiles for every update loop. 50 | getAnimatedTilesUNFINISHED: function (tileData) { 51 | // Buildning the array with tiles that should be animated 52 | let animatedTiles = []; 53 | Object.keys(tileData).forEach( 54 | (index) => { 55 | console.log(gid); 56 | index = parseInt(index); 57 | if (tileData[index].hasOwnProperty("animation")) { 58 | let tile = { 59 | index, 60 | frames: [], 61 | currentFrame: 0 62 | }; 63 | tileData[index].animation.forEach((frame) => { frame.tileid++; tile.frames.push(frame) }); 64 | tile.next = tile.frames[0].duration; 65 | animatedTiles.push(tile); 66 | this.tileRate[index] = 1; 67 | } 68 | } 69 | ); 70 | // Add animIndex to all tiles which is the original index set by Tiled 71 | // animIndex is constant while the rendered index is in flux 72 | animatedTiles.forEach( 73 | (animatedTile) => { 74 | this.map.layers[0].data.forEach( 75 | (tileCol) => { 76 | tileCol.forEach( 77 | (tile) => { 78 | if (tile.index === animatedTile.index) { 79 | tile.animIndex = animatedTile.index; 80 | } 81 | } 82 | ); 83 | } 84 | ) 85 | } 86 | ); 87 | return animatedTiles; 88 | }, 89 | 90 | // First version. Works well but I prefer the new way to store all tiles to aniamte in arrays. 91 | getAnimatedTilesOLD: function (tileData) { 92 | // Buildning the array with tiles that should be animated 93 | let animatedTiles = []; 94 | Object.keys(tileData).forEach( 95 | (key) => { 96 | console.log(key); 97 | if (tileData[key].hasOwnProperty("animation")) { 98 | let tile = { 99 | key, 100 | frames: [], 101 | currentFrame: 0 102 | }; 103 | tileData[key].animation.forEach((frame) => { frame.tileid++; tile.frames.push(frame) }); 104 | tile.next = tile.frames[0].duration; 105 | animatedTiles.push(tile); 106 | this.tileRate[key] = 1; 107 | /** 108 | * 109 | * TODO: Add animationIndex to all 110 | * 111 | */ 112 | 113 | } 114 | } 115 | ) 116 | return animatedTiles; 117 | } 118 | 119 | 120 | 121 | // Started and abondoned. I figured to check which tiles is within current view, update all tiles when animation updates and 122 | // only new tiles when visible. Figured that culledtiles will be as performant as well and already supports multiple cameras. 123 | /*** 124 | let width = 20; debugger; 125 | 126 | let height = 15; 127 | let rect1 = { 128 | x: 5, 129 | y: 2, 130 | }; 131 | let rect2 = { 132 | x: 0, 133 | y: 0, 134 | }; 135 | if(rect2.x>rect1.x){ 136 | // Update tiles in rectangle to the right 137 | for(let x=rect1.x+width; xrect1.y){ 154 | // Update tiles in rectangle below 155 | for(let x=rect2.x; x