├── 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 |
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 |
25 | - Left Click: create cell
26 | - Shift + Left Click: paint cells
27 | - Right Click: remove cell
28 | - Shift + Right Click: erase cells
29 | - Mousewheel: adjust zoom of camera
30 | - Shift+Mousewheel: adjust height of cell
31 | - Left click and drag: orbit camera
32 | - Right click and drag: pan camera
33 |
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 | 
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 | 
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 | 
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+""+f;u=te(r,o);if(u)e.appendChild(u)}return r}(g);function y(e,t,n){var r={};r[e.key]=t;if(e.pos)r[e.pos]=n;return r}function b(e,t){var n=t.length,r=e.length;while(n>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 |
--------------------------------------------------------------------------------
|