├── editor.png ├── hex-grid.jpg ├── hex-grid-basic.jpg ├── examples ├── img │ ├── air.png │ ├── earth.png │ ├── fire.png │ ├── marker.png │ ├── water.png │ └── obstacle.png ├── index.html ├── load-map.html ├── css │ └── normalize.css ├── simple-square.html ├── simple-hex.html ├── js │ └── Sprite.js ├── sprite-selection.html └── pathfinding.html ├── editor ├── modules │ ├── nexus.js │ ├── tower.js │ ├── data.js │ ├── motor.js │ ├── EditorPlane.js │ ├── Input.js │ ├── keyboard.js │ ├── Editor.js │ └── main.js ├── css │ ├── style.css │ └── normalize.css ├── lib │ ├── define.min.js │ ├── bliss.min.js │ └── riot.min.js ├── index.html └── app.js ├── .gitignore ├── .gitattributes ├── src ├── vg.js ├── utils │ ├── Loader.js │ ├── SelectionManager.js │ ├── Scene.js │ ├── Tools.js │ └── MouseCaster.js ├── grids │ ├── Cell.js │ ├── Tile.js │ ├── SqrGrid.js │ └── HexGrid.js ├── pathing │ ├── AStarFinder.js │ └── PathUtil.js ├── Board.js └── lib │ ├── LinkedList.js │ └── Signal.js ├── package.json ├── LICENSE ├── .eslintrc ├── README.md ├── gulpfile.js └── lib └── OrbitControls.js /editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/editor.png -------------------------------------------------------------------------------- /hex-grid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/hex-grid.jpg -------------------------------------------------------------------------------- /hex-grid-basic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/hex-grid-basic.jpg -------------------------------------------------------------------------------- /examples/img/air.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/air.png -------------------------------------------------------------------------------- /examples/img/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/earth.png -------------------------------------------------------------------------------- /examples/img/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/fire.png -------------------------------------------------------------------------------- /examples/img/marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/marker.png -------------------------------------------------------------------------------- /examples/img/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/water.png -------------------------------------------------------------------------------- /examples/img/obstacle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vonWolfehaus/von-grid/HEAD/examples/img/obstacle.png -------------------------------------------------------------------------------- /editor/modules/nexus.js: -------------------------------------------------------------------------------- 1 | define('nexus', { 2 | grid: null, 3 | board: null, 4 | mouse: null, 5 | scene: null, 6 | input: null, 7 | plane: null, 8 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | node_modules 10 | 11 | # Users Environment Variables 12 | .lock-wscript 13 | 14 | hex-map.json -------------------------------------------------------------------------------- /editor/css/style.css: -------------------------------------------------------------------------------- 1 | .absolute { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | } 6 | #control-panel { 7 | padding: 10px; 8 | width: 200px; 9 | min-height: 200px; 10 | background-color: rgba(200, 200, 200, 0.8); 11 | } -------------------------------------------------------------------------------- /editor/modules/tower.js: -------------------------------------------------------------------------------- 1 | define('tower', { 2 | tileAction: new vg.Signal(), 3 | objAction: new vg.Signal(), 4 | userAction: new vg.Signal(), 5 | 6 | saveMap: new vg.Signal(), 7 | loadMap: new vg.Signal(), 8 | 9 | TILE_CHANGE_HEIGHT: 'cell.change.height', 10 | TILE_ADD: 'cell.add', 11 | TILE_REMOVE: 'cell.remove', 12 | }); -------------------------------------------------------------------------------- /editor/lib/define.min.js: -------------------------------------------------------------------------------- 1 | !function(){function e(e){if(n.hasOwnProperty(e))return n[e] 2 | throw'[require-shim] Cannot find module "'+e+'"'}function i(i,o,r){var d=null,t=r&&void 0!==r 3 | if(t){if(r.hasOwnProperty(i))throw"[define-shim] Module "+i+" already exists"}else if(n.hasOwnProperty(i))throw"[define-shim] Module "+i+" already exists" 4 | d="function"==typeof o?o(e):o,t?r[i]=d:n[i]=d}var n={} 5 | window.define=i,window.define.amd=!1,window.require=e}() 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce unix line ending 2 | * text eol=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted to native line endings on checkout. 5 | *.css text 6 | *.html text 7 | *.js text 8 | *.xml text 9 | *.json text 10 | *.md text 11 | .gitignore -text 12 | 13 | # Denote all files that are truly binary and should not be modified. 14 | *.png binary 15 | *.jpg binary 16 | *.eot binary 17 | *.ttf binary 18 | *.woff binary -------------------------------------------------------------------------------- /src/vg.js: -------------------------------------------------------------------------------- 1 | var vg = { // eslint-disable-line 2 | VERSION: '0.1.1', 3 | 4 | PI: Math.PI, 5 | TAU: Math.PI * 2, 6 | DEG_TO_RAD: 0.0174532925, 7 | RAD_TO_DEG: 57.2957795, 8 | SQRT3: Math.sqrt(3), // used often in hex conversions 9 | 10 | // useful enums for type checking. change to whatever fits your game. these are just examples 11 | TILE: 'tile', // visual representation of a grid cell 12 | ENT: 'entity', // dynamic things 13 | STR: 'structure', // static things 14 | 15 | HEX: 'hex', 16 | SQR: 'square', 17 | ABS: 'abstract' 18 | }; -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |

Examples

24 | Simple (hexagon)
25 | Simple (square)
26 | Sprite selection
27 | Pathfinding
28 | Load map
29 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hex-grid", 3 | "description": "Hexagonal (and other) grid stuff with three.js", 4 | "repository": "https://github.com/vonWolfehaus/hex-grid", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "browser-sync": "^2.2.3", 8 | "del": "^2.0.2", 9 | "gulp": "^3.9.0", 10 | "gulp-add-src": "^0.2.0", 11 | "gulp-autoprefixer": "^3.0.2", 12 | "gulp-concat": "^2.4.1", 13 | "gulp-doxx": "0.0.5", 14 | "gulp-eslint": "^1.1.0", 15 | "gulp-flatten": "^0.2.0", 16 | "gulp-if": "^2.0.0", 17 | "gulp-load-plugins": "^1.0.0", 18 | "gulp-notify": "^2.2.0", 19 | "gulp-plumber": "^1.0.1", 20 | "gulp-riot": "^0.3.1", 21 | "gulp-sort-amd": "^0.9.1", 22 | "gulp-sourcemaps": "^1.3.0", 23 | "gulp-stylus": "^2.1.0", 24 | "gulp-uglify": "^1.5.1", 25 | "gulp-util": "^3.0.4", 26 | "gulp-wrap": "^0.11.0", 27 | "run-sequence": "^1.0.2" 28 | }, 29 | "dependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /editor/modules/data.js: -------------------------------------------------------------------------------- 1 | /* 2 | Handles JSON for whatever data needs to be saved to localStorage, and provides a convenient signal for whenever that data changes. 3 | */ 4 | define('data', { 5 | _store: {}, 6 | changed: new vg.Signal(), 7 | 8 | get: function(key) { 9 | return this._store[key] || null; 10 | }, 11 | 12 | set: function(key, val) { 13 | // fire event first so we can retrieve old data before it's overwritten (just in case) 14 | this.changed.dispatch(key, this._store[key], val); 15 | this._store[key] = val; 16 | }, 17 | 18 | save: function() { 19 | window.localStorage['vongrid'] = JSON.stringify(this._store); 20 | }, 21 | 22 | load: function(json) { 23 | var data = window.localStorage['vongrid']; 24 | if (json || data) { 25 | try { 26 | this._store = json || JSON.parse(data); 27 | this.changed.dispatch('load-success'); 28 | } 29 | catch (err) { 30 | console.warn('Error loading editor data'); 31 | this.changed.dispatch('load-failure'); 32 | } 33 | } 34 | } 35 | }); -------------------------------------------------------------------------------- /src/utils/Loader.js: -------------------------------------------------------------------------------- 1 | vg.Loader = { 2 | manager: null, 3 | imageLoader: null, 4 | crossOrigin: false, 5 | 6 | init: function(crossOrigin) { 7 | this.crossOrigin = crossOrigin || false; 8 | 9 | this.manager = new THREE.LoadingManager(function() { 10 | // called when all images are loaded, so call your state manager or something 11 | }, function() { 12 | // noop 13 | }, function() { 14 | console.warn('Error loading images'); 15 | }); 16 | 17 | this.imageLoader = new THREE.ImageLoader(this.manager); 18 | this.imageLoader.crossOrigin = crossOrigin; 19 | }, 20 | 21 | loadTexture: function(url, mapping, onLoad, onError) { 22 | var texture = new THREE.Texture(null, mapping); 23 | this.imageLoader.load(url, function(image) { // on load 24 | texture.image = image; 25 | texture.needsUpdate = true; 26 | if (onLoad) onLoad(texture); 27 | }, 28 | null, // on progress 29 | function (evt) { // on error 30 | if (onError) onError(evt); 31 | }); 32 | texture.sourceFile = url; 33 | 34 | return texture; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Corey Birnbaum 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 | 23 | -------------------------------------------------------------------------------- /src/grids/Cell.js: -------------------------------------------------------------------------------- 1 | /* 2 | Simple structure for holding grid coordinates and extra data about them. 3 | 4 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 5 | */ 6 | vg.Cell = function(q, r, s, h) { 7 | this.q = q || 0; // x grid coordinate (using different letters so that it won't be confused with pixel/world coordinates) 8 | this.r = r || 0; // y grid coordinate 9 | this.s = s || 0; // z grid coordinate 10 | this.h = h || 1; // 3D height of the cell, used by visual representation and pathfinder, cannot be less than 1 11 | this.tile = null; // optional link to the visual representation's class instance 12 | this.userData = {}; // populate with any extra data needed in your game 13 | this.walkable = true; // if true, pathfinder will use as a through node 14 | // rest of these are used by the pathfinder and overwritten at runtime, so don't touch 15 | this._calcCost = 0; 16 | this._priority = 0; 17 | this._visited = false; 18 | this._parent = null; 19 | this.uniqueID = vg.LinkedList.generateID(); 20 | }; 21 | 22 | vg.Cell.prototype = { 23 | set: function(q, r, s) { 24 | this.q = q; 25 | this.r = r; 26 | this.s = s; 27 | return this; 28 | }, 29 | 30 | copy: function(cell) { 31 | this.q = cell.q; 32 | this.r = cell.r; 33 | this.s = cell.s; 34 | this.h = cell.h; 35 | this.tile = cell.tile || null; 36 | this.userData = cell.userData || {}; 37 | this.walkable = cell.walkable; 38 | return this; 39 | }, 40 | 41 | add: function(cell) { 42 | this.q += cell.q; 43 | this.r += cell.r; 44 | this.s += cell.s; 45 | return this; 46 | }, 47 | 48 | equals: function(cell) { 49 | return this.q === cell.q && this.r === cell.r && this.s === cell.s; 50 | } 51 | }; 52 | 53 | vg.Cell.prototype.constructor = vg.Cell; 54 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "eslint:recommended", 4 | "globals": { 5 | "vg": true, 6 | "THREE": true 7 | }, 8 | "rules": { 9 | "arrow-spacing": [1, {"before": true, "after": true}], 10 | "arrow-parens": 1, 11 | "block-spacing": [1, "always"], 12 | "brace-style": [1, "stroustrup", {"allowSingleLine": true}], 13 | "camelcase": [1, {"properties": "always"}], 14 | "comma-dangle": 0, 15 | "comma-spacing": [1, {"before": false, "after": true}], 16 | "comma-style": 1, 17 | "consistent-this": [1, "self"], 18 | "eol-last": 2, 19 | "func-style": 0, 20 | "indent": 0, 21 | "key-spacing": 1, 22 | "linebreak-style": [2, "unix"], 23 | "new-cap": 1, 24 | "no-catch-shadow": 0, 25 | "no-console": 0, 26 | "no-delete-var": 0, 27 | "no-empty": 0, 28 | "no-fallthrough": 0, 29 | "no-multiple-empty-lines": [1, {"max": 3}], 30 | "no-mixed-spaces-and-tabs": [1, "smart-tabs"], 31 | "no-nested-ternary": 1, 32 | "no-spaced-func": 1, 33 | "no-trailing-spaces": [1, {"skipBlankLines": true}], 34 | "no-undefined": 0, 35 | "no-unneeded-ternary": [1, {"defaultAssignment": true}], 36 | "no-unreachable": 2, 37 | "no-label-var": 1, 38 | "no-undef-init": 2, 39 | "no-undefined": 0, 40 | "no-unused-vars": [1, {args: "none"}], 41 | "no-use-before-define": 0, 42 | "operator-linebreak": [1, "after"], 43 | "padded-blocks": 0, 44 | "quotes": [1, "single", "avoid-escape"], 45 | "semi-spacing": [1, {"before": false, "after": true}], 46 | "semi": [2, "always"], 47 | "space-before-keywords": 1, 48 | "space-after-keywords": 1, 49 | "space-before-blocks": 1, 50 | "space-before-function-paren": [1, "never"], 51 | "space-in-parens": [1, "never"], 52 | "space-unary-ops": 1, 53 | "spaced-comment": 0 54 | }, 55 | "env": { 56 | "browser": true 57 | } 58 | } -------------------------------------------------------------------------------- /src/utils/SelectionManager.js: -------------------------------------------------------------------------------- 1 | // 'utils/Tools', 'lib/LinkedList', 'utils/MouseCaster', 'lib/Signal' 2 | vg.SelectionManager = function(mouse) { 3 | this.mouse = mouse; 4 | 5 | this.onSelect = new vg.Signal(); 6 | this.onDeselect = new vg.Signal(); 7 | 8 | this.selected = null; 9 | // deselect if player clicked on the same thing twice 10 | this.toggleSelection = false; 11 | 12 | // allow multiple entities to be selected at once 13 | // this.multiselect = false; // todo 14 | // this.allSelected = new LinkedList(); 15 | 16 | this.mouse.signal.add(this.onMouse, this); 17 | } 18 | 19 | vg.SelectionManager.prototype = { 20 | select: function(obj, fireSignal) { 21 | if (!obj) return; 22 | fireSignal = fireSignal || true; 23 | 24 | if (this.selected !== obj) { 25 | // deselect previous object 26 | this.clearSelection(fireSignal); 27 | } 28 | if (obj.selected) { 29 | if (this.toggleSelection) { 30 | if (fireSignal) { 31 | this.onDeselect.dispatch(obj); 32 | } 33 | obj.deselect(); 34 | } 35 | } 36 | else { 37 | obj.select(); 38 | } 39 | 40 | this.selected = obj; 41 | if (fireSignal) { 42 | this.onSelect.dispatch(obj); 43 | } 44 | }, 45 | 46 | clearSelection: function(fireSignal) { 47 | fireSignal = fireSignal || true; 48 | if (this.selected) { 49 | if (fireSignal) { 50 | this.onDeselect.dispatch(this.selected); 51 | } 52 | this.selected.deselect(); 53 | } 54 | this.selected = null; 55 | }, 56 | 57 | onMouse: function(type, obj) { 58 | switch (type) { 59 | case vg.MouseCaster.DOWN: 60 | if (!obj) { 61 | this.clearSelection(); 62 | } 63 | break; 64 | 65 | case vg.MouseCaster.CLICK: 66 | this.select(obj); 67 | break; 68 | } 69 | } 70 | }; 71 | 72 | vg.SelectionManager.prototype.constructor = vg.SelectionManager; 73 | -------------------------------------------------------------------------------- /editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |

Instructions

24 | 34 | 35 |

Editor settings

36 | Map size: 20 37 | Height step: 3 38 |
39 | 40 |

File

41 | 42 |
43 | 44 | 45 |

Materials

46 | ... 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/load-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /editor/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} -------------------------------------------------------------------------------- /examples/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} -------------------------------------------------------------------------------- /examples/simple-square.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/simple-hex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /editor/modules/motor.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is the ONLY place in the app that has a requestAnimationFrame handler. 3 | All modules attach their functions to this module if they want in on the RAF. 4 | */ 5 | define('motor', function() { 6 | var _brake = false; 7 | var _steps = []; 8 | 9 | function on() { 10 | _brake = false; 11 | window.requestAnimationFrame(_update); 12 | window.addEventListener('focus', onFocus, false); 13 | window.addEventListener('blur', onBlur, false); 14 | } 15 | 16 | function off() { 17 | _brake = true; 18 | window.removeEventListener('focus', onFocus, false); 19 | window.removeEventListener('blur', onBlur, false); 20 | } 21 | 22 | // in order to be able to ID functions we have to hash them to generate unique-ish keys for us to find them with later 23 | // if we don't do this, we won't be able to remove callbacks that were bound and save us from binding callbacks multiple times all over the place 24 | function add(cb, scope) { 25 | var k = _hashStr(cb.toString()); 26 | var h = _has(k); 27 | if (h === -1) { 28 | _steps.push({ 29 | func: cb, 30 | scope: scope, 31 | key: k 32 | }); 33 | } 34 | } 35 | 36 | function remove(cb) { 37 | var k = _hashStr(cb.toString()); 38 | var i = _has(k); 39 | if (i !== -1) { 40 | _steps.splice(i, 1); 41 | } 42 | } 43 | 44 | function _update() { 45 | if (_brake) return; 46 | window.requestAnimationFrame(_update); 47 | 48 | for (var i = 0; i < _steps.length; i++) { 49 | var o = _steps[i]; 50 | o.func.call(o.scope || null); 51 | } 52 | } 53 | 54 | // check if the handler already has iaw.motor particular callback 55 | function _has(k) { 56 | var n = -1; 57 | var i; 58 | for (i = 0; i < _steps.length; i++) { 59 | n = _steps[i].key; 60 | if (n === k) { 61 | return i; 62 | } 63 | } 64 | return -1; 65 | } 66 | 67 | function onFocus(evt) { 68 | _brake = false; 69 | _update(); 70 | } 71 | 72 | function onBlur(evt) { 73 | _brake = true; 74 | } 75 | 76 | function _hashStr(str) { 77 | var hash = 0, i, chr, len; 78 | if (str.length === 0) return hash; 79 | for (i = 0, len = str.length; i < len; i++) { 80 | chr = str.charCodeAt(i); 81 | hash = ((hash << 5) - hash) + chr; 82 | hash |= 0; 83 | } 84 | return hash; 85 | } 86 | 87 | return { 88 | on: on, 89 | off: off, 90 | add: add, 91 | remove: remove, 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTE: This repo is no longer maintained 2 | 3 | Feel free to fork and do whatever. There is a dev branch that has an incomplete rewrite, but everything here is so old that even I don't get what I was thinking. Enjoy! 4 | 5 | # 3D hex tile system 6 | 7 | ![screenshot](hex-grid.jpg) 8 | 9 | I never found a good (and free!) library for creating perfect hexagons and arranging them in a grid. But I did find [Amit's wonderful explanation](http://www.redblobgames.com/grids/hexagons/), and finally had the time to throw something together. 10 | 11 | You can use the `Board` class with different graph types (hex and square), or you can make your own if you implement the interface. 12 | 13 | Please use this to make awesome hex-based web games. Or port the code and make awesome hex games there. Just make awesome hex games, ok? 14 | 15 | ## Features 16 | 17 | - Simple API for attaching objects to the grid through `Board.js` 18 | - **A* pathfinding** with or without weighted nodes, and a `walkable` flag 19 | - Make maps with [the editor](http://vonwolfehaus.github.io/von-grid/editor/) (autosaves to localstorage, and save/load as `.json` files) 20 | - Varied height 21 | - Sparse maps 22 | - **Mouse interaction** with the grid's cells (over, out, down, up, click, wheel) 23 | - Programmatic geometry, allow you to precisely adjust every aspect of the hexagon 24 | - Square grid that can be used interchangeably 25 | - Include only the hex grid by downloading `dist/hex-grid.js`, or all grid types with `von-grid.js`, etc 26 | 27 | #### Roadmap 28 | 29 | - Improved editor 30 | - Improved API 31 | - Abstract grid 32 | 33 | ## Usage 34 | 35 | #### Basic board 36 | 37 | ![screenshot](hex-grid-basic.jpg) 38 | 39 | ```javascript 40 | var scene = new vg.Scene({ // I made a very handy util for creating three.js scenes quickly 41 | cameraPosition: {x:0, y:150, z:150} 42 | }, true); // 'true' or a config object adds orbit controls 43 | 44 | var grid = new vg.HexGrid(); 45 | 46 | grid.generate({ 47 | size: 4 48 | }); 49 | 50 | var board = new vg.Board(grid); 51 | 52 | board.generateTilemap(); 53 | 54 | scene.add(board.group); 55 | scene.focusOn(board.group); 56 | 57 | update(); 58 | 59 | function update() { 60 | scene.render(); 61 | requestAnimationFrame(update); 62 | } 63 | ``` 64 | 65 | #### Examples 66 | 67 | For the simple examples you can drop them into Chrome, but for ones that require images or models, you'll have to run `gulp serve-examples`. A browser tab will be opened to the examples directory for you. 68 | 69 | ## Editor 70 | 71 | #### [Try it out](http://vonwolfehaus.github.io/von-grid/editor/) 72 | 73 | ![screenshot](editor.png) 74 | -------------------------------------------------------------------------------- /editor/modules/EditorPlane.js: -------------------------------------------------------------------------------- 1 | /* 2 | 2D plane that the user moves mouse around on in order to build maps. Provides a working plane to navigate, and a visual aid for tile placement. 3 | 4 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 5 | */ 6 | define('EditorPlane', function() { 7 | 8 | function EditorPlane(scene, grid, mouse) { 9 | this.nexus = require('nexus'); 10 | this.tower = require('tower'); 11 | 12 | this.geometry = null; 13 | this.mesh = null; 14 | this.material = new THREE.MeshBasicMaterial({ 15 | color: 0xffffff, 16 | side: THREE.DoubleSide 17 | }); 18 | 19 | this.scene = scene; 20 | this.grid = grid; 21 | 22 | this.hoverMesh = this.grid.generateTilePoly(new THREE.MeshBasicMaterial({ 23 | color: 0x1aaeff, 24 | side: THREE.DoubleSide 25 | })); 26 | 27 | this.mouse = mouse; 28 | 29 | /*this.mouse.signal.add(onUserAction, this); 30 | function onUserAction(type, overCell) { 31 | switch (type) { 32 | case vg.MouseCaster.OVER: 33 | if (overCell) { 34 | this.hoverMesh.mesh.visible = false; 35 | } 36 | break; 37 | 38 | case vg.MouseCaster.OUT: 39 | this.hoverMesh.mesh.visible = true; 40 | break; 41 | 42 | case vg.MouseCaster.DOWN: 43 | this.hoverMesh.mesh.visible = false; 44 | break; 45 | 46 | case vg.MouseCaster.UP: 47 | if (!overCell) { 48 | this.hoverMesh.mesh.visible = true; 49 | } 50 | else { 51 | this.hoverMesh.mesh.visible = false; 52 | } 53 | break; 54 | } 55 | }*/ 56 | } 57 | 58 | EditorPlane.prototype = { 59 | 60 | generatePlane: function(width, height) { 61 | if (this.mesh && this.mesh.parent) { 62 | this.mesh.parent.remove(this.mesh); 63 | } 64 | this.geometry = new THREE.PlaneBufferGeometry(width, width, 1, 1); 65 | this.mesh = new THREE.Mesh(this.geometry, this.material); 66 | this.mesh.rotation.x = 90 * vg.DEG_TO_RAD; 67 | this.mesh.position.y -= 0.1; 68 | this.scene.add(this.mesh); 69 | }, 70 | 71 | addHoverMeshToGroup: function(group) { 72 | if (this.hoverMesh.parent) { 73 | this.hoverMesh.parent.remove(this.hoverMesh); 74 | } 75 | group.add(this.hoverMesh); 76 | }, 77 | 78 | update: function() { 79 | if (this.mouse.allHits.length && !this.mouse.pickedObject) { 80 | var cell = this.grid.pixelToCell(this.nexus.input.editorWorldPos); 81 | this.hoverMesh.position.copy(this.grid.cellToPixel(cell)); 82 | this.hoverMesh.position.y += 0.1; 83 | this.hoverMesh.visible = true; 84 | } 85 | else { 86 | this.hoverMesh.visible = false; 87 | } 88 | } 89 | }; 90 | 91 | return EditorPlane; 92 | }); 93 | -------------------------------------------------------------------------------- /examples/js/Sprite.js: -------------------------------------------------------------------------------- 1 | /* 2 | Wraps three.sprite to take care of boilerplate and add data for the board to use. 3 | */ 4 | var Sprite = function(settings) { 5 | var config = { 6 | material: null, 7 | geo: null, 8 | url: null, 9 | container: null, 10 | texture: null, 11 | scale: 1, 12 | highlight: 'rgb(0, 168, 228)', 13 | heightOffset: 0, // how high off the board this object sits 14 | obstacle: false 15 | }; 16 | // attribute override 17 | config = vg.Tools.merge(config, settings); 18 | 19 | this.material = config.material; 20 | this.geo = config.geo; 21 | this.url = config.url; 22 | this.container = config.container; 23 | this.texture = config.texture; 24 | this.scale = config.scale; 25 | this.highlight = config.highlight; 26 | this.heightOffset = config.heightOffset; 27 | this.obstacle = config.obstacle; 28 | 29 | // other objects like the SelectionManager expect these on all objects that are added to the scene 30 | this.active = false; 31 | this.uniqueId = vg.Tools.generateID(); 32 | this.objectType = vg.ENT; 33 | this.tile = null; 34 | 35 | // sanity checks 36 | if (!this.texture) { 37 | if (!this.url) { 38 | console.error('[Sprite] Either provide an image URL, or Threejs Texture'); 39 | } 40 | this.texture = THREE.ImageUtils.loadTexture(this.url); 41 | } 42 | 43 | if (!this.material) { 44 | // for better performance, reuse materials as much as possible 45 | this.material = new THREE.SpriteMaterial({ 46 | map: this.texture, 47 | color: 0xffffff, 48 | // color: 0xff0000, 49 | fog: true 50 | }); 51 | } 52 | 53 | if (!this.highlightMaterial) { 54 | this.highlightMaterial = this.material; 55 | } 56 | 57 | this.view = new THREE.Sprite(this.material); 58 | this.view.scale.set(this.scale, this.scale, this.scale); 59 | this.view.visible = false; 60 | this.view.userData.structure = this; 61 | this.geo = this.view.geometry; 62 | 63 | this.position = this.view.position; 64 | }; 65 | 66 | Sprite.prototype = { 67 | activate: function(x, y, z) { 68 | this.active = true; 69 | this.view.visible = true; 70 | this.position.set(x || 0, y || 0, z || 0); 71 | this.container.add(this.view); 72 | }, 73 | 74 | disable: function() { 75 | this.active = false; 76 | this.view.visible = false; 77 | this.container.remove(this.view); 78 | }, 79 | 80 | update: function() { 81 | 82 | }, 83 | 84 | select: function() { 85 | this.material.color.set(this.highlight); 86 | }, 87 | 88 | deselect: function() { 89 | this.material.color.set('rgb(255, 255, 255)'); 90 | }, 91 | 92 | dispose: function() { 93 | this.container = null; 94 | this.tile = null; 95 | this.position = null; 96 | this.view = null; 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /editor/modules/Input.js: -------------------------------------------------------------------------------- 1 | /* 2 | Translates the MouseCaster's events into more relevant data that the editor uses. 3 | */ 4 | define('Input', function() { 5 | var tower = require('tower'); 6 | var nexus = require('nexus'); 7 | var keyboard = require('keyboard'); 8 | 9 | var Input = function(scene, mouse) { 10 | this.mouse = mouse; 11 | this.mouse.signal.add(this.onMouse, this); 12 | 13 | this.mouseDelta = new THREE.Vector3(); 14 | this.mousePanMinDistance = 0.1; 15 | this.heightStep = 5; 16 | this.editorWorldPos = new THREE.Vector3(); // current grid position of mouse 17 | 18 | this.overTile = null; 19 | 20 | this._travel = 0; 21 | 22 | keyboard.signal.add(function(type, code) { 23 | if (type === keyboard.eventType.DOWN) { 24 | if (code === keyboard.code.SHIFT) nexus.scene.controls.enabled = false; 25 | } 26 | else { 27 | if (code === keyboard.code.SHIFT) nexus.scene.controls.enabled = true; 28 | } 29 | }, this); 30 | }; 31 | 32 | Input.prototype = { 33 | update: function() { 34 | var hit = this.mouse.allHits[0]; 35 | if (hit) { 36 | this.editorWorldPos.x = hit.point.x; 37 | this.editorWorldPos.y = hit.point.y; 38 | this.editorWorldPos.z = hit.point.z; 39 | } 40 | var dx = this.mouseDelta.x - this.mouse.screenPosition.x; 41 | var dy = this.mouseDelta.y - this.mouse.screenPosition.y; 42 | this._travel += Math.sqrt(dx * dx + dy * dy); 43 | }, 44 | 45 | onMouse: function(type, obj) { 46 | var hit, cell; 47 | if (this.mouse.allHits && this.mouse.allHits[0]) { 48 | hit = this.mouse.allHits[0]; 49 | } 50 | switch (type) { 51 | case vg.MouseCaster.WHEEL: 52 | tower.userAction.dispatch(vg.MouseCaster.WHEEL, this.overTile, obj); 53 | break; 54 | 55 | case vg.MouseCaster.OVER: 56 | if (obj) { 57 | this.overTile = obj.select(); 58 | } 59 | tower.userAction.dispatch(vg.MouseCaster.OVER, this.overTile, hit); 60 | break; 61 | 62 | case vg.MouseCaster.OUT: 63 | if (obj) { 64 | obj.deselect(); 65 | this.overTile = null; 66 | } 67 | tower.userAction.dispatch(vg.MouseCaster.OUT, this.overTile, hit); 68 | break; 69 | 70 | case vg.MouseCaster.DOWN: 71 | this.mouseDelta.copy(this.mouse.screenPosition); 72 | tower.userAction.dispatch(vg.MouseCaster.DOWN, this.overTile, hit); 73 | this._travel = 0; 74 | break; 75 | 76 | case vg.MouseCaster.UP: 77 | if (this._travel > this.mousePanMinDistance) { 78 | break; 79 | } 80 | tower.userAction.dispatch(vg.MouseCaster.UP, this.overTile, hit); 81 | break; 82 | 83 | case vg.MouseCaster.CLICK: 84 | tower.userAction.dispatch(vg.MouseCaster.CLICK, this.overTile, hit); 85 | break; 86 | } 87 | } 88 | }; 89 | 90 | return Input; 91 | }); 92 | -------------------------------------------------------------------------------- /src/grids/Tile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Example tile class that constructs its geometry for rendering and holds some gameplay properties. 3 | 4 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 5 | */ 6 | vg.Tile = function(config) { 7 | config = config || {}; 8 | var settings = { 9 | cell: null, // required vg.Cell 10 | geometry: null, // required threejs geometry 11 | material: null // not required but it would improve performance significantly 12 | }; 13 | settings = vg.Tools.merge(settings, config); 14 | 15 | if (!settings.cell || !settings.geometry) { 16 | throw new Error('Missing vg.Tile configuration'); 17 | } 18 | 19 | this.cell = settings.cell; 20 | if (this.cell.tile && this.cell.tile !== this) this.cell.tile.dispose(); // remove whatever was there 21 | this.cell.tile = this; 22 | 23 | this.uniqueID = vg.Tools.generateID(); 24 | 25 | this.geometry = settings.geometry; 26 | this.material = settings.material; 27 | if (!this.material) { 28 | this.material = new THREE.MeshPhongMaterial({ 29 | color: vg.Tools.randomizeRGB('30, 30, 30', 13) 30 | }); 31 | } 32 | 33 | this.objectType = vg.TILE; 34 | this.entity = null; 35 | this.userData = {}; 36 | 37 | this.selected = false; 38 | this.highlight = '0x0084cc'; 39 | 40 | this.mesh = new THREE.Mesh(this.geometry, this.material); 41 | this.mesh.userData.structure = this; 42 | 43 | // create references so we can control orientation through this (Tile), instead of drilling down 44 | this.position = this.mesh.position; 45 | this.rotation = this.mesh.rotation; 46 | 47 | // rotate it to face "up" (the threejs coordinate space is Y+) 48 | this.rotation.x = -90 * vg.DEG_TO_RAD; 49 | this.mesh.scale.set(settings.scale, settings.scale, 1); 50 | 51 | if (this.material.emissive) { 52 | this._emissive = this.material.emissive.getHex(); 53 | } 54 | else { 55 | this._emissive = null; 56 | } 57 | }; 58 | 59 | vg.Tile.prototype = { 60 | select: function() { 61 | if (this.material.emissive) { 62 | this.material.emissive.setHex(this.highlight); 63 | } 64 | this.selected = true; 65 | return this; 66 | }, 67 | 68 | deselect: function() { 69 | if (this._emissive !== null && this.material.emissive) { 70 | this.material.emissive.setHex(this._emissive); 71 | } 72 | this.selected = false; 73 | return this; 74 | }, 75 | 76 | toggle: function() { 77 | if (this.selected) { 78 | this.deselect(); 79 | } 80 | else { 81 | this.select(); 82 | } 83 | return this; 84 | }, 85 | 86 | dispose: function() { 87 | if (this.cell && this.cell.tile) this.cell.tile = null; 88 | this.cell = null; 89 | this.position = null; 90 | this.rotation = null; 91 | if (this.mesh.parent) this.mesh.parent.remove(this.mesh); 92 | this.mesh.userData.structure = null; 93 | this.mesh = null; 94 | this.material = null; 95 | this.userData = null; 96 | this.entity = null; 97 | this.geometry = null; 98 | this._emissive = null; 99 | } 100 | }; 101 | 102 | vg.Tile.prototype.constructor = vg.Tile; 103 | -------------------------------------------------------------------------------- /src/pathing/AStarFinder.js: -------------------------------------------------------------------------------- 1 | /* 2 | A* path-finder based upon http://www.redblobgames.com/pathfinding/a-star/introduction.html 3 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 4 | */ 5 | // 'utils/Tools', 'lib/LinkedList' 6 | vg.AStarFinder = function(finderConfig) { 7 | finderConfig = finderConfig || {}; 8 | 9 | var settings = { 10 | allowDiagonal: false, 11 | heuristicFilter: null 12 | }; 13 | settings = vg.Tools.merge(settings, finderConfig); 14 | 15 | this.allowDiagonal = settings.allowDiagonal; 16 | this.heuristicFilter = settings.heuristicFilter; 17 | 18 | this.list = new vg.LinkedList(); 19 | }; 20 | 21 | vg.AStarFinder.prototype = { 22 | /* 23 | Find and return the path. 24 | @return Array The path, including both start and end positions. Null if it failed. 25 | */ 26 | findPath: function(startNode, endNode, heuristic, grid) { 27 | var current, costSoFar, neighbors, n, i, l; 28 | heuristic = heuristic || this.heuristicFilter; 29 | // clear old values from previous finding 30 | grid.clearPath(); 31 | this.list.clear(); 32 | 33 | // push the start current into the open list 34 | this.list.add(startNode); 35 | 36 | // while the open list is not empty 37 | while (this.list.length > 0) { 38 | // sort so lowest cost is first 39 | this.list.sort(this.compare); 40 | 41 | // pop the position of current which has the minimum `_calcCost` value. 42 | current = this.list.shift(); 43 | current._visited = true; 44 | 45 | // if reached the end position, construct the path and return it 46 | if (current === endNode) { 47 | return vg.PathUtil.backtrace(endNode); 48 | } 49 | 50 | // cycle through each neighbor of the current current 51 | neighbors = grid.getNeighbors(current, this.allowDiagonal, heuristic); 52 | for (i = 0, l = neighbors.length; i < l; i++) { 53 | n = neighbors[i]; 54 | 55 | if (!n.walkable) { 56 | continue; 57 | } 58 | 59 | costSoFar = current._calcCost + grid.distance(current, n); 60 | 61 | // check if the neighbor has not been inspected yet, or can be reached with smaller cost from the current node 62 | if (!n._visited || costSoFar < n._calcCost) { 63 | n._visited = true; 64 | n._parent = current; 65 | n._calcCost = costSoFar; 66 | // console.log(n); 67 | // _priority is the most important property, since it makes the algorithm "greedy" and seek the goal. 68 | // otherwise it behaves like a brushfire/breadth-first 69 | n._priority = costSoFar + grid.distance(endNode, n); 70 | 71 | // check neighbor if it's the end current as well--often cuts steps by a significant amount 72 | if (n === endNode) { 73 | return vg.PathUtil.backtrace(endNode); 74 | } 75 | // console.log(n); 76 | this.list.add(n); 77 | } 78 | // console.log(this.list); 79 | } // end for each neighbor 80 | } // end while not open list empty 81 | // failed to find the path 82 | return null; 83 | }, 84 | 85 | compare: function(nodeA, nodeB) { 86 | return nodeA._priority - nodeB._priority; 87 | } 88 | }; 89 | 90 | vg.AStarFinder.prototype.constructor = vg.AStarFinder; -------------------------------------------------------------------------------- /editor/modules/keyboard.js: -------------------------------------------------------------------------------- 1 | define('keyboard', function() { 2 | 3 | function onDown(evt) { 4 | switch (evt.keyCode) { 5 | case 16: 6 | k.shift = true; 7 | break; 8 | case 17: 9 | k.ctrl = true; 10 | break; 11 | } 12 | k.signal.dispatch(k.eventType.DOWN, evt.keyCode); 13 | } 14 | 15 | function onUp(evt) { 16 | switch (evt.keyCode) { 17 | case 16: 18 | k.shift = false; 19 | break; 20 | case 17: 21 | k.ctrl = false; 22 | break; 23 | } 24 | k.signal.dispatch(k.eventType.UP, evt.keyCode); 25 | } 26 | 27 | var k = { 28 | shift: false, 29 | ctrl: false, 30 | 31 | eventType: { 32 | DOWN: 'down', 33 | UP: 'up' 34 | }, 35 | 36 | signal: new vg.Signal(), 37 | 38 | on: function() { 39 | document.addEventListener('keydown', onDown, false); 40 | document.addEventListener('keyup', onUp, false); 41 | }, 42 | 43 | off: function() { 44 | document.removeEventListener('keydown', onDown); 45 | document.removeEventListener('keyup', onUp); 46 | }, 47 | 48 | code: { 49 | A: 'A'.charCodeAt(0), 50 | B: 'B'.charCodeAt(0), 51 | C: 'C'.charCodeAt(0), 52 | D: 'D'.charCodeAt(0), 53 | E: 'E'.charCodeAt(0), 54 | F: 'F'.charCodeAt(0), 55 | G: 'G'.charCodeAt(0), 56 | H: 'H'.charCodeAt(0), 57 | I: 'I'.charCodeAt(0), 58 | J: 'J'.charCodeAt(0), 59 | K: 'K'.charCodeAt(0), 60 | L: 'L'.charCodeAt(0), 61 | M: 'M'.charCodeAt(0), 62 | N: 'N'.charCodeAt(0), 63 | O: 'O'.charCodeAt(0), 64 | P: 'P'.charCodeAt(0), 65 | Q: 'Q'.charCodeAt(0), 66 | R: 'R'.charCodeAt(0), 67 | S: 'S'.charCodeAt(0), 68 | T: 'T'.charCodeAt(0), 69 | U: 'U'.charCodeAt(0), 70 | V: 'V'.charCodeAt(0), 71 | W: 'W'.charCodeAt(0), 72 | X: 'X'.charCodeAt(0), 73 | Y: 'Y'.charCodeAt(0), 74 | Z: 'Z'.charCodeAt(0), 75 | ZERO: '0'.charCodeAt(0), 76 | ONE: '1'.charCodeAt(0), 77 | TWO: '2'.charCodeAt(0), 78 | THREE: '3'.charCodeAt(0), 79 | FOUR: '4'.charCodeAt(0), 80 | FIVE: '5'.charCodeAt(0), 81 | SIX: '6'.charCodeAt(0), 82 | SEVEN: '7'.charCodeAt(0), 83 | EIGHT: '8'.charCodeAt(0), 84 | NINE: '9'.charCodeAt(0), 85 | NUMPAD_0: 96, 86 | NUMPAD_1: 97, 87 | NUMPAD_2: 98, 88 | NUMPAD_3: 99, 89 | NUMPAD_4: 100, 90 | NUMPAD_5: 101, 91 | NUMPAD_6: 102, 92 | NUMPAD_7: 103, 93 | NUMPAD_8: 104, 94 | NUMPAD_9: 105, 95 | NUMPAD_MULTIPLY: 106, 96 | NUMPAD_ADD: 107, 97 | NUMPAD_ENTER: 108, 98 | NUMPAD_SUBTRACT: 109, 99 | NUMPAD_DECIMAL: 110, 100 | NUMPAD_DIVIDE: 111, 101 | F1: 112, 102 | F2: 113, 103 | F3: 114, 104 | F4: 115, 105 | F5: 116, 106 | F6: 117, 107 | F7: 118, 108 | F8: 119, 109 | F9: 120, 110 | F10: 121, 111 | F11: 122, 112 | F12: 123, 113 | F13: 124, 114 | F14: 125, 115 | F15: 126, 116 | COLON: 186, 117 | EQUALS: 187, 118 | UNDERSCORE: 189, 119 | QUESTION_MARK: 191, 120 | TILDE: 192, 121 | OPEN_BRACKET: 219, 122 | BACKWARD_SLASH: 220, 123 | CLOSED_BRACKET: 221, 124 | QUOTES: 222, 125 | BACKSPACE: 8, 126 | TAB: 9, 127 | CLEAR: 12, 128 | ENTER: 13, 129 | SHIFT: 16, 130 | CTRL: 17, 131 | ALT: 18, 132 | CAPS_LOCK: 20, 133 | ESC: 27, 134 | SPACEBAR: 32, 135 | PAGE_UP: 33, 136 | PAGE_DOWN: 34, 137 | END: 35, 138 | HOME: 36, 139 | LEFT: 37, 140 | UP: 38, 141 | RIGHT: 39, 142 | DOWN: 40, 143 | INSERT: 45, 144 | DELETE: 46, 145 | HELP: 47, 146 | NUM_LOCK: 144 147 | } 148 | }; 149 | 150 | return k; 151 | }); 152 | -------------------------------------------------------------------------------- /editor/modules/Editor.js: -------------------------------------------------------------------------------- 1 | /* 2 | Manages cells and objects on the map. 3 | */ 4 | define('Editor', function() { 5 | var tower = require('tower'); 6 | var nexus = require('nexus'); 7 | var keyboard = require('keyboard'); 8 | var motor = require('motor'); 9 | 10 | // TODO: get these values from UI 11 | var heightStep = 3; 12 | 13 | // PRIVATE 14 | var lastHeight = 1; 15 | var currentGridCell = null; 16 | var prevGridCell = new THREE.Vector3(); 17 | var _cel = new vg.Cell(); 18 | 19 | tower.userAction.add(onUserAction, this); 20 | motor.add(update); 21 | 22 | function update() { 23 | currentGridCell = nexus.grid.pixelToCell(nexus.input.editorWorldPos); 24 | if (nexus.mouse.down && keyboard.shift && nexus.mouse.allHits && nexus.mouse.allHits.length) { 25 | // only check if the user's mouse is over the editor plane 26 | if (!currentGridCell.equals(prevGridCell)) { 27 | addTile(currentGridCell); 28 | } 29 | prevGridCell.copy(currentGridCell); 30 | } 31 | } 32 | 33 | function onUserAction(type, overTile, data) { 34 | var hit = nexus.mouse.allHits[0] 35 | switch (type) { 36 | case vg.MouseCaster.WHEEL: 37 | if (keyboard.shift && overTile) { 38 | if (!overTile.cell) { 39 | overTile.dispose(); 40 | return; 41 | } 42 | _cel.copy(overTile.cell); 43 | _cel.tile = null; 44 | 45 | var dif = lastHeight - data; 46 | var last = _cel.h; 47 | _cel.h += dif > 0 ? -heightStep : heightStep; 48 | if (_cel.h < 1) _cel.h = 1; 49 | 50 | nexus.mouse.wheel = Math.round((_cel.h / heightStep) + (dif > 0 ? -1 : 1)); 51 | lastHeight = nexus.mouse.wheel; 52 | 53 | if (last === _cel.h) return; 54 | removeTile(overTile); 55 | 56 | var tile = addTile(_cel); 57 | tile.select(); 58 | 59 | tower.tileAction.dispatch(tower.TILE_CHANGE_HEIGHT, tile); 60 | } 61 | break; 62 | 63 | case vg.MouseCaster.OVER: 64 | if (keyboard.shift) { 65 | if (overTile && nexus.mouse.rightDown) { 66 | removeTile(overTile); 67 | } 68 | else if (!overTile && nexus.mouse.down) { 69 | addTile(currentGridCell); 70 | } 71 | } 72 | break; 73 | 74 | case vg.MouseCaster.OUT: 75 | 76 | break; 77 | 78 | case vg.MouseCaster.DOWN: 79 | if (keyboard.shift && nexus.mouse.down && data && !overTile) { 80 | // if shift is down then they're painting, so add a tile immediately 81 | addTile(currentGridCell); 82 | } 83 | break; 84 | 85 | case vg.MouseCaster.UP: 86 | if (nexus.mouse.down && data && !overTile) { 87 | // create a new tile, if one isn't already there 88 | addTile(currentGridCell); 89 | } 90 | else if (nexus.mouse.rightDown && overTile) { 91 | // remove a tile if it's there and right mouse is down 92 | removeTile(overTile); 93 | } 94 | break; 95 | } 96 | } 97 | 98 | function addTile(cell) { 99 | if (!cell || nexus.board.getTileAtCell(cell)) return; 100 | 101 | var newCell = new vg.Cell(); 102 | newCell.copy(cell); 103 | newCell.h = Math.abs(nexus.mouse.wheel * heightStep); 104 | 105 | var newTile = nexus.grid.generateTile(newCell, 0.95); 106 | 107 | nexus.board.addTile(newTile); 108 | 109 | tower.tileAction.dispatch(tower.TILE_ADD, newTile); 110 | 111 | return newTile; 112 | } 113 | 114 | function removeTile(overTile) { 115 | nexus.board.removeTile(overTile); 116 | 117 | tower.tileAction.dispatch(tower.TILE_REMOVE, overTile); 118 | } 119 | 120 | return { 121 | 122 | } 123 | }); -------------------------------------------------------------------------------- /examples/sprite-selection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/utils/Scene.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sets up and manages a THREEjs container, camera, and light, making it easy to get going. 3 | Also provides camera control. 4 | 5 | Assumes full screen. 6 | */ 7 | // 'utils/Tools' 8 | vg.Scene = function(sceneConfig, controlConfig) { 9 | var sceneSettings = { 10 | element: document.body, 11 | alpha: true, 12 | antialias: true, 13 | clearColor: '#fff', 14 | sortObjects: false, 15 | fog: null, 16 | light: new THREE.DirectionalLight(0xffffff), 17 | lightPosition: null, 18 | cameraType: 'PerspectiveCamera', 19 | cameraPosition: null, // {x, y, z} 20 | orthoZoom: 4 21 | }; 22 | 23 | var controlSettings = { 24 | minDistance: 100, 25 | maxDistance: 1000, 26 | zoomSpeed: 2, 27 | noZoom: false 28 | }; 29 | 30 | sceneSettings = vg.Tools.merge(sceneSettings, sceneConfig); 31 | if (typeof controlConfig !== 'boolean') { 32 | controlSettings = vg.Tools.merge(controlSettings, controlConfig); 33 | } 34 | 35 | this.renderer = new THREE.WebGLRenderer({ 36 | alpha: sceneSettings.alpha, 37 | antialias: sceneSettings.antialias 38 | }); 39 | this.renderer.setClearColor(sceneSettings.clearColor, 0); 40 | this.renderer.sortObjects = sceneSettings.sortObjects; 41 | 42 | this.width = window.innerWidth; 43 | this.height = window.innerHeight; 44 | 45 | this.orthoZoom = sceneSettings.orthoZoom; 46 | 47 | this.container = new THREE.Scene(); 48 | this.container.fog = sceneSettings.fog; 49 | 50 | this.container.add(new THREE.AmbientLight(0xdddddd)); 51 | 52 | if (!sceneSettings.lightPosition) { 53 | sceneSettings.light.position.set(-1, 1, -1).normalize(); 54 | } 55 | this.container.add(sceneSettings.light); 56 | 57 | if (sceneSettings.cameraType === 'OrthographicCamera') { 58 | var width = window.innerWidth / this.orthoZoom; 59 | var height = window.innerHeight / this.orthoZoom; 60 | this.camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 1, 5000); 61 | } 62 | else { 63 | this.camera = new THREE.PerspectiveCamera(50, this.width / this.height, 1, 5000); 64 | } 65 | 66 | this.contolled = !!controlConfig; 67 | if (this.contolled) { 68 | this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); 69 | this.controls.minDistance = controlSettings.minDistance; 70 | this.controls.maxDistance = controlSettings.maxDistance; 71 | this.controls.zoomSpeed = controlSettings.zoomSpeed; 72 | this.controls.noZoom = controlSettings.noZoom; 73 | } 74 | 75 | if (sceneSettings.cameraPosition) { 76 | this.camera.position.copy(sceneSettings.cameraPosition); 77 | } 78 | 79 | window.addEventListener('resize', function onWindowResize() { 80 | this.width = window.innerWidth; 81 | this.height = window.innerHeight; 82 | if (this.camera.type === 'OrthographicCamera') { 83 | var width = this.width / this.orthoZoom; 84 | var height = this.height / this.orthoZoom; 85 | this.camera.left = width / -2; 86 | this.camera.right = width / 2; 87 | this.camera.top = height / 2; 88 | this.camera.bottom = height / -2; 89 | } 90 | else { 91 | this.camera.aspect = this.width / this.height; 92 | } 93 | this.camera.updateProjectionMatrix(); 94 | this.renderer.setSize(this.width, this.height); 95 | }.bind(this), false); 96 | 97 | this.attachTo(sceneSettings.element); 98 | }; 99 | 100 | vg.Scene.prototype = { 101 | 102 | attachTo: function(element) { 103 | element.style.width = this.width + 'px'; 104 | element.style.height = this.height + 'px'; 105 | this.renderer.setPixelRatio(window.devicePixelRatio); 106 | this.renderer.setSize(this.width, this.height); 107 | element.appendChild(this.renderer.domElement); 108 | }, 109 | 110 | add: function(mesh) { 111 | this.container.add(mesh); 112 | }, 113 | 114 | remove: function(mesh) { 115 | this.container.remove(mesh); 116 | }, 117 | 118 | render: function() { 119 | if (this.contolled) this.controls.update(); 120 | this.renderer.render(this.container, this.camera); 121 | }, 122 | 123 | updateOrthoZoom: function() { 124 | if (this.orthoZoom <= 0) { 125 | this.orthoZoom = 0; 126 | return; 127 | } 128 | var width = this.width / this.orthoZoom; 129 | var height = this.height / this.orthoZoom; 130 | this.camera.left = width / -2; 131 | this.camera.right = width / 2; 132 | this.camera.top = height / 2; 133 | this.camera.bottom = height / -2; 134 | this.camera.updateProjectionMatrix(); 135 | }, 136 | 137 | focusOn: function(obj) { 138 | this.camera.lookAt(obj.position); 139 | } 140 | }; 141 | 142 | vg.Scene.prototype.constructor = vg.Scene; 143 | -------------------------------------------------------------------------------- /src/Board.js: -------------------------------------------------------------------------------- 1 | /* 2 | Interface to the grid. Holds data about the visual representation of the cells (tiles). 3 | 4 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 5 | */ 6 | vg.Board = function(grid, finderConfig) { 7 | if (!grid) throw new Error('You must pass in a grid system for the board to use.'); 8 | 9 | this.tiles = []; 10 | this.tileGroup = null; // only for tiles 11 | 12 | this.group = new THREE.Object3D(); // can hold all entities, also holds tileGroup, never trashed 13 | 14 | this.grid = null; 15 | this.overlay = null; 16 | this.finder = new vg.AStarFinder(finderConfig); 17 | // need to keep a resource cache around, so this Loader does that, use it instead of THREE.ImageUtils 18 | vg.Loader.init(); 19 | 20 | this.setGrid(grid); 21 | }; 22 | 23 | vg.Board.prototype = { 24 | setEntityOnTile: function(entity, tile) { 25 | // snap an entity's position to a tile; merely copies position 26 | var pos = this.grid.cellToPixel(tile.cell); 27 | entity.position.copy(pos); 28 | // adjust for any offset after the entity was set directly onto the tile 29 | entity.position.y += entity.heightOffset || 0; 30 | // remove entity from old tile 31 | if (entity.tile) { 32 | entity.tile.entity = null; 33 | } 34 | // set new situation 35 | entity.tile = tile; 36 | tile.entity = entity; 37 | }, 38 | 39 | addTile: function(tile) { 40 | var i = this.tiles.indexOf(tile); 41 | if (i === -1) this.tiles.push(tile); 42 | else return; 43 | 44 | this.snapTileToGrid(tile); 45 | tile.position.y = 0; 46 | 47 | this.tileGroup.add(tile.mesh); 48 | this.grid.add(tile.cell); 49 | 50 | tile.cell.tile = tile; 51 | }, 52 | 53 | removeTile: function(tile) { 54 | if (!tile) return; // was already removed somewhere 55 | var i = this.tiles.indexOf(tile); 56 | this.grid.remove(tile.cell); 57 | 58 | if (i !== -1) this.tiles.splice(i, 1); 59 | // this.tileGroup.remove(tile.mesh); 60 | 61 | tile.dispose(); 62 | }, 63 | 64 | removeAllTiles: function() { 65 | if (!this.tileGroup) return; 66 | var tiles = this.tileGroup.children; 67 | for (var i = 0; i < tiles.length; i++) { 68 | this.tileGroup.remove(tiles[i]); 69 | } 70 | }, 71 | 72 | getTileAtCell: function(cell) { 73 | var h = this.grid.cellToHash(cell); 74 | return cell.tile || (typeof this.grid.cells[h] !== 'undefined' ? this.grid.cells[h].tile : null); 75 | }, 76 | 77 | snapToGrid: function(pos) { 78 | var cell = this.grid.pixelToCell(pos); 79 | pos.copy(this.grid.cellToPixel(cell)); 80 | }, 81 | 82 | snapTileToGrid: function(tile) { 83 | if (tile.cell) { 84 | tile.position.copy(this.grid.cellToPixel(tile.cell)); 85 | } 86 | else { 87 | var cell = this.grid.pixelToCell(tile.position); 88 | tile.position.copy(this.grid.cellToPixel(cell)); 89 | } 90 | return tile; 91 | }, 92 | 93 | getRandomTile: function() { 94 | var i = vg.Tools.randomInt(0, this.tiles.length-1); 95 | return this.tiles[i]; 96 | }, 97 | 98 | findPath: function(startTile, endTile, heuristic) { 99 | return this.finder.findPath(startTile.cell, endTile.cell, heuristic, this.grid); 100 | }, 101 | 102 | setGrid: function(newGrid) { 103 | this.group.remove(this.tileGroup); 104 | if (this.grid && newGrid !== this.grid) { 105 | this.removeAllTiles(); 106 | this.tiles.forEach(function(t) { 107 | this.grid.remove(t.cell); 108 | t.dispose(); 109 | }); 110 | this.grid.dispose(); 111 | } 112 | this.grid = newGrid; 113 | this.tiles = []; 114 | this.tileGroup = new THREE.Object3D(); 115 | this.group.add(this.tileGroup); 116 | }, 117 | 118 | generateOverlay: function(size) { 119 | var mat = new THREE.LineBasicMaterial({ 120 | color: 0x000000, 121 | opacity: 0.3 122 | }); 123 | 124 | if (this.overlay) { 125 | this.group.remove(this.overlay); 126 | } 127 | 128 | this.overlay = new THREE.Object3D(); 129 | 130 | this.grid.generateOverlay(size, this.overlay, mat); 131 | 132 | this.group.add(this.overlay); 133 | }, 134 | 135 | generateTilemap: function(config) { 136 | this.reset(); 137 | 138 | var tiles = this.grid.generateTiles(config); 139 | this.tiles = tiles; 140 | 141 | this.tileGroup = new THREE.Object3D(); 142 | for (var i = 0; i < tiles.length; i++) { 143 | this.tileGroup.add(tiles[i].mesh); 144 | } 145 | 146 | this.group.add(this.tileGroup); 147 | }, 148 | 149 | reset: function() { 150 | // removes all tiles from the scene, but leaves the grid intact 151 | this.removeAllTiles(); 152 | if (this.tileGroup) this.group.remove(this.tileGroup); 153 | } 154 | }; 155 | 156 | vg.Board.prototype.constructor = vg.Board; 157 | -------------------------------------------------------------------------------- /editor/modules/main.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function(evt) { 2 | var data = require('data'); 3 | var tower = require('tower'); 4 | var nexus = require('nexus'); 5 | var keyboard = require('keyboard'); 6 | var motor = require('motor'); 7 | 8 | var Input = require('Input'); 9 | var EditorPlane = require('EditorPlane'); 10 | 11 | data.load(); 12 | var map = data.get('map'); 13 | 14 | var timeTilAutoSave = 200; // timer runs per frame, 60fps 15 | var saveTimer = 10; 16 | var dirtyMap = false; 17 | var shiftDown = false; 18 | var paintMode = false; 19 | var deleteMode = false; 20 | var addMode = false; 21 | 22 | var saveBtn = document.getElementById('save-btn'); 23 | saveBtn.onmouseup = function(evt) { 24 | saveMap(); 25 | return false; 26 | }; 27 | 28 | var loadBtn = document.getElementById('load-btn'); 29 | loadBtn.addEventListener('click', function() { 30 | fileInput.click(); 31 | }, false); 32 | 33 | var fileInput = document.createElement('input'); 34 | fileInput.type = 'file'; 35 | fileInput.addEventListener('change', function(evt) { 36 | var file = fileInput.files[0]; 37 | if (!file) { 38 | return; 39 | } 40 | 41 | var reader = new FileReader(); 42 | reader.onload = function(e) { 43 | var json = null; 44 | try { 45 | json = JSON.parse(e.target.result); 46 | } 47 | catch(err) { 48 | console.warn('File is not json format'); 49 | return; 50 | } 51 | loadMap(json); 52 | }; 53 | 54 | reader.readAsText(file); 55 | 56 | return false; 57 | }); 58 | 59 | keyboard.on(); 60 | motor.on(); 61 | 62 | // setup the thing 63 | var canvas = document.getElementById('view'); 64 | var scene = new vg.Scene({ 65 | element: canvas, 66 | cameraPosition: {x:0, y:300, z:120} 67 | }, true); 68 | 69 | // listen to the orbit controls to disable the raycaster while user adjusts the view 70 | scene.controls.addEventListener('wheel', onControlWheel); 71 | 72 | var grid = new vg.HexGrid({ 73 | rings: 5, 74 | cellSize: 10 75 | }); 76 | var board = new vg.Board(grid); 77 | var mouse = new vg.MouseCaster(board.group, scene.camera, canvas); 78 | var input = new Input(board.group, mouse); 79 | var plane = new EditorPlane(board.group, grid, mouse); 80 | 81 | nexus.input = input; 82 | nexus.plane = plane; 83 | nexus.board = board; 84 | nexus.grid = grid; 85 | nexus.scene = scene; 86 | nexus.mouse = mouse; 87 | 88 | var boardSize = 20; // TODO: get from settings 89 | plane.generatePlane(boardSize * boardSize * 1.8, boardSize * boardSize * 1.8); 90 | plane.addHoverMeshToGroup(scene.container); 91 | 92 | board.generateOverlay(boardSize); 93 | 94 | tower.tileAction.add(onMapChange, this); 95 | 96 | // scene.add(board.group); 97 | scene.focusOn(board.group); 98 | 99 | if (map) { 100 | loadMap(map); 101 | } 102 | else { 103 | board.generateTilemap(); 104 | map = grid.toJSON(); 105 | data.set('map', map); 106 | console.log('Created a new map'); 107 | } 108 | scene.add(board.group); 109 | 110 | function update() { 111 | if (wheelTimer < 10) { 112 | wheelTimer++; 113 | if (wheelTimer === 10) { 114 | mouse.active = true; 115 | } 116 | } 117 | if (dirtyMap) { 118 | saveTimer--; 119 | if (saveTimer === 0) { 120 | dirtyMap = false; 121 | data.set('map', map); 122 | data.save(); 123 | console.log('Map saved'); 124 | } 125 | } 126 | mouse.update(); 127 | input.update(); 128 | plane.update(); 129 | scene.render(); 130 | }; 131 | motor.add(update); 132 | 133 | var wheelTimer = 10; 134 | function onControlWheel() { 135 | mouse.active = false; 136 | wheelTimer = 0; 137 | } 138 | 139 | function onMapChange() { 140 | dirtyMap = true; 141 | saveTimer = timeTilAutoSave; 142 | map = grid.toJSON(); 143 | } 144 | 145 | function loadMap(json) { 146 | grid.fromJSON(json); 147 | board.setGrid(grid); 148 | 149 | if (json.autogenerated) { 150 | board.generateTilemap(); 151 | } 152 | console.log('Map load complete'); 153 | } 154 | 155 | function saveMap() { 156 | var output = null; 157 | 158 | map = grid.toJSON(); 159 | 160 | try { 161 | output = JSON.stringify(map, null, '\t'); 162 | output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1'); 163 | } catch (e) { 164 | output = JSON.stringify(map); 165 | } 166 | 167 | exportString(output, 'hex-map.json'); 168 | } 169 | 170 | // taken from https://github.com/mrdoob/three.js/blob/master/editor/js/Menubar.File.js 171 | var link = document.createElement('a'); 172 | link.style.display = 'none'; 173 | document.body.appendChild(link); 174 | 175 | function exportString(output, filename) { 176 | var blob = new Blob([output], {type: 'text/plain'}); 177 | var objectURL = URL.createObjectURL(blob); 178 | 179 | link.href = objectURL; 180 | link.download = filename || 'data.json'; 181 | link.target = '_blank'; 182 | 183 | var evt = document.createEvent('MouseEvents'); 184 | evt.initMouseEvent( 185 | 'click', true, false, window, 0, 0, 0, 0, 0, 186 | false, false, false, false, 0, null 187 | ); 188 | link.dispatchEvent(evt); 189 | } 190 | }); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var fs = require('fs'); 3 | var $ = require('gulp-load-plugins')(); 4 | var del = require('del'); 5 | var runSequence = require('run-sequence'); 6 | var browserSync = require('browser-sync').create(); 7 | var reload = browserSync.reload; 8 | var path = require('path'); 9 | 10 | var pkg = require('./package.json'); 11 | //var preprocessOpts = {context: { NODE_ENV: process.env.NODE_ENV || 'development', DEBUG: true}}; 12 | 13 | var dist = 'dist'; 14 | var src = 'src'; 15 | 16 | var glob = { 17 | scripts: [src+'/vg.js', src+'/**/*.js'], 18 | hexScripts: [src+'/vg.js', src+'/**/*.js', '!src/grids/Square.js', '!src/grids/SquareGrid.js'], 19 | sqrScripts: [src+'/vg.js', src+'/**/*.js', '!src/grids/Hex.js', '!src/grids/HexGrid.js'], 20 | editorScripts: ['editor/ui/**/*.js', 'editor/modules/**/*.js'], 21 | styles: src+'/**/*.styl' 22 | }; 23 | 24 | 25 | /*---------------------------------------------------------------------- 26 | MACRO 27 | */ 28 | 29 | gulp.task('default', ['clean'], function() { 30 | runSequence( 31 | ['scripts'] 32 | ); 33 | }); 34 | 35 | gulp.task('clean', del.bind(null, [dist])); 36 | 37 | gulp.task('dev', ['clean'], function() { 38 | runSequence( 39 | ['scripts'], 40 | ['watch'] 41 | ); 42 | }); 43 | 44 | gulp.task('dev-ed', ['clean'], function() { 45 | runSequence( 46 | ['scripts', 'scripts-editor'], 47 | ['serve-editor'] 48 | ); 49 | }); 50 | 51 | /*---------------------------------------------------------------------- 52 | SCRIPTS 53 | */ 54 | 55 | gulp.task('scripts', ['all', 'hex', 'sqr']); 56 | 57 | gulp.task('all', function() { 58 | return gulp.src(glob.scripts) 59 | .pipe($.plumber({errorHandler: handleErrors})) 60 | .pipe($.eslint({ fix: true })) 61 | .pipe($.eslint.formatEach()) 62 | .pipe($.eslint.failOnError()) 63 | .pipe($.sourcemaps.init()) 64 | .pipe($.concat('von-grid.min.js')) 65 | .pipe($.uglify()) 66 | .pipe($.sourcemaps.write('.')) 67 | .pipe(gulp.dest(dist)) 68 | .pipe(browserSync.stream()); 69 | }); 70 | 71 | gulp.task('hex', function() { 72 | return gulp.src(glob.hexScripts) 73 | .pipe($.plumber({errorHandler: handleErrors})) 74 | .pipe($.sourcemaps.init()) 75 | .pipe($.concat('hex-grid.min.js')) 76 | .pipe($.uglify()) 77 | .pipe($.sourcemaps.write('.')) 78 | .pipe(gulp.dest(dist)) 79 | .pipe(browserSync.stream()); 80 | }); 81 | 82 | gulp.task('sqr', function() { 83 | return gulp.src(glob.sqrScripts) 84 | .pipe($.plumber({errorHandler: handleErrors})) 85 | .pipe($.sourcemaps.init()) 86 | .pipe($.concat('sqr-grid.min.js')) 87 | .pipe($.uglify()) 88 | .pipe($.sourcemaps.write('.')) 89 | .pipe(gulp.dest(dist)) 90 | .pipe(browserSync.stream()); 91 | }); 92 | 93 | gulp.task('scripts-editor', function() { 94 | return gulp.src(glob.editorScripts) 95 | .pipe($.plumber({errorHandler: handleErrors})) 96 | .pipe($.sortAmd()) 97 | //.pipe($.eslint({ fix: true })) 98 | //.pipe($.eslint.formatEach()) 99 | //.pipe($.eslint.failOnError()) 100 | .pipe($.addSrc.prepend('./editor/lib/define.min.js')) 101 | .pipe($.sourcemaps.init()) 102 | .pipe($.concat('app.js')) 103 | //.pipe($.uglify()) 104 | .pipe($.sourcemaps.write('.')) 105 | .pipe(gulp.dest('editor')) 106 | .pipe(browserSync.stream()); 107 | }); 108 | 109 | /*---------------------------------------------------------------------- 110 | CSS 111 | */ 112 | /* 113 | gulp.task('styles', function() { 114 | return gulp.src(glob.styles) 115 | .pipe($.plumber({errorHandler: handleErrors})) 116 | .pipe($.sourcemaps.init()) 117 | .pipe($.stylus({ 118 | compress: true 119 | })) 120 | .pipe($.autoprefixer()) 121 | .pipe($.concat('styles.css')) 122 | .pipe($.sourcemaps.write('.')) 123 | .pipe(gulp.dest(dist)) 124 | }); 125 | */ 126 | 127 | /*---------------------------------------------------------------------- 128 | SERVER 129 | */ 130 | 131 | // Defines the list of resources to watch for changes. 132 | function watch() { 133 | gulp.watch(glob.scripts, ['scripts', reload]); 134 | //gulp.watch(glob.styles, ['styles', reload]); 135 | } 136 | 137 | function serve(dir) { 138 | browserSync.init({ 139 | notify: false, 140 | server: { 141 | baseDir: ['./', './'+dir], 142 | index: './'+dir+'/index.html' 143 | } 144 | }); 145 | 146 | browserSync.watch(dist+'/**/*.*').on('change', reload); 147 | gulp.watch(glob.scripts, ['scripts']); 148 | } 149 | 150 | gulp.task('watch', function() { 151 | watch(); 152 | }); 153 | 154 | gulp.task('serve-editor', function() { 155 | gulp.watch(glob.editorScripts, ['scripts-editor', reload]); 156 | serve('editor'); 157 | }); 158 | 159 | gulp.task('serve-examples', function() { 160 | //gulp.watch(glob.editorScripts, ['scripts-editor', reload]); 161 | browserSync.watch('examples/**/*.*').on('change', reload); 162 | serve('examples'); 163 | }); 164 | 165 | /*---------------------------------------------------------------------- 166 | HELPERS 167 | */ 168 | 169 | function handleErrors() { 170 | var args = Array.prototype.slice.call(arguments); 171 | // Send error to notification center with gulp-notify 172 | $.notify.onError({ 173 | title: 'Build error', 174 | message: '<%= error%>', 175 | showStack: true 176 | }).apply(this, args); 177 | 178 | // Keep gulp from hanging on this task 179 | this.emit('end'); 180 | } 181 | -------------------------------------------------------------------------------- /src/utils/Tools.js: -------------------------------------------------------------------------------- 1 | vg.Tools = { 2 | clamp: function(val, min, max) { 3 | return Math.max(min, Math.min(max, val)); 4 | }, 5 | 6 | sign: function(val) { 7 | return val && val / Math.abs(val); 8 | }, 9 | 10 | /** 11 | * If one value is passed, it will return something from -val to val. 12 | * Else it returns a value between the range specified by min, max. 13 | */ 14 | random: function(min, max) { 15 | if (arguments.length === 1) { 16 | return (Math.random() * min) - (min * 0.5); 17 | } 18 | return Math.random() * (max - min) + min; 19 | }, 20 | 21 | // from min to (and including) max 22 | randomInt: function(min, max) { 23 | if (arguments.length === 1) { 24 | return (Math.random() * min) - (min * 0.5) | 0; 25 | } 26 | return (Math.random() * (max - min + 1) + min) | 0; 27 | }, 28 | 29 | normalize: function(v, min, max) { 30 | return (v - min) / (max - min); 31 | }, 32 | 33 | getShortRotation: function(angle) { 34 | angle %= this.TAU; 35 | if (angle > this.PI) { 36 | angle -= this.TAU; 37 | } 38 | else if (angle < -this.PI) { 39 | angle += this.TAU; 40 | } 41 | return angle; 42 | }, 43 | 44 | generateID: function() { 45 | return Math.random().toString(36).slice(2) + Date.now(); 46 | }, 47 | 48 | isPlainObject: function(obj) { 49 | if (typeof(obj) !== 'object' || obj.nodeType || obj === obj.window) { 50 | return false; 51 | } 52 | // The try/catch suppresses exceptions thrown when attempting to access the 'constructor' property of certain host objects, ie. |window.location| 53 | // https://bugzilla.mozilla.org/show_bug.cgi?id=814622 54 | try { 55 | if (obj.constructor && !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf')) { 56 | return false; 57 | } 58 | } 59 | catch (err) { 60 | return false; 61 | } 62 | // If the function hasn't returned already, we're confident that 63 | // |obj| is a plain object, created by {} or constructed with new Object 64 | return true; 65 | }, 66 | 67 | // https://github.com/KyleAMathews/deepmerge/blob/master/index.js 68 | merge: function(target, src) { 69 | var self = this, array = Array.isArray(src); 70 | var dst = array && [] || {}; 71 | if (array) { 72 | target = target || []; 73 | dst = dst.concat(target); 74 | src.forEach(function(e, i) { 75 | if (typeof dst[i] === 'undefined') { 76 | dst[i] = e; 77 | } 78 | else if (self.isPlainObject(e)) { 79 | dst[i] = self.merge(target[i], e); 80 | } 81 | else { 82 | if (target.indexOf(e) === -1) { 83 | dst.push(e); 84 | } 85 | } 86 | }); 87 | return dst; 88 | } 89 | if (target && self.isPlainObject(target)) { 90 | Object.keys(target).forEach(function (key) { 91 | dst[key] = target[key]; 92 | }); 93 | } 94 | Object.keys(src).forEach(function (key) { 95 | if (!src[key] || !self.isPlainObject(src[key])) { 96 | dst[key] = src[key]; 97 | } 98 | else { 99 | if (!target[key]) { 100 | dst[key] = src[key]; 101 | } 102 | else { 103 | dst[key] = self.merge(target[key], src[key]); 104 | } 105 | } 106 | }); 107 | return dst; 108 | }, 109 | 110 | now: function() { 111 | return window.nwf ? window.nwf.system.Performance.elapsedTime : window.performance.now(); 112 | }, 113 | 114 | empty: function(node) { 115 | while (node.lastChild) { 116 | node.removeChild(node.lastChild); 117 | } 118 | }, 119 | 120 | /* 121 | @source: http://jsperf.com/radix-sort 122 | */ 123 | radixSort: function(arr, idxBegin, idxEnd, bit) { 124 | idxBegin = idxBegin || 0; 125 | idxEnd = idxEnd || arr.length; 126 | bit = bit || 31; 127 | if (idxBegin >= (idxEnd - 1) || bit < 0) { 128 | return; 129 | } 130 | var idx = idxBegin; 131 | var idxOnes = idxEnd; 132 | var mask = 0x1 << bit; 133 | while (idx < idxOnes) { 134 | if (arr[idx] & mask) { 135 | --idxOnes; 136 | var tmp = arr[idx]; 137 | arr[idx] = arr[idxOnes]; 138 | arr[idxOnes] = tmp; 139 | } 140 | else { 141 | ++idx; 142 | } 143 | } 144 | this.radixSort(arr, idxBegin, idxOnes, bit-1); 145 | this.radixSort(arr, idxOnes, idxEnd, bit-1); 146 | }, 147 | 148 | randomizeRGB: function(base, range) { 149 | var rgb = base.split(','); 150 | var color = 'rgb('; 151 | var i, c; 152 | range = this.randomInt(range); 153 | for (i = 0; i < 3; i++) { 154 | c = parseInt(rgb[i]) + range; 155 | if (c < 0) c = 0; 156 | else if (c > 255) c = 255; 157 | color += c + ','; 158 | } 159 | color = color.substring(0, color.length-1); 160 | color += ')'; 161 | return color; 162 | }, 163 | 164 | getJSON: function(config) { 165 | var xhr = new XMLHttpRequest(); 166 | var cache = typeof config.cache === 'undefined' ? false : config.cache; 167 | var uri = cache ? config.url : config.url + '?t=' + Math.floor(Math.random() * 10000) + Date.now(); 168 | xhr.onreadystatechange = function() { 169 | if (this.status === 200) { 170 | var json = null; 171 | try { 172 | json = JSON.parse(this.responseText); 173 | } 174 | catch (err) { 175 | // console.warn('[Tools.getJSON] Error: '+config.url+' is not a json resource'); 176 | return; 177 | } 178 | config.callback.call(config.scope || null, json); 179 | return; 180 | } 181 | else if (this.status !== 0) { 182 | console.warn('[Tools.getJSON] Error: '+this.status+' ('+this.statusText+') :: '+config.url); 183 | } 184 | } 185 | xhr.open('GET', uri, true); 186 | xhr.setRequestHeader('Accept', 'application/json'); 187 | xhr.setRequestHeader('Content-Type', 'application/json'); 188 | xhr.send(''); 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /src/utils/MouseCaster.js: -------------------------------------------------------------------------------- 1 | /* 2 | Translates mouse interactivity into 3D positions, so we can easily pick objects in the scene. 3 | 4 | Like everything else in ThreeJS, ray casting creates a ton of new objects each time it's used. This contributes to frequent garbage collections (causing frame hitches), so if you're limited to low-end hardware like mobile, it would be better to only update it when the user clicks, instead of every frame (so no hover effects, but on mobile those don't work anyway). You'll want to create a version that handles touch anyway. 5 | 6 | group - any Object3D (Scene, Group, Mesh, Sprite, etc) that the mouse will cast against 7 | camera - the camera to cast from 8 | [element] - optional element to attach mouse event to 9 | 10 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 11 | */ 12 | vg.MouseCaster = function(group, camera, element) { 13 | this.down = false; // left click 14 | this.rightDown = false; 15 | // the object that was just clicked on 16 | this.pickedObject = null; 17 | // the object currently being 'held' 18 | this.selectedObject = null; 19 | // store the results of the last cast 20 | this.allHits = null; 21 | // disable the caster easily to temporarily prevent user input 22 | this.active = true; 23 | 24 | this.shift = false; 25 | this.ctrl = false; 26 | this.wheel = 0; 27 | 28 | // you can track exactly where the mouse is in the 3D scene by using the z component 29 | this.position = new THREE.Vector3(); 30 | this.screenPosition = new THREE.Vector2(); 31 | this.signal = new vg.Signal(); 32 | this.group = group; 33 | 34 | // behind-the-scenes stuff you shouldn't worry about 35 | this._camera = camera; 36 | this._raycaster = new THREE.Raycaster(); 37 | this._preventDefault = false; 38 | 39 | element = element || document; 40 | 41 | element.addEventListener('mousemove', this._onDocumentMouseMove.bind(this), false); 42 | element.addEventListener('mousedown', this._onDocumentMouseDown.bind(this), false); 43 | element.addEventListener('mouseup', this._onDocumentMouseUp.bind(this), false); 44 | element.addEventListener('mousewheel', this._onMouseWheel.bind(this), false); 45 | element.addEventListener('DOMMouseScroll', this._onMouseWheel.bind(this), false); // firefox 46 | }; 47 | 48 | // statics to describe the events we dispatch 49 | vg.MouseCaster.OVER = 'over'; 50 | vg.MouseCaster.OUT = 'out'; 51 | vg.MouseCaster.DOWN = 'down'; 52 | vg.MouseCaster.UP = 'up'; 53 | vg.MouseCaster.CLICK = 'click'; // only fires if the user clicked down and up while on the same object 54 | vg.MouseCaster.WHEEL = 'wheel'; 55 | 56 | vg.MouseCaster.prototype = { 57 | update: function() { 58 | if (!this.active) { 59 | return; 60 | } 61 | 62 | this._raycaster.setFromCamera(this.screenPosition, this._camera); 63 | 64 | var intersects = this._raycaster.intersectObject(this.group, true); 65 | var hit, obj; 66 | 67 | if (intersects.length > 0) { 68 | // get the first object under the mouse 69 | hit = intersects[0]; 70 | obj = hit.object.userData.structure; 71 | if (this.pickedObject != obj) { 72 | // the first object changed, meaning there's a different one, or none at all 73 | if (this.pickedObject) { 74 | // it's a new object, notify the old object is going away 75 | this.signal.dispatch(vg.MouseCaster.OUT, this.pickedObject); 76 | } 77 | /*else { 78 | // hit a new object when nothing was there previously 79 | }*/ 80 | this.pickedObject = obj; 81 | this.selectedObject = null; // cancel click, otherwise it'll confuse the user 82 | 83 | this.signal.dispatch(vg.MouseCaster.OVER, this.pickedObject); 84 | } 85 | this.position.copy(hit.point); 86 | this.screenPosition.z = hit.distance; 87 | } 88 | else { 89 | // there isn't anything under the mouse 90 | if (this.pickedObject) { 91 | // there was though, we just moved out 92 | this.signal.dispatch(vg.MouseCaster.OUT, this.pickedObject); 93 | } 94 | this.pickedObject = null; 95 | this.selectedObject = null; 96 | } 97 | 98 | this.allHits = intersects; 99 | }, 100 | 101 | preventDefault: function() { 102 | this._preventDefault = true; 103 | }, 104 | 105 | _onDocumentMouseDown: function(evt) { 106 | evt = evt || window.event; 107 | evt.preventDefault(); 108 | if (this._preventDefault) { 109 | this._preventDefault = false; 110 | return false; 111 | } 112 | if (this.pickedObject) { 113 | this.selectedObject = this.pickedObject; 114 | } 115 | this.shift = evt.shiftKey; 116 | this.ctrl = evt.ctrlKey; 117 | 118 | this.down = evt.which === 1; 119 | this.rightDown = evt.which === 3; 120 | 121 | this.signal.dispatch(vg.MouseCaster.DOWN, this.pickedObject); 122 | }, 123 | 124 | _onDocumentMouseUp: function(evt) { 125 | evt.preventDefault(); 126 | if (this._preventDefault) { 127 | this._preventDefault = false; 128 | return false; 129 | } 130 | this.shift = evt.shiftKey; 131 | this.ctrl = evt.ctrlKey; 132 | 133 | this.signal.dispatch(vg.MouseCaster.UP, this.pickedObject); 134 | if (this.selectedObject && this.pickedObject && this.selectedObject.uniqueID === this.pickedObject.uniqueID) { 135 | this.signal.dispatch(vg.MouseCaster.CLICK, this.pickedObject); 136 | } 137 | 138 | this.down = evt.which === 1 ? false : this.down; 139 | this.rightDown = evt.which === 3 ? false : this.rightDown; 140 | }, 141 | 142 | _onDocumentMouseMove: function(evt) { 143 | evt.preventDefault(); 144 | this.screenPosition.x = (evt.clientX / window.innerWidth) * 2 - 1; 145 | this.screenPosition.y = -(evt.clientY / window.innerHeight) * 2 + 1; 146 | }, 147 | 148 | _onMouseWheel: function(evt) { 149 | if (!this.active) { 150 | return; 151 | } 152 | evt.preventDefault(); 153 | evt.stopPropagation(); 154 | 155 | var delta = 0; 156 | if (evt.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9 157 | delta = evt.wheelDelta; 158 | } 159 | else if (evt.detail !== undefined) { // Firefox 160 | delta = -evt.detail; 161 | } 162 | if (delta > 0) { 163 | this.wheel++; 164 | } 165 | else { 166 | this.wheel--; 167 | } 168 | // console.log(this.wheel); 169 | this.signal.dispatch(vg.MouseCaster.WHEEL, this.wheel); 170 | } 171 | }; 172 | 173 | vg.MouseCaster.prototype.constructor = vg.MouseCaster; 174 | -------------------------------------------------------------------------------- /src/pathing/PathUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | @source https://github.com/qiao/PathFinding.js/ 3 | */ 4 | vg.PathUtil = { 5 | /** 6 | * Backtrace according to the parent records and return the path. 7 | * (including both start and end nodes) 8 | * @param {Node} node End node 9 | * @return {Array.>} the path 10 | */ 11 | backtrace: function(node) { 12 | var path = [node]; 13 | while (node._parent) { 14 | node = node._parent; 15 | path.push(node); 16 | } 17 | return path.reverse(); 18 | }, 19 | 20 | /** 21 | * Backtrace from start and end node, and return the path. 22 | * (including both start and end nodes) 23 | * @param {Node} 24 | * @param {Node} 25 | */ 26 | biBacktrace: function(nodeA, nodeB) { 27 | var pathA = this.backtrace(nodeA), 28 | pathB = this.backtrace(nodeB); 29 | return pathA.concat(pathB.reverse()); 30 | }, 31 | 32 | /** 33 | * Compute the length of the path. 34 | * @param {Array.>} path The path 35 | * @return {number} The length of the path 36 | */ 37 | pathLength: function(path) { 38 | var i, sum = 0, a, b, dx, dy; 39 | for (i = 1; i < path.length; ++i) { 40 | a = path[i - 1]; 41 | b = path[i]; 42 | dx = a[0] - b[0]; 43 | dy = a[1] - b[1]; 44 | sum += Math.sqrt(dx * dx + dy * dy); 45 | } 46 | return sum; 47 | }, 48 | 49 | 50 | /** 51 | * Given the start and end coordinates, return all the coordinates lying 52 | * on the line formed by these coordinates, based on Bresenham's algorithm. 53 | * http://en.wikipedia.org/wiki/Bresenham's_line_algorithm#Simplification 54 | * @param {number} x0 Start x coordinate 55 | * @param {number} y0 Start y coordinate 56 | * @param {number} x1 End x coordinate 57 | * @param {number} y1 End y coordinate 58 | * @return {Array.>} The coordinates on the line 59 | */ 60 | interpolate: function(x0, y0, x1, y1) { 61 | var abs = Math.abs, 62 | line = [], 63 | sx, sy, dx, dy, err, e2; 64 | 65 | dx = abs(x1 - x0); 66 | dy = abs(y1 - y0); 67 | 68 | sx = (x0 < x1) ? 1 : -1; 69 | sy = (y0 < y1) ? 1 : -1; 70 | 71 | err = dx - dy; 72 | 73 | while (x0 !== x1 || y0 !== y1) { 74 | line.push([x0, y0]); 75 | 76 | e2 = 2 * err; 77 | if (e2 > -dy) { 78 | err = err - dy; 79 | x0 = x0 + sx; 80 | } 81 | if (e2 < dx) { 82 | err = err + dx; 83 | y0 = y0 + sy; 84 | } 85 | } 86 | 87 | return line; 88 | }, 89 | 90 | 91 | /** 92 | * Given a compressed path, return a new path that has all the segments 93 | * in it interpolated. 94 | * @param {Array.>} path The path 95 | * @return {Array.>} expanded path 96 | */ 97 | expandPath: function(path) { 98 | var expanded = [], 99 | len = path.length, 100 | coord0, coord1, 101 | interpolated, 102 | interpolatedLen, 103 | i, j; 104 | 105 | if (len < 2) { 106 | return expanded; 107 | } 108 | 109 | for (i = 0; i < len - 1; ++i) { 110 | coord0 = path[i]; 111 | coord1 = path[i + 1]; 112 | 113 | interpolated = this.interpolate(coord0[0], coord0[1], coord1[0], coord1[1]); 114 | interpolatedLen = interpolated.length; 115 | for (j = 0; j < interpolatedLen - 1; ++j) { 116 | expanded.push(interpolated[j]); 117 | } 118 | } 119 | expanded.push(path[len - 1]); 120 | 121 | return expanded; 122 | }, 123 | 124 | 125 | /** 126 | * Smoothen the give path. 127 | * The original path will not be modified; a new path will be returned. 128 | * @param {PF.Grid} grid 129 | * @param {Array.>} path The path 130 | */ 131 | smoothenPath: function(grid, path) { 132 | var len = path.length, 133 | x0 = path[0][0], // path start x 134 | y0 = path[0][1], // path start y 135 | x1 = path[len - 1][0], // path end x 136 | y1 = path[len - 1][1], // path end y 137 | sx, sy, // current start coordinate 138 | ex, ey, // current end coordinate 139 | newPath, 140 | lastValidCoord, 141 | i, j, coord, line, testCoord, blocked; 142 | 143 | sx = x0; 144 | sy = y0; 145 | newPath = [[sx, sy]]; 146 | 147 | for (i = 2; i < len; ++i) { 148 | coord = path[i]; 149 | ex = coord[0]; 150 | ey = coord[1]; 151 | line = this.interpolate(sx, sy, ex, ey); 152 | 153 | blocked = false; 154 | for (j = 1; j < line.length; ++j) { 155 | testCoord = line[j]; 156 | 157 | if (!grid.isWalkableAt(testCoord[0], testCoord[1])) { 158 | blocked = true; 159 | break; 160 | } 161 | } 162 | if (blocked) { 163 | lastValidCoord = path[i - 1]; 164 | newPath.push(lastValidCoord); 165 | sx = lastValidCoord[0]; 166 | sy = lastValidCoord[1]; 167 | } 168 | } 169 | newPath.push([x1, y1]); 170 | 171 | return newPath; 172 | }, 173 | 174 | 175 | /** 176 | * Compress a path, remove redundant nodes without altering the shape 177 | * The original path is not modified 178 | * @param {Array.>} path The path 179 | * @return {Array.>} The compressed path 180 | */ 181 | compressPath: function(path) { 182 | 183 | // nothing to compress 184 | if (path.length < 3) { 185 | return path; 186 | } 187 | 188 | var compressed = [], 189 | sx = path[0][0], // start x 190 | sy = path[0][1], // start y 191 | px = path[1][0], // second point x 192 | py = path[1][1], // second point y 193 | dx = px - sx, // direction between the two points 194 | dy = py - sy, // direction between the two points 195 | lx, ly, 196 | ldx, ldy, 197 | sq, i; 198 | 199 | // normalize the direction 200 | sq = Math.sqrt(dx*dx + dy*dy); 201 | dx /= sq; 202 | dy /= sq; 203 | 204 | // start the new path 205 | compressed.push([sx,sy]); 206 | 207 | for (i = 2; i < path.length; i++) { 208 | 209 | // store the last point 210 | lx = px; 211 | ly = py; 212 | 213 | // store the last direction 214 | ldx = dx; 215 | ldy = dy; 216 | 217 | // next point 218 | px = path[i][0]; 219 | py = path[i][1]; 220 | 221 | // next direction 222 | dx = px - lx; 223 | dy = py - ly; 224 | 225 | // normalize 226 | sq = Math.sqrt(dx*dx + dy*dy); 227 | dx /= sq; 228 | dy /= sq; 229 | 230 | // if the direction has changed, store the point 231 | if (dx !== ldx || dy !== ldy) { 232 | compressed.push([lx,ly]); 233 | } 234 | } 235 | 236 | // store the last point 237 | compressed.push([px,py]); 238 | 239 | return compressed; 240 | } 241 | }; 242 | -------------------------------------------------------------------------------- /editor/lib/bliss.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function t(t,n,r){for(var s in n){if(r){var i=e.type(r);if("own"===r&&!n.hasOwnProperty(s)||"array"===i&&-1===r.indexOf(s)||"regexp"===i&&!r.test(s)||"function"===i&&!r.call(n,s))continue}var o=Object.getOwnPropertyDescriptor(n,s);!o||o.writable&&o.configurable&&o.enumerable&&!o.get&&!o.set?t[s]=n[s]:(delete t[s],Object.defineProperty(t,s,o))}return t}var e=self.Bliss=t(function(t,n){return"string"===e.type(t)?(n||document).querySelector(t):t||null},self.Bliss);t(e,{extend:t,property:e.property||"_",sources:{},$:function(t,e){return t instanceof Node||t instanceof Window?[t]:Array.prototype.slice.call("string"==typeof t?(e||document).querySelectorAll(t):t||[])},type:function(t){if(null===t)return"null";if(void 0===t)return"undefined";var e=(Object.prototype.toString.call(t).match(/^\[object\s+(.*?)\]$/)[1]||"").toLowerCase();return"number"==e&&isNaN(t)?"nan":e},defined:function(){for(var t=0;t=3)Object.defineProperty(t,n,{get:function(){return delete this[n],this[n]=r.call(this)},configurable:!0,enumerable:!0});else if(2===arguments.length)for(var s in n)e.lazy(t,s,n[s]);return t},live:function(t,n,r){if(arguments.length>=3)Object.defineProperty(t,n,{get:function(){var t=this["_"+n],e=r.get&&r.get.call(this,t);return void 0!==e?e:t},set:function(t){var e=this["_"+n],s=r.set&&r.set.call(this,t,e);this["_"+n]=void 0!==s?s:t},configurable:r.configurable,enumerable:r.enumerable});else if(2===arguments.length)for(var s in n)e.live(t,s,n[s]);return t}},include:function(){var t=arguments[arguments.length-1],n=2===arguments.length?arguments[0]:!1,r=document.createElement("script");return n?Promise.resolve():new Promise(function(n,s){e.set(r,{async:!0,onload:function(){n(),e.remove(r)},onerror:function(){s()},src:t,inside:document.head})})},fetch:function(t,n){if(!t)throw new TypeError("URL parameter is mandatory and cannot be "+t);t=new URL(t,location),n=n||{},n.data=n.data||"",n.method=n.method||"GET",n.headers=n.headers||{};var r=new XMLHttpRequest;"string"!==e.type(n.data)&&(n.data=Object.keys(n.data).map(function(t){return t+"="+encodeURIComponent(n.data[t])}).join("&")),"GET"===n.method&&n.data&&(t.search+=n.data),document.body.setAttribute("data-loading",t),r.open(n.method,t,!n.sync);for(var s in n)if(s in r)try{r[s]=n[s]}catch(i){self.console&&console.error(i)}"GET"===n.method||n.headers["Content-type"]||n.headers["Content-Type"]||r.setRequestHeader("Content-type","application/x-www-form-urlencoded");for(var o in n.headers)r.setRequestHeader(o,n.headers[o]);return new Promise(function(t,e){r.onload=function(){document.body.removeAttribute("data-loading"),0===r.status||r.status>=200&&r.status<300||304===r.status?t(r):e(Error(r.statusText))},r.onerror=function(){document.body.removeAttribute("data-loading"),e(Error("Network Error"))},r.send("GET"===n.method?null:n.data)})}});var n=e.property;e.Element=function(t){this.subject=t,this.data={},this.bliss={}},e.Element.prototype={set:function(t){"string"===e.type(arguments[0])&&(t={},t[arguments[0]]=arguments[1]);for(var n in t)n in e.setProps?e.setProps[n].call(this,t[n]):n in this?this[n]=t[n]:(this.setAttribute||console.log(this),this.setAttribute(n,t[n]))},transition:function(t,n){return n=+n||400,new Promise(function(r,s){if("transition"in this.style){var i=e.extend({},this.style,/^transition(Duration|Property)$/);e.style(this,{transitionDuration:(n||400)+"ms",transitionProperty:Object.keys(t).join(", ")}),e.once(this,"transitionend",function(){clearTimeout(o),e.style(this,i),r(this)});var o=setTimeout(r,n+50,this);e.style(this,t)}else e.style(this,t),r(this)}.bind(this))},fire:function(t,n){var r=document.createEvent("HTMLEvents");r.initEvent(t,!0,!0),this.dispatchEvent(e.extend(r,n))}},e.setProps={style:function(t){e.extend(this.style,t)},attributes:function(t){for(var e in t)this.setAttribute(e,t[e])},properties:function(t){e.extend(this,t)},events:function(t){if(t&&t.addEventListener){var e=this;if(t[n]&&t[n].bliss){var r=t[n].bliss.listeners;for(var s in r)r[s].forEach(function(t){e.addEventListener(s,t.callback,t.capture)})}for(var i in t)0===i.indexOf("on")&&(this[i]=t[i])}else for(var o in t)o.split(/\s+/).forEach(function(e){this.addEventListener(e,t[o])},this)},once:function(t){2==arguments.length&&(t={},t[arguments[0]]=arguments[1]);var n=this;e.each(t,function(t,e){t=t.split(/\s+/);var r=function(){return t.forEach(function(t){n.removeEventListener(t,r)}),e.apply(n,arguments)};t.forEach(function(t){n.addEventListener(t,r)})})},delegate:function(t){3===arguments.length?(t={},t[arguments[0]]={},t[arguments[0]][arguments[1]]=arguments[2]):2===arguments.length&&(t={},t[arguments[0]]=arguments[1]);var n=this;e.each(t,function(t,e){n.addEventListener(t,function(t){for(var n in e)t.target.matches(n)&&e[n].call(this,t)})})},contents:function(t){(t||0===t)&&(Array.isArray(t)?t:[t]).forEach(function(t){var n=e.type(t);/^(string|number)$/.test(n)?t=document.createTextNode(t+""):"object"===n&&(t=e.create(t)),t instanceof Node&&this.appendChild(t)},this)},inside:function(t){t.appendChild(this)},before:function(t){t.parentNode.insertBefore(this,t)},after:function(t){t.parentNode.insertBefore(this,t.nextSibling)},start:function(t){t.insertBefore(this,t.firstChild)},around:function(t){t.parentNode&&e.before(this,t),(/^template$/i.test(this.nodeName)?this.content||this:this).appendChild(t)}},e.Array=function(t){this.subject=t},e.Array.prototype={all:function(t){var e=$$(arguments).slice(1);return this[t].apply(this,e)}},e.add=function(t,n,r){n=e.extend({$:!0,element:!0,array:!0},n),"string"===e.type(arguments[0])&&(t={},t[arguments[0]]=arguments[1]),e.each(t,function(t,s){"function"==e.type(s)&&(!n.element||t in e.Element.prototype&&r||(e.Element.prototype[t]=function(){return this.subject&&e.defined(s.apply(this.subject,arguments),this.subject)}),!n.array||t in e.Array.prototype&&r||(e.Array.prototype[t]=function(){var t=arguments;return this.subject.map(function(n){return n&&e.defined(s.apply(n,t),n)})}),n.$&&(e.sources[t]=e[t]=s,(n.array||n.element)&&(e[t]=function(){var r=[].slice.apply(arguments),s=r.shift(),i=n.array&&Array.isArray(s)?"Array":"Element";return e[i].prototype[t].apply({subject:s},r)})))})},e.add(e.Array.prototype,{element:!1}),e.add(e.Element.prototype),e.add(e.setProps),e.add(e.classProps,{element:!1,array:!1});var r=document.createElement("_");e.add(e.extend({},HTMLElement.prototype,function(t){return"function"===e.type(r[t])}),null,!0)}(),function(t){"use strict";if(Bliss&&!Bliss.shy){var e=Bliss.property;if(t.add({clone:function(){var e=this.cloneNode(!0),n=t.$("*",e).concat(e);return t.$("*",this).concat(this).forEach(function(e,r,s){t.events(n[r],e)}),e}},{array:!1}),Object.defineProperty(Node.prototype,e,{get:function o(){return Object.defineProperty(Node.prototype,e,{get:void 0}),Object.defineProperty(this,e,{value:new t.Element(this)}),Object.defineProperty(Node.prototype,e,{get:o}),this[e]},configurable:!0}),Object.defineProperty(Array.prototype,e,{get:function(){return Object.defineProperty(this,e,{value:new t.Array(this)}),this[e]},configurable:!0}),self.EventTarget&&"addEventListener"in EventTarget.prototype){var n=EventTarget.prototype.addEventListener,r=EventTarget.prototype.removeEventListener,s=function(t,e,n){return n.callback===t&&n.capture==e},i=function(){return!s.apply(this,arguments)};EventTarget.prototype.addEventListener=function(t,r,i){if(this[e]&&r){var o=this[e].bliss.listeners=this[e].bliss.listeners||{};o[t]=o[t]||[],0===o[t].filter(s.bind(null,r,i)).length&&o[t].push({callback:r,capture:i})}return n.call(this,t,r,i)},EventTarget.prototype.removeEventListener=function(t,n,s){if(this[e]&&n){var o=this[e].bliss.listeners=this[e].bliss.listeners||{};o[t]&&(o[t]=o[t].filter(i.bind(null,n,s)))}return r.call(this,t,n,s)}}self.$=self.$||t,self.$$=self.$$||t.$}}(Bliss); -------------------------------------------------------------------------------- /src/lib/LinkedList.js: -------------------------------------------------------------------------------- 1 | /* 2 | A high-speed doubly-linked list of objects. Note that for speed reasons (using a dictionary lookup of 3 | cached nodes) there can only be a single instance of an object in the list at the same time. Adding the same 4 | object a second time will result in a silent return from the add method. 5 | 6 | In order to keep a track of node links, an object must be able to identify itself with a uniqueID function. 7 | 8 | To add an item use: 9 |

 10 | 		list.add(newItem);
 11 | 	
12 |

13 | You can iterate using the first and next members, such as: 14 |


 15 | 		var node = list.first;
 16 | 		while (node)
 17 | 		{
 18 | 			node.object().DOSOMETHING();
 19 | 			node = node.next();
 20 | 		}
 21 | 	
22 | */ 23 | (function() { 24 | var LinkedListNode = function() { 25 | this.obj = null; 26 | this.next = null; 27 | this.prev = null; 28 | this.free = true; 29 | }; 30 | 31 | var LinkedList = function() { 32 | this.first = null; 33 | this.last = null; 34 | this.length = 0; 35 | this.objToNodeMap = {}; // a quick lookup list to map linked list nodes to objects 36 | this.uniqueID = Date.now() + '' + Math.floor(Math.random()*1000); 37 | 38 | this.sortArray = []; 39 | }; 40 | 41 | // static function for utility 42 | LinkedList.generateID = function() { 43 | return Math.random().toString(36).slice(2) + Date.now(); 44 | }; 45 | 46 | LinkedList.prototype = { 47 | /* 48 | Get the LinkedListNode for this object. 49 | @param obj The object to get the node for 50 | */ 51 | getNode: function(obj) { 52 | // objects added to a list must implement a uniqueID which returns a unique object identifier string 53 | return this.objToNodeMap[obj.uniqueID]; 54 | }, 55 | 56 | /* 57 | Adds a new node to the list -- typically only used internally unless you're doing something funky 58 | Use add() to add an object to the list, not this. 59 | */ 60 | addNode: function(obj) { 61 | var node = new LinkedListNode(); 62 | if (!obj.uniqueID) { 63 | try { 64 | obj.uniqueID = LinkedList.generateID(); 65 | // console.log('New ID: '+obj.uniqueID); 66 | } 67 | catch (err) { 68 | console.error('[LinkedList.addNode] obj passed is immutable: cannot attach necessary identifier'); 69 | return null; 70 | } 71 | } 72 | 73 | node.obj = obj; 74 | node.free = false; 75 | this.objToNodeMap[obj.uniqueID] = node; 76 | return node; 77 | }, 78 | 79 | swapObjects: function(node, newObj) { 80 | this.objToNodeMap[node.obj.uniqueID] = null; 81 | this.objToNodeMap[newObj.uniqueID] = node; 82 | node.obj = newObj; 83 | }, 84 | 85 | /* 86 | Add an item to the list 87 | @param obj The object to add 88 | */ 89 | add: function(obj) { 90 | var node = this.objToNodeMap[obj.uniqueID]; 91 | 92 | if (!node) { 93 | node = this.addNode(obj); 94 | } 95 | else { 96 | if (node.free === false) return; 97 | 98 | // reusing a node, so we clean it up 99 | // this caching of node/object pairs is the reason an object can only exist 100 | // once in a list -- which also makes things faster (not always creating new node 101 | // object every time objects are moving on and off the list 102 | node.obj = obj; 103 | node.free = false; 104 | node.next = null; 105 | node.prev = null; 106 | } 107 | 108 | // append this obj to the end of the list 109 | if (!this.first) { // is this the first? 110 | this.first = node; 111 | this.last = node; 112 | node.next = null; // clear just in case 113 | node.prev = null; 114 | } 115 | else { 116 | if (!this.last) { 117 | throw new Error("[LinkedList.add] No last in the list -- that shouldn't happen here"); 118 | } 119 | 120 | // add this entry to the end of the list 121 | this.last.next = node; // current end of list points to the new end 122 | node.prev = this.last; 123 | this.last = node; // new object to add becomes last in the list 124 | node.next = null; // just in case this was previously set 125 | } 126 | this.length++; 127 | 128 | if (this.showDebug) this.dump('after add'); 129 | }, 130 | 131 | has: function(obj) { 132 | return !!this.objToNodeMap[obj.uniqueID]; 133 | }, 134 | 135 | /* 136 | Moves this item upwards in the list 137 | @param obj 138 | */ 139 | moveUp: function(obj) { 140 | this.dump('before move up'); 141 | var c = this.getNode(obj); 142 | if (!c) throw "Oops, trying to move an object that isn't in the list"; 143 | if (!c.prev) return; // already first, ignore 144 | 145 | // This operation makes C swap places with B: 146 | // A <-> B <-> C <-> D 147 | // A <-> C <-> B <-> D 148 | 149 | var b = c.prev; 150 | var a = b.prev; 151 | 152 | // fix last 153 | if (c == this.last) this.last = b; 154 | 155 | var oldCNext = c.next; 156 | 157 | if (a) a.next = c; 158 | c.next = b; 159 | c.prev = b.prev; 160 | 161 | b.next = oldCNext; 162 | b.prev = c; 163 | 164 | // check to see if we are now first 165 | if (this.first == b) this.first = c; 166 | }, 167 | 168 | /* 169 | Moves this item downwards in the list 170 | @param obj 171 | */ 172 | moveDown: function(obj) { 173 | var b = this.getNode(obj); 174 | if (!b) throw "Oops, trying to move an object that isn't in the list"; 175 | if (!b.next) return; // already last, ignore 176 | 177 | // This operation makes B swap places with C: 178 | // A <-> B <-> C <-> D 179 | // A <-> C <-> B <-> D 180 | 181 | var c = b.next; 182 | this.moveUp(c.obj); 183 | 184 | // check to see if we are now last 185 | if (this.last == c) this.last = b; 186 | }, 187 | 188 | /* 189 | Take everything off the list and put it in an array, sort it, then put it back. 190 | */ 191 | sort: function(compare) { 192 | var sortArray = this.sortArray; 193 | var i, l, node = this.first; 194 | sortArray.length = 0; 195 | 196 | while (node) { 197 | sortArray.push(node.obj); 198 | node = node.next; 199 | } 200 | 201 | this.clear(); 202 | 203 | sortArray.sort(compare); 204 | // console.log(sortArray); 205 | l = sortArray.length; 206 | for (i = 0; i < l; i++) { 207 | this.add(sortArray[i]); 208 | } 209 | }, 210 | 211 | /* 212 | Removes an item from the list 213 | @param obj The object to remove 214 | @returns boolean true if the item was removed, false if the item was not on the list 215 | */ 216 | remove: function(obj) { 217 | var node = this.getNode(obj); 218 | if (!node || node.free){ 219 | return false; // ignore this error (trying to remove something not there) 220 | } 221 | 222 | // pull this object out and tie up the ends 223 | if (node.prev) node.prev.next = node.next; 224 | if (node.next) node.next.prev = node.prev; 225 | 226 | // fix first and last 227 | if (!node.prev) // if this was first on the list 228 | this.first = node.next; // make the next on the list first (can be null) 229 | if (!node.next) // if this was the last 230 | this.last = node.prev; // then this node's previous becomes last 231 | 232 | node.free = true; 233 | node.prev = null; 234 | node.next = null; 235 | 236 | this.length--; 237 | 238 | return true; 239 | }, 240 | 241 | // remove the head and return it's object 242 | shift: function() { 243 | var node = this.first; 244 | if (this.length === 0) return null; 245 | // if (node == null || node.free == true) return null; 246 | 247 | // pull this object out and tie up the ends 248 | if (node.prev) { 249 | node.prev.next = node.next; 250 | } 251 | if (node.next) { 252 | node.next.prev = node.prev; 253 | } 254 | 255 | // make the next on the list first (can be null) 256 | this.first = node.next; 257 | if (!node.next) this.last = null; // make sure we clear this 258 | 259 | node.free = true; 260 | node.prev = null; 261 | node.next = null; 262 | 263 | this.length--; 264 | return node.obj; 265 | }, 266 | 267 | // remove the tail and return it's object 268 | pop: function() { 269 | var node = this.last; 270 | if (this.length === 0) return null; 271 | 272 | // pull this object out and tie up the ends 273 | if (node.prev) { 274 | node.prev.next = node.next; 275 | } 276 | if (node.next) { 277 | node.next.prev = node.prev; 278 | } 279 | 280 | // this node's previous becomes last 281 | this.last = node.prev; 282 | if (!node.prev) this.first = null; // make sure we clear this 283 | 284 | node.free = true; 285 | node.prev = null; 286 | node.next = null; 287 | 288 | this.length--; 289 | return node.obj; 290 | }, 291 | 292 | /** 293 | * Add the passed list to this list, leaving it untouched. 294 | */ 295 | concat: function(list) { 296 | var node = list.first; 297 | while (node) { 298 | this.add(node.obj); 299 | node = node.next; 300 | } 301 | }, 302 | 303 | /** 304 | * Clears the list out 305 | */ 306 | clear: function() { 307 | var next = this.first; 308 | 309 | while (next) { 310 | next.free = true; 311 | next = next.next; 312 | } 313 | 314 | this.first = null; 315 | this.length = 0; 316 | }, 317 | 318 | dispose: function() { 319 | var next = this.first; 320 | 321 | while (next) { 322 | next.obj = null; 323 | next = next.next; 324 | } 325 | this.first = null; 326 | 327 | this.objToNodeMap = null; 328 | }, 329 | 330 | /* 331 | Outputs the contents of the current list for debugging. 332 | */ 333 | dump: function(msg) { 334 | console.log('====================' + msg + '====================='); 335 | var a = this.first; 336 | while (a) { 337 | console.log("{" + a.obj.toString() + "} previous=" + (a.prev ? a.prev.obj : "NULL")); 338 | a = a.next(); 339 | } 340 | console.log("==================================="); 341 | console.log("Last: {" + (this.last ? this.last.obj : 'NULL') + "} " + 342 | "First: {" + (this.first ? this.first.obj : 'NULL') + "}"); 343 | } 344 | }; 345 | 346 | LinkedList.prototype.constructor = LinkedList; 347 | 348 | vg.LinkedList = LinkedList; 349 | }()); -------------------------------------------------------------------------------- /examples/pathfinding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Grid 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /src/grids/SqrGrid.js: -------------------------------------------------------------------------------- 1 | /* 2 | Graph of squares. Handles grid cell management (placement math for eg pathfinding, range, etc) and grid conversion math. 3 | Interface: 4 | type 5 | size - number of cells (in radius); only used if the map is generated 6 | cellSize 7 | cells - a hash so we can have sparse maps 8 | numCells 9 | extrudeSettings 10 | autogenerated 11 | cellShape 12 | cellGeo 13 | cellShapeGeo 14 | 15 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 16 | */ 17 | vg.SqrGrid = function(config) { 18 | config = config || {}; 19 | /* ______________________________________________ 20 | GRID INTERFACE: 21 | */ 22 | this.type = vg.SQR; 23 | this.size = 5; // only used for generated maps 24 | this.cellSize = typeof config.cellSize === 'undefined' ? 10 : config.cellSize; 25 | this.cells = {}; 26 | this.numCells = 0; 27 | 28 | this.extrudeSettings = null; 29 | this.autogenerated = false; 30 | 31 | // create base shape used for building geometry 32 | var verts = []; 33 | verts.push(new THREE.Vector3()); 34 | verts.push(new THREE.Vector3(-this.cellSize, this.cellSize)); 35 | verts.push(new THREE.Vector3(this.cellSize, this.cellSize)); 36 | verts.push(new THREE.Vector3(this.cellSize, -this.cellSize)); 37 | // copy the verts into a shape for the geometry to use 38 | this.cellShape = new THREE.Shape(); 39 | this.cellShape.moveTo(-this.cellSize, -this.cellSize); 40 | this.cellShape.lineTo(-this.cellSize, this.cellSize); 41 | this.cellShape.lineTo(this.cellSize, this.cellSize); 42 | this.cellShape.lineTo(this.cellSize, -this.cellSize); 43 | this.cellShape.lineTo(-this.cellSize, -this.cellSize); 44 | 45 | this.cellGeo = new THREE.Geometry(); 46 | this.cellGeo.vertices = verts; 47 | this.cellGeo.verticesNeedUpdate = true; 48 | 49 | this.cellShapeGeo = new THREE.ShapeGeometry(this.cellShape); 50 | 51 | /* ______________________________________________ 52 | PRIVATE 53 | */ 54 | 55 | this._fullCellSize = this.cellSize * 2; 56 | this._hashDelimeter = '.'; 57 | // pre-computed permutations 58 | this._directions = [new vg.Cell(+1, 0, 0), new vg.Cell(0, -1, 0), 59 | new vg.Cell(-1, 0, 0), new vg.Cell(0, +1, 0)]; 60 | this._diagonals = [new vg.Cell(-1, -1, 0), new vg.Cell(-1, +1, 0), 61 | new vg.Cell(+1, +1, 0), new vg.Cell(+1, -1, 0)]; 62 | // cached objects 63 | this._list = []; 64 | this._vec3 = new THREE.Vector3(); 65 | this._cel = new vg.Cell(); 66 | this._conversionVec = new THREE.Vector3(); 67 | this._geoCache = []; 68 | this._matCache = []; 69 | }; 70 | 71 | vg.SqrGrid.prototype = { 72 | /* 73 | ________________________________________________________________________ 74 | High-level functions that the Board interfaces with (all grids implement) 75 | */ 76 | 77 | cellToPixel: function(cell) { 78 | this._vec3.x = cell.q * this._fullCellSize; 79 | this._vec3.y = cell.h; 80 | this._vec3.z = cell.r * this._fullCellSize; 81 | return this._vec3; 82 | }, 83 | 84 | pixelToCell: function(pos) { 85 | var q = Math.round(pos.x / this._fullCellSize); 86 | var r = Math.round(pos.z / this._fullCellSize); 87 | return this._cel.set(q, r, 0); 88 | }, 89 | 90 | getCellAt: function(pos) { 91 | var q = Math.round(pos.x / this._fullCellSize); 92 | var r = Math.round(pos.z / this._fullCellSize); 93 | this._cel.set(q, r); 94 | return this.cells[this.cellToHash(this._cel)]; 95 | }, 96 | 97 | getNeighbors: function(cell, diagonal, filter) { 98 | // always returns an array 99 | var i, n, l = this._directions.length; 100 | this._list.length = 0; 101 | for (i = 0; i < l; i++) { 102 | this._cel.copy(cell); 103 | this._cel.add(this._directions[i]); 104 | n = this.cells[this.cellToHash(this._cel)]; 105 | if (!n || (filter && !filter(cell, n))) { 106 | continue; 107 | } 108 | this._list.push(n); 109 | } 110 | if (diagonal) { 111 | for (i = 0; i < l; i++) { 112 | this._cel.copy(cell); 113 | this._cel.add(this._diagonals[i]); 114 | n = this.cells[this.cellToHash(this._cel)]; 115 | if (!n || (filter && !filter(cell, n))) { 116 | continue; 117 | } 118 | this._list.push(n); 119 | } 120 | } 121 | return this._list; 122 | }, 123 | 124 | getRandomCell: function() { 125 | var c, i = 0, x = vg.Tools.randomInt(0, this.numCells); 126 | for (c in this.cells) { 127 | if (i === x) { 128 | return this.cells[c]; 129 | } 130 | i++; 131 | } 132 | return this.cells[c]; 133 | }, 134 | 135 | cellToHash: function(cell) { 136 | return cell.q+this._hashDelimeter+cell.r; // s is not used in a square grid 137 | }, 138 | 139 | distance: function(cellA, cellB) { 140 | var d = Math.max(Math.abs(cellA.q - cellB.q), Math.abs(cellA.r - cellB.r)); 141 | d += cellB.h - cellA.h; // include vertical size 142 | return d; 143 | }, 144 | 145 | clearPath: function() { 146 | var i, c; 147 | for (i in this.cells) { 148 | c = this.cells[i]; 149 | c._calcCost = 0; 150 | c._priority = 0; 151 | c._parent = null; 152 | c._visited = false; 153 | } 154 | }, 155 | 156 | traverse: function(cb) { 157 | var i; 158 | for (i in this.cells) { 159 | cb(this.cells[i]); 160 | } 161 | }, 162 | 163 | generateTile: function(cell, scale, material) { 164 | var height = Math.abs(cell.h); 165 | if (height < 1) height = 1; 166 | 167 | var geo = this._geoCache[height]; 168 | if (!geo) { 169 | this.extrudeSettings.depth = height; 170 | geo = new THREE.ExtrudeGeometry(this.cellShape, this.extrudeSettings); 171 | this._geoCache[height] = geo; 172 | } 173 | 174 | /*mat = this._matCache[c.matConfig.mat_cache_id]; 175 | if (!mat) { // MaterialLoader? we currently only support basic stuff though. maybe later 176 | mat.map = Loader.loadTexture(c.matConfig.imgURL); 177 | delete c.matConfig.imgURL; 178 | mat = new THREE[c.matConfig.type](c.matConfig); 179 | this._matCache[c.matConfig.mat_cache_id] = mat; 180 | }*/ 181 | 182 | var t = new vg.Tile({ 183 | size: this.cellSize, 184 | scale: scale, 185 | cell: cell, 186 | geometry: geo, 187 | material: material 188 | }); 189 | 190 | cell.tile = t; 191 | 192 | return t; 193 | }, 194 | 195 | generateTiles: function(config) { 196 | config = config || {}; 197 | var tiles = []; 198 | var settings = { 199 | tileScale: 0.95, 200 | cellSize: this.cellSize, 201 | material: null, 202 | extrudeSettings: { 203 | depth: 1, 204 | bevelEnabled: true, 205 | bevelSegments: 1, 206 | steps: 1, 207 | bevelSize: this.cellSize/20, 208 | bevelThickness: this.cellSize/20 209 | } 210 | } 211 | settings = vg.Tools.merge(settings, config); 212 | 213 | /*if (!settings.material) { 214 | settings.material = new THREE.MeshPhongMaterial({ 215 | color: vg.Tools.randomizeRGB('30, 30, 30', 10) 216 | }); 217 | }*/ 218 | 219 | // overwrite with any new dimensions 220 | this.cellSize = settings.cellSize; 221 | this._fullCellSize = this.cellSize * 2; 222 | 223 | this.autogenerated = true; 224 | this.extrudeSettings = settings.extrudeSettings; 225 | 226 | var i, t, c; 227 | for (i in this.cells) { 228 | c = this.cells[i]; 229 | t = this.generateTile(c, settings.tileScale, settings.material); 230 | t.position.copy(this.cellToPixel(c)); 231 | t.position.y = 0; 232 | tiles.push(t); 233 | } 234 | return tiles; 235 | }, 236 | 237 | generateTilePoly: function(material) { 238 | if (!material) { 239 | material = new THREE.MeshBasicMaterial({color: 0x24b4ff}); 240 | } 241 | var mesh = new THREE.Mesh(this.cellShapeGeo, material); 242 | this._vec3.set(1, 0, 0); 243 | mesh.rotateOnAxis(this._vec3, vg.PI/2); 244 | return mesh; 245 | }, 246 | 247 | // create a flat, square-shaped grid 248 | generate: function(config) { 249 | config = config || {}; 250 | this.size = typeof config.size === 'undefined' ? this.size : config.size; 251 | var x, y, c; 252 | var half = Math.ceil(this.size / 2); 253 | for (x = -half; x < half; x++) { 254 | for (y = -half; y < half; y++) { 255 | c = new vg.Cell(x, y + 1); 256 | this.add(c); 257 | } 258 | } 259 | }, 260 | 261 | generateOverlay: function(size, overlayObj, overlayMat) { 262 | var x, y; 263 | var half = Math.ceil(size / 2); 264 | for (x = -half; x < half; x++) { 265 | for (y = -half; y < half; y++) { 266 | this._cel.set(x, y); // define the cell 267 | var line = new THREE.Line(this.cellGeo, overlayMat); 268 | line.position.copy(this.cellToPixel(this._cel)); 269 | line.rotation.x = 90 * vg.DEG_TO_RAD; 270 | overlayObj.add(line); 271 | } 272 | } 273 | }, 274 | 275 | add: function(cell) { 276 | var h = this.cellToHash(cell); 277 | if (this.cells[h]) { 278 | // console.warn('A cell already exists there'); 279 | return; 280 | } 281 | this.cells[h] = cell; 282 | this.numCells++; 283 | 284 | return cell; 285 | }, 286 | 287 | remove: function(cell) { 288 | var h = this.cellToHash(cell); 289 | if (this.cells[h]) { 290 | delete this.cells[h]; 291 | this.numCells--; 292 | } 293 | }, 294 | 295 | dispose: function() { 296 | this.cells = null; 297 | this.numCells = 0; 298 | this.cellShape = null; 299 | this.cellGeo.dispose(); 300 | this.cellGeo = null; 301 | this.cellShapeGeo.dispose(); 302 | this.cellShapeGeo = null; 303 | this._list = null; 304 | this._vec3 = null; 305 | this._conversionVec = null; 306 | this._geoCache = null; 307 | this._matCache = null; 308 | }, 309 | 310 | /* 311 | Load a grid from a parsed json object. 312 | json = { 313 | extrudeSettings, 314 | size, 315 | cellSize, 316 | autogenerated, 317 | cells: [], 318 | materials: [ 319 | { 320 | cache_id: 0, 321 | type: 'MeshLambertMaterial', 322 | color, ambient, emissive, reflectivity, refractionRatio, wrapAround, 323 | imgURL: url 324 | }, 325 | { 326 | cacheId: 1, ... 327 | } 328 | ... 329 | ] 330 | } 331 | */ 332 | load: function(url, callback, scope) { 333 | vg.Tools.getJSON({ 334 | url: url, 335 | callback: function(json) { 336 | this.fromJSON(json); 337 | callback.call(scope || null, json); 338 | }, 339 | cache: false, 340 | scope: this 341 | }); 342 | }, 343 | 344 | fromJSON: function(json) { 345 | var i, c; 346 | var cells = json.cells; 347 | 348 | this.cells = {}; 349 | this.numCells = 0; 350 | 351 | this.size = json.size; 352 | this.cellSize = json.cellSize; 353 | this._fullCellSize = this.cellSize * 2; 354 | this.extrudeSettings = json.extrudeSettings; 355 | this.autogenerated = json.autogenerated; 356 | 357 | for (i = 0; i < cells.length; i++) { 358 | c = new vg.Cell(); 359 | c.copy(cells[i]); 360 | this.add(c); 361 | } 362 | }, 363 | 364 | toJSON: function() { 365 | var json = { 366 | size: this.size, 367 | cellSize: this.cellSize, 368 | extrudeSettings: this.extrudeSettings, 369 | autogenerated: this.autogenerated 370 | }; 371 | var cells = []; 372 | var c, k; 373 | 374 | for (k in this.cells) { 375 | c = this.cells[k]; 376 | cells.push({ 377 | q: c.q, 378 | r: c.r, 379 | s: c.s, 380 | h: c.h, 381 | walkable: c.walkable, 382 | userData: c.userData 383 | }); 384 | } 385 | json.cells = cells; 386 | 387 | return json; 388 | } 389 | }; 390 | 391 | vg.SqrGrid.prototype.constructor = vg.SqrGrid; 392 | -------------------------------------------------------------------------------- /src/grids/HexGrid.js: -------------------------------------------------------------------------------- 1 | /* 2 | Graph of hexagons. Handles grid cell management (placement math for eg pathfinding, range, etc) and grid conversion math. 3 | [Cube/axial coordinate system](http://www.redblobgames.com/grids/hexagons/), "flat top" version only. Since this is 3D, just rotate your camera for pointy top maps. 4 | Interface: 5 | type 6 | size - number of cells (in radius); only used if the map is generated 7 | cellSize 8 | cells - a hash so we can have sparse maps 9 | numCells 10 | extrudeSettings 11 | autogenerated 12 | cellShape 13 | cellGeo 14 | cellShapeGeo 15 | 16 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 17 | */ 18 | // 'utils/Loader', 'graphs/Hex', 'utils/Tools' 19 | vg.HexGrid = function(config) { 20 | config = config || {}; 21 | /* ______________________________________________ 22 | GRID INTERFACE: 23 | */ 24 | this.type = vg.HEX; 25 | this.size = 5; // only used for generated maps 26 | this.cellSize = typeof config.cellSize === 'undefined' ? 10 : config.cellSize; 27 | this.cells = {}; 28 | this.numCells = 0; 29 | 30 | this.extrudeSettings = null; 31 | this.autogenerated = false; 32 | 33 | // create base shape used for building geometry 34 | var i, verts = []; 35 | // create the skeleton of the hex 36 | for (i = 0; i < 6; i++) { 37 | verts.push(this._createVertex(i)); 38 | } 39 | // copy the verts into a shape for the geometry to use 40 | this.cellShape = new THREE.Shape(); 41 | this.cellShape.moveTo(verts[0].x, verts[0].y); 42 | for (i = 1; i < 6; i++) { 43 | this.cellShape.lineTo(verts[i].x, verts[i].y); 44 | } 45 | this.cellShape.lineTo(verts[0].x, verts[0].y); 46 | this.cellShape.autoClose = true; 47 | 48 | this.cellGeo = new THREE.Geometry(); 49 | this.cellGeo.vertices = verts; 50 | this.cellGeo.verticesNeedUpdate = true; 51 | 52 | this.cellShapeGeo = new THREE.ShapeGeometry(this.cellShape); 53 | 54 | /* ______________________________________________ 55 | PRIVATE 56 | */ 57 | 58 | this._cellWidth = this.cellSize * 2; 59 | this._cellLength = (vg.SQRT3 * 0.5) * this._cellWidth; 60 | this._hashDelimeter = '.'; 61 | // pre-computed permutations 62 | this._directions = [new vg.Cell(+1, -1, 0), new vg.Cell(+1, 0, -1), new vg.Cell(0, +1, -1), 63 | new vg.Cell(-1, +1, 0), new vg.Cell(-1, 0, +1), new vg.Cell(0, -1, +1)]; 64 | this._diagonals = [new vg.Cell(+2, -1, -1), new vg.Cell(+1, +1, -2), new vg.Cell(-1, +2, -1), 65 | new vg.Cell(-2, +1, +1), new vg.Cell(-1, -1, +2), new vg.Cell(+1, -2, +1)]; 66 | // cached objects 67 | this._list = []; 68 | this._vec3 = new THREE.Vector3(); 69 | this._cel = new vg.Cell(); 70 | this._conversionVec = new THREE.Vector3(); 71 | this._geoCache = []; 72 | this._matCache = []; 73 | }; 74 | 75 | vg.HexGrid.TWO_THIRDS = 2 / 3; 76 | 77 | vg.HexGrid.prototype = { 78 | /* ________________________________________________________________________ 79 | High-level functions that the Board interfaces with (all grids implement) 80 | */ 81 | 82 | // grid cell (Hex in cube coordinate space) to position in pixels/world 83 | cellToPixel: function(cell) { 84 | this._vec3.x = cell.q * this._cellWidth * 0.75; 85 | this._vec3.y = cell.h; 86 | this._vec3.z = -((cell.s - cell.r) * this._cellLength * 0.5); 87 | return this._vec3; 88 | }, 89 | 90 | pixelToCell: function(pos) { 91 | // convert a position in world space ("pixels") to cell coordinates 92 | var q = pos.x * (vg.HexGrid.TWO_THIRDS / this.cellSize); 93 | var r = ((-pos.x / 3) + (vg.SQRT3/3) * pos.z) / this.cellSize; 94 | this._cel.set(q, r, -q-r); 95 | return this._cubeRound(this._cel); 96 | }, 97 | 98 | getCellAt: function(pos) { 99 | // get the Cell (if any) at the passed world position 100 | var q = pos.x * (vg.HexGrid.TWO_THIRDS / this.cellSize); 101 | var r = ((-pos.x / 3) + (vg.SQRT3/3) * pos.z) / this.cellSize; 102 | this._cel.set(q, r, -q-r); 103 | this._cubeRound(this._cel); 104 | return this.cells[this.cellToHash(this._cel)]; 105 | }, 106 | 107 | getNeighbors: function(cell, diagonal, filter) { 108 | // always returns an array 109 | var i, n, l = this._directions.length; 110 | this._list.length = 0; 111 | for (i = 0; i < l; i++) { 112 | this._cel.copy(cell); 113 | this._cel.add(this._directions[i]); 114 | n = this.cells[this.cellToHash(this._cel)]; 115 | if (!n || (filter && !filter(cell, n))) { 116 | continue; 117 | } 118 | this._list.push(n); 119 | } 120 | if (diagonal) { 121 | for (i = 0; i < l; i++) { 122 | this._cel.copy(cell); 123 | this._cel.add(this._diagonals[i]); 124 | n = this.cells[this.cellToHash(this._cel)]; 125 | if (!n || (filter && !filter(cell, n))) { 126 | continue; 127 | } 128 | this._list.push(n); 129 | } 130 | } 131 | return this._list; 132 | }, 133 | 134 | getRandomCell: function() { 135 | var c, i = 0, x = vg.Tools.randomInt(0, this.numCells); 136 | for (c in this.cells) { 137 | if (i === x) { 138 | return this.cells[c]; 139 | } 140 | i++; 141 | } 142 | return this.cells[c]; 143 | }, 144 | 145 | cellToHash: function(cell) { 146 | return cell.q+this._hashDelimeter+cell.r+this._hashDelimeter+cell.s; 147 | }, 148 | 149 | distance: function(cellA, cellB) { 150 | var d = Math.max(Math.abs(cellA.q - cellB.q), Math.abs(cellA.r - cellB.r), Math.abs(cellA.s - cellB.s)); 151 | d += cellB.h - cellA.h; // include vertical height 152 | return d; 153 | }, 154 | 155 | clearPath: function() { 156 | var i, c; 157 | for (i in this.cells) { 158 | c = this.cells[i]; 159 | c._calcCost = 0; 160 | c._priority = 0; 161 | c._parent = null; 162 | c._visited = false; 163 | } 164 | }, 165 | 166 | traverse: function(cb) { 167 | var i; 168 | for (i in this.cells) { 169 | cb(this.cells[i]); 170 | } 171 | }, 172 | 173 | generateTile: function(cell, scale, material) { 174 | var height = Math.abs(cell.h); 175 | if (height < 1) height = 1; 176 | 177 | var geo = this._geoCache[height]; 178 | if (!geo) { 179 | this.extrudeSettings.depth = height; 180 | geo = new THREE.ExtrudeGeometry(this.cellShape, this.extrudeSettings); 181 | this._geoCache[height] = geo; 182 | } 183 | 184 | /*mat = this._matCache[c.matConfig.mat_cache_id]; 185 | if (!mat) { // MaterialLoader? we currently only support basic stuff though. maybe later 186 | mat.map = Loader.loadTexture(c.matConfig.imgURL); 187 | delete c.matConfig.imgURL; 188 | mat = new THREE[c.matConfig.type](c.matConfig); 189 | this._matCache[c.matConfig.mat_cache_id] = mat; 190 | }*/ 191 | 192 | var tile = new vg.Tile({ 193 | size: this.cellSize, 194 | scale: scale, 195 | cell: cell, 196 | geometry: geo, 197 | material: material 198 | }); 199 | 200 | cell.tile = tile; 201 | 202 | return tile; 203 | }, 204 | 205 | generateTiles: function(config) { 206 | config = config || {}; 207 | var tiles = []; 208 | var settings = { 209 | tileScale: 0.95, 210 | cellSize: this.cellSize, 211 | material: null, 212 | extrudeSettings: { 213 | depth: 1, 214 | bevelEnabled: true, 215 | bevelSegments: 1, 216 | steps: 1, 217 | bevelSize: this.cellSize/20, 218 | bevelThickness: this.cellSize/20 219 | } 220 | } 221 | settings = vg.Tools.merge(settings, config); 222 | 223 | /*if (!settings.material) { 224 | settings.material = new THREE.MeshPhongMaterial({ 225 | color: vg.Tools.randomizeRGB('30, 30, 30', 10) 226 | }); 227 | }*/ 228 | 229 | // overwrite with any new dimensions 230 | this.cellSize = settings.cellSize; 231 | this._cellWidth = this.cellSize * 2; 232 | this._cellLength = (vg.SQRT3 * 0.5) * this._cellWidth; 233 | 234 | this.autogenerated = true; 235 | this.extrudeSettings = settings.extrudeSettings; 236 | 237 | var i, t, c; 238 | for (i in this.cells) { 239 | c = this.cells[i]; 240 | t = this.generateTile(c, settings.tileScale, settings.material); 241 | t.position.copy(this.cellToPixel(c)); 242 | t.position.y = 0; 243 | tiles.push(t); 244 | } 245 | return tiles; 246 | }, 247 | 248 | generateTilePoly: function(material) { 249 | if (!material) { 250 | material = new THREE.MeshBasicMaterial({color: 0x24b4ff}); 251 | } 252 | var mesh = new THREE.Mesh(this.cellShapeGeo, material); 253 | this._vec3.set(1, 0, 0); 254 | mesh.rotateOnAxis(this._vec3, vg.PI/2); 255 | return mesh; 256 | }, 257 | 258 | // create a flat, hexagon-shaped grid 259 | generate: function(config) { 260 | config = config || {}; 261 | this.size = typeof config.size === 'undefined' ? this.size : config.size; 262 | var x, y, z, c; 263 | for (x = -this.size; x < this.size+1; x++) { 264 | for (y = -this.size; y < this.size+1; y++) { 265 | z = -x-y; 266 | if (Math.abs(x) <= this.size && Math.abs(y) <= this.size && Math.abs(z) <= this.size) { 267 | c = new vg.Cell(x, y, z); 268 | this.add(c); 269 | } 270 | } 271 | } 272 | }, 273 | 274 | generateOverlay: function(size, overlayObj, overlayMat) { 275 | var x, y, z; 276 | var geo = this.cellShape.createPointsGeometry(); 277 | for (x = -size; x < size+1; x++) { 278 | for (y = -size; y < size+1; y++) { 279 | z = -x-y; 280 | if (Math.abs(x) <= size && Math.abs(y) <= size && Math.abs(z) <= size) { 281 | this._cel.set(x, y, z); // define the cell 282 | var line = new THREE.Line(geo, overlayMat); 283 | line.position.copy(this.cellToPixel(this._cel)); 284 | line.rotation.x = 90 * vg.DEG_TO_RAD; 285 | overlayObj.add(line); 286 | } 287 | } 288 | } 289 | }, 290 | 291 | add: function(cell) { 292 | var h = this.cellToHash(cell); 293 | if (this.cells[h]) { 294 | // console.warn('A cell already exists there'); 295 | return; 296 | } 297 | this.cells[h] = cell; 298 | this.numCells++; 299 | 300 | return cell; 301 | }, 302 | 303 | remove: function(cell) { 304 | var h = this.cellToHash(cell); 305 | if (this.cells[h]) { 306 | delete this.cells[h]; 307 | this.numCells--; 308 | } 309 | }, 310 | 311 | dispose: function() { 312 | this.cells = null; 313 | this.numCells = 0; 314 | this.cellShape = null; 315 | this.cellGeo.dispose(); 316 | this.cellGeo = null; 317 | this.cellShapeGeo.dispose(); 318 | this.cellShapeGeo = null; 319 | this._list = null; 320 | this._vec3 = null; 321 | this._conversionVec = null; 322 | this._geoCache = null; 323 | this._matCache = null; 324 | }, 325 | 326 | /* 327 | Load a grid from a parsed json object. 328 | json = { 329 | extrudeSettings, 330 | size, 331 | cellSize, 332 | autogenerated, 333 | cells: [], 334 | materials: [ 335 | { 336 | cache_id: 0, 337 | type: 'MeshLambertMaterial', 338 | color, ambient, emissive, reflectivity, refractionRatio, wrapAround, 339 | imgURL: url 340 | }, 341 | { 342 | cacheId: 1, ... 343 | } 344 | ... 345 | ] 346 | } 347 | */ 348 | load: function(url, cb, scope) { 349 | var self = this; 350 | vg.Tools.getJSON({ 351 | url: url, 352 | callback: function(json) { 353 | self.fromJSON(json); 354 | cb.call(scope || null, json); 355 | }, 356 | cache: false, 357 | scope: self 358 | }); 359 | }, 360 | 361 | fromJSON: function(json) { 362 | var i, c; 363 | var cells = json.cells; 364 | 365 | this.cells = {}; 366 | this.numCells = 0; 367 | 368 | this.size = json.size; 369 | this.cellSize = json.cellSize; 370 | this._cellWidth = this.cellSize * 2; 371 | this._cellLength = (vg.SQRT3 * 0.5) * this._cellWidth; 372 | 373 | this.extrudeSettings = json.extrudeSettings; 374 | this.autogenerated = json.autogenerated; 375 | 376 | for (i = 0; i < cells.length; i++) { 377 | c = new vg.Cell(); 378 | c.copy(cells[i]); 379 | this.add(c); 380 | } 381 | }, 382 | 383 | toJSON: function() { 384 | var json = { 385 | size: this.size, 386 | cellSize: this.cellSize, 387 | extrudeSettings: this.extrudeSettings, 388 | autogenerated: this.autogenerated 389 | }; 390 | var cells = []; 391 | var c, k; 392 | 393 | for (k in this.cells) { 394 | c = this.cells[k]; 395 | cells.push({ 396 | q: c.q, 397 | r: c.r, 398 | s: c.s, 399 | h: c.h, 400 | walkable: c.walkable, 401 | userData: c.userData 402 | }); 403 | } 404 | json.cells = cells; 405 | 406 | return json; 407 | }, 408 | 409 | /* ________________________________________________________________________ 410 | Hexagon-specific conversion math 411 | Mostly commented out because they're inlined whenever possible to increase performance. 412 | They're still here for reference. 413 | */ 414 | 415 | _createVertex: function(i) { 416 | var angle = (vg.TAU / 6) * i; 417 | return new THREE.Vector3((this.cellSize * Math.cos(angle)), (this.cellSize * Math.sin(angle)), 0); 418 | }, 419 | 420 | /*_pixelToAxial: function(pos) { 421 | var q, r; // = x, y 422 | q = pos.x * ((2/3) / this.cellSize); 423 | r = ((-pos.x / 3) + (vg.SQRT3/3) * pos.y) / this.cellSize; 424 | this._cel.set(q, r, -q-r); 425 | return this._cubeRound(this._cel); 426 | },*/ 427 | 428 | /*_axialToCube: function(h) { 429 | return { 430 | q: h.q, 431 | r: h.r, 432 | s: -h.q - h.r 433 | }; 434 | },*/ 435 | 436 | /*_cubeToAxial: function(cell) { 437 | return cell; // yep 438 | },*/ 439 | 440 | /*_axialToPixel: function(cell) { 441 | var x, y; // = q, r 442 | x = cell.q * this._cellWidth * 0.75; 443 | y = (cell.s - cell.r) * this._cellLength * 0.5; 444 | return {x: x, y: -y}; 445 | },*/ 446 | 447 | /*_hexToPixel: function(h) { 448 | var x, y; // = q, r 449 | x = this.cellSize * 1.5 * h.x; 450 | y = this.cellSize * vg.SQRT3 * (h.y + (h.x * 0.5)); 451 | return {x: x, y: y}; 452 | },*/ 453 | 454 | /*_axialRound: function(h) { 455 | return this._cubeRound(this.axialToCube(h)); 456 | },*/ 457 | 458 | _cubeRound: function(h) { 459 | var rx = Math.round(h.q); 460 | var ry = Math.round(h.r); 461 | var rz = Math.round(h.s); 462 | 463 | var xDiff = Math.abs(rx - h.q); 464 | var yDiff = Math.abs(ry - h.r); 465 | var zDiff = Math.abs(rz - h.s); 466 | 467 | if (xDiff > yDiff && xDiff > zDiff) { 468 | rx = -ry-rz; 469 | } 470 | else if (yDiff > zDiff) { 471 | ry = -rx-rz; 472 | } 473 | else { 474 | rz = -rx-ry; 475 | } 476 | 477 | return this._cel.set(rx, ry, rz); 478 | }, 479 | 480 | /*_cubeDistance: function(a, b) { 481 | return Math.max(Math.abs(a.q - b.q), Math.abs(a.r - b.r), Math.abs(a.s - b.s)); 482 | }*/ 483 | }; 484 | 485 | vg.HexGrid.prototype.constructor = vg.HexGrid; 486 | -------------------------------------------------------------------------------- /src/lib/Signal.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var SignalBinding = function (signal, listener, isOnce, listenerContext, priority) { 3 | /** 4 | * @property _listener - Handler function bound to the signal. 5 | * @private 6 | */ 7 | this._listener = listener; 8 | 9 | /** 10 | * @property {boolean} isOnce - If binding should be executed just once. 11 | * @private 12 | */ 13 | this.isOnce = isOnce; 14 | 15 | /** 16 | * @property {object|undefined|null} context - Context on which listener will be executed (object that should represent the `this` variable inside listener function). 17 | */ 18 | this.context = listenerContext; 19 | 20 | /** 21 | * @property {Signal} signal - Reference to Signal object that listener is currently bound to. 22 | * @private 23 | */ 24 | this.signal = signal; 25 | 26 | /** 27 | * @property {number} _priority - Listener priority. 28 | * @private 29 | */ 30 | this._priority = priority || 0; 31 | }; 32 | 33 | SignalBinding.prototype = { 34 | /** 35 | * If binding is active and should be executed. 36 | * @property {boolean} active 37 | * @default 38 | */ 39 | active: true, 40 | 41 | /** 42 | * Default parameters passed to listener during `Signal.dispatch` and `SignalBinding.execute` (curried parameters). 43 | * @property {array|null} params 44 | * @default 45 | */ 46 | params: null, 47 | 48 | /** 49 | * Call listener passing arbitrary parameters. 50 | * If binding was added using `Signal.addOnce()` it will be automatically removed from signal dispatch queue, this method is used internally for the signal dispatch. 51 | * @method SignalBinding#execute 52 | * @param {array} [paramsArr] - Array of parameters that should be passed to the listener. 53 | * @return {any} Value returned by the listener. 54 | */ 55 | execute: function(paramsArr) { 56 | 57 | var handlerReturn, params; 58 | 59 | if (this.active && !!this._listener) { 60 | params = this.params ? this.params.concat(paramsArr) : paramsArr; 61 | handlerReturn = this._listener.apply(this.context, params); 62 | 63 | if (this.isOnce) { 64 | this.detach(); 65 | } 66 | } 67 | 68 | return handlerReturn; 69 | 70 | }, 71 | 72 | /** 73 | * Detach binding from signal. 74 | * alias to: @see mySignal.remove(myBinding.listener); 75 | * @method SignalBinding#detach 76 | * @return {function|null} Handler function bound to the signal or `null` if binding was previously detached. 77 | */ 78 | detach: function () { 79 | return this.isBound() ? this.signal.remove(this._listener, this.context) : null; 80 | }, 81 | 82 | /** 83 | * @method SignalBinding#isBound 84 | * @return {boolean} True if binding is still bound to the signal and has a listener. 85 | */ 86 | isBound: function () { 87 | return (!!this.signal && !!this._listener); 88 | }, 89 | 90 | /** 91 | * Delete instance properties 92 | * @method SignalBinding#_destroy 93 | * @private 94 | */ 95 | _destroy: function () { 96 | delete this.signal; 97 | delete this._listener; 98 | delete this.context; 99 | }, 100 | 101 | /** 102 | * @method SignalBinding#toString 103 | * @return {string} String representation of the object. 104 | */ 105 | toString: function () { 106 | return '[SignalBinding isOnce:' + this.isOnce +', isBound:'+ this.isBound() +', active:' + this.active + ']'; 107 | } 108 | }; 109 | 110 | SignalBinding.prototype.constructor = SignalBinding; 111 | 112 | 113 | 114 | /** 115 | * @author Miller Medeiros http://millermedeiros.github.com/js-signals/ 116 | * @author Richard Davey 117 | * @copyright 2014 Photon Storm Ltd. 118 | * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} 119 | */ 120 | 121 | /** 122 | * A Signal is used for object communication via a custom broadcaster instead of Events. 123 | * 124 | * @class Signal 125 | * @constructor 126 | */ 127 | var Signal = function () { 128 | /** 129 | * @property {Array.} _bindings - Internal variable. 130 | * @private 131 | */ 132 | this._bindings = []; 133 | 134 | /** 135 | * @property {any} _prevParams - Internal variable. 136 | * @private 137 | */ 138 | this._prevParams = null; 139 | 140 | // enforce dispatch to aways work on same context (#47) 141 | var self = this; 142 | 143 | /** 144 | * @property {function} dispatch - The dispatch function is what sends the Signal out. 145 | */ 146 | this.dispatch = function(){ 147 | Signal.prototype.dispatch.apply(self, arguments); 148 | }; 149 | 150 | }; 151 | 152 | Signal.prototype = { 153 | /** 154 | * If Signal should keep record of previously dispatched parameters and 155 | * automatically execute listener during `add()`/`addOnce()` if Signal was 156 | * already dispatched before. 157 | * @property {boolean} memorize 158 | */ 159 | memorize: false, 160 | 161 | /** 162 | * @property {boolean} _shouldPropagate 163 | * @private 164 | */ 165 | _shouldPropagate: true, 166 | 167 | /** 168 | * If Signal is active and should broadcast events. 169 | * IMPORTANT: Setting this property during a dispatch will only affect the next dispatch, if you want to stop the propagation of a signal use `halt()` instead. 170 | * @property {boolean} active 171 | * @default 172 | */ 173 | active: true, 174 | 175 | /** 176 | * @method Signal#validateListener 177 | * @param {function} listener - Signal handler function. 178 | * @param {string} fnName - Function name. 179 | * @private 180 | */ 181 | validateListener: function (listener, fnName) { 182 | if (typeof listener !== 'function') { 183 | throw new Error('Signal: listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName)); 184 | } 185 | }, 186 | 187 | /** 188 | * @method Signal#_registerListener 189 | * @private 190 | * @param {function} listener - Signal handler function. 191 | * @param {boolean} isOnce - Should the listener only be called once? 192 | * @param {object} [listenerContext] - The context under which the listener is invoked. 193 | * @param {number} [priority] - The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added. (default = 0). 194 | * @return {SignalBinding} An Object representing the binding between the Signal and listener. 195 | */ 196 | _registerListener: function (listener, isOnce, listenerContext, priority) { 197 | var prevIndex = this._indexOfListener(listener, listenerContext); 198 | var binding; 199 | 200 | if (prevIndex !== -1) { 201 | binding = this._bindings[prevIndex]; 202 | 203 | if (binding.isOnce !== isOnce) { 204 | throw new Error('You cannot add' + (isOnce ? '' : 'Once') + '() then add' + (!isOnce ? '' : 'Once') + '() the same listener without removing the relationship first.'); 205 | } 206 | } 207 | else { 208 | binding = new SignalBinding(this, listener, isOnce, listenerContext, priority); 209 | this._addBinding(binding); 210 | } 211 | 212 | if (this.memorize && this._prevParams) { 213 | binding.execute(this._prevParams); 214 | } 215 | 216 | return binding; 217 | }, 218 | 219 | /** 220 | * @method Signal#_addBinding 221 | * @private 222 | * @param {SignalBinding} binding - An Object representing the binding between the Signal and listener. 223 | */ 224 | _addBinding: function (binding) { 225 | // Simplified insertion sort 226 | var n = this._bindings.length; 227 | 228 | do { 229 | n--; 230 | } 231 | while (this._bindings[n] && binding._priority <= this._bindings[n]._priority); 232 | 233 | this._bindings.splice(n + 1, 0, binding); 234 | }, 235 | 236 | /** 237 | * @method Signal#_indexOfListener 238 | * @private 239 | * @param {function} listener - Signal handler function. 240 | * @return {number} The index of the listener within the private bindings array. 241 | */ 242 | _indexOfListener: function (listener, context) { 243 | var n = this._bindings.length; 244 | var cur; 245 | 246 | while (n--) { 247 | cur = this._bindings[n]; 248 | 249 | if (cur._listener === listener && cur.context === context) { 250 | return n; 251 | } 252 | } 253 | 254 | return -1; 255 | }, 256 | 257 | /** 258 | * Check if listener was attached to Signal. 259 | * 260 | * @method Signal#has 261 | * @param {function} listener - Signal handler function. 262 | * @param {object} [context] - Context on which listener will be executed (object that should represent the `this` variable inside listener function). 263 | * @return {boolean} If Signal has the specified listener. 264 | */ 265 | has: function (listener, context) { 266 | return this._indexOfListener(listener, context) !== -1; 267 | }, 268 | 269 | /** 270 | * Add a listener to the signal. 271 | * 272 | * @method Signal#add 273 | * @param {function} listener - The function to call when this Signal is dispatched. 274 | * @param {object} [listenerContext] - The context under which the listener will be executed (i.e. the object that should represent the `this` variable). 275 | * @param {number} [priority] - The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added (default = 0) 276 | * @return {SignalBinding} An Object representing the binding between the Signal and listener. 277 | */ 278 | add: function (listener, listenerContext, priority) { 279 | this.validateListener(listener, 'add'); 280 | 281 | return this._registerListener(listener, false, listenerContext, priority); 282 | }, 283 | 284 | /** 285 | * Add listener to the signal that should be removed after first execution (will be executed only once). 286 | * 287 | * @method Signal#addOnce 288 | * @param {function} listener - The function to call when this Signal is dispatched. 289 | * @param {object} [listenerContext] - The context under which the listener will be executed (i.e. the object that should represent the `this` variable). 290 | * @param {number} [priority] - The priority level of the event listener. Listeners with higher priority will be executed before listeners with lower priority. Listeners with same priority level will be executed at the same order as they were added (default = 0) 291 | * @return {SignalBinding} An Object representing the binding between the Signal and listener. 292 | */ 293 | addOnce: function (listener, listenerContext, priority) { 294 | this.validateListener(listener, 'addOnce'); 295 | 296 | return this._registerListener(listener, true, listenerContext, priority); 297 | }, 298 | 299 | /** 300 | * Remove a single listener from the dispatch queue. 301 | * 302 | * @method Signal#remove 303 | * @param {function} listener - Handler function that should be removed. 304 | * @param {object} [context] - Execution context (since you can add the same handler multiple times if executing in a different context). 305 | * @return {function} Listener handler function. 306 | */ 307 | remove: function (listener, context) { 308 | this.validateListener(listener, 'remove'); 309 | 310 | var i = this._indexOfListener(listener, context); 311 | 312 | if (i !== -1) { 313 | this._bindings[i]._destroy(); //no reason to a SignalBinding exist if it isn't attached to a signal 314 | this._bindings.splice(i, 1); 315 | } 316 | 317 | return listener; 318 | }, 319 | 320 | /** 321 | * Remove all listeners from the Signal. 322 | * 323 | * @method Signal#removeAll 324 | * @param {object} [context=null] - If specified only listeners for the given context will be removed. 325 | */ 326 | removeAll: function (context) { 327 | if (typeof context === 'undefined') { context = null; } 328 | 329 | var n = this._bindings.length; 330 | 331 | while (n--) { 332 | if (context) { 333 | if (this._bindings[n].context === context) { 334 | this._bindings[n]._destroy(); 335 | this._bindings.splice(n, 1); 336 | } 337 | } 338 | else { 339 | this._bindings[n]._destroy(); 340 | } 341 | } 342 | 343 | if (!context) { 344 | this._bindings.length = 0; 345 | } 346 | }, 347 | 348 | /** 349 | * Gets the total number of listeneres attached to ths Signal. 350 | * 351 | * @method Signal#getNumListeners 352 | * @return {number} Number of listeners attached to the Signal. 353 | */ 354 | getNumListeners: function () { 355 | return this._bindings.length; 356 | }, 357 | 358 | /** 359 | * Stop propagation of the event, blocking the dispatch to next listeners on the queue. 360 | * IMPORTANT: should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast. 361 | * @see Signal.prototype.disable 362 | * 363 | * @method Signal#halt 364 | */ 365 | halt: function () { 366 | this._shouldPropagate = false; 367 | }, 368 | 369 | /** 370 | * Dispatch/Broadcast Signal to all listeners added to the queue. 371 | * 372 | * @method Signal#dispatch 373 | * @param {any} [params] - Parameters that should be passed to each handler. 374 | */ 375 | dispatch: function () { 376 | if (!this.active) { 377 | return; 378 | } 379 | 380 | var paramsArr = Array.prototype.slice.call(arguments); 381 | var n = this._bindings.length; 382 | var bindings; 383 | 384 | if (this.memorize) { 385 | this._prevParams = paramsArr; 386 | } 387 | 388 | if (!n) { 389 | // Should come after memorize 390 | return; 391 | } 392 | 393 | bindings = this._bindings.slice(); //clone array in case add/remove items during dispatch 394 | this._shouldPropagate = true; //in case `halt` was called before dispatch or during the previous dispatch. 395 | 396 | //execute all callbacks until end of the list or until a callback returns `false` or stops propagation 397 | //reverse loop since listeners with higher priority will be added at the end of the list 398 | do { 399 | n--; 400 | } 401 | while (bindings[n] && this._shouldPropagate && bindings[n].execute(paramsArr) !== false); 402 | }, 403 | 404 | /** 405 | * Forget memorized arguments. 406 | * @see Signal.memorize 407 | * 408 | * @method Signal#forget 409 | */ 410 | forget: function() { 411 | this._prevParams = null; 412 | }, 413 | 414 | /** 415 | * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). 416 | * IMPORTANT: calling any method on the signal instance after calling dispose will throw errors. 417 | * 418 | * @method Signal#dispose 419 | */ 420 | dispose: function () { 421 | this.removeAll(); 422 | 423 | delete this._bindings; 424 | delete this._prevParams; 425 | }, 426 | 427 | /** 428 | * 429 | * @method Signal#toString 430 | * @return {string} String representation of the object. 431 | */ 432 | toString: function () { 433 | return '[Signal active:'+ this.active +' numListeners:'+ this.getNumListeners() +']'; 434 | } 435 | 436 | }; 437 | 438 | Signal.prototype.constructor = Signal; 439 | 440 | vg.Signal = Signal; 441 | }()); 442 | -------------------------------------------------------------------------------- /editor/lib/riot.min.js: -------------------------------------------------------------------------------- 1 | /* Riot v2.3.12, @license MIT, (c) 2015 Muut Inc. + contributors */ 2 | (function(e,t){"use strict";var n={version:"v2.3.12",settings:{}},r=0,i=[],o={},f="riot-",u=f+"tag",a="string",s="object",c="undefined",l="function",p=/^(?:opt(ion|group)|tbody|col|t[rhd])$/,d=["_item","_id","_parent","update","root","mount","unmount","mixin","isMounted","isLoop","tags","parent","opts","trigger","on","off","one"],g=(e&&e.document||{}).documentMode|0;n.observable=function(e){e=e||{};var t={},n=function(e,t){e.replace(/\S+/g,t)},r=function(t,n){Object.defineProperty(e,t,{value:n,enumerable:false,writable:false,configurable:false})};r("on",function(r,i){if(typeof i!="function")return e;n(r,function(e,n){(t[e]=t[e]||[]).push(i);i.typed=n>0});return e});r("off",function(r,i){if(r=="*")t={};else{n(r,function(e){if(i){var n=t[e];for(var r=0,o;o=n&&n[r];++r){if(o==i)n.splice(r--,1)}}else delete t[e]})}return e});r("one",function(t,n){function r(){e.off(t,r);n.apply(e,arguments)}return e.on(t,r)});r("trigger",function(r){var i=arguments.length-1,o=new Array(i);for(var f=0;fa-zA-Z0-9'",;\\]/.test(e)){throw new Error('Unsupported brackets "'+e+'"')}r=r.concat(e.replace(/(?=[[\]()*+?.^$|])/g,"\\").split(" "));n=p}r[4]=n(r[1].length>1?/{[\S\s]*?}/:/{[^}]*}/,r);r[5]=n(/\\({|})/g,r);r[6]=n(/(\\?)({)/g,r);r[7]=RegExp("(\\\\?)(?:([[({])|("+r[3]+"))|"+o,t);r[8]=e;return r}function g(e){if(!e)e=f;if(e!==c[8]){c=d(e);s=e===f?l:p;c[9]=s(/^\s*{\^?\s*([$\w]+)(?:\s*,\s*(\S+))?\s+in\s+(\S.*)\s*}/);c[10]=s(/(^|[^\\]){=[\S\s]*?}/);h._rawOffset=c[0].length}a=e}function h(e){return e instanceof RegExp?s(e):c[e]}h.split=function y(e,t,n){if(!n)n=c;var r=[],i,o,f,a,s=n[6];o=f=s.lastIndex=0;while(i=s.exec(e)){a=i.index;if(o){if(i[2]){s.lastIndex=p(i[2],s.lastIndex);continue}if(!i[3])continue}if(!i[1]){l(e.slice(f,a));f=s.lastIndex;s=n[6+(o^=1)];s.lastIndex=f}}if(e&&f2||r[0]){var i,o,u=[];for(i=o=0;i2&&!t?""+(n.push(e)-1)+"~":e}).replace(/\s+/g," ").trim().replace(/\ ?([[\({},?\.:])\ ?/g,"$1");if(e){var r=[],i=0,f;while(e&&(f=e.match(a))&&!f.index){var u,s,c=/,|([[{(])|$/g;e=RegExp.rightContext;u=f[2]?n[f[2]].slice(1,-1).trim().replace(/\s+/g," "):f[1];while(s=(f=c.exec(e))[1])l(s,c);s=e.slice(0,f.index);e=RegExp.rightContext;r[i++]=d(s,1,u)}e=!i?d(e,t):i>1?"["+r.join(",")+'].join(" ").trim()':r[0]}return e;function l(t,n){var r,i=1,o=t==="("?/[()]/g:t==="["?/[[\]]/g:/[{}]/g;o.lastIndex=n.lastIndex;while(r=o.exec(e)){if(r[0]===t)++i;else if(!--i)break}n.lastIndex=i?e.length:o.lastIndex}}var l='"in this?this:'+(typeof e!=="object"?"global":"window")+").";var p=/[,{][$\w]+:|(^ *|[^$\w\.])(?!(?:typeof|true|false|null|undefined|in|instanceof|is(?:Finite|NaN)|void|NaN|new|Date|RegExp|Math)(?![$\w]))([$_A-Za-z][$\w]*)/g;function d(e,t,n){var r;e=e.replace(p,function(e,t,n,i,o){if(n){i=r?0:i+e.length;if(n!=="this"&&n!=="global"&&n!=="window"){e=t+'("'+n+l+n;if(i)r=(o=o[i])==="."||o==="("||o==="["}else if(i)r=!/^(?=(\.[$\w]+))\1(?:[^.[(]|$)/.test(o.slice(i))}return e});if(r){e="try{return "+e+"}catch(e){E(e,this)}"}if(n){e=(r?"function(){"+e+"}.call(this)":"("+e+")")+'?"'+n+'":""'}else if(t){e="function(v){"+(r?e.replace("return ","v="):"v=("+e+")")+';return v||v===0?v:""}.call(this)'}return e}n.parse=function(e){return e};return n}();v.version=h.version="v2.3.19";var m=function(e){var t={tr:"tbody",th:"tr",td:"tr",tbody:"table",col:"colgroup"},n="div";e=e&&e<10;function r(r){var o=r&&r.match(/^\s*<([-\w]+)/),f=o&&o[1].toLowerCase(),u=t[f]||n,a=W(u);a.stub=true;if(e&&f&&(o=f.match(p)))i(a,r,f,!!o[1]);else a.innerHTML=r;return a}function i(e,t,r,i){var o=W(n),f=i?"select>":"table>",u;o.innerHTML="<"+f+t+"r){var i=t[--n];t.splice(n,1);i.unmount()}}function w(e,t){Object.keys(e.tags).forEach(function(n){var r=e.tags[n];if(U(r))A(r,function(e){F(e,n,t)});else F(r,n,t)})}function x(e,t,n){var r=e._root;e._virts=[];while(r){var i=r.nextSibling;if(n)t.insertBefore(r,n._root);else t.appendChild(r);e._virts.push(r);r=i}}function _(e,t,n,r){var i=e._root;for(var o=0;o|>\s*<\/yield\s*>)/gi,t||"")}function ee(e,t){return(t||document).querySelectorAll(e)}function te(e,t){return(t||document).querySelector(e)}function ne(e){function t(){}t.prototype=e;return new t}function re(e){return R(e,"id")||R(e,"name")}function ie(e,t,n){var r=re(e),i=function(i){if(z(n,r))return;var o=U(i);if(!i)t[r]=e;else if(!o||o&&!z(i,e)){if(o)i.push(e);else t[r]=[i,e]}};if(!r)return;if(v.hasExpr(r))t.one("updated",function(){r=re(e);i(t[r])});else i(t[r])}function oe(e,t){return e.slice(0,t.length)===t}var fe=function(){if(!e)return;var t=W("style"),n=te("style[type=riot]");k(t,"type","text/css");if(n){n.parentNode.replaceChild(t,n);n=null}else document.getElementsByTagName("head")[0].appendChild(t);return t.styleSheet?function(e){t.styleSheet.cssText+=e}:function(e){t.innerHTML+=e}}();var ue=function(e){return e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||function(e){setTimeout(e,1e3/60)}}(e||{});function ae(e,t,n){var r=o[t],f=e._innerHTML=e._innerHTML||e.innerHTML;e.innerHTML="";if(r&&e)r=new C(r,{root:e,opts:n},f);if(r&&r.mount){r.mount();if(!z(i,r))i.push(r)}return r}n.util={brackets:h,tmpl:v};n.mixin=function(){var e={};return function(t,n){if(!n)return e[t];e[t]=n}}();n.tag=function(e,t,n,r,i){if(O(r)){i=r;if(/^[\w\-]+\s?=/.test(n)){r=n;n=""}else r=""}if(n){if(O(n))i=n;else if(fe)fe(n)}o[e]={name:e,tmpl:t,attrs:r,fn:i};return e};n.tag2=function(e,t,n,r,i,f){if(n&&fe)fe(n);o[e]={name:e,tmpl:t,attrs:r,fn:i};return e};n.mount=function(e,t,n){var r,i,f=[];function c(e){var t="";A(e,function(e){t+=", *["+u+'="'+e.trim()+'"]'});return t}function l(){var e=Object.keys(o);return e+c(e)}function p(e){var r;if(e.tagName){if(t&&(!(r=R(e,u))||r!=t))k(e,u,t);var i=ae(e,t||e.getAttribute(u)||e.tagName.toLowerCase(),n);if(i)f.push(i)}else if(e.length)A(e,p)}if(typeof t===s){n=t;t=0}if(typeof e===a){if(e==="*")e=i=l();else e+=c(e.split(","));r=e?ee(e):[]}else r=e;if(t==="*"){t=i||l();if(r.tagName)r=ee(t,r);else{var d=[];A(r,function(e){d.push(ee(t,e))});r=d}t=0}if(r.tagName)p(r);else A(r,p);return f};n.update=function(){return A(i,function(e){e.update()})};n.Tag=C;if(typeof exports===s)module.exports=n;else if(typeof define===l&&typeof define.amd!==c)define(function(){return e.riot=n});else e.riot=n})(typeof window!="undefined"?window:void 0); 3 | -------------------------------------------------------------------------------- /lib/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | /*global THREE, console */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains 11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is 12 | // supported. 13 | // 14 | // Orbit - left mouse / touch: one finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 16 | // Pan - right mouse, or arrow keys / touch: three finter swipe 17 | // 18 | // This is a drop-in replacement for (most) TrackballControls used in examples. 19 | // That is, include this js file and wherever you see: 20 | // controls = new THREE.TrackballControls( camera ); 21 | // controls.target.z = 150; 22 | // Simple substitute "OrbitControls" and the control should work as-is. 23 | 24 | THREE.OrbitControls = function ( object, domElement ) { 25 | 26 | this.object = object; 27 | this.domElement = ( domElement !== undefined ) ? domElement : document; 28 | 29 | // API 30 | 31 | // Set to false to disable this control 32 | this.enabled = true; 33 | 34 | // "target" sets the location of focus, where the control orbits around 35 | // and where it pans with respect to. 36 | this.target = new THREE.Vector3(); 37 | 38 | // center is old, deprecated; use "target" instead 39 | this.center = this.target; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for 42 | // backwards compatibility 43 | this.noZoom = false; 44 | this.zoomSpeed = 1.0; 45 | 46 | // Limits to how far you can dolly in and out 47 | this.minDistance = 0; 48 | this.maxDistance = Infinity; 49 | 50 | // Set to true to disable this control 51 | this.noRotate = false; 52 | this.rotateSpeed = 1.0; 53 | 54 | // Set to true to disable this control 55 | this.noPan = false; 56 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 57 | 58 | // Set to true to automatically rotate around the target 59 | this.autoRotate = false; 60 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 61 | 62 | // How far you can orbit vertically, upper and lower limits. 63 | // Range is 0 to Math.PI radians. 64 | this.minPolarAngle = 0; // radians 65 | this.maxPolarAngle = Math.PI; // radians 66 | 67 | // How far you can orbit horizontally, upper and lower limits. 68 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 69 | this.minAzimuthAngle = - Infinity; // radians 70 | this.maxAzimuthAngle = Infinity; // radians 71 | 72 | // Set to true to disable use of the keys 73 | this.noKeys = false; 74 | 75 | // The four arrow keys 76 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 77 | 78 | // Mouse buttons 79 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 80 | 81 | //////////// 82 | // internals 83 | 84 | var scope = this; 85 | 86 | var EPS = 0.000001; 87 | 88 | var rotateStart = new THREE.Vector2(); 89 | var rotateEnd = new THREE.Vector2(); 90 | var rotateDelta = new THREE.Vector2(); 91 | 92 | var panStart = new THREE.Vector2(); 93 | var panEnd = new THREE.Vector2(); 94 | var panDelta = new THREE.Vector2(); 95 | var panOffset = new THREE.Vector3(); 96 | 97 | var offset = new THREE.Vector3(); 98 | 99 | var dollyStart = new THREE.Vector2(); 100 | var dollyEnd = new THREE.Vector2(); 101 | var dollyDelta = new THREE.Vector2(); 102 | 103 | var theta; 104 | var phi; 105 | var phiDelta = 0; 106 | var thetaDelta = 0; 107 | var scale = 1; 108 | var pan = new THREE.Vector3(); 109 | 110 | var lastPosition = new THREE.Vector3(); 111 | var lastQuaternion = new THREE.Quaternion(); 112 | 113 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 114 | 115 | var state = STATE.NONE; 116 | 117 | // for reset 118 | 119 | this.target0 = this.target.clone(); 120 | this.position0 = this.object.position.clone(); 121 | 122 | // so camera.up is the orbit axis 123 | 124 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 125 | var quatInverse = quat.clone().inverse(); 126 | 127 | // events 128 | 129 | var changeEvent = { type: 'change' }; 130 | var startEvent = { type: 'start'}; 131 | var endEvent = { type: 'end'}; 132 | var wheelEvent = { type: 'wheel'}; 133 | 134 | this.rotateLeft = function ( angle ) { 135 | 136 | if ( angle === undefined ) { 137 | 138 | angle = getAutoRotationAngle(); 139 | 140 | } 141 | 142 | thetaDelta -= angle; 143 | 144 | }; 145 | 146 | this.rotateUp = function ( angle ) { 147 | 148 | if ( angle === undefined ) { 149 | 150 | angle = getAutoRotationAngle(); 151 | 152 | } 153 | 154 | phiDelta -= angle; 155 | 156 | }; 157 | 158 | // pass in distance in world space to move left 159 | this.panLeft = function ( distance ) { 160 | 161 | var te = this.object.matrix.elements; 162 | 163 | // get X column of matrix 164 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); 165 | panOffset.multiplyScalar( - distance ); 166 | 167 | pan.add( panOffset ); 168 | 169 | }; 170 | 171 | // pass in distance in world space to move up 172 | this.panUp = function ( distance ) { 173 | 174 | var te = this.object.matrix.elements; 175 | 176 | // get Y column of matrix 177 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); 178 | panOffset.multiplyScalar( distance ); 179 | 180 | pan.add( panOffset ); 181 | 182 | }; 183 | 184 | // pass in x,y of change desired in pixel space, 185 | // right and down are positive 186 | this.pan = function ( deltaX, deltaY ) { 187 | 188 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 189 | 190 | if ( scope.object.fov !== undefined ) { 191 | 192 | // perspective 193 | var position = scope.object.position; 194 | var offset = position.clone().sub( scope.target ); 195 | var targetDistance = offset.length(); 196 | 197 | // half of the fov is center to top of screen 198 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 199 | 200 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 201 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); 202 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); 203 | 204 | } else if ( scope.object.top !== undefined ) { 205 | 206 | // orthographic 207 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); 208 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); 209 | 210 | } else { 211 | 212 | // camera neither orthographic or perspective 213 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 214 | 215 | } 216 | 217 | }; 218 | 219 | this.dollyIn = function ( dollyScale ) { 220 | 221 | if ( dollyScale === undefined ) { 222 | 223 | dollyScale = getZoomScale(); 224 | 225 | } 226 | 227 | scale /= dollyScale; 228 | 229 | }; 230 | 231 | this.dollyOut = function ( dollyScale ) { 232 | 233 | if ( dollyScale === undefined ) { 234 | 235 | dollyScale = getZoomScale(); 236 | 237 | } 238 | 239 | scale *= dollyScale; 240 | 241 | }; 242 | 243 | this.update = function () { 244 | 245 | var position = this.object.position; 246 | 247 | offset.copy( position ).sub( this.target ); 248 | 249 | // rotate offset to "y-axis-is-up" space 250 | offset.applyQuaternion( quat ); 251 | 252 | // angle from z-axis around y-axis 253 | 254 | theta = Math.atan2( offset.x, offset.z ); 255 | 256 | // angle from y-axis 257 | 258 | phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 259 | 260 | if ( this.autoRotate && state === STATE.NONE ) { 261 | 262 | this.rotateLeft( getAutoRotationAngle() ); 263 | 264 | } 265 | 266 | theta += thetaDelta; 267 | phi += phiDelta; 268 | 269 | // restrict theta to be between desired limits 270 | theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); 271 | 272 | // restrict phi to be between desired limits 273 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 274 | 275 | // restrict phi to be betwee EPS and PI-EPS 276 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 277 | 278 | var radius = offset.length() * scale; 279 | 280 | // restrict radius to be between desired limits 281 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 282 | 283 | // move target to panned location 284 | this.target.add( pan ); 285 | 286 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 287 | offset.y = radius * Math.cos( phi ); 288 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 289 | 290 | // rotate offset back to "camera-up-vector-is-up" space 291 | offset.applyQuaternion( quatInverse ); 292 | 293 | position.copy( this.target ).add( offset ); 294 | 295 | this.object.lookAt( this.target ); 296 | 297 | thetaDelta = 0; 298 | phiDelta = 0; 299 | scale = 1; 300 | pan.set( 0, 0, 0 ); 301 | 302 | // update condition is: 303 | // min(camera displacement, camera rotation in radians)^2 > EPS 304 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 305 | 306 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS 307 | || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { 308 | 309 | this.dispatchEvent( changeEvent ); 310 | 311 | lastPosition.copy( this.object.position ); 312 | lastQuaternion.copy (this.object.quaternion ); 313 | 314 | } 315 | 316 | }; 317 | 318 | 319 | this.reset = function () { 320 | 321 | state = STATE.NONE; 322 | 323 | this.target.copy( this.target0 ); 324 | this.object.position.copy( this.position0 ); 325 | 326 | this.update(); 327 | 328 | }; 329 | 330 | this.getPolarAngle = function () { 331 | 332 | return phi; 333 | 334 | }; 335 | 336 | this.getAzimuthalAngle = function () { 337 | 338 | return theta 339 | 340 | }; 341 | 342 | function getAutoRotationAngle() { 343 | 344 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 345 | 346 | } 347 | 348 | function getZoomScale() { 349 | 350 | return Math.pow( 0.95, scope.zoomSpeed ); 351 | 352 | } 353 | 354 | function onMouseDown( event ) { 355 | 356 | if ( scope.enabled === false ) return; 357 | event.preventDefault(); 358 | 359 | if ( event.button === scope.mouseButtons.ORBIT ) { 360 | if ( scope.noRotate === true ) return; 361 | 362 | state = STATE.ROTATE; 363 | 364 | rotateStart.set( event.clientX, event.clientY ); 365 | 366 | } else if ( event.button === scope.mouseButtons.ZOOM ) { 367 | if ( scope.noZoom === true ) return; 368 | 369 | state = STATE.DOLLY; 370 | 371 | dollyStart.set( event.clientX, event.clientY ); 372 | 373 | } else if ( event.button === scope.mouseButtons.PAN ) { 374 | if ( scope.noPan === true ) return; 375 | 376 | state = STATE.PAN; 377 | 378 | panStart.set( event.clientX, event.clientY ); 379 | 380 | } 381 | 382 | if ( state !== STATE.NONE ) { 383 | document.addEventListener( 'mousemove', onMouseMove, false ); 384 | document.addEventListener( 'mouseup', onMouseUp, false ); 385 | scope.dispatchEvent( startEvent ); 386 | } 387 | 388 | } 389 | 390 | function onMouseMove( event ) { 391 | 392 | if ( scope.enabled === false ) return; 393 | 394 | event.preventDefault(); 395 | 396 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 397 | 398 | if ( state === STATE.ROTATE ) { 399 | 400 | if ( scope.noRotate === true ) return; 401 | 402 | rotateEnd.set( event.clientX, event.clientY ); 403 | rotateDelta.subVectors( rotateEnd, rotateStart ); 404 | 405 | // rotating across whole screen goes 360 degrees around 406 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 407 | 408 | // rotating up and down along whole screen attempts to go 360, but limited to 180 409 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 410 | 411 | rotateStart.copy( rotateEnd ); 412 | 413 | } else if ( state === STATE.DOLLY ) { 414 | 415 | if ( scope.noZoom === true ) return; 416 | 417 | dollyEnd.set( event.clientX, event.clientY ); 418 | dollyDelta.subVectors( dollyEnd, dollyStart ); 419 | 420 | if ( dollyDelta.y > 0 ) { 421 | 422 | scope.dollyIn(); 423 | 424 | } else { 425 | 426 | scope.dollyOut(); 427 | 428 | } 429 | 430 | dollyStart.copy( dollyEnd ); 431 | 432 | } else if ( state === STATE.PAN ) { 433 | 434 | if ( scope.noPan === true ) return; 435 | 436 | panEnd.set( event.clientX, event.clientY ); 437 | panDelta.subVectors( panEnd, panStart ); 438 | 439 | scope.pan( panDelta.x, panDelta.y ); 440 | 441 | panStart.copy( panEnd ); 442 | 443 | } 444 | 445 | if ( state !== STATE.NONE ) scope.update(); 446 | 447 | } 448 | 449 | function onMouseUp( /* event */ ) { 450 | 451 | if ( scope.enabled === false ) return; 452 | 453 | document.removeEventListener( 'mousemove', onMouseMove, false ); 454 | document.removeEventListener( 'mouseup', onMouseUp, false ); 455 | scope.dispatchEvent( endEvent ); 456 | state = STATE.NONE; 457 | 458 | } 459 | 460 | function onMouseWheel( event ) { 461 | 462 | if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return; 463 | 464 | event.preventDefault(); 465 | event.stopPropagation(); 466 | 467 | var delta = 0; 468 | 469 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 470 | 471 | delta = event.wheelDelta; 472 | 473 | } else if ( event.detail !== undefined ) { // Firefox 474 | 475 | delta = - event.detail; 476 | 477 | } 478 | 479 | if ( delta > 0 ) { 480 | 481 | scope.dollyOut(); 482 | 483 | } else { 484 | 485 | scope.dollyIn(); 486 | 487 | } 488 | 489 | scope.update(); 490 | scope.dispatchEvent( wheelEvent ); 491 | // scope.dispatchEvent( endEvent ); 492 | 493 | } 494 | 495 | function onKeyDown( event ) { 496 | 497 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; 498 | 499 | switch ( event.keyCode ) { 500 | 501 | case scope.keys.UP: 502 | scope.pan( 0, scope.keyPanSpeed ); 503 | scope.update(); 504 | break; 505 | 506 | case scope.keys.BOTTOM: 507 | scope.pan( 0, - scope.keyPanSpeed ); 508 | scope.update(); 509 | break; 510 | 511 | case scope.keys.LEFT: 512 | scope.pan( scope.keyPanSpeed, 0 ); 513 | scope.update(); 514 | break; 515 | 516 | case scope.keys.RIGHT: 517 | scope.pan( - scope.keyPanSpeed, 0 ); 518 | scope.update(); 519 | break; 520 | 521 | } 522 | 523 | } 524 | 525 | function touchstart( event ) { 526 | 527 | if ( scope.enabled === false ) return; 528 | 529 | switch ( event.touches.length ) { 530 | 531 | case 1: // one-fingered touch: rotate 532 | 533 | if ( scope.noRotate === true ) return; 534 | 535 | state = STATE.TOUCH_ROTATE; 536 | 537 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 538 | break; 539 | 540 | case 2: // two-fingered touch: dolly 541 | 542 | if ( scope.noZoom === true ) return; 543 | 544 | state = STATE.TOUCH_DOLLY; 545 | 546 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 547 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 548 | var distance = Math.sqrt( dx * dx + dy * dy ); 549 | dollyStart.set( 0, distance ); 550 | break; 551 | 552 | case 3: // three-fingered touch: pan 553 | 554 | if ( scope.noPan === true ) return; 555 | 556 | state = STATE.TOUCH_PAN; 557 | 558 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 559 | break; 560 | 561 | default: 562 | 563 | state = STATE.NONE; 564 | 565 | } 566 | 567 | if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); 568 | 569 | } 570 | 571 | function touchmove( event ) { 572 | 573 | if ( scope.enabled === false ) return; 574 | 575 | event.preventDefault(); 576 | event.stopPropagation(); 577 | 578 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 579 | 580 | switch ( event.touches.length ) { 581 | 582 | case 1: // one-fingered touch: rotate 583 | 584 | if ( scope.noRotate === true ) return; 585 | if ( state !== STATE.TOUCH_ROTATE ) return; 586 | 587 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 588 | rotateDelta.subVectors( rotateEnd, rotateStart ); 589 | 590 | // rotating across whole screen goes 360 degrees around 591 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 592 | // rotating up and down along whole screen attempts to go 360, but limited to 180 593 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 594 | 595 | rotateStart.copy( rotateEnd ); 596 | 597 | scope.update(); 598 | break; 599 | 600 | case 2: // two-fingered touch: dolly 601 | 602 | if ( scope.noZoom === true ) return; 603 | if ( state !== STATE.TOUCH_DOLLY ) return; 604 | 605 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 606 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 607 | var distance = Math.sqrt( dx * dx + dy * dy ); 608 | 609 | dollyEnd.set( 0, distance ); 610 | dollyDelta.subVectors( dollyEnd, dollyStart ); 611 | 612 | if ( dollyDelta.y > 0 ) { 613 | 614 | scope.dollyOut(); 615 | 616 | } else { 617 | 618 | scope.dollyIn(); 619 | 620 | } 621 | 622 | dollyStart.copy( dollyEnd ); 623 | 624 | scope.update(); 625 | break; 626 | 627 | case 3: // three-fingered touch: pan 628 | 629 | if ( scope.noPan === true ) return; 630 | if ( state !== STATE.TOUCH_PAN ) return; 631 | 632 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 633 | panDelta.subVectors( panEnd, panStart ); 634 | 635 | scope.pan( panDelta.x, panDelta.y ); 636 | 637 | panStart.copy( panEnd ); 638 | 639 | scope.update(); 640 | break; 641 | 642 | default: 643 | 644 | state = STATE.NONE; 645 | 646 | } 647 | 648 | } 649 | 650 | function touchend( /* event */ ) { 651 | 652 | if ( scope.enabled === false ) return; 653 | 654 | scope.dispatchEvent( endEvent ); 655 | state = STATE.NONE; 656 | 657 | } 658 | 659 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); 660 | this.domElement.addEventListener( 'mousedown', onMouseDown, false ); 661 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); 662 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox 663 | 664 | this.domElement.addEventListener( 'touchstart', touchstart, false ); 665 | this.domElement.addEventListener( 'touchend', touchend, false ); 666 | this.domElement.addEventListener( 'touchmove', touchmove, false ); 667 | 668 | window.addEventListener( 'keydown', onKeyDown, false ); 669 | 670 | // force an update at start 671 | this.update(); 672 | 673 | }; 674 | 675 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 676 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 677 | -------------------------------------------------------------------------------- /editor/app.js: -------------------------------------------------------------------------------- 1 | !function(){function e(e){if(n.hasOwnProperty(e))return n[e] 2 | throw'[require-shim] Cannot find module "'+e+'"'}function i(i,o,r){var d=null,t=r&&void 0!==r 3 | if(t){if(r.hasOwnProperty(i))throw"[define-shim] Module "+i+" already exists"}else if(n.hasOwnProperty(i))throw"[define-shim] Module "+i+" already exists" 4 | d="function"==typeof o?o(e):o,t?r[i]=d:n[i]=d}var n={} 5 | window.define=i,window.define.amd=!1,window.require=e}() 6 | 7 | window.addEventListener('load', function(evt) { 8 | var data = require('data'); 9 | var tower = require('tower'); 10 | var nexus = require('nexus'); 11 | var keyboard = require('keyboard'); 12 | var motor = require('motor'); 13 | 14 | var Input = require('Input'); 15 | var EditorPlane = require('EditorPlane'); 16 | 17 | data.load(); 18 | var map = data.get('map'); 19 | 20 | var timeTilAutoSave = 200; // timer runs per frame, 60fps 21 | var saveTimer = 10; 22 | var dirtyMap = false; 23 | var shiftDown = false; 24 | var paintMode = false; 25 | var deleteMode = false; 26 | var addMode = false; 27 | 28 | var saveBtn = document.getElementById('save-btn'); 29 | saveBtn.onmouseup = function(evt) { 30 | saveMap(); 31 | return false; 32 | }; 33 | 34 | var loadBtn = document.getElementById('load-btn'); 35 | loadBtn.addEventListener('click', function() { 36 | fileInput.click(); 37 | }, false); 38 | 39 | var fileInput = document.createElement('input'); 40 | fileInput.type = 'file'; 41 | fileInput.addEventListener('change', function(evt) { 42 | var file = fileInput.files[0]; 43 | if (!file) { 44 | return; 45 | } 46 | 47 | var reader = new FileReader(); 48 | reader.onload = function(e) { 49 | var json = null; 50 | try { 51 | json = JSON.parse(e.target.result); 52 | } 53 | catch(err) { 54 | console.warn('File is not json format'); 55 | return; 56 | } 57 | loadMap(json); 58 | }; 59 | 60 | reader.readAsText(file); 61 | 62 | return false; 63 | }); 64 | 65 | keyboard.on(); 66 | motor.on(); 67 | 68 | // setup the thing 69 | var canvas = document.getElementById('view'); 70 | var scene = new vg.Scene({ 71 | element: canvas, 72 | cameraPosition: {x:0, y:300, z:120} 73 | }, true); 74 | 75 | // listen to the orbit controls to disable the raycaster while user adjusts the view 76 | scene.controls.addEventListener('wheel', onControlWheel); 77 | 78 | var grid = new vg.HexGrid({ 79 | rings: 5, 80 | cellSize: 10 81 | }); 82 | var board = new vg.Board(grid); 83 | var mouse = new vg.MouseCaster(board.group, scene.camera, canvas); 84 | var input = new Input(board.group, mouse); 85 | var plane = new EditorPlane(board.group, grid, mouse); 86 | 87 | nexus.input = input; 88 | nexus.plane = plane; 89 | nexus.board = board; 90 | nexus.grid = grid; 91 | nexus.scene = scene; 92 | nexus.mouse = mouse; 93 | 94 | var boardSize = 20; // TODO: get from settings 95 | plane.generatePlane(boardSize * boardSize * 1.8, boardSize * boardSize * 1.8); 96 | plane.addHoverMeshToGroup(scene.container); 97 | 98 | board.generateOverlay(boardSize); 99 | 100 | tower.tileAction.add(onMapChange, this); 101 | 102 | // scene.add(board.group); 103 | scene.focusOn(board.group); 104 | 105 | if (map) { 106 | loadMap(map); 107 | } 108 | else { 109 | board.generateTilemap(); 110 | map = grid.toJSON(); 111 | data.set('map', map); 112 | console.log('Created a new map'); 113 | } 114 | scene.add(board.group); 115 | 116 | function update() { 117 | if (wheelTimer < 10) { 118 | wheelTimer++; 119 | if (wheelTimer === 10) { 120 | mouse.active = true; 121 | } 122 | } 123 | if (dirtyMap) { 124 | saveTimer--; 125 | if (saveTimer === 0) { 126 | dirtyMap = false; 127 | data.set('map', map); 128 | data.save(); 129 | console.log('Map saved'); 130 | } 131 | } 132 | mouse.update(); 133 | input.update(); 134 | plane.update(); 135 | scene.render(); 136 | }; 137 | motor.add(update); 138 | 139 | var wheelTimer = 10; 140 | function onControlWheel() { 141 | mouse.active = false; 142 | wheelTimer = 0; 143 | } 144 | 145 | function onMapChange() { 146 | dirtyMap = true; 147 | saveTimer = timeTilAutoSave; 148 | map = grid.toJSON(); 149 | } 150 | 151 | function loadMap(json) { 152 | grid.fromJSON(json); 153 | board.setGrid(grid); 154 | 155 | if (json.autogenerated) { 156 | board.generateTilemap(); 157 | } 158 | console.log('Map load complete'); 159 | } 160 | 161 | function saveMap() { 162 | var output = null; 163 | 164 | map = grid.toJSON(); 165 | 166 | try { 167 | output = JSON.stringify(map, null, '\t'); 168 | output = output.replace(/[\n\t]+([\d\.e\-\[\]]+)/g, '$1'); 169 | } catch (e) { 170 | output = JSON.stringify(map); 171 | } 172 | 173 | exportString(output, 'hex-map.json'); 174 | } 175 | 176 | // taken from https://github.com/mrdoob/three.js/blob/master/editor/js/Menubar.File.js 177 | var link = document.createElement('a'); 178 | link.style.display = 'none'; 179 | document.body.appendChild(link); 180 | 181 | function exportString(output, filename) { 182 | var blob = new Blob([output], {type: 'text/plain'}); 183 | var objectURL = URL.createObjectURL(blob); 184 | 185 | link.href = objectURL; 186 | link.download = filename || 'data.json'; 187 | link.target = '_blank'; 188 | 189 | var evt = document.createEvent('MouseEvents'); 190 | evt.initMouseEvent( 191 | 'click', true, false, window, 0, 0, 0, 0, 0, 192 | false, false, false, false, 0, null 193 | ); 194 | link.dispatchEvent(evt); 195 | } 196 | }); 197 | define('keyboard', function() { 198 | 199 | function onDown(evt) { 200 | switch (evt.keyCode) { 201 | case 16: 202 | k.shift = true; 203 | break; 204 | case 17: 205 | k.ctrl = true; 206 | break; 207 | } 208 | k.signal.dispatch(k.eventType.DOWN, evt.keyCode); 209 | } 210 | 211 | function onUp(evt) { 212 | switch (evt.keyCode) { 213 | case 16: 214 | k.shift = false; 215 | break; 216 | case 17: 217 | k.ctrl = false; 218 | break; 219 | } 220 | k.signal.dispatch(k.eventType.UP, evt.keyCode); 221 | } 222 | 223 | var k = { 224 | shift: false, 225 | ctrl: false, 226 | 227 | eventType: { 228 | DOWN: 'down', 229 | UP: 'up' 230 | }, 231 | 232 | signal: new vg.Signal(), 233 | 234 | on: function() { 235 | document.addEventListener('keydown', onDown, false); 236 | document.addEventListener('keyup', onUp, false); 237 | }, 238 | 239 | off: function() { 240 | document.removeEventListener('keydown', onDown); 241 | document.removeEventListener('keyup', onUp); 242 | }, 243 | 244 | code: { 245 | A: 'A'.charCodeAt(0), 246 | B: 'B'.charCodeAt(0), 247 | C: 'C'.charCodeAt(0), 248 | D: 'D'.charCodeAt(0), 249 | E: 'E'.charCodeAt(0), 250 | F: 'F'.charCodeAt(0), 251 | G: 'G'.charCodeAt(0), 252 | H: 'H'.charCodeAt(0), 253 | I: 'I'.charCodeAt(0), 254 | J: 'J'.charCodeAt(0), 255 | K: 'K'.charCodeAt(0), 256 | L: 'L'.charCodeAt(0), 257 | M: 'M'.charCodeAt(0), 258 | N: 'N'.charCodeAt(0), 259 | O: 'O'.charCodeAt(0), 260 | P: 'P'.charCodeAt(0), 261 | Q: 'Q'.charCodeAt(0), 262 | R: 'R'.charCodeAt(0), 263 | S: 'S'.charCodeAt(0), 264 | T: 'T'.charCodeAt(0), 265 | U: 'U'.charCodeAt(0), 266 | V: 'V'.charCodeAt(0), 267 | W: 'W'.charCodeAt(0), 268 | X: 'X'.charCodeAt(0), 269 | Y: 'Y'.charCodeAt(0), 270 | Z: 'Z'.charCodeAt(0), 271 | ZERO: '0'.charCodeAt(0), 272 | ONE: '1'.charCodeAt(0), 273 | TWO: '2'.charCodeAt(0), 274 | THREE: '3'.charCodeAt(0), 275 | FOUR: '4'.charCodeAt(0), 276 | FIVE: '5'.charCodeAt(0), 277 | SIX: '6'.charCodeAt(0), 278 | SEVEN: '7'.charCodeAt(0), 279 | EIGHT: '8'.charCodeAt(0), 280 | NINE: '9'.charCodeAt(0), 281 | NUMPAD_0: 96, 282 | NUMPAD_1: 97, 283 | NUMPAD_2: 98, 284 | NUMPAD_3: 99, 285 | NUMPAD_4: 100, 286 | NUMPAD_5: 101, 287 | NUMPAD_6: 102, 288 | NUMPAD_7: 103, 289 | NUMPAD_8: 104, 290 | NUMPAD_9: 105, 291 | NUMPAD_MULTIPLY: 106, 292 | NUMPAD_ADD: 107, 293 | NUMPAD_ENTER: 108, 294 | NUMPAD_SUBTRACT: 109, 295 | NUMPAD_DECIMAL: 110, 296 | NUMPAD_DIVIDE: 111, 297 | F1: 112, 298 | F2: 113, 299 | F3: 114, 300 | F4: 115, 301 | F5: 116, 302 | F6: 117, 303 | F7: 118, 304 | F8: 119, 305 | F9: 120, 306 | F10: 121, 307 | F11: 122, 308 | F12: 123, 309 | F13: 124, 310 | F14: 125, 311 | F15: 126, 312 | COLON: 186, 313 | EQUALS: 187, 314 | UNDERSCORE: 189, 315 | QUESTION_MARK: 191, 316 | TILDE: 192, 317 | OPEN_BRACKET: 219, 318 | BACKWARD_SLASH: 220, 319 | CLOSED_BRACKET: 221, 320 | QUOTES: 222, 321 | BACKSPACE: 8, 322 | TAB: 9, 323 | CLEAR: 12, 324 | ENTER: 13, 325 | SHIFT: 16, 326 | CTRL: 17, 327 | ALT: 18, 328 | CAPS_LOCK: 20, 329 | ESC: 27, 330 | SPACEBAR: 32, 331 | PAGE_UP: 33, 332 | PAGE_DOWN: 34, 333 | END: 35, 334 | HOME: 36, 335 | LEFT: 37, 336 | UP: 38, 337 | RIGHT: 39, 338 | DOWN: 40, 339 | INSERT: 45, 340 | DELETE: 46, 341 | HELP: 47, 342 | NUM_LOCK: 144 343 | } 344 | }; 345 | 346 | return k; 347 | }); 348 | 349 | define('nexus', { 350 | grid: null, 351 | board: null, 352 | mouse: null, 353 | scene: null, 354 | input: null, 355 | plane: null, 356 | }); 357 | define('tower', { 358 | tileAction: new vg.Signal(), 359 | objAction: new vg.Signal(), 360 | userAction: new vg.Signal(), 361 | 362 | saveMap: new vg.Signal(), 363 | loadMap: new vg.Signal(), 364 | 365 | TILE_CHANGE_HEIGHT: 'cell.change.height', 366 | TILE_ADD: 'cell.add', 367 | TILE_REMOVE: 'cell.remove', 368 | }); 369 | /* 370 | Translates the MouseCaster's events into more relevant data that the editor uses. 371 | */ 372 | define('Input', function() { 373 | var tower = require('tower'); 374 | var nexus = require('nexus'); 375 | var keyboard = require('keyboard'); 376 | 377 | var Input = function(scene, mouse) { 378 | this.mouse = mouse; 379 | this.mouse.signal.add(this.onMouse, this); 380 | 381 | this.mouseDelta = new THREE.Vector3(); 382 | this.mousePanMinDistance = 0.1; 383 | this.heightStep = 5; 384 | this.editorWorldPos = new THREE.Vector3(); // current grid position of mouse 385 | 386 | this.overTile = null; 387 | 388 | this._travel = 0; 389 | 390 | keyboard.signal.add(function(type, code) { 391 | if (type === keyboard.eventType.DOWN) { 392 | if (code === keyboard.code.SHIFT) nexus.scene.controls.enabled = false; 393 | } 394 | else { 395 | if (code === keyboard.code.SHIFT) nexus.scene.controls.enabled = true; 396 | } 397 | }, this); 398 | }; 399 | 400 | Input.prototype = { 401 | update: function() { 402 | var hit = this.mouse.allHits[0]; 403 | if (hit) { 404 | this.editorWorldPos.x = hit.point.x; 405 | this.editorWorldPos.y = hit.point.y; 406 | this.editorWorldPos.z = hit.point.z; 407 | } 408 | var dx = this.mouseDelta.x - this.mouse.screenPosition.x; 409 | var dy = this.mouseDelta.y - this.mouse.screenPosition.y; 410 | this._travel += Math.sqrt(dx * dx + dy * dy); 411 | }, 412 | 413 | onMouse: function(type, obj) { 414 | var hit, cell; 415 | if (this.mouse.allHits && this.mouse.allHits[0]) { 416 | hit = this.mouse.allHits[0]; 417 | } 418 | switch (type) { 419 | case vg.MouseCaster.WHEEL: 420 | tower.userAction.dispatch(vg.MouseCaster.WHEEL, this.overTile, obj); 421 | break; 422 | 423 | case vg.MouseCaster.OVER: 424 | if (obj) { 425 | this.overTile = obj.select(); 426 | } 427 | tower.userAction.dispatch(vg.MouseCaster.OVER, this.overTile, hit); 428 | break; 429 | 430 | case vg.MouseCaster.OUT: 431 | if (obj) { 432 | obj.deselect(); 433 | this.overTile = null; 434 | } 435 | tower.userAction.dispatch(vg.MouseCaster.OUT, this.overTile, hit); 436 | break; 437 | 438 | case vg.MouseCaster.DOWN: 439 | this.mouseDelta.copy(this.mouse.screenPosition); 440 | tower.userAction.dispatch(vg.MouseCaster.DOWN, this.overTile, hit); 441 | this._travel = 0; 442 | break; 443 | 444 | case vg.MouseCaster.UP: 445 | if (this._travel > this.mousePanMinDistance) { 446 | break; 447 | } 448 | tower.userAction.dispatch(vg.MouseCaster.UP, this.overTile, hit); 449 | break; 450 | 451 | case vg.MouseCaster.CLICK: 452 | tower.userAction.dispatch(vg.MouseCaster.CLICK, this.overTile, hit); 453 | break; 454 | } 455 | } 456 | }; 457 | 458 | return Input; 459 | }); 460 | 461 | /* 462 | 2D plane that the user moves mouse around on in order to build maps. Provides a working plane to navigate, and a visual aid for tile placement. 463 | 464 | @author Corey Birnbaum https://github.com/vonWolfehaus/ 465 | */ 466 | define('EditorPlane', function() { 467 | 468 | function EditorPlane(scene, grid, mouse) { 469 | this.nexus = require('nexus'); 470 | this.tower = require('tower'); 471 | 472 | this.geometry = null; 473 | this.mesh = null; 474 | this.material = new THREE.MeshBasicMaterial({ 475 | color: 0xffffff, 476 | side: THREE.DoubleSide 477 | }); 478 | 479 | this.scene = scene; 480 | this.grid = grid; 481 | 482 | this.hoverMesh = this.grid.generateTilePoly(new THREE.MeshBasicMaterial({ 483 | color: 0x1aaeff, 484 | side: THREE.DoubleSide 485 | })); 486 | 487 | this.mouse = mouse; 488 | 489 | /*this.mouse.signal.add(onUserAction, this); 490 | function onUserAction(type, overCell) { 491 | switch (type) { 492 | case vg.MouseCaster.OVER: 493 | if (overCell) { 494 | this.hoverMesh.mesh.visible = false; 495 | } 496 | break; 497 | 498 | case vg.MouseCaster.OUT: 499 | this.hoverMesh.mesh.visible = true; 500 | break; 501 | 502 | case vg.MouseCaster.DOWN: 503 | this.hoverMesh.mesh.visible = false; 504 | break; 505 | 506 | case vg.MouseCaster.UP: 507 | if (!overCell) { 508 | this.hoverMesh.mesh.visible = true; 509 | } 510 | else { 511 | this.hoverMesh.mesh.visible = false; 512 | } 513 | break; 514 | } 515 | }*/ 516 | } 517 | 518 | EditorPlane.prototype = { 519 | 520 | generatePlane: function(width, height) { 521 | if (this.mesh && this.mesh.parent) { 522 | this.mesh.parent.remove(this.mesh); 523 | } 524 | this.geometry = new THREE.PlaneBufferGeometry(width, width, 1, 1); 525 | this.mesh = new THREE.Mesh(this.geometry, this.material); 526 | this.mesh.rotation.x = 90 * vg.DEG_TO_RAD; 527 | this.mesh.position.y -= 0.1; 528 | this.scene.add(this.mesh); 529 | }, 530 | 531 | addHoverMeshToGroup: function(group) { 532 | if (this.hoverMesh.parent) { 533 | this.hoverMesh.parent.remove(this.hoverMesh); 534 | } 535 | group.add(this.hoverMesh); 536 | }, 537 | 538 | update: function() { 539 | if (this.mouse.allHits.length && !this.mouse.pickedObject) { 540 | var cell = this.grid.pixelToCell(this.nexus.input.editorWorldPos); 541 | this.hoverMesh.position.copy(this.grid.cellToPixel(cell)); 542 | this.hoverMesh.position.y += 0.1; 543 | this.hoverMesh.visible = true; 544 | } 545 | else { 546 | this.hoverMesh.visible = false; 547 | } 548 | } 549 | }; 550 | 551 | return EditorPlane; 552 | }); 553 | 554 | /* 555 | This is the ONLY place in the app that has a requestAnimationFrame handler. 556 | All modules attach their functions to this module if they want in on the RAF. 557 | */ 558 | define('motor', function() { 559 | var _brake = false; 560 | var _steps = []; 561 | 562 | function on() { 563 | _brake = false; 564 | window.requestAnimationFrame(_update); 565 | window.addEventListener('focus', onFocus, false); 566 | window.addEventListener('blur', onBlur, false); 567 | } 568 | 569 | function off() { 570 | _brake = true; 571 | window.removeEventListener('focus', onFocus, false); 572 | window.removeEventListener('blur', onBlur, false); 573 | } 574 | 575 | // in order to be able to ID functions we have to hash them to generate unique-ish keys for us to find them with later 576 | // if we don't do this, we won't be able to remove callbacks that were bound and save us from binding callbacks multiple times all over the place 577 | function add(cb, scope) { 578 | var k = _hashStr(cb.toString()); 579 | var h = _has(k); 580 | if (h === -1) { 581 | _steps.push({ 582 | func: cb, 583 | scope: scope, 584 | key: k 585 | }); 586 | } 587 | } 588 | 589 | function remove(cb) { 590 | var k = _hashStr(cb.toString()); 591 | var i = _has(k); 592 | if (i !== -1) { 593 | _steps.splice(i, 1); 594 | } 595 | } 596 | 597 | function _update() { 598 | if (_brake) return; 599 | window.requestAnimationFrame(_update); 600 | 601 | for (var i = 0; i < _steps.length; i++) { 602 | var o = _steps[i]; 603 | o.func.call(o.scope || null); 604 | } 605 | } 606 | 607 | // check if the handler already has iaw.motor particular callback 608 | function _has(k) { 609 | var n = -1; 610 | var i; 611 | for (i = 0; i < _steps.length; i++) { 612 | n = _steps[i].key; 613 | if (n === k) { 614 | return i; 615 | } 616 | } 617 | return -1; 618 | } 619 | 620 | function onFocus(evt) { 621 | _brake = false; 622 | _update(); 623 | } 624 | 625 | function onBlur(evt) { 626 | _brake = true; 627 | } 628 | 629 | function _hashStr(str) { 630 | var hash = 0, i, chr, len; 631 | if (str.length === 0) return hash; 632 | for (i = 0, len = str.length; i < len; i++) { 633 | chr = str.charCodeAt(i); 634 | hash = ((hash << 5) - hash) + chr; 635 | hash |= 0; 636 | } 637 | return hash; 638 | } 639 | 640 | return { 641 | on: on, 642 | off: off, 643 | add: add, 644 | remove: remove, 645 | } 646 | }); 647 | 648 | /* 649 | Manages cells and objects on the map. 650 | */ 651 | define('Editor', function() { 652 | var tower = require('tower'); 653 | var nexus = require('nexus'); 654 | var keyboard = require('keyboard'); 655 | var motor = require('motor'); 656 | 657 | // TODO: get these values from UI 658 | var heightStep = 3; 659 | 660 | // PRIVATE 661 | var lastHeight = 1; 662 | var currentGridCell = null; 663 | var prevGridCell = new THREE.Vector3(); 664 | var _cel = new vg.Cell(); 665 | 666 | tower.userAction.add(onUserAction, this); 667 | motor.add(update); 668 | 669 | function update() { 670 | currentGridCell = nexus.grid.pixelToCell(nexus.input.editorWorldPos); 671 | if (nexus.mouse.down && keyboard.shift && nexus.mouse.allHits && nexus.mouse.allHits.length) { 672 | // only check if the user's mouse is over the editor plane 673 | if (!currentGridCell.equals(prevGridCell)) { 674 | addTile(currentGridCell); 675 | } 676 | prevGridCell.copy(currentGridCell); 677 | } 678 | } 679 | 680 | function onUserAction(type, overTile, data) { 681 | var hit = nexus.mouse.allHits[0] 682 | switch (type) { 683 | case vg.MouseCaster.WHEEL: 684 | if (keyboard.shift && overTile) { 685 | if (!overTile.cell) { 686 | overTile.dispose(); 687 | return; 688 | } 689 | _cel.copy(overTile.cell); 690 | _cel.tile = null; 691 | 692 | var dif = lastHeight - data; 693 | var last = _cel.h; 694 | _cel.h += dif > 0 ? -heightStep : heightStep; 695 | if (_cel.h < 1) _cel.h = 1; 696 | 697 | nexus.mouse.wheel = Math.round((_cel.h / heightStep) + (dif > 0 ? -1 : 1)); 698 | lastHeight = nexus.mouse.wheel; 699 | 700 | if (last === _cel.h) return; 701 | removeTile(overTile); 702 | 703 | var tile = addTile(_cel); 704 | tile.select(); 705 | 706 | tower.tileAction.dispatch(tower.TILE_CHANGE_HEIGHT, tile); 707 | } 708 | break; 709 | 710 | case vg.MouseCaster.OVER: 711 | if (keyboard.shift) { 712 | if (overTile && nexus.mouse.rightDown) { 713 | removeTile(overTile); 714 | } 715 | else if (!overTile && nexus.mouse.down) { 716 | addTile(currentGridCell); 717 | } 718 | } 719 | break; 720 | 721 | case vg.MouseCaster.OUT: 722 | 723 | break; 724 | 725 | case vg.MouseCaster.DOWN: 726 | if (keyboard.shift && nexus.mouse.down && data && !overTile) { 727 | // if shift is down then they're painting, so add a tile immediately 728 | addTile(currentGridCell); 729 | } 730 | break; 731 | 732 | case vg.MouseCaster.UP: 733 | if (nexus.mouse.down && data && !overTile) { 734 | // create a new tile, if one isn't already there 735 | addTile(currentGridCell); 736 | } 737 | else if (nexus.mouse.rightDown && overTile) { 738 | // remove a tile if it's there and right mouse is down 739 | removeTile(overTile); 740 | } 741 | break; 742 | } 743 | } 744 | 745 | function addTile(cell) { 746 | if (!cell || nexus.board.getTileAtCell(cell)) return; 747 | 748 | var newCell = new vg.Cell(); 749 | newCell.copy(cell); 750 | newCell.h = Math.abs(nexus.mouse.wheel * heightStep); 751 | 752 | var newTile = nexus.grid.generateTile(newCell, 0.95); 753 | 754 | nexus.board.addTile(newTile); 755 | 756 | tower.tileAction.dispatch(tower.TILE_ADD, newTile); 757 | 758 | return newTile; 759 | } 760 | 761 | function removeTile(overTile) { 762 | nexus.board.removeTile(overTile); 763 | 764 | tower.tileAction.dispatch(tower.TILE_REMOVE, overTile); 765 | } 766 | 767 | return { 768 | 769 | } 770 | }); 771 | /* 772 | Handles JSON for whatever data needs to be saved to localStorage, and provides a convenient signal for whenever that data changes. 773 | */ 774 | define('data', { 775 | _store: {}, 776 | changed: new vg.Signal(), 777 | 778 | get: function(key) { 779 | return this._store[key] || null; 780 | }, 781 | 782 | set: function(key, val) { 783 | // fire event first so we can retrieve old data before it's overwritten (just in case) 784 | this.changed.dispatch(key, this._store[key], val); 785 | this._store[key] = val; 786 | }, 787 | 788 | save: function() { 789 | window.localStorage['vongrid'] = JSON.stringify(this._store); 790 | }, 791 | 792 | load: function(json) { 793 | var data = window.localStorage['vongrid']; 794 | if (json || data) { 795 | try { 796 | this._store = json || JSON.parse(data); 797 | this.changed.dispatch('load-success'); 798 | } 799 | catch (err) { 800 | console.warn('Error loading editor data'); 801 | this.changed.dispatch('load-failure'); 802 | } 803 | } 804 | } 805 | }); 806 | //# sourceMappingURL=app.js.map 807 | --------------------------------------------------------------------------------