├── img ├── environment │ ├── acid.png │ ├── arctic.png │ ├── cactus.png │ ├── coast.png │ ├── crowd.png │ ├── desert.png │ ├── forest.png │ ├── gecko.png │ ├── grass.png │ ├── jungle.png │ ├── magic.png │ ├── plants.png │ ├── rubble.png │ ├── swamp.png │ ├── swamp2.png │ ├── urban.png │ ├── water.png │ ├── wheat.png │ ├── current.png │ ├── furniture.png │ ├── grassland.png │ ├── mountain.png │ ├── palm-tree.png │ ├── spiderweb.png │ ├── underdark.png │ ├── frozen-orb.png │ └── magick-trick.png ├── solid2x.svg ├── solid3x.svg ├── solid4x.svg ├── solid0.5x.svg ├── oldschool2x.svg ├── oldschool3x.svg ├── oldschool4x.svg ├── triangle2x.svg ├── oldschool0.5x.svg ├── triangle0.5x.svg ├── triangle3x.svg ├── triangle4x.svg ├── diagonal3x.svg ├── diagonal4x.svg ├── diagonal2x.svg ├── diagonal0.5x.svg ├── horizontal2x.svg ├── vertical4x.svg ├── horizontal0.5x.svg ├── horizontal3x.svg ├── horizontal4x.svg ├── vertical0.5x.svg ├── vertical2x.svg └── vertical3x.svg ├── Documentation ├── TerrainTool.webp └── TerrainLayerTools.webp ├── lang ├── es.json ├── zh-tw.json ├── ko.json ├── de.json └── en.json ├── templates ├── terrain-controls.html ├── terrain-color.html ├── terrain-hud.html ├── terrain-form.html └── terrain-config.html ├── LICENSE ├── classes ├── ruleprovider.js ├── terraincontrols.js ├── terraincolor.js ├── terraininfo.js ├── terrainconfig.js ├── terrainhud.js ├── terrainshape.js ├── terraindocument.js └── terrainlayer.js ├── module.json ├── js ├── controls.js ├── settings.js └── api.js ├── css └── terrainlayer.css ├── README.md ├── CHANGELOG.md └── terrain-main.js /img/environment/acid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/acid.png -------------------------------------------------------------------------------- /img/environment/arctic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/arctic.png -------------------------------------------------------------------------------- /img/environment/cactus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/cactus.png -------------------------------------------------------------------------------- /img/environment/coast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/coast.png -------------------------------------------------------------------------------- /img/environment/crowd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/crowd.png -------------------------------------------------------------------------------- /img/environment/desert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/desert.png -------------------------------------------------------------------------------- /img/environment/forest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/forest.png -------------------------------------------------------------------------------- /img/environment/gecko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/gecko.png -------------------------------------------------------------------------------- /img/environment/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/grass.png -------------------------------------------------------------------------------- /img/environment/jungle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/jungle.png -------------------------------------------------------------------------------- /img/environment/magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/magic.png -------------------------------------------------------------------------------- /img/environment/plants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/plants.png -------------------------------------------------------------------------------- /img/environment/rubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/rubble.png -------------------------------------------------------------------------------- /img/environment/swamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/swamp.png -------------------------------------------------------------------------------- /img/environment/swamp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/swamp2.png -------------------------------------------------------------------------------- /img/environment/urban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/urban.png -------------------------------------------------------------------------------- /img/environment/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/water.png -------------------------------------------------------------------------------- /img/environment/wheat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/wheat.png -------------------------------------------------------------------------------- /img/environment/current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/current.png -------------------------------------------------------------------------------- /img/environment/furniture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/furniture.png -------------------------------------------------------------------------------- /img/environment/grassland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/grassland.png -------------------------------------------------------------------------------- /img/environment/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/mountain.png -------------------------------------------------------------------------------- /img/environment/palm-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/palm-tree.png -------------------------------------------------------------------------------- /img/environment/spiderweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/spiderweb.png -------------------------------------------------------------------------------- /img/environment/underdark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/underdark.png -------------------------------------------------------------------------------- /Documentation/TerrainTool.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/Documentation/TerrainTool.webp -------------------------------------------------------------------------------- /img/environment/frozen-orb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/frozen-orb.png -------------------------------------------------------------------------------- /img/environment/magick-trick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/img/environment/magick-trick.png -------------------------------------------------------------------------------- /Documentation/TerrainLayerTools.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ironmonk108/enhanced-terrain-layer/HEAD/Documentation/TerrainLayerTools.webp -------------------------------------------------------------------------------- /img/solid2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/solid3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/solid4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/solid0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/oldschool2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/oldschool3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/oldschool4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/triangle2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/oldschool0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /img/triangle0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnhancedTerrainLayer.tool": "Cuadrícula del Terreno", 3 | "EnhancedTerrainLayer.select": "Seleccionar Terreno Dificil", 4 | "EnhancedTerrainLayer.add": "Añadir Terreno Dificil", 5 | "EnhancedTerrainLayer.onoff": "Habilitar/Inhabilitar Terreno", 6 | "EnhancedTerrainLayer.reset": "Reajustar Terreno" 7 | } 8 | 9 | -------------------------------------------------------------------------------- /img/triangle3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /img/triangle4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /img/diagonal3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /img/diagonal4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /img/diagonal2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /img/diagonal0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /img/horizontal2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/vertical4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/horizontal0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/horizontal3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/horizontal4x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/vertical0.5x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/vertical2x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /img/vertical3x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /lang/zh-tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnhancedTerrainLayer.tool": "地形網格", 3 | "EnhancedTerrainLayer.select": "選擇困難地形", 4 | "EnhancedTerrainLayer.add": "添加困難地形", 5 | "EnhancedTerrainLayer.onoff": "啟用/禁用地形", 6 | "EnhancedTerrainLayer.reset": "重置地形", 7 | "EnhancedTerrainLayer.IncreaseCost": "增加Cost", 8 | "EnhancedTerrainLayer.DecreaseCost": "降低Cost", 9 | "EnhancedTerrainLayer.Cost": "Cost", 10 | "EnhancedTerrainLayer.Configure": "配置此場景的地形。", 11 | "EnhancedTerrainLayer.Configuration": "地形設定", 12 | "EnhancedTerrainLayer.UpdateTerrain": "更新地形", 13 | 14 | "EnhancedTerrainLayer.opacity.name": "地形圖示的不透明度", 15 | "EnhancedTerrainLayer.opacity.hint": "圖示和數字的不透明度。", 16 | "EnhancedTerrainLayer.show-text.name": "顯示文字", 17 | "EnhancedTerrainLayer.show-text.hint": "顯示有關地形難度的文字。" 18 | } 19 | -------------------------------------------------------------------------------- /templates/terrain-controls.html: -------------------------------------------------------------------------------- 1 |
    2 |
  1. 3 | 6 |
  2. 7 |
  3. 8 | {{#if disabled}}{{/if}} 9 | {{{multiple}}} 10 |
  4. 11 |
  5. 12 | 15 |
  6. 16 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ironmonk 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 | -------------------------------------------------------------------------------- /classes/ruleprovider.js: -------------------------------------------------------------------------------- 1 | export class RuleProvider { 2 | calculateCombinedCost(terrain, options) { 3 | let calculate = options.calculate || "maximum"; 4 | let calculateFn; 5 | if (typeof calculate == "function") { 6 | calculateFn = calculate; 7 | } else { 8 | switch (calculate) { 9 | case "maximum": 10 | calculateFn = function (cost, total) { 11 | return Math.max(cost, total); 12 | }; 13 | break; 14 | case "additive": 15 | calculateFn = function (cost, total) { 16 | return cost + total; 17 | }; 18 | break; 19 | default: 20 | throw new Error(i18n("EnhancedTerrainLayer.ErrorCalculate")); 21 | } 22 | } 23 | 24 | let total = null; 25 | for (const terrainInfo of terrain) { 26 | if (typeof calculateFn == "function") { 27 | total = calculateFn(terrainInfo.cost, total, terrainInfo.object); 28 | } 29 | } 30 | return total ?? 1; 31 | } 32 | 33 | /** 34 | * Constructs a new instance of the speed provider 35 | * 36 | * This function should neither be called or overridden by rule provider implementations 37 | */ 38 | constructor(id) { 39 | this.id = id; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Enhanced Terrain Layer", 3 | "description": "A base module that adds a Terrain Layer to Foundry. Used as a library for Rulers and other modules", 4 | "version": "10.09", 5 | "authors": [ 6 | { 7 | "name": "IronMonk", 8 | "discord": "ironmonk88#4075", 9 | "flags": { 10 | "github": "ironmonk88", 11 | "patreon": "ironmonk", 12 | "ko-fi": "ironmonk88" 13 | } 14 | } 15 | ], 16 | "socket": true, 17 | "languages": [ 18 | { 19 | "lang": "en", 20 | "name": "English", 21 | "path": "lang/en.json" 22 | }, 23 | { 24 | "lang": "es", 25 | "name": "Spanish", 26 | "path": "lang/es.json" 27 | }, 28 | { 29 | "lang": "de", 30 | "name": "Deutsch", 31 | "path": "lang/de.json" 32 | }, 33 | { 34 | "lang": "ko", 35 | "name": "Korean", 36 | "path": "lang/ko.json" 37 | }, 38 | { 39 | "lang": "zh-tw", 40 | "name": "正體中文", 41 | "path": "lang/zh-tw.json" 42 | } 43 | ], 44 | "esmodules": [ 45 | "terrain-main.js", 46 | "js/controls.js", 47 | "js/settings.js" 48 | ], 49 | "styles": [ 50 | "css/terrainlayer.css" 51 | ], 52 | "url": "https://github.com/ironmonk88/enhanced-terrain-layer", 53 | "download": "https://github.com/ironmonk88/enhanced-terrain-layer/archive/10.09.zip", 54 | "manifest": "https://github.com/ironmonk88/enhanced-terrain-layer/releases/latest/download/module.json", 55 | "bugs": "https://github.com/ironmonk88/enhanced-terrain-layer/issues", 56 | "allowBugReporter": true, 57 | "id": "enhanced-terrain-layer", 58 | "compatibility": { 59 | "minimum": "10", 60 | "verified": "10" 61 | }, 62 | "name": "enhanced-terrain-layer", 63 | "minimumCoreVersion": "10", 64 | "compatibleCoreVersion": "10" 65 | } -------------------------------------------------------------------------------- /templates/terrain-color.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 | 8 | 9 |
10 |
11 |

Environments

12 | {{#each environment}} 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 | {{/each}} 21 |

Obstacles

22 | {{#each obstacle}} 23 |
24 | 25 |
26 | 27 | 28 |
29 |
30 | {{/each}} 31 |
32 | 36 |
37 |
-------------------------------------------------------------------------------- /classes/terraincontrols.js: -------------------------------------------------------------------------------- 1 | import { TerrainLayer } from './terrainlayer.js'; 2 | import { setting, i18n, getflag } from '../terrain-main.js'; 3 | 4 | export class TerrainLayerToolBar extends Application { 5 | constructor() { 6 | super(...arguments); 7 | } 8 | static get defaultOptions() { 9 | const options = { 10 | classes: ['form'], 11 | left: 98, 12 | popOut: false, 13 | template: 'modules/enhanced-terrain-layer/templates/terrain-controls.html', 14 | id: 'terrainlayer-config', 15 | title: i18n('Default Terrain Cost'), 16 | closeOnSubmit: false, 17 | submitOnChange: false, 18 | submitOnClose: false 19 | }; 20 | options['editable'] = game.user.isGM; 21 | return mergeObject(super.defaultOptions, options); 22 | } 23 | 24 | activateListeners(html) { 25 | super.activateListeners(html); 26 | 27 | $('.control-btn[data-tool]', html).on("click", this._onHandleClick.bind(this)); 28 | } 29 | 30 | getData(options) { 31 | let sceneMult = getflag(canvas.scene, 'multiple'); 32 | let multiple = (sceneMult == undefined || sceneMult == "" ? canvas.terrain.defaultmultiple : Math.clamped(parseInt(sceneMult), setting('minimum-cost'), setting('maximum-cost'))); 33 | let disabled = !(sceneMult == undefined || sceneMult == ""); 34 | return { 35 | multiple: TerrainLayer.multipleText(multiple), 36 | disabled: disabled, 37 | title: (disabled ? i18n("EnhancedTerrainLayer.DefaultCost") : i18n("EnhancedTerrainLayer.Cost")) 38 | }; 39 | } 40 | 41 | _onHandleClick(event) { 42 | const btn = event.currentTarget; 43 | 44 | let inc = ($(btn).attr('id') == 'tl-inc-cost'); 45 | canvas.terrain.defaultmultiple = TerrainLayer.alterMultiple(canvas.terrain.defaultmultiple, inc); 46 | $('#tl-defaultcost', this.element).html(TerrainLayer.multipleText(canvas.terrain.defaultmultiple)); 47 | } 48 | 49 | async _render(...args) { 50 | await super._render(...args); 51 | $('#controls').append(this.element); 52 | } 53 | } -------------------------------------------------------------------------------- /classes/terraincolor.js: -------------------------------------------------------------------------------- 1 | import { log, error, i18n, setting } from "../terrain-main.js"; 2 | 3 | export class TerrainColor extends FormApplication { 4 | constructor(object, options) { 5 | super(object, options); 6 | } 7 | 8 | static get defaultOptions() { 9 | return mergeObject(super.defaultOptions, { 10 | id: "terraincolor", 11 | title: i18n("EnhancedTerrainLayer.TerrainColor"), 12 | template: "./modules/enhanced-terrain-layer/templates/terrain-color.html", 13 | width: 400, 14 | height: 400, 15 | popOut: true 16 | }); 17 | } 18 | 19 | getData(options) { 20 | let colors = setting('environment-color'); 21 | 22 | var obstacleColor = []; 23 | var environmentColor = canvas.terrain.getEnvironments().reduce(function (map, obj) { 24 | if (colors[obj.id] != undefined) 25 | obj.color = colors[obj.id]; 26 | 27 | if (obj.obstacle === true) { 28 | obstacleColor.push(obj); 29 | } else 30 | map.push(obj); 31 | return map; 32 | }, []); 33 | 34 | return { 35 | main: { id: '_default', color: (colors['_default'] || '#FFFFFF')}, 36 | environment: environmentColor, 37 | obstacle: obstacleColor 38 | }; 39 | } 40 | 41 | saveChanges(ev) { 42 | let colors = setting('environment-color'); 43 | let updateColor = function (id, value) { 44 | if (value == '') { 45 | delete colors[id]; 46 | } else { 47 | colors[id] = value; 48 | } 49 | } 50 | 51 | updateColor('_default', $('#_default', this.element).val()); 52 | 53 | for (let env of canvas.terrain.getEnvironments()) { 54 | updateColor(env.id, $('#' + env.id, this.element).val()); 55 | } 56 | /* 57 | for (let obs of canvas.terrain.getObstacles()) { 58 | updateColor(obs.id, $('#' + obs.id, this.element).val()); 59 | }*/ 60 | 61 | game.settings.set('enhanced-terrain-layer', 'environment-color', colors).then(() => { 62 | canvas.terrain.refresh(true); 63 | }); 64 | 65 | this.close(); 66 | } 67 | 68 | resetSetting() { 69 | game.settings.set('enhanced-terrain-layer', 'environment-color', {'_default':'#FFFFFF'}); 70 | this.render(true); 71 | } 72 | 73 | activateListeners(html) { 74 | super.activateListeners(html); 75 | 76 | $('button[name="reset"]', html).click(this.resetSetting.bind(this)); 77 | $('button[name="submit"]', html).click(this.saveChanges.bind(this)); 78 | } 79 | } -------------------------------------------------------------------------------- /templates/terrain-hud.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {{{text}}} 6 |
7 | 8 |
9 | 10 |
11 | {{#each environments}} 12 |
{{this.text}}
13 | {{/each}} 14 |
15 |
16 | 17 |
18 | 19 |
{{elevation}}
20 |
21 |
22 | 23 |
{{depth}}
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 | {{#if isGM}} 32 |
33 | 34 |
35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 | {{/if}} 48 |
49 |
-------------------------------------------------------------------------------- /templates/terrain-form.html: -------------------------------------------------------------------------------- 1 | {{#if full}} 2 |

Set the default settings when terrain is created on this scene

3 |
4 | 5 |
6 | 7 | 8 |
9 |
10 | {{/if}} 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 | 35 |
36 |
37 | {{#if useObstacles}} 38 |
39 | 40 |
41 | 44 |
45 |
46 | {{/if}} 47 | {{#if full}} 48 |
49 | 50 |
51 | {{ colorPicker name="flags.enhanced-terrain-layer.drawcolor" value=data.drawcolor}} 52 |
53 |
54 | {{/if}} 55 | -------------------------------------------------------------------------------- /js/controls.js: -------------------------------------------------------------------------------- 1 | import { TerrainLayerToolBar } from '../classes/terraincontrols.js'; 2 | 3 | Hooks.on('getSceneControlButtons', (controls) => { 4 | const isGM = game.user.isGM; 5 | controls.push({ 6 | name: 'terrain', 7 | title: game.i18n.localize('EnhancedTerrainLayer.tool'), 8 | icon: 'fas fa-mountain', 9 | visible: isGM, 10 | layer: 'terrain', 11 | activeTool: 'select', 12 | flags: { 13 | 'enhanced-terrain-layer': { valid: true } 14 | }, 15 | tools: [ 16 | { 17 | name: 'select', 18 | title: game.i18n.localize('EnhancedTerrainLayer.select'), 19 | icon: 'fas fa-expand' 20 | }, 21 | { 22 | name: "rect", 23 | title: "CONTROLS.DrawingRect", 24 | icon: "fa-regular fa-square" 25 | }, 26 | { 27 | name: "ellipse", 28 | title: "CONTROLS.DrawingEllipse", 29 | icon: "fa-regular fa-circle" 30 | }, 31 | { 32 | name: "polygon", 33 | title: "CONTROLS.DrawingPoly", 34 | icon: "fa-solid fa-draw-polygon" 35 | }, 36 | { 37 | name: "freehand", 38 | title: "CONTROLS.DrawingFree", 39 | icon: "fa-solid fa-signature" 40 | }, 41 | { 42 | name: 'terraintoggle', 43 | title: game.i18n.localize('EnhancedTerrainLayer.onoff'), 44 | icon: 'fas fa-eye', 45 | onClick: () => { 46 | canvas.terrain.toggle(null, true); 47 | }, 48 | active: (canvas?.terrain?.showterrain || game.settings.get("enhanced-terrain-layer", "showterrain")), 49 | toggle: true 50 | }, 51 | { 52 | name: 'clearterrain', 53 | title: game.i18n.localize('EnhancedTerrainLayer.reset'), 54 | icon: 'fas fa-trash', 55 | visible: isGM, 56 | onClick: () => { 57 | canvas.terrain.deleteAll() 58 | }, 59 | button: true, 60 | } 61 | ] 62 | }); 63 | }); 64 | Hooks.on('renderSceneControls', (controls) => { 65 | if (canvas != null && canvas.terrain) { 66 | canvas.terrain.visible = (canvas.terrain.showterrain || controls.activeControl == 'terrain'); 67 | 68 | if (controls.activeControl == 'terrain') { 69 | if (canvas.terrain.toolbar == undefined) 70 | canvas.terrain.toolbar = new TerrainLayerToolBar(); 71 | canvas.terrain.toolbar.render(true); 72 | //$('#terrainlayer-tools').toggle(controls.activeTool == 'addterrain'); 73 | } else { 74 | if (!canvas.terrain.toolbar) 75 | return; 76 | canvas.terrain.toolbar.close(); 77 | } 78 | } 79 | }); 80 | Hooks.on('renderTerrainLayerToolBar', () => { 81 | const tools = $(canvas.terrain.toolbar.form).parent(); 82 | if (!tools) 83 | return; 84 | if (isNewerVersion(game.version, "9")) { 85 | const controltools = $('li[data-tool="addterrain"]').closest('.sub-controls'); 86 | controltools.addClass('terrain-controls'); 87 | canvas.terrain.toolbar.element.addClass('active'); 88 | //const offset = controltools.offset(); 89 | //tools.css({ top: `${offset.top}px`, left: `${offset.left + controltools.width()}px` }); 90 | } else { 91 | const controltools = $('li[data-control="terrain"] ol.control-tools'); 92 | const offset = controltools.offset(); 93 | tools.css({ top: `${offset.top}px`, left: `${offset.left + controltools.width() + 6}px` }); 94 | } 95 | }); -------------------------------------------------------------------------------- /lang/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnhancedTerrainLayer.tool": "지형 레이어", 3 | "EnhancedTerrainLayer.select": "험지 선택", 4 | "EnhancedTerrainLayer.add": "험지 추가", 5 | "EnhancedTerrainLayer.onoff": "지형 항상 표시 전환", 6 | "EnhancedTerrainLayer.reset": "지형 재설정", 7 | "EnhancedTerrainLayer.IncreaseCost": "비용 증가", 8 | "EnhancedTerrainLayer.DecreaseCost": "비용 감소", 9 | "EnhancedTerrainLayer.TerrainCost": "지형 비용", 10 | "EnhancedTerrainLayer.Cost": "비용", 11 | "EnhancedTerrainLayer.Configure": "이 씬의 지형 설정.", 12 | "EnhancedTerrainLayer.Configuration": "지형 설정", 13 | "EnhancedTerrainLayer.UpdateTerrain": "지형 업데이트", 14 | "EnhancedTerrainLayer.MovementCost": "이동 비용", 15 | "EnhancedTerrainLayer.TerrainHeight": "지형 높이", 16 | "EnhancedTerrainLayer.Environment": "환경", 17 | "EnhancedTerrainLayer.Obstacle": "장애물", 18 | "EnhancedTerrainLayer.TerrainColor": "지형 색상", 19 | "EnhancedTerrainLayer.DefaultTerrainColor": "지형 색상 기본값", 20 | "EnhancedTerrainLayer.ResetDefaults": "기본값 재설정", 21 | "EnhancedTerrainLayer.SaveChanges": "변경사항 저장", 22 | "EnhancedTerrainLayer.ErrorCalculate": "계산 함수가 정의되지 않았습니다.", 23 | "EnhancedTerrainLayer.Min": "최소", 24 | "EnhancedTerrainLayer.Max": "최대", 25 | "EnhancedTerrainLayer.visibility": "활성 상태 전환", 26 | "EnhancedTerrainLayer.terrain": "지형", 27 | "EnhancedTerrainLayer.Inactive": "비활성화", 28 | "EnhancedTerrainLayer.Locked": "잠김", 29 | 30 | "EnhancedTerrainLayer.opacity.name": "지형 아이콘 불투명도", 31 | "EnhancedTerrainLayer.opacity.hint": "아이콘과 숫자의 불투명도이다.", 32 | "EnhancedTerrainLayer.show-text.name": "텍스트 보기", 33 | "EnhancedTerrainLayer.show-text.hint": "텍스트로 얼마나 험한 지형인지 표시한다.", 34 | "EnhancedTerrainLayer.show-icon.name": "아이콘 보기", 35 | "EnhancedTerrainLayer.show-icon.hint": "지형 환경에 대한 아이콘을 표시한다.", 36 | "EnhancedTerrainLayer.show-on-drag.name": "드래그 시 표시", 37 | "EnhancedTerrainLayer.show-on-drag.hint": "토큰 드래그 시 지형을 강조 표시 한다.", 38 | "EnhancedTerrainLayer.tokens-cause-difficult.name": "토큰 험지 유발", 39 | "EnhancedTerrainLayer.tokens-cause-difficult.hint": "토큰을 다른 토큰 위로 끌어다 놓으면 험지로 간주한다.", 40 | "EnhancedTerrainLayer.use-obstacles.name": "추가 장애물 사용", 41 | "EnhancedTerrainLayer.use-obstacles.hint": "환경과 함께 장애물을 추가로 사용할 수 있다.", 42 | "EnhancedTerrainLayer.only-show-active.name": "활성화 지형만 표시", 43 | "EnhancedTerrainLayer.only-show-active.hint": "숨겨진 지형은 지형 도구가 활성화 되었을 때에만 GM에게 표시된다.", 44 | "EnhancedTerrainLayer.minimum-cost.name": "최소 비용", 45 | "EnhancedTerrainLayer.minimum-cost.hint": "험지에 설정할 수 있는 최소 비용", 46 | "EnhancedTerrainLayer.maximum-cost.name": "최대 비용", 47 | "EnhancedTerrainLayer.maximum-cost.hint": "험지에 설정할 수 있는 최대 비용", 48 | "EnhancedTerrainLayer.dead-cause-difficult.name": "시체 험지 유발", 49 | "EnhancedTerrainLayer.dead-cause-difficult.hint": "죽은 토큰을 험지로 처리한다", 50 | "EnhancedTerrainLayer.draw-border.name": "테두리 그리기", 51 | "EnhancedTerrainLayer.draw-border.hint": "테두리를 그리는지에 대한 여부를 설정한다.", 52 | "EnhancedTerrainLayer.terrain-image.name": "지형 이미지", 53 | "EnhancedTerrainLayer.terrain-image.hint": "지형의 배경 텍스처 변경", 54 | 55 | "EnhancedTerrainLayer.environment.arctic": "극지", 56 | "EnhancedTerrainLayer.environment.coast": "해안", 57 | "EnhancedTerrainLayer.environment.desert": "사막", 58 | "EnhancedTerrainLayer.environment.forest": "숲", 59 | "EnhancedTerrainLayer.environment.grassland": "초원", 60 | "EnhancedTerrainLayer.environment.jungle": "정글", 61 | "EnhancedTerrainLayer.environment.mountain": "산악", 62 | "EnhancedTerrainLayer.environment.swamp": "늪지", 63 | "EnhancedTerrainLayer.environment.underdark": "지하", 64 | "EnhancedTerrainLayer.environment.urban": "도시", 65 | "EnhancedTerrainLayer.environment.water": "물가", 66 | 67 | "EnhancedTerrainLayer.obstacle.crowd": "무리 집단", 68 | "EnhancedTerrainLayer.obstacle.current": "수류", 69 | "EnhancedTerrainLayer.obstacle.furniture": "가구", 70 | "EnhancedTerrainLayer.obstacle.magic": "마법", 71 | "EnhancedTerrainLayer.obstacle.plants": "식물", 72 | "EnhancedTerrainLayer.obstacle.rubble": "자갈", 73 | "EnhancedTerrainLayer.obstacle.water": "물" 74 | 75 | } 76 | -------------------------------------------------------------------------------- /templates/terrain-config.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {{localize "EnhancedTerrainLayer.Configure"}} 4 |

5 |
6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 | {{ colorPicker name="drawcolor" value=data.drawcolor}} 35 |
36 |
37 |
38 | 39 |
40 | 41 | {{data.opacity}} 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 | 67 |
68 | {{#if useObstacles}} 69 |
70 | 71 | 74 |
75 | {{/if}} 76 |
77 | 78 | 79 |
80 | 81 |
82 | 83 | 84 |
85 | 86 | 87 |
-------------------------------------------------------------------------------- /classes/terraininfo.js: -------------------------------------------------------------------------------- 1 | import { makeid, log, setting, debug, getflag } from '../terrain-main.js'; 2 | import { TerrainLayer } from './terrainlayer.js'; 3 | 4 | class TerrainInfo { 5 | constructor(reducers) { 6 | if (this.constructor === TerrainInfo) { 7 | throw new Error("TerrainInfo is an abstract class and cannot be directly instantiated"); 8 | } 9 | this.reducers = reducers; 10 | } 11 | 12 | get cost() { 13 | let terraincost = this.rawCost; 14 | if (!this.reducers) 15 | return terraincost; 16 | for (const reduce of this.reducers) { 17 | let value = parseFloat(reduce.value); 18 | 19 | if (typeof reduce.value == 'string' && (reduce.value.startsWith('+') || reduce.value.startsWith('-'))) { 20 | value = terraincost + value; 21 | if (reduce.stop) { 22 | if (reduce.value.startsWith('+')) 23 | value = Math.min(value, reduce.stop); 24 | else 25 | value = Math.max(value, reduce.stop); 26 | } 27 | } 28 | terraincost = value; //Math.max(value, 0); 29 | } 30 | return terraincost; 31 | } 32 | 33 | get object() { 34 | throw new Error("The getter 'object' must be implemented by subclasses of TerrainInfo"); 35 | } 36 | 37 | get rawCost() { 38 | throw new Error("The getter 'rawCost' must be implemented by subclasses of TerrainInfo"); 39 | } 40 | 41 | get shape() { 42 | throw new Error("The getter 'shape' must be implemented by subclasses of TerrainInfo"); 43 | } 44 | 45 | get environment() { 46 | throw new Error("The getter 'environment' must be implemented by subclasses of TerrainInfo"); 47 | } 48 | 49 | get obstacle() { 50 | throw new Error("The getter 'obstracle' must be implemented by subclasses of TerrainInfo"); 51 | } 52 | } 53 | 54 | export class PolygonTerrainInfo extends TerrainInfo { 55 | constructor(terrain, reducers) { 56 | super(reducers); 57 | this.terrain = terrain; 58 | } 59 | 60 | get object() { 61 | return this.terrain; 62 | } 63 | 64 | get rawCost() { 65 | return this.terrain.cost(); 66 | } 67 | 68 | get shape() { 69 | return this.terrain.shape._pixishape; 70 | } 71 | 72 | get environment() { 73 | return this.terrain.document.environment; 74 | } 75 | 76 | get obstacle() { 77 | return this.terrain.document.obstacle; 78 | } 79 | } 80 | 81 | export class TemplateTerrainInfo extends TerrainInfo { 82 | constructor(template, reducers) { 83 | super(reducers); 84 | this.template = template; 85 | } 86 | 87 | get object() { 88 | return this.template; 89 | } 90 | 91 | get rawCost() { 92 | return this.template.data.flags['enhanced-terrain-layer'].multiple; 93 | } 94 | 95 | get shape() { 96 | return this.template.shape; 97 | } 98 | 99 | get environment() { 100 | return this.template.flags["enhanced-terrain-layer"]?.environment; 101 | } 102 | 103 | get obstacle() { 104 | return this.template.flags["enhanced-terrain-layer"]?.obstacle; 105 | } 106 | } 107 | 108 | export class TokenTerrainInfo extends TerrainInfo { 109 | constructor(token, reducers) { 110 | super(reducers); 111 | this.token = token; 112 | } 113 | 114 | get object() { 115 | return this.token; 116 | } 117 | 118 | get rawCost() { 119 | return 2; 120 | } 121 | 122 | get shape() { 123 | if (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS) { 124 | const hw = (this.token.document.width * canvas.dimensions.size) / 2; 125 | const hh = (this.token.document.height * canvas.dimensions.size) / 2; 126 | 127 | return new PIXI.Circle(hw, hh, Math.max(hw, hh)); 128 | } else { 129 | const left = 0; 130 | const top = 0; 131 | const width = this.token.document.width * canvas.dimensions.size; 132 | const height = this.token.document.height * canvas.dimensions.size; 133 | 134 | return new PIXI.Rectangle(left, top, width, height); 135 | } 136 | } 137 | 138 | get environment() { 139 | return undefined; 140 | } 141 | 142 | get obstacle() { 143 | return undefined; 144 | } 145 | } -------------------------------------------------------------------------------- /lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnhancedTerrainLayer.tool": "Gelände Ebene", 3 | "EnhancedTerrainLayer.select": "Wähle schwieriges Gelände aus", 4 | "EnhancedTerrainLayer.add": "Füge schwieriges Gelände hinzu", 5 | "EnhancedTerrainLayer.onoff": "Umschalten Gelände ist immer sichtbar", 6 | "EnhancedTerrainLayer.reset": "Gelände zurücksetzen", 7 | "EnhancedTerrainLayer.IncreaseCost": "Kosten erhöhen", 8 | "EnhancedTerrainLayer.DecreaseCost": "Kosten senken", 9 | "EnhancedTerrainLayer.TerrainCost": "Gelände Kosten", 10 | "EnhancedTerrainLayer.Cost": "Kosten", 11 | "EnhancedTerrainLayer.Configure": "Das Gelände dieser Szene konfigurieren.", 12 | "EnhancedTerrainLayer.Configuration": "Gelände Konfiguration", 13 | "EnhancedTerrainLayer.UpdateTerrain": "Gelände updaten", 14 | "EnhancedTerrainLayer.MovementCost": "Kosten für Bewegung", 15 | "EnhancedTerrainLayer.TerrainHeight": "Gelände Höhe", 16 | "EnhancedTerrainLayer.Environment": "Umgebung", 17 | "EnhancedTerrainLayer.Obstacle": "Hindernis", 18 | "EnhancedTerrainLayer.TerrainColor": "Farbe des Geländes", 19 | "EnhancedTerrainLayer.DefaultTerrainColor": "Standard Farbe des Geländes", 20 | "EnhancedTerrainLayer.ResetDefaults": "Auf Standard zurücksetzen", 21 | "EnhancedTerrainLayer.SaveChanges": "Änderungen speichern", 22 | "EnhancedTerrainLayer.ErrorCalculate": "Calculate function is undefined", 23 | "EnhancedTerrainLayer.Min": "Min", 24 | "EnhancedTerrainLayer.Max": "Max", 25 | "EnhancedTerrainLayer.visibility": "Umschalten des Aktivstatus", 26 | "EnhancedTerrainLayer.terrain": "Gelände", 27 | "EnhancedTerrainLayer.Inactive": "Inaktiv", 28 | "EnhancedTerrainLayer.Locked": "Gesperrt", 29 | 30 | "EnhancedTerrainLayer.opacity.name": "Gelände-Symbol Transparenz", 31 | "EnhancedTerrainLayer.opacity.hint": "Transparenz des Symbols und der Nummer.", 32 | "EnhancedTerrainLayer.show-text.name": "Text anzeigen", 33 | "EnhancedTerrainLayer.show-text.hint": "Zeigt den Schwierigkeitstext des Geländes an.", 34 | "EnhancedTerrainLayer.show-icon.name": "Symbol anzeigen", 35 | "EnhancedTerrainLayer.show-icon.hint": "Zeigt das Symbol für die Umgebung des Geländes an.", 36 | "EnhancedTerrainLayer.show-on-drag.name": "Zeigen beim Ziehen", 37 | "EnhancedTerrainLayer.show-on-drag.hint": "Zeigt das Gelände-highlight, wenn ein Token verschoben wird.", 38 | "EnhancedTerrainLayer.tokens-cause-difficult.name": "Tokens verursachen schwieriges Gelände", 39 | "EnhancedTerrainLayer.tokens-cause-difficult.hint": "Wenn ein Token über ein anderes Token bewegt wird, gilt dieses als schwieriges Gelände.", 40 | "EnhancedTerrainLayer.use-obstacles.name": "Benutze zusätzliche Hindernisse", 41 | "EnhancedTerrainLayer.use-obstacles.hint": "Erlaube das Verwenden von zusätzlichen Hindernissen in Kombination mit der Umgebung.", 42 | "EnhancedTerrainLayer.only-show-active.name": "Zeige nur aktives Gelände an", 43 | "EnhancedTerrainLayer.only-show-active.hint": "Verstecktes Gelände wird nur für den SL angezeigt, wenn das gelände-Werkzeug ausgewählt.", 44 | "EnhancedTerrainLayer.minimum-cost.name": "Minimum Kosten", 45 | "EnhancedTerrainLayer.minimum-cost.hint": "Minimum Kosten, die man als Schwierigkeit für ein Gelände setzten kann.", 46 | "EnhancedTerrainLayer.maximum-cost.name": "Maximum Kosten", 47 | "EnhancedTerrainLayer.maximum-cost.hint": "Maximum Kosten, die man als Schwierigkeit für ein Gelände setzten kann.", 48 | "EnhancedTerrainLayer.dead-cause-difficult.name": "Tote zählen als schwieriges Gelände", 49 | "EnhancedTerrainLayer.dead-cause-difficult.hint": "Tote Tokens zählen als schwieriges Gelände.", 50 | "EnhancedTerrainLayer.draw-border.name": "Rand zeichnen", 51 | "EnhancedTerrainLayer.draw-border.hint": "Definiert, ob der Rand gezeichnet wird order nicht.", 52 | "EnhancedTerrainLayer.terrain-image.name": "Gelände Bild", 53 | "EnhancedTerrainLayer.terrain-image.hint": "Ändert die Hintergrundtextur des Geländes.", 54 | 55 | "EnhancedTerrainLayer.environment.arctic": "Arktisch", 56 | "EnhancedTerrainLayer.environment.coast": "Küste", 57 | "EnhancedTerrainLayer.environment.desert": "Wüste", 58 | "EnhancedTerrainLayer.environment.forest": "Wald", 59 | "EnhancedTerrainLayer.environment.grassland": "Grasland", 60 | "EnhancedTerrainLayer.environment.jungle": "Dschungel", 61 | "EnhancedTerrainLayer.environment.mountain": "Berge", 62 | "EnhancedTerrainLayer.environment.swamp": "Sumpf", 63 | "EnhancedTerrainLayer.environment.underdark": "Underdark", 64 | "EnhancedTerrainLayer.environment.urban": "Städtisch", 65 | "EnhancedTerrainLayer.environment.water": "Wasser", 66 | 67 | "EnhancedTerrainLayer.obstacle.crowd": "Menschenmenge", 68 | "EnhancedTerrainLayer.obstacle.current": "Fluss", 69 | "EnhancedTerrainLayer.obstacle.furniture": "Möbel", 70 | "EnhancedTerrainLayer.obstacle.magic": "Magie", 71 | "EnhancedTerrainLayer.obstacle.plants": "Pflanzen", 72 | "EnhancedTerrainLayer.obstacle.rubble": "Trümmer", 73 | "EnhancedTerrainLayer.obstacle.water": "Wasser" 74 | 75 | } 76 | -------------------------------------------------------------------------------- /classes/terrainconfig.js: -------------------------------------------------------------------------------- 1 | import { TerrainLayer } from './terrainlayer.js'; 2 | import { TerrainDocument } from './terraindocument.js'; 3 | import { log, setting, i18n, getflag} from '../terrain-main.js'; 4 | 5 | export class TerrainConfig extends DocumentSheet { 6 | 7 | /** @override */ 8 | static get defaultOptions() { 9 | return mergeObject(super.defaultOptions, { 10 | id: "terrain-config", 11 | classes: ["sheet", "terrain-sheet"], 12 | //title: i18n("EnhancedTerrainLayer.Configuration"), 13 | template: "modules/enhanced-terrain-layer/templates/terrain-config.html", 14 | width: 400, 15 | height: "auto", 16 | configureDefault: false, 17 | submitOnChange: false 18 | }); 19 | } 20 | 21 | /* -------------------------------------------- */ 22 | 23 | /** @override */ 24 | getData(options) { 25 | var _obstacles = {}; 26 | var _environments = canvas.terrain.getEnvironments().reduce(function (map, obj) { 27 | if (obj.obstacle === true) { 28 | _obstacles[obj.id] = i18n(obj.text); 29 | }else 30 | map[obj.id] = i18n(obj.text); 31 | return map; 32 | }, {}); 33 | 34 | const data = super.getData(); 35 | return mergeObject(data, { 36 | author: game.users.get(this.document.author)?.name || "", 37 | environments: _environments, 38 | obstacles: _obstacles, 39 | useObstacles: setting('use-obstacles'), 40 | submitText: this.document.id ? "Update" : "Create" 41 | }) 42 | } 43 | 44 | /* -------------------------------------------- */ 45 | 46 | /** @override */ 47 | _onChangeInput(event) { 48 | if ($(event.target).attr('name') == 'multiple') { 49 | let val = $(event.target).val(); 50 | $(event.target).next().html(TerrainDocument.text(val)); 51 | } 52 | super._onChangeInput.call(this, event); 53 | } 54 | 55 | /* -------------------------------------------- */ 56 | 57 | /** @override */ 58 | async _updateObject(event, formData) { 59 | if (!game.user.isGM) throw "You do not have the ability to configure a Terrain object."; 60 | 61 | // Un-scale the bezier factor 62 | //formData.bezierFactor /= 2; 63 | 64 | if (formData.width != this.object.width || formData.height != this.object.height) { 65 | let reshape = this.object.object._rescaleDimensions(this.object, formData.width - this.object.width, formData.height - this.object.height); 66 | formData["shape.width"] = reshape.shape.width; 67 | formData["shape.height"] = reshape.shape.height; 68 | if (this.object.object.isPolygon) 69 | formData["shape.points"] = reshape.shape.points; 70 | } 71 | delete formData.width; 72 | delete formData.height; 73 | 74 | let data = expandObject(formData); 75 | data.multiple = Math.clamped(data.multiple, setting('minimum-cost'), setting('maximum-cost')); 76 | 77 | let defaultOpacity = getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1; 78 | if (data.opacity == defaultOpacity) 79 | data.opacity = null; 80 | 81 | if (this.object.id) { 82 | return this.object.update(data); 83 | } 84 | return this.object.constructor.create(data); 85 | } 86 | 87 | async close(options) { 88 | await super.close(options); 89 | if (this.preview) { 90 | this.preview.removeChildren(); 91 | this.preview = null; 92 | } 93 | } 94 | 95 | activateListeners(html) { 96 | super.activateListeners(html); 97 | 98 | if (setting('use-obstacles')) { 99 | $('select[name="environment"], select[name="obstacle"]', html).on('change', function () { 100 | //make sure that the environment is always set if using obstacles 101 | if ($('select[name="environment"]', html).val() == '' && $('select[name="obstacle"]', html).val() != '') { 102 | $('select[name="environment"]', html).val($('select[name="obstacle"]', html).val()); 103 | $('select[name="obstacle"]', html).val(''); 104 | } 105 | 106 | //make sure that obstacle is only set once, can't have an obstacle + obstacle, can only be environment + obstacle 107 | if ($('select[name="environment"] option:selected', html).parent().attr('data-type') == 'obstacle' && $('select[name="obstacle"]', html).val() != '') { 108 | if ($(this).attr('name') == 'obstacle') { 109 | $('select[name="environment"]', html).val($('select[name="obstacle"]', html).val()); 110 | } 111 | $('select[name="obstacle"]', html).val(''); 112 | }; 113 | }); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "EnhancedTerrainLayer.tool": "Terrain Layer", 3 | "EnhancedTerrainLayer.select": "Select Difficult Terrain", 4 | "EnhancedTerrainLayer.add": "Add Difficult Terrain", 5 | "EnhancedTerrainLayer.onoff": "Toggle terrain always displayed", 6 | "EnhancedTerrainLayer.reset": "Reset Terrain", 7 | "EnhancedTerrainLayer.IncreaseCost": "Increase Cost", 8 | "EnhancedTerrainLayer.DecreaseCost": "Decrease Cost", 9 | "EnhancedTerrainLayer.TerrainCost": "Terrain Cost", 10 | "EnhancedTerrainLayer.Cost": "Cost", 11 | "EnhancedTerrainLayer.Configure": "Configure this terrain.", 12 | "EnhancedTerrainLayer.Configuration": "Terrain Configuration", 13 | "EnhancedTerrainLayer.UpdateTerrain": "Update Terrain", 14 | "EnhancedTerrainLayer.MovementCost": "Movement Cost", 15 | "EnhancedTerrainLayer.TerrainHeight": "Terrain Height", 16 | "EnhancedTerrainLayer.TerrainElevation": "Terrain Elevation", 17 | "EnhancedTerrainLayer.TerrainDepth": "Terrain Depth", 18 | "EnhancedTerrainLayer.Environment": "Environment", 19 | "EnhancedTerrainLayer.Obstacle": "Obstacle", 20 | "EnhancedTerrainLayer.TerrainColor": "Terrain Color", 21 | "EnhancedTerrainLayer.DefaultTerrainColor": "Default Terrain Color", 22 | "EnhancedTerrainLayer.ResetDefaults": "Reset Defaults", 23 | "EnhancedTerrainLayer.SaveChanges": "Save Changes", 24 | "EnhancedTerrainLayer.ErrorCalculate": "Calculate function is undefined", 25 | "EnhancedTerrainLayer.Min": "Min", 26 | "EnhancedTerrainLayer.Max": "Max", 27 | "EnhancedTerrainLayer.visibility": "Toggle Active State", 28 | "EnhancedTerrainLayer.terrain": "Terrain", 29 | "EnhancedTerrainLayer.Inactive": "Inactive", 30 | "EnhancedTerrainLayer.Locked": "Locked", 31 | "EnhancedTerrainLayer.DefaultCost": "Default value set from the Scene configuration", 32 | "EnhancedTerrainLayer.ToggleView": "Toggle terrain always viewed", 33 | 34 | "EnhancedTerrainLayer.opacity.name": "Terrain Opacity", 35 | "EnhancedTerrainLayer.opacity.hint": "Opacity of the terrain layer", 36 | "EnhancedTerrainLayer.show-text.name": "Show Text", 37 | "EnhancedTerrainLayer.show-text.hint": "Show the text of how difficult the terrain is.", 38 | "EnhancedTerrainLayer.show-icon.name": "Show Icons", 39 | "EnhancedTerrainLayer.show-icon.hint": "Show the icon for the environment of the terrain.", 40 | "EnhancedTerrainLayer.show-on-drag.name": "Show on drag", 41 | "EnhancedTerrainLayer.show-on-drag.hint": "Show the terrain highlight when a token is dragged", 42 | "EnhancedTerrainLayer.tokens-cause-difficult.name": "Include Live Tokens", 43 | "EnhancedTerrainLayer.tokens-cause-difficult.hint": "When a token is dragged over another token, it's considered difficult terrain.", 44 | "EnhancedTerrainLayer.dead-cause-difficult.name": "Include Dead Tokens", 45 | "EnhancedTerrainLayer.dead-cause-difficult.hint": "When a token is dragged over a dead token, it's considered difficult terrain.", 46 | "EnhancedTerrainLayer.use-obstacles.name": "Use Additional Obstacles", 47 | "EnhancedTerrainLayer.use-obstacles.hint": "Allow the additional use of obstacles in combination with an environment.", 48 | "EnhancedTerrainLayer.only-show-active.name": "Only show active terrain", 49 | "EnhancedTerrainLayer.only-show-active.hint": "Hidden terrain will only be visible for the GM when the terrain tool is active.", 50 | "EnhancedTerrainLayer.minimum-cost.name": "Minimum Cost", 51 | "EnhancedTerrainLayer.minimum-cost.hint": "Minimum cost you can set difficult terrain to", 52 | "EnhancedTerrainLayer.maximum-cost.name": "Maximum Cost", 53 | "EnhancedTerrainLayer.maximum-cost.hint": "Maximum cost you can set difficult terrain to", 54 | "EnhancedTerrainLayer.draw-border.name": "Draw Border", 55 | "EnhancedTerrainLayer.draw-border.hint": "Set if the border is drawn or not", 56 | "EnhancedTerrainLayer.terrain-image.name": "Terrain Image", 57 | "EnhancedTerrainLayer.terrain-image.hint": "Change the background texture for terrain", 58 | "EnhancedTerrainLayer.rule-provider.name": "Rule provider", 59 | "EnhancedTerrainLayer.rule-provider.hint": "Which rule provider should Enhanced Terrain Layer pull it's terrain rules from", 60 | "EnhancedTerrainLayer.transfer-color.name": "Transfer spell colour", 61 | "EnhancedTerrainLayer.transfer-color.hint": "When casting a spell that causes difficult terrain, transfer the default environment colour to the measured template.", 62 | 63 | "EnhancedTerrainLayer.rule-provider.choices.builtin": "Built-in", 64 | "EnhancedTerrainLayer.rule-provider.choices.module": "Module {name}", 65 | "EnhancedTerrainLayer.rule-provider.choices.system": "System {name}", 66 | 67 | "EnhancedTerrainLayer.environment.arctic": "Arctic", 68 | "EnhancedTerrainLayer.environment.coast": "Coast", 69 | "EnhancedTerrainLayer.environment.desert": "Desert", 70 | "EnhancedTerrainLayer.environment.forest": "Forest", 71 | "EnhancedTerrainLayer.environment.grassland": "Grassland", 72 | "EnhancedTerrainLayer.environment.jungle": "Jungle", 73 | "EnhancedTerrainLayer.environment.mountain": "Mountain", 74 | "EnhancedTerrainLayer.environment.swamp": "Swamp", 75 | "EnhancedTerrainLayer.environment.underdark": "Underdark", 76 | "EnhancedTerrainLayer.environment.urban": "Urban", 77 | "EnhancedTerrainLayer.environment.water": "Water", 78 | 79 | "EnhancedTerrainLayer.obstacle.crowd": "Crowd", 80 | "EnhancedTerrainLayer.obstacle.current": "Current", 81 | "EnhancedTerrainLayer.obstacle.furniture": "Furniture", 82 | "EnhancedTerrainLayer.obstacle.magic": "Magic", 83 | "EnhancedTerrainLayer.obstacle.plants": "Plants", 84 | "EnhancedTerrainLayer.obstacle.rubble": "Rubble", 85 | "EnhancedTerrainLayer.obstacle.water": "Water", 86 | "EnhancedTerrainLayer.obstacle.webbing": "Webbing" 87 | } 88 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | import { TerrainColor } from "../classes/terraincolor.js"; 2 | import { updateRuleProviderVariable } from "./api.js"; 3 | 4 | export const registerSettings = function () { 5 | let modulename = "enhanced-terrain-layer"; 6 | 7 | const debouncedRefresh = foundry.utils.debounce(function () { canvas.terrain.refresh(); }, 100); 8 | 9 | let imageoptions = { 10 | 'solid': 'Solid', 11 | 'diagonal': 'Diagonal', 12 | 'oldschool': 'Old School', 13 | 'triangle': 'Triangle', 14 | 'horizontal': 'Horizontal', 15 | 'vertical': 'Vertical', 16 | 'clear': 'Clear' 17 | }; 18 | 19 | let tokenoptions = { 20 | 'false': 'None', 21 | 'friendly': 'Friendly', 22 | 'hostile': 'Hostile', 23 | 'true': 'Any' 24 | }; 25 | 26 | game.settings.registerMenu(modulename, 'edit-colors', { 27 | name: 'Edit Colors', 28 | label: 'Edit Colors', 29 | hint: 'Edit default color, environment colrs, and obstacle colors', 30 | icon: 'fas fa-palette', 31 | restricted: true, 32 | type: TerrainColor, 33 | onClick: (value) => { 34 | } 35 | }); 36 | 37 | game.settings.register(modulename, 'opacity', { 38 | name: "EnhancedTerrainLayer.opacity.name", 39 | hint: "EnhancedTerrainLayer.opacity.hint", 40 | scope: "world", 41 | config: true, 42 | default: 1, 43 | type: Number, 44 | range: { 45 | min: 0, 46 | max: 1, 47 | step: 0.1 48 | }, 49 | onChange: debouncedRefresh 50 | }); 51 | game.settings.register(modulename, 'draw-border', { 52 | name: "EnhancedTerrainLayer.draw-border.name", 53 | hint: "EnhancedTerrainLayer.draw-border.hint", 54 | scope: "world", 55 | config: true, 56 | default: true, 57 | type: Boolean, 58 | onChange: debouncedRefresh 59 | }); 60 | game.settings.register(modulename, 'terrain-image', { 61 | name: "EnhancedTerrainLayer.terrain-image.name", 62 | hint: "EnhancedTerrainLayer.terrain-image.hint", 63 | scope: "world", 64 | config: true, 65 | default: 'diagonal', 66 | type: String, 67 | choices: imageoptions, 68 | requiresReload: true 69 | }); 70 | game.settings.register(modulename, 'show-text', { 71 | name: "EnhancedTerrainLayer.show-text.name", 72 | hint: "EnhancedTerrainLayer.show-text.hint", 73 | scope: "world", 74 | config: true, 75 | default: false, 76 | type: Boolean, 77 | onChange: debouncedRefresh 78 | }); 79 | game.settings.register(modulename, 'show-icon', { 80 | name: "EnhancedTerrainLayer.show-icon.name", 81 | hint: "EnhancedTerrainLayer.show-icon.hint", 82 | scope: "world", 83 | config: true, 84 | default: false, 85 | type: Boolean, 86 | onChange: debouncedRefresh 87 | }); 88 | game.settings.register(modulename, 'show-on-drag', { 89 | name: "EnhancedTerrainLayer.show-on-drag.name", 90 | hint: "EnhancedTerrainLayer.show-on-drag.hint", 91 | scope: "world", 92 | config: true, 93 | default: true, 94 | type: Boolean 95 | }); 96 | game.settings.register(modulename, 'only-show-active', { 97 | name: "EnhancedTerrainLayer.only-show-active.name", 98 | hint: "EnhancedTerrainLayer.only-show-active.hint", 99 | scope: "world", 100 | config: true, 101 | default: false, 102 | type: Boolean, 103 | onChange: debouncedRefresh 104 | }); 105 | game.settings.register(modulename, 'tokens-cause-difficult', { 106 | name: "EnhancedTerrainLayer.tokens-cause-difficult.name", 107 | hint: "EnhancedTerrainLayer.tokens-cause-difficult.hint", 108 | scope: "world", 109 | config: true, 110 | choices: tokenoptions, 111 | default: "false", 112 | type: String 113 | }); 114 | game.settings.register(modulename, 'dead-cause-difficult', { 115 | name: "EnhancedTerrainLayer.dead-cause-difficult.name", 116 | hint: "EnhancedTerrainLayer.dead-cause-difficult.hint", 117 | scope: "world", 118 | config: true, 119 | choices: tokenoptions, 120 | default: "false", 121 | type: String 122 | }); 123 | game.settings.register(modulename, 'use-obstacles', { 124 | name: "EnhancedTerrainLayer.use-obstacles.name", 125 | hint: "EnhancedTerrainLayer.use-obstacles.hint", 126 | scope: "world", 127 | config: true, 128 | default: false, 129 | type: Boolean 130 | }); 131 | game.settings.register(modulename, 'transfer-color', { 132 | name: "EnhancedTerrainLayer.transfer-color.name", 133 | hint: "EnhancedTerrainLayer.transfer-color.hint", 134 | scope: "world", 135 | config: game.system.id == 'dnd5e', 136 | default: false, 137 | type: Boolean 138 | }); 139 | game.settings.register(modulename, 'minimum-cost', { 140 | name: "EnhancedTerrainLayer.minimum-cost.name", 141 | hint: "EnhancedTerrainLayer.minimum-cost.hint", 142 | scope: "world", 143 | config: true, 144 | default: 0.5, 145 | type: Number 146 | }); 147 | game.settings.register(modulename, 'maximum-cost', { 148 | name: "EnhancedTerrainLayer.maximum-cost.name", 149 | hint: "EnhancedTerrainLayer.maximum-cost.hint", 150 | scope: "world", 151 | config: true, 152 | default: 4, 153 | type: Number 154 | }); 155 | 156 | game.settings.register(modulename, "rule-provider", { 157 | name: "EnhancedTerrainLayer.rule-provider.name", 158 | hint: "EnhancedTerrainLayer.rule-provider.hint", 159 | scope: "world", 160 | config: false, 161 | default: "bulitin", 162 | type: String, 163 | choices: {}, 164 | onChange: updateRuleProviderVariable, 165 | }); 166 | 167 | game.settings.register(modulename, 'showterrain', { 168 | scope: "world", 169 | config: false, 170 | default: false, 171 | type: Boolean 172 | }); 173 | 174 | game.settings.register(modulename, 'conversion', { 175 | scope: "world", 176 | config: false, 177 | default: false, 178 | type: Boolean 179 | }); 180 | 181 | game.settings.register(modulename, 'environment-color', { 182 | scope: "world", 183 | config: false, 184 | default: {}, 185 | type: Object 186 | }); 187 | }; -------------------------------------------------------------------------------- /css/terrainlayer.css: -------------------------------------------------------------------------------- 1 | #controls .sub-controls.terrain-controls { 2 | flex-grow: 0; 3 | margin-right: 4px; 4 | } 5 | 6 | /* 7 | #terrainlayer-tools { 8 | position: absolute; 9 | width: auto; 10 | text-align: center; 11 | }*/ 12 | 13 | #controls ol.control-tools.sub-controls { 14 | flex-grow: 0; 15 | margin-right: 6px; 16 | } 17 | 18 | #terrainlayer-tools.control-tools { 19 | list-style: none; 20 | padding: 0; 21 | margin: 0; 22 | } 23 | 24 | #terrainlayer-tools.control-tools > li.control-tool.disabled:hover { 25 | box-shadow: none; 26 | border: 1px solid var(--color-border-dark); 27 | cursor: default; 28 | } 29 | 30 | #terrainlayer-tools .control-btn { 31 | width: 36px; 32 | height: 36px; 33 | box-sizing: content-box; 34 | font-size: 24px; 35 | line-height: 36px; 36 | margin: 0 0 8px; 37 | color: #BBB; 38 | text-align: center; 39 | border: 0px; 40 | background: transparent; 41 | border-radius: 5px; 42 | cursor: pointer; 43 | padding: 0; 44 | } 45 | 46 | #terrainlayer-tools.control-tools > li.control-tool.disabled:hover { 47 | box-shadow: 0 0 10px var(--color-shadow-dark); 48 | } 49 | /* 50 | #terrainlayer-tools .control-btn:hover { 51 | color: #FFF; 52 | box-shadow: 0 0 10px #ff6400; 53 | }*/ 54 | #terrainlayer-tools .control-btn:disabled { 55 | color: var(--color-text-dark-secondary); 56 | box-shadow: none; 57 | cursor: default; 58 | } 59 | 60 | #terrainlayer-tools table { 61 | margin: 0; 62 | border: none; 63 | background: none; 64 | } 65 | 66 | #terrainlayer-tools td { 67 | padding: 0; 68 | vertical-align: top; 69 | } 70 | 71 | #terrainlayer-tools td:nth-child(n+1) { 72 | padding-left: 8px; 73 | } 74 | 75 | #terrainlayer-tools #tl-defaultcost { 76 | cursor: default; 77 | position: relative; 78 | } 79 | #terrainlayer-tools #tl-defaultcost:hover { 80 | box-shadow: none; 81 | border: 1px solid var(--color-border-dark); 82 | box-shadow: 0 0 10px var(--color-shadow-dark); 83 | } 84 | 85 | #terrainlayer-tools #tl-defaultcost i { 86 | position: absolute; 87 | top: -5px; 88 | left: -5px; 89 | font-size: 14px; 90 | } 91 | /* 92 | .terrain-color input[type=color]::-webkit-color-swatch { 93 | background-color: transparent !important; 94 | }*/ 95 | /* 96 | #terrain-config input[type="range"] { 97 | z-index: 2; 98 | } 99 | 100 | #terrain-config input[type="range"] + input[type="range"] { 101 | position: absolute; 102 | top: 0px; 103 | left: 0px; 104 | width: calc(100% - 58px); 105 | -webkit-appearance: none; 106 | z-index: 1; 107 | } 108 | 109 | #terrain-config input[type="range"] + input[type="range"]::-webkit-slider-runnable-track, 110 | #terrain-config input[type="range"] + input[type="range"]::-moz-range-track, 111 | #terrain-config input[type="range"] + input[type="range"]::-ms-track { 112 | background: none; 113 | color:transparent; 114 | }*/ 115 | #terrain-hud .environment-list { 116 | visibility: hidden; 117 | position: absolute; 118 | right: 60px; 119 | top: 45px; 120 | display: grid; 121 | padding: 3px; 122 | box-sizing: content-box; 123 | grid-template-columns: 130px 130px 130px; 124 | background: rgba(0, 0, 0, 0.8); 125 | box-shadow: 0 0 15px #000; 126 | border: 1px solid #333; 127 | border-radius: 4px; 128 | pointer-events: all; 129 | font-size: 16px; 130 | line-height: 24px; 131 | text-align: left; 132 | } 133 | 134 | #terrain-hud .attribute { 135 | position: relative; 136 | } 137 | 138 | #terrain-hud .attribute i.fas { 139 | position: absolute; 140 | top: -5px; 141 | left: -3px; 142 | font-size: var(--font-size-18); 143 | color: var(--color-text-light-5); 144 | } 145 | 146 | #terrain-hud .environment-list.active { 147 | visibility: visible; 148 | } 149 | 150 | #terrain-hud .environment-container { 151 | cursor: pointer; 152 | position: relative; 153 | padding: 1px; 154 | border-radius: 4px; 155 | border: 1px solid transparent; 156 | } 157 | 158 | #terrain-hud .environment-control { 159 | display: block; 160 | width: 100%; 161 | height: 24px; 162 | margin: 0; 163 | margin-top: -1px; 164 | border-radius: 4px; 165 | border: 1px solid transparent; 166 | padding: 0; 167 | opacity: 0.5; 168 | padding-right: calc(100% - 24px); 169 | } 170 | 171 | #terrain-hud .environment-name { 172 | vertical-align: top; 173 | padding-left: 4px; 174 | overflow: hidden; 175 | text-overflow: ellipsis; 176 | white-space: nowrap; 177 | max-width: calc(100% - 24px); 178 | display: inline-block; 179 | pointer-events: none; 180 | position: absolute; 181 | top: 0px; 182 | left: 24px; 183 | opacity: 0.6; 184 | color: #ccc; 185 | } 186 | 187 | #terrain-hud .environment-list .environment-container.active { 188 | border: 1px solid #ff6400; 189 | opacity: 0.7; 190 | } 191 | 192 | #terrain-hud .environment-list .environment-container:hover .environment-control { 193 | opacity: 1; 194 | } 195 | 196 | #terrain-hud .environment-list .environment-container.active .environment-control { 197 | filter: sepia(100%) saturate(2000%) hue-rotate( -50deg ); 198 | } 199 | 200 | #terrain-hud .environment-list .environment-container.active:hover { 201 | color: #ffc163 !important; 202 | } 203 | 204 | #terrain-hud .environment-list .environment-container.active .environment-name { 205 | color: #ff6400; 206 | opacity: 0.8; 207 | } 208 | 209 | #terrain-hud .environment-list .environment-container:hover .environment-name { 210 | opacity:1; 211 | } 212 | 213 | .terrainheight-label { 214 | font-size: 13px; 215 | } 216 | 217 | .terrain-height.smaller { 218 | font-size: 12px; 219 | line-height: 14px; 220 | margin-top: 2px; 221 | } -------------------------------------------------------------------------------- /js/api.js: -------------------------------------------------------------------------------- 1 | import {RuleProvider} from "../classes/ruleprovider.js"; 2 | import {i18n} from "../terrain-main.js"; 3 | 4 | const availableRuleProviders = {}; 5 | let currentRuleProvider = undefined; 6 | 7 | function register(module, type, ruleProvider) { 8 | const id = `${type}.${module.id}`; 9 | const ruleProviderInstance = new ruleProvider(id); 10 | setupProvider(ruleProviderInstance); 11 | game.settings.settings.get("enhanced-terrain-layer.rule-provider").config = true; 12 | } 13 | 14 | function setupProvider(ruleProvider) { 15 | availableRuleProviders[ruleProvider.id] = ruleProvider; 16 | refreshProviderSetting(); 17 | updateRuleProviderVariable(); 18 | } 19 | 20 | function refreshProviderSetting() { 21 | const choices = {}; 22 | for (const provider of Object.values(availableRuleProviders)) { 23 | let dotPosition = provider.id.indexOf("."); 24 | if (dotPosition === -1) { 25 | dotPosition = provider.id.length; 26 | } 27 | const type = provider.id.substring(0, dotPosition); 28 | const id = provider.id.substring(dotPosition + 1); 29 | let text; 30 | if (type === "bultin") { 31 | text = i18n("EnhancedTerrainLayer.rule-provider.choices.builtin"); 32 | } else { 33 | let name; 34 | if (type === "module") { 35 | name = game.modules.get(id).title; 36 | } else { 37 | name = game.system.title; 38 | } 39 | text = game.i18n.format(`EnhancedTerrainLayer.rule-provider.choices.${type}`, {name}); 40 | } 41 | choices[provider.id] = text; 42 | } 43 | game.settings.settings.get("enhanced-terrain-layer.rule-provider").choices = choices; 44 | game.settings.settings.get("enhanced-terrain-layer.rule-provider").default = 45 | getDefaultRuleProvider(); 46 | } 47 | 48 | function getDefaultRuleProvider() { 49 | const providerIds = Object.keys(availableRuleProviders); 50 | 51 | // Game systems take the highest precedence for the being the default 52 | const gameSystem = providerIds.find(key => key.startsWith("system.")); 53 | if (gameSystem) return gameSystem; 54 | 55 | // If no game system is registered modules are next up. 56 | // For lack of a method to select the best module we're just falling back to taking the next best module 57 | // Object keys should always be sorted the same way so this should achive a stable default 58 | const module = providerIds.find(key => key.startsWith("module.")); 59 | if (module) return module; 60 | 61 | // If neither a game system or a module is found fall back to the native implementation 62 | return providerIds[0]; 63 | } 64 | 65 | export function updateRuleProviderVariable() { 66 | // If the configured provider is registered use that one. If not use the default provider 67 | const configuredProvider = game.settings.get("enhanced-terrain-layer", "rule-provider"); 68 | currentRuleProvider = 69 | availableRuleProviders[configuredProvider] ?? 70 | availableRuleProviders[game.settings.settings.get("enhanced-terrain-layer.rule-provider")]; 71 | } 72 | 73 | export function initApi() { 74 | const builtinRuleProviderInstance = new RuleProvider("builtin"); 75 | setupProvider(builtinRuleProviderInstance); 76 | } 77 | 78 | export function registerModule(moduleId, ruleProvider) { 79 | // Check if a module with the given id exists and is currently enabled 80 | const module = game.modules.get(moduleId); 81 | // If it doesn't the calling module did something wrong. Log a warning and ignore this module 82 | if (!module) { 83 | console.warn( 84 | `Enhanced Terrain Layer | A module tried to register with the id "${moduleId}". However no active module with this id was found.` + 85 | "This api registration call was ignored. " + 86 | "If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." + 87 | "If this call was made form a game system instead of a module please use `registerSystem` instead.", 88 | ); 89 | return; 90 | } 91 | // Using Enhanced Terrain Layer's id is not allowed 92 | if (moduleId === "enhanced-terrain-layer") { 93 | console.warn( 94 | `Enhanced Terrain Layer | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` + 95 | "If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " + 96 | "If this call was made form a game system instead of a module please use `registerSystem` instead.", 97 | ); 98 | return; 99 | } 100 | 101 | register(module, "module", ruleProvider); 102 | } 103 | 104 | export function registerSystem(systemId, speedProvider) { 105 | const system = game.system; 106 | // If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module 107 | if (system.id != systemId) { 108 | console.warn( 109 | `Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` + 110 | "This api registration call was ignored. " + 111 | "If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." + 112 | "If this call was made form a module instead of a game system please use `registerModule` instead.", 113 | ); 114 | return; 115 | } 116 | 117 | register(system, "system", speedProvider); 118 | } 119 | 120 | export function calculateCombinedCost(terrain, options = {}) { 121 | const cost = currentRuleProvider.calculateCombinedCost(terrain, options); 122 | // Check if the provider returned a number. If not, log an error and fall back to returning 1 123 | if (isNaN(cost)) { 124 | console.error(`The active rule provider returned an invalid cost value: ${cost}`); 125 | return 1; 126 | } 127 | return cost; 128 | } 129 | -------------------------------------------------------------------------------- /classes/terrainhud.js: -------------------------------------------------------------------------------- 1 | import { TerrainLayer } from './terrainlayer.js'; 2 | import { log, setting, i18n } from '../terrain-main.js'; 3 | 4 | export class TerrainHUD extends BasePlaceableHUD { 5 | _showEnvironments = false; 6 | 7 | /** @override */ 8 | static get defaultOptions() { 9 | return mergeObject(super.defaultOptions, { 10 | id: "terrain-hud", 11 | template: "modules/enhanced-terrain-layer/templates/terrain-hud.html" 12 | }); 13 | } 14 | 15 | bind(object) { 16 | this._showEnvironments = false; 17 | return super.bind(object); 18 | } 19 | 20 | /* -------------------------------------------- */ 21 | 22 | /** @override */ 23 | getData() { 24 | var _environments = canvas.terrain.getEnvironments().map(obj => { 25 | obj.text = i18n(obj.text); 26 | obj.active = (this.object.document.environment == obj.id); 27 | 28 | return obj; 29 | }); 30 | 31 | /* 32 | var _obstacles = canvas.terrain.getObstacles().map(obj => { 33 | obj.text = i18n(obj.text); 34 | obj.active = (setting('use-obstacles') ? this.object.data.obstacle == obj.id : (this.object.data.environment || this.object.data.obstacle) == obj.id); 35 | return obj; 36 | });*/ 37 | 38 | const data = super.getData(); 39 | return mergeObject(data, { 40 | lockedClass: data.locked ? "active" : "", 41 | visibilityClass: data.hidden ? "active" : "", 42 | text: TerrainLayer.multipleText(data.multiple), 43 | environment: this.object.document.environmentObject, 44 | environments: _environments 45 | }); 46 | } 47 | 48 | activateListeners(html) { 49 | super.activateListeners(html); 50 | 51 | $('.inc-multiple', this.element).on("click", this._onHandleClick.bind(this, true)); 52 | $('.dec-multiple', this.element).on("click", this._onHandleClick.bind(this, false)); 53 | html.find(".environments > img").click(this._onClickEnvironments.bind(this)); 54 | 55 | html.find(".environment-list") 56 | .on("click", ".environment-container", this._onToggleEnvironment.bind(this)) 57 | .on("contextmenu", ".environment-container", event => this._onToggleEnvironment(event)); 58 | 59 | /* 60 | this.frame.handle.off("mouseover").off("mouseout").off("mousedown") 61 | .on("mouseover", this._onHandleHoverIn.bind(this)) 62 | .on("mouseout", this._onHandleHoverOut.bind(this)) 63 | .on("mousedown", this._onHandleMouseDown.bind(this)); 64 | this.frame.handle.interactive = true;*/ 65 | } 66 | 67 | _onClickEnvironments(event) { 68 | event.preventDefault(); 69 | this._toggleEnvironments(!this._showEnvironments); 70 | } 71 | 72 | /* -------------------------------------------- */ 73 | 74 | _toggleEnvironments(active) { 75 | this._showEnvironments = active; 76 | const button = this.element.find(".control-icon.environments")[0]; 77 | button.classList.toggle("active", active); 78 | const palette = button.querySelector(".environment-list"); 79 | palette.classList.toggle("active", active); 80 | } 81 | 82 | /* -------------------------------------------- */ 83 | 84 | _onToggleEnvironment(event) { 85 | event.preventDefault(); 86 | let ctrl = event.currentTarget; 87 | let id = ctrl.dataset.environmentId; 88 | $('.environment-list .environment-container.active', this.element).removeClass('active'); 89 | if (id != this.object.document.environment) 90 | $('.environment-list .environment-container[data-environment-id="' + id + '"]', this.element).addClass('active'); 91 | 92 | const updates = this.layer.controlled.map(o => { 93 | return { _id: o.id, environment: (id != this.object.document.environment ? id : '') }; 94 | }); 95 | 96 | return canvas.scene.updateEmbeddedDocuments("Terrain", updates).then(() => { 97 | $('.environments > img', this.element).attr('src', this.object.document.environmentObject?.icon || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); 98 | }); 99 | } 100 | 101 | /* 102 | * async _onToggleVisibility(event) { 103 | event.preventDefault(); 104 | 105 | // Toggle the visible state 106 | const isHidden = this.object.data.hidden; 107 | const updates = this.layer.controlled.map(o => { 108 | return {_id: o.id, hidden: !isHidden}; 109 | }); 110 | 111 | // Update all objects 112 | await this.layer.updateMany(updates); 113 | event.currentTarget.classList.toggle("active", !isHidden); 114 | } 115 | */ 116 | 117 | _onHandleClick(increase, event) { 118 | const updates = this.layer.controlled.map(o => { 119 | let mult = TerrainLayer.alterMultiple(o.document.multiple, increase); 120 | //let idx = TerrainLayer.multipleOptions.indexOf(mult); 121 | //idx = Math.clamped((increase ? idx + 1 : idx - 1), 0, TerrainLayer.multipleOptions.length - 1); 122 | return { _id: o.id, multiple: mult }; //TerrainLayer.multipleOptions[idx] }; 123 | }); 124 | 125 | let that = this; 126 | return canvas.scene.updateEmbeddedDocuments("Terrain", updates).then(() => { 127 | $('.terrain-cost', that.element).html(`${TerrainLayer.multipleText(that.object.document.multiple)}`); 128 | }); 129 | 130 | /* 131 | this.layer.updateMany(updates).then(() => { 132 | for (let terrain of this.layer.controlled) { 133 | let data = updates.find(u => { return u._id == terrain.data._id }); 134 | terrain.update(data, { save: false }).then(() => { 135 | $('.terrain-cost', this.element).html(String.fromCharCode(215) + this.object.multiple); 136 | }); 137 | } 138 | });*/ 139 | 140 | /* 141 | let mult = this.object.data.multiple; 142 | let idx = TerrainLayer.multipleOptions.indexOf(mult); 143 | idx = Math.clamped((increase ? idx + 1 : idx - 1), 0, TerrainLayer.multipleOptions.length - 1); 144 | this.object.update({ multiple: TerrainLayer.multipleOptions[idx] }); 145 | this.object.refresh();*/ 146 | } 147 | 148 | async _onToggleVisibility(event) { 149 | event.preventDefault(); 150 | 151 | const isHidden = this.object.document.hidden; 152 | 153 | event.currentTarget.classList.toggle("active", !isHidden); 154 | 155 | // Toggle the visible state 156 | const updates = this.layer.controlled.map(o => { return { _id: o.id, hidden: !isHidden }; }); 157 | return canvas.scene.updateEmbeddedDocuments("Terrain", updates); 158 | } 159 | 160 | async _onToggleLocked(event) { 161 | event.preventDefault(); 162 | 163 | const isLocked = this.object.document.locked; 164 | 165 | event.currentTarget.classList.toggle("active", !isLocked); 166 | 167 | // Toggle the locked state 168 | const updates = this.layer.controlled.map(o => { return { _id: o.id, locked: !isLocked }; }); 169 | return canvas.scene.updateEmbeddedDocuments("Terrain", updates); 170 | 171 | } 172 | 173 | /* -------------------------------------------- */ 174 | 175 | /** @override */ 176 | setPosition() { 177 | $('#hud').append(this.element); 178 | let { x, y, width, height } = this.object.hitArea; 179 | /* 180 | const c = 70; 181 | const p = 0; 182 | const position = { 183 | width: width + (c * 2), // + (p * 2), 184 | height: height + (p * 2), 185 | left: x + this.object.document.x - c - p, 186 | top: y + this.object.document.y - p 187 | }; 188 | this.element.css(position); 189 | */ 190 | const c = 70; 191 | const p = -10; 192 | const position = { 193 | width: width + (c * 2) + (p * 2), 194 | height: height + (p * 2), 195 | left: x + this.object.document.x - c - p, 196 | top: y + this.object.document.y - p 197 | }; 198 | this.element.css(position); 199 | } 200 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important Notice 2 | It is with a heavy heart that I have reached the difficult decision to part ways with Enhanced Terrain Layer. At least for the time being. Despite dedicating a substantial amount of time to make it compatible with v11, all my efforts encountered challenges presented by Foundry. Even when attempting to pivot the module's direction to replicate the Drawings layer, luck was not on my side. Unfortunately, v11 has restricted too many essential resources that this module relies upon, leaving me with limited options for making it work. I truly did my utmost to revive it, but it seems that my endeavors have been in vain. 3 | 4 | Enhanced Terrain Layer holds a special place among my favorite modules, and it pains me to bid it farewell. However, given the incompatibility with what I aim to achieve, v11 leaves me with no other choice. While I hold a glimmer of hope that v12 might bring some positive changes, I cannot say I'm overly optimistic about it. 5 | 6 | Should Terrain Layer emerge victorious in a future poll, I will gladly return and update the module for that release. Until then, I must bid goodnight to a beloved creation, knowing that I gave it my all. 7 | 8 | 9 | # Enhanced Terrain Layer 10 | Adds a Terrain Layer to Foundry that can be used by other modules to calculate difficult terrain. 11 | 12 | ## Installation 13 | Simply use the install module screen within the FoundryVTT setup 14 | 15 | Please note, this module by itself only records the difficult terrain. You'll need to use a ruler that accesses this module to see the changes when dragging a token. 16 | 17 | ## Usage & Current Features 18 | 19 | ### Terrain Layer 20 | 21 | TerrainTool 22 | 23 | TerrainLayerTools 24 | 25 | The various drawing tools work identically to the core Drawings tools. Left drag to start the shape, left-click to add a point, right-click to remove a point, and double-click to close the shape when you're done. You can also add a grid square by double-clicking on the canvas. 26 | 27 | You can then set how difficult that terrain is to move through, and what type of terrain it is, and if it affects ground based tokens or air based tokens. 28 | 29 | Switching to the select tool you can resize an area or reposition the area as you would with most object in Foundry. You can also delete an area by pressing the delete key while the the area is selected. 30 | 31 | The Terrain Layer will also let you assign difficulty to measured templates. You can use this for spells that set difficult terrain. 32 | 33 | And it will also calculate other tokens so that when you're moving through another creatures square it will count as difficult terrain. 34 | 35 | Terrain Layer can either be shown all the time, or hidden until a token is selected and dragged across the screen. This can be changed using the Enable/Disable Terrain button with the other terrain controls. 36 | 37 | You can also set blocks of Terrain to be active or not active, so if the difficult terrain is only temporary or conditional you can control it. 38 | 39 | You can also set the environment that the difficult terrain represents. So if you have water, or rocks, or arctic tundra you can record this information. If you have a system that allows characters to ignore difficult terrain of a certain type, and a ruler that supports checking on this, then it can be added to the calculations. 40 | 41 | You can set the color of the terrain, on an individual basis, a default colour for that environment, a default for the scene, or a general color. 42 | 43 | ### Token Integration 44 | 45 | In the module settings, you can set whether to include live or dead tokens of either friendly tokens, hostile tokens, or both as difficult terrain. 46 | 47 | ## Rulers and measuring distance 48 | 49 | Enhanced Terrain Layer only records difficult terrain, it doesn't do any measuring based on that information. To get drag distances for Tokens I'd recommend using both Terrain Ruler and Drag Ruler. Terrain Ruler will calculate the correct distance based on difficult terrain, and Drag Ruler provides a more visual representation of drag distances. 50 | 51 | ## Coding 52 | ### Requesting terrain cost for coordinates on the map 53 | For those who are developing Rulers based on the Enhanced Terrain Layer, to get access to the difficulty cost of terrain grid you call the cost function. 54 | `canvas.terrain.cost(pts, options);` 55 | pts can be a single object {x: 0, y:0}, or an array of point objects. 56 | options {elevation: 0, reduce:[], tokenId: token.id, token:token} lets the terrain layer know certain things about what you're asking for. 57 | 58 | - elevation: adding a value for elevation will ignore all terrain that is a ground type if the elevation is greater than 0 and ignore any air terrain if the elevation is less than or equal to 0. It will also ignore any tokens that aren't at the same elevation. 59 | - reduce: [{id:'arctic',value:1}] will result in any calculation essentially ignoring arctic terrain. [{id:'arctic',value:'-1',stop:1}] will result in any calculation reducing the difficulty by 1 and stopping at 1. You can also use '+1' to add to the difficulty. stop is an optional parameter. And you can use the id 'token' to have these settings applied when calculating cost through another token's space. 60 | - tokenId - pass in the token id to avoid having the result use the token's own space as difficult terrain. 61 | - token - pass in the token, will use both the id and elevation of that token. passing in elevation:false will result in the the function ignoring the token's elevation. 62 | - calculate - this is how you'd like the cost to be calculated. default is 'maximum', which returns the highest value found while looking through all terrains. you can also pass in 'additive' if you want all costs to be added together. And if neither of those work, you can pass your own function in to make the final calculation `calculate(cost, total, object)` with cost being the current cost and total being the running total so far and object being either the terrain, measure, or token that's caused the difficult terrain. 63 | - verbose - setting this to true will return an object with 'cost' set to the total cost and 'details' as an array of all terrain object found. 64 | 65 | A list of Terrain Environments can be found by calling `canvas.terrain.getEnvironments();` and can be overridden if the environments in your game differ. 66 | 67 | if you need to find the terrain at a certain grid co-ordinate you can call `canvas.terrain.terrainFromGrid(x, y);` or `canvas.terrain.terrainFromPixels(x, y);`. This is useful if you want to determine if the terrain in question is water, and use the swim speed instead of walking speed to calculate speed. 68 | 69 | ### Integrating game system rules 70 | Other modules or game systems systems can indicate to Enhanced Terrain Layer how a given token should interact with the terrain present in a scene and how to handle stacked terrain. That way it's possible to integrate the rules of a given game system into Enhanced Terrain Layer. Enhanced Terrain Layer offers an API to which modules and game systems can register to provide the implementation of the respective rules to Enhanced Terrain Layer. Registering with the API works as follows: 71 | 72 | ```javascript 73 | Hooks.once("enhancedTerrainLayer.ready", (RuleProvider) => { 74 | class ExampleGameSystemRuleProvider extends RuleProvider { 75 | calculateCombinedCost(terrain, options) { 76 | let cost; 77 | // Calculate the cost for this terrain 78 | return cost; 79 | } 80 | } 81 | enhancedTerrainLayer.registerModule("my-module-id", ExampleGameSystemRuleProvider); 82 | }); 83 | ``` 84 | 85 | If you're accessing the Enahanced Terrain Layer API from a game system, use `registerSystem` instead of `registerModule`. The `calculateCombinedCost` needs to implemented in a way that reflects the rules of your system. The function receives two parameters: The first parameter is a list of `TerrainInfo` objects (more on those in the next paragraph) for which the function should calculate the cost. The second parameter is an `options` object that contains all the options that were specified by the caller of `canvas.terrain.cost`. The function shall return a number that indicates a multiplier indicating how much more expensive it is to move through a square of indicated terrain than moving through a square that has no terrain at all. For example if moving thorugh a given terrain should be twice as expensive as moving through no terrain, the function should return 2. If moving through the given terrain should be equally expensive as moving through no terrain, the function should return 1. 86 | 87 | The `TerrainInfo` objects received by this function are wrappers around objects that create terrain and allow unified access to the terrain specific properties. The following properties are offered by `TerrainInfo` objects: 88 | - `cost`: The cost multiplicator that has been specified for this type of terrain 89 | - `environment`: The environment speficied for this terrain 90 | - `obstacle`: The obstacle value specified for this terrain 91 | - `object`: The object that is causing this terrain 92 | 93 | ## Credit 94 | The orginal idea came from the Terrain Layer module. But in the process of re-developing it I realised that none of the original code remained. This is why I branched out into a new module. But I want to give credit to the original author Will Saunders. 95 | 96 | ## Bug Reporting 97 | Please feel free to contact me on discord if you have any questions or concerns. ironmonk88#4075 98 | 99 | ## Support 100 | 101 | If you feel like being generous, stop by my patreon. Not necessary but definitely appreciated. 102 | 103 | ## License 104 | This Foundry VTT module, writen by Ironmonk, is licensed under MIT License 105 | 106 | This work is licensed under Foundry Virtual Tabletop EULA - Limited License Agreement for module development from May 29, 2020. 107 | -------------------------------------------------------------------------------- /classes/terrainshape.js: -------------------------------------------------------------------------------- 1 | import { makeid, log, setting, debug, getflag } from '../terrain-main.js'; 2 | 3 | export class TerrainShape extends DrawingShape { 4 | refresh() { 5 | if (this._destroyed) return; 6 | const doc = this.document; 7 | this.clear(); 8 | 9 | let drawAlpha = (ui.controls.activeControl == 'terrain' ? 1.0 : doc.alpha); 10 | 11 | // Outer Stroke 12 | let sc = Color.from(doc.color || "#FFFFFF"); 13 | let lStyle = new PIXI.LineStyle(); 14 | mergeObject(lStyle, { width: doc.strokeWidth, color: sc, alpha: (setting('draw-border') ? drawAlpha : 0), cap: PIXI.LINE_CAP.ROUND, join: PIXI.LINE_JOIN.ROUND, visible: true }); 15 | this.lineStyle(lStyle); 16 | 17 | // Fill Color or Texture 18 | if (doc.fillType) { 19 | const fc = Color.from(doc.color || "#FFFFFF"); 20 | if ((doc.fillType === CONST.DRAWING_FILL_TYPES.PATTERN)) { 21 | if (this.object.texture) { 22 | let sW = (canvas.dimensions.size / (this.object.texture.width * (setting('terrain-image') == 'diagonal' ? 2 : 1))); 23 | let sH = (canvas.dimensions.size / (this.object.texture.height * (setting('terrain-image') == 'diagonal' ? 2 : 1))); 24 | this.beginTextureFill({ 25 | texture: this.object.texture, 26 | color: fc || 0xFFFFFF, 27 | alpha: drawAlpha, 28 | matrix: new PIXI.Matrix().scale(sW, sH) 29 | }); 30 | } 31 | } else this.beginFill(fc, doc.fillAlpha); 32 | } 33 | 34 | // Draw the shape 35 | switch (doc.shape.type) { 36 | case Drawing.SHAPE_TYPES.RECTANGLE: 37 | this.#drawRectangle(); 38 | break; 39 | case Drawing.SHAPE_TYPES.ELLIPSE: 40 | this.#drawEllipse(); 41 | break; 42 | case Drawing.SHAPE_TYPES.POLYGON: 43 | if (this.document.bezierFactor) this.#drawFreehand(); 44 | else this.#drawPolygon(); 45 | break; 46 | } 47 | 48 | // Conclude fills 49 | this.lineStyle(0x000000, 0.0).closePath().endFill(); 50 | 51 | // Set the drawing position 52 | this.setPosition(); 53 | } 54 | 55 | get _pixishape() { 56 | let { x, y, width, height, shape } = this.document; 57 | let result; 58 | switch (shape.type) { 59 | case Drawing.SHAPE_TYPES.RECTANGLE: 60 | result = new PIXI.Rectangle(0, 0, width, height); 61 | break; 62 | case Drawing.SHAPE_TYPES.ELLIPSE: 63 | result = new PIXI.Ellipse(width / 2, height / 2, Math.max(Math.abs(width / 2), 0), Math.max(Math.abs(height / 2), 0)); 64 | break; 65 | case Drawing.SHAPE_TYPES.POLYGON: 66 | result = new PIXI.Polygon(shape.points); 67 | break; 68 | } 69 | return result; 70 | } 71 | 72 | contains(x, y) { 73 | let shape = this._pixishape; 74 | if (shape) 75 | return shape.contains(x, y); 76 | return false; 77 | } 78 | 79 | /* -------------------------------------------- */ 80 | 81 | /** 82 | * Draw rectangular shapes. 83 | * @private 84 | */ 85 | #drawRectangle() { 86 | const { shape, strokeWidth } = this.document; 87 | const hs = strokeWidth / 2; 88 | 89 | if (this.document.hidden) { 90 | this.drawDashedPolygon([0, 0, shape.width, 0, shape.width, shape.height, 0, shape.height,0, 0], 0, 0, 0, strokeWidth * 2, strokeWidth * 3, 0); 91 | this._lineStyle.width = 0; 92 | } 93 | this.drawRect(hs, hs, shape.width - (2 * hs), shape.height - (2 * hs)); 94 | 95 | this._lineStyle.width = strokeWidth; 96 | } 97 | 98 | /* -------------------------------------------- */ 99 | 100 | /** 101 | * Draw ellipsoid shapes. 102 | * @private 103 | */ 104 | #drawEllipse() { 105 | const { shape, strokeWidth } = this.document; 106 | const hw = shape.width / 2; 107 | const hh = shape.height / 2; 108 | const hs = strokeWidth / 2; 109 | const width = Math.max(Math.abs(hw) - hs, 0); 110 | const height = Math.max(Math.abs(hh) - hs, 0); 111 | this.drawEllipse(hw, hh, width, height); 112 | } 113 | 114 | /* -------------------------------------------- */ 115 | 116 | /** 117 | * Draw polygonal shapes. 118 | * @private 119 | */ 120 | #drawPolygon() { 121 | const { shape, strokeWidth } = this.document; 122 | const points = shape.points; 123 | if (points.length < 4) return; 124 | else if (points.length === 4) this.endFill(); 125 | 126 | if (this.document.hidden) { 127 | this.drawDashedPolygon(points, 0, 0, 0, strokeWidth * 2, strokeWidth * 3, 0); 128 | this._lineStyle.width = 0; 129 | } 130 | this.drawPolygon(points); 131 | this._lineStyle.width = strokeWidth; 132 | } 133 | 134 | /* -------------------------------------------- */ 135 | 136 | /** 137 | * Draw freehand shapes with bezier spline smoothing. 138 | * @private 139 | */ 140 | #drawFreehand() { 141 | const { bezierFactor, fillType, shape } = this.document; 142 | 143 | // Get drawing points 144 | let points = shape.points; 145 | 146 | // Draw simple polygons if only 2 points are present 147 | if (points.length <= 4) return this.#drawPolygon(); 148 | 149 | // Set initial conditions 150 | const factor = bezierFactor ?? 0.5; 151 | let previous = first; 152 | let point = points.slice(2, 4); 153 | points = points.concat(last); // Repeat the final point so the bezier control points know how to finish 154 | let cp0 = this.#getBezierControlPoints(factor, last, previous, point).nextCP; 155 | let cp1; 156 | let nextCP; 157 | 158 | // Begin iteration 159 | this.moveTo(first[0], first[1]); 160 | for (let i = 4; i < points.length - 1; i += 2) { 161 | const next = [points[i], points[i + 1]]; 162 | if (next) { 163 | let bp = this.#getBezierControlPoints(factor, previous, point, next); 164 | cp1 = bp.cp1; 165 | nextCP = bp.nextCP; 166 | } 167 | 168 | // First point 169 | if ((i === 4) && !isClosed) { 170 | this.quadraticCurveTo(cp1.x, cp1.y, point[0], point[1]); 171 | } 172 | 173 | // Last Point 174 | else if ((i === points.length - 2) && !isClosed) { 175 | this.quadraticCurveTo(cp0.x, cp0.y, point[0], point[1]); 176 | } 177 | 178 | // Bezier points 179 | else { 180 | this.bezierCurveTo(cp0.x, cp0.y, cp1.x, cp1.y, point[0], point[1]); 181 | } 182 | 183 | // Increment 184 | previous = point; 185 | point = next; 186 | cp0 = nextCP; 187 | } 188 | } 189 | 190 | /* -------------------------------------------- */ 191 | 192 | /** 193 | * Attribution: The equations for how to calculate the bezier control points are derived from Rob Spencer's article: 194 | * http://scaledinnovation.com/analytics/splines/aboutSplines.html 195 | * @param {number} factor The smoothing factor 196 | * @param {number[]} previous The prior point 197 | * @param {number[]} point The current point 198 | * @param {number[]} next The next point 199 | * @returns {{cp1: Point, nextCP: Point}} The bezier control points 200 | * @private 201 | */ 202 | #getBezierControlPoints(factor, previous, point, next) { 203 | 204 | // Calculate distance vectors 205 | const vector = { x: next[0] - previous[0], y: next[1] - previous[1] }; 206 | const preDistance = Math.hypot(point[0] - previous[0], point[1] - previous[1]); 207 | const postDistance = Math.hypot(next[0] - point[0], next[1] - point[1]); 208 | const distance = preDistance + postDistance; 209 | 210 | // Compute control point locations 211 | const cp0d = distance === 0 ? 0 : factor * (preDistance / distance); 212 | const cp1d = distance === 0 ? 0 : factor * (postDistance / distance); 213 | 214 | // Return points 215 | return { 216 | cp1: { 217 | x: point[0] - (vector.x * cp0d), 218 | y: point[1] - (vector.y * cp0d) 219 | }, 220 | nextCP: { 221 | x: point[0] + (vector.x * cp1d), 222 | y: point[1] + (vector.y * cp1d) 223 | } 224 | }; 225 | } 226 | 227 | drawDashedPolygon(points, x, y, rotation, dash, gap, offsetPercentage) { 228 | var i; 229 | var p1; 230 | var p2; 231 | var dashLeft = 0; 232 | var gapLeft = 0; 233 | if (offsetPercentage > 0) { 234 | var progressOffset = (dash + gap) * offsetPercentage; 235 | if (progressOffset < dash) dashLeft = dash - progressOffset; 236 | else gapLeft = gap - (progressOffset - dash); 237 | } 238 | var rotatedPolygons = []; 239 | for (i = 0; i < points.length - 1; i += 2) { 240 | var p = { x: points[i], y: points[i + 1] }; 241 | var cosAngle = Math.cos(rotation); 242 | var sinAngle = Math.sin(rotation); 243 | var dx = p.x; 244 | var dy = p.y; 245 | p.x = (dx * cosAngle - dy * sinAngle); 246 | p.y = (dx * sinAngle + dy * cosAngle); 247 | rotatedPolygons.push(p); 248 | } 249 | for (i = 0; i < rotatedPolygons.length; i++) { 250 | p1 = rotatedPolygons[i]; 251 | if (i == rotatedPolygons.length - 1) p2 = rotatedPolygons[0]; 252 | else p2 = rotatedPolygons[i + 1]; 253 | var dx = p2.x - p1.x; 254 | var dy = p2.y - p1.y; 255 | if (dx == 0 && dy == 0) 256 | continue; 257 | var len = Math.sqrt(dx * dx + dy * dy); 258 | var normal = { x: dx / len, y: dy / len }; 259 | var progressOnLine = 0; 260 | let mx = x + p1.x + gapLeft * normal.x; 261 | let my = y + p1.y + gapLeft * normal.y; 262 | this.moveTo(mx, my); 263 | while (progressOnLine <= len) { 264 | progressOnLine += gapLeft; 265 | if (dashLeft > 0) progressOnLine += dashLeft; 266 | else progressOnLine += dash; 267 | if (progressOnLine > len) { 268 | dashLeft = progressOnLine - len; 269 | progressOnLine = len; 270 | } else { 271 | dashLeft = 0; 272 | } 273 | let lx = x + p1.x + progressOnLine * normal.x; 274 | let ly = y + p1.y + progressOnLine * normal.y; 275 | this.lineTo(lx, ly); 276 | progressOnLine += gap; 277 | if (progressOnLine > len && dashLeft == 0) { 278 | gapLeft = progressOnLine - len; 279 | //console.log(progressOnLine, len, gap); 280 | } else { 281 | gapLeft = 0; 282 | let mx = x + p1.x + progressOnLine * normal.x; 283 | let my = y + p1.y + progressOnLine * normal.y; 284 | this.moveTo(mx, my); 285 | } 286 | } 287 | } 288 | } 289 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 10.09 2 | 3 | Fixed issue with terrain not allowing "transparent" for a colour. 4 | 5 | Fixed issue with getting terrain if there's no terrain on the scene. 6 | 7 | # Version 10.8 8 | 9 | Fixed issue with trying to access pixishape when it doesn't exist 10 | 11 | Fixed issue when creating new terrain that it would only appear on a refresh. 12 | 13 | # Version 10.7 14 | 15 | Fixed issue with holding shift to snap to grid 16 | 17 | Fixed issue with negative depth. 18 | 19 | Fixed issue getting access to the terrain from the canvas scene 20 | 21 | Added webbing as an obstacle 22 | 23 | Fixed issue with tokens causing difficult terrain. 24 | 25 | Swapped setting up terrain to initialization rather than on draw. 26 | 27 | # Version 10.6 28 | 29 | Fixed issue refreshing the terrain shape if the shape doesn't exist 30 | 31 | Added opacity as a default value when creating terrain. 32 | 33 | Fixed issue when treying to use the get function on the terrain layer to get terrain information 34 | 35 | Fixed issue with getting terrain info from measured templates 36 | 37 | Fixed issues with getting terrain from grid and pixels, with the missing options property 38 | 39 | Fixed issues with double-clicking to create a hex terrain. 40 | 41 | Added the option to have the measured template created from a spell template, get the colour from the default environment colour. 42 | 43 | Fixed the terrain config interface so that the default values are used as placeholders 44 | 45 | Fixed issue with Midi Qol settings being inserted between the terrain header and settings. 46 | 47 | Fixed issues with the terrain tab not shrinking to fit the contents when Token Magic FX is enabled. 48 | 49 | Add a Rules Provider API, so systems and modules can register how they want difficult terrain to be calculated. Thank you Stäbchenfisch!! 50 | 51 | # Version 10.5 52 | 53 | Fixed an issue with determining when a token causes difficult terrain. 54 | 55 | # Version 10.4 56 | 57 | Fixed issue when restoring a Terrain after it's been deleted when using Ctrl-Z. 58 | 59 | Fixed issue finding data when the Measured Template has no flags set. 60 | 61 | Addeed the option to set if you only want hostile creatures to cause difficult terrain. 62 | 63 | Updated the API for calculating terrain cost. 64 | 65 | Fixed issue with drawing shapes towards the upper left. 66 | 67 | Fixed issue getting the correct shape data. 68 | 69 | # Version 10.3 70 | 71 | Fixed a spelling mistake in one of my fixes. 72 | 73 | # Version 10.2 74 | 75 | Fixed issues with checking if the terrain contains a point. 76 | 77 | # Version 10.1 78 | 79 | Fixed issue with migrating to v10. 80 | 81 | Fixed detecting if a token is defeated. 82 | 83 | # Version 1.0.43 84 | 85 | Updates to support v10 86 | 87 | # Version 1.0.42 88 | 89 | Fixed icon sizing for the HUD Terrain cost. 90 | 91 | Fixed issue setting settings when the canvas terrain layer doesn't exist yet. 92 | 93 | Fixed issues with adding the Terrain tab to various config dialogs. 94 | 95 | # Version 1.0.41 96 | 97 | Fixed issue where editing the colours in the settings was erasing all the colour information. 98 | 99 | Added the option to set a terrain's individual opacity. 100 | 101 | Added the option to get a list of all available terrains. Thank you Stäbchenfisch the code looks amazing. Technically nothing should change for the interface, but this improvement can help Rulers be more effecient. 102 | 103 | Fixed an issue with how Enhanced Terrain Layer was determining if a token was dead. 104 | 105 | Added the option for Rulers to pass in a function to determine if a Token is considered dead or not. This will allow system specific ruler to change how tokens are considered "dead". 106 | 107 | Fixed some styling with the terrain control buttons when the Scene has set the default terrain cost. Made it more apparent what's happening, and that the buttons are no longer clickable. 108 | 109 | Added key binding to toggle terrain showing. Used Alt-T to switch between states. 110 | 111 | Changed the interface for spells/items so that terrain controls will only appear if the spell requires a measured template. As terrain details aren't needed otherwise. 112 | 113 | Changed the measured template config screen to have tabs, with the terrain onformation on a separate tab. This should make the dialog a little less cluttered and add more visibility into the terrain controls. 114 | 115 | # Version 1.0.40 116 | 117 | Improved efficiency by calling the flag data directly instead of through the getFlag function, thank you Stabchenfisch 118 | 119 | Added the option to set the background to transparent, in case you want difficult terrain to cover the entire map. 120 | 121 | Fixed issues with the placement of the terrain controls 122 | 123 | Fixed issue with clearing all the terrain objects from a Scene. 124 | 125 | Improved the effeciency of the function that calculates the cost of movement. 126 | 127 | # Version 1.0.39 128 | 129 | Fixing an issue rendering canvas.terrain.toolbar when it might not be there. 130 | 131 | # Version 1.0.38 132 | 133 | Fixed issue with the default values for scenes. 134 | 135 | Updated the terrain controls so that if a scene had a default it would show that it was being overridden and what the value was. 136 | 137 | Added API to get the elevation from a set of points. 138 | 139 | # Version 1.0.37 140 | 141 | Changed from using min/max to elevation and depth 142 | 143 | Added the option to set custom terrain cost. So you can set a minimum and a maximum range, and set the value within that range. 144 | 145 | Fixed issue with newly created terrain, making a change, then hitting undo. Instead of undoing the move, it was undoing the create. 146 | 147 | Allowed tokens causing difficult terrain and dead tokens causing difficult terrain to work idependantly. 148 | 149 | Allow terrain height to use decimal numbers. 150 | 151 | Fixed issue where Enhanced Terrain layer was resetting the scroll position after a change. 152 | 153 | # Version 1.0.36 154 | 155 | Well... this is embarassing. I guess in the effort to get modules up to date, I forgot to include a template. Should be fixed now. 156 | 157 | # Version 1.0.35 158 | 159 | Adding v9 support 160 | 161 | # Version 1.0.34 162 | 163 | Fixing issues with wrapping some functions. 164 | 165 | # Version 1.0.33 166 | 167 | Fixed issue with terrain being created that doesn't have the minimum number of points. 168 | 169 | Fixed issue with dashedPolygon. 170 | 171 | Added more support for consistency with other layers. 172 | 173 | Added opacity to individual scenes 174 | 175 | Fixed issue with Terrain HUD not updating the cost when using the up and down arrows 176 | 177 | Added libWrapper support 178 | 179 | Added German translations, thank you BlueSkyBlackBird! 180 | 181 | # Version 1.0.32 182 | 183 | Added support for Levels module 184 | 185 | Fixing issue with relative URLs, thank you vexofp 186 | 187 | Added Korean support, thank you drdwing 188 | 189 | # Version 1.0.31 190 | Added option to ignore snap to grid when calculating cost 191 | 192 | Added option to not show border 193 | 194 | Added option to change the picture shown 195 | 196 | # Version 1.0.30 197 | Split terrainAt into two functions terrainFromPixel and terrainFromGrid to make it more understandable. 198 | 199 | Fixed the text in the terrain config dialog to reflect that it's the active state and not the hidden state that's changing. 200 | 201 | Changes to the README file to correct some errors, Thank you caewok! 202 | 203 | Added setting to change dead token to not count as difficult terrain. 204 | 205 | # Version 1.0.29 206 | Fixed issue with the config dialog 207 | 208 | Added option to set the color for individual terrain 209 | 210 | # Version 1.0.28 211 | Support for Foundry 0.8.x 212 | 213 | # Version 1.0.27 214 | Fixing issue when attempting to move a terrain, but not really moving it would cause it to disappear. 215 | 216 | Added option to not show inactive terrain for the GM unless on the terrain layer. 217 | 218 | Changed some of the language to better describe what toggle switches do. 219 | 220 | Added option to create default terrain by double clicking 221 | 222 | # Version 1.0.26 223 | Fixing issue with determining token as difficult terrain on a gridless maps 224 | 225 | Fixing issue with the Terrain Config not updating the terrain to clients 226 | 227 | Fixing weird issue where deactivating the terrain was causing the objects array not to be populated. 228 | 229 | # Version 1.0.25 230 | Fixing issue with wrapping the AbilityTemplate.fromItem function 231 | 232 | Adding icons for Urban and Furniture environments 233 | 234 | Adding a Hook for Terrain Environments 235 | 236 | Adding support for gridless maps 237 | 238 | Better error handling for calculate function, to confirm options passed into the function 239 | 240 | Changed terraintype to terrain height to make it a little more transparent as to what's being calculated. And added extra controls on the terrain HUD to display the height of the terrain. 241 | 242 | Updated the cost function to be more effecient 243 | 244 | # Version 1.0.24 245 | Fixing an issue with Enhanced Terrain Layer not finding a place to put the additional controls on an item. 246 | 247 | Adding Urban environment, and Furniture obstacle. 248 | 249 | # Version 1.0.23 250 | Fixing an issue with changing environment back to blank affecting the icon. 251 | 252 | Merging environment and obstacles so that it's just one list. But added the option to set an item in the environment list as being an obstacle, so they can still be shown in separate lists. This fixes issues when using the option to use obstacles with environment. Setting an obstacle without an environment caused issues. 253 | 254 | Adding a side menu to change the environment type from the terrain HUD. 255 | 256 | Added integration with spells, so you can set the difficulty, environment, and terrain type of the spell and it will translate to the measured template produced. Only works for DnD5e right now, but if it gets added to more systems then I'll update it. 257 | 258 | Fixed the image path names so that it wasn't hard coded to the enhanced terrain layer folder. And overriding the environment will now let you override the image used. 259 | 260 | # Version 1.0.21 261 | Added icons for the different environments 262 | 263 | Added the option to set different colours for each environment, aswell as a default color for the scene and a global default colour. 264 | 265 | # Version 1.0.20 266 | Added obstacles 267 | 268 | Added option to combine obstacles with the environment, or to use them independantly. 269 | 270 | # Version 1.0.18 271 | Add setting to not show terrain when dragging a token 272 | 273 | The original Terrain Layer does not play nice with the Enhanced Terrain Layer. Added code to make sure they can exist at the same time. 274 | 275 | Fixed issue with opacity 276 | 277 | Added different border for hidden terrain instead of setting the opacity 278 | 279 | # Version 1.0.17 280 | Added function to try and copy old data from TerrainLayer 281 | 282 | Changed the way environment and terraintype are handled to better override 283 | 284 | Added verbose option 285 | 286 | Added reduce functionality 287 | 288 | Added refresh when show text is changed 289 | 290 | # Version 1.0.16 291 | Fixed an issue with ignoring environment 292 | 293 | # Version 1.0.15 294 | Fixed and issue with elevation 295 | 296 | Fixed an issue with terrainAt 297 | 298 | # Version 1.0.14 299 | update the code so that tokens don't think of themselves as difficult terrain. This will require the ruler to pass in the token that is moving. 300 | 301 | Updated the english settings. Missed some text. 302 | 303 | Fixed an issue where environment type wasn't showing. 304 | 305 | # Version 1.0.12 306 | costGrid wasn't updating when you cahnged scenes. It now does. 307 | 308 | # Version 1.0.11 309 | Initial Release. 310 | -------------------------------------------------------------------------------- /classes/terraindocument.js: -------------------------------------------------------------------------------- 1 | import { makeid, log, error, i18n, setting, getflag } from '../terrain-main.js'; 2 | import { Terrain } from './terrain.js'; 3 | 4 | /* 5 | export class TerrainData extends DocumentData { 6 | static defineSchema() { 7 | return { 8 | _id: fields.DOCUMENT_ID, 9 | x: fields.NUMERIC_FIELD, 10 | y: fields.NUMERIC_FIELD, 11 | width: fields.NUMERIC_FIELD, 12 | height: fields.NUMERIC_FIELD, 13 | locked: fields.BOOLEAN_FIELD, 14 | hidden: fields.BOOLEAN_FIELD, 15 | points: fields.OBJECT_FIELD, 16 | multiple: fields.NUMERIC_FIELD, 17 | elevation: fields.NUMERIC_FIELD, 18 | depth: fields.NUMERIC_FIELD, 19 | opacity: fields.NUMERIC_FIELD, 20 | drawcolor: fields.STRING_FIELD, 21 | environment: fields.STRING_FIELD, 22 | obstacle: fields.STRING_FIELD, 23 | flags: fields.OBJECT_FIELD 24 | } 25 | } 26 | } 27 | */ 28 | 29 | export class BaseTerrain extends foundry.abstract.Document { 30 | /** @inheritdoc */ 31 | static metadata = Object.freeze(mergeObject(super.metadata, { 32 | name: "Terrain", 33 | collection: "terrain", 34 | label: "EnhancedTerrainLayer.terrain", 35 | isEmbedded: true, 36 | permissions: { 37 | create: "TEMPLATE_CREATE", 38 | update: this.#canModify, 39 | delete: this.#canModify 40 | } 41 | }, { inplace: false })); 42 | 43 | static defineSchema() { 44 | return { 45 | _id: new foundry.data.fields.DocumentIdField(), 46 | //author: new foundry.data.fields.ForeignDocumentField(BaseUser, { nullable: false, initial: () => game.user?.id }), 47 | shape: new foundry.data.fields.EmbeddedDataField(foundry.data.ShapeData), 48 | x: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "XCoord" }), 49 | y: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "YCoord" }), 50 | hidden: new foundry.data.fields.BooleanField(), 51 | locked: new foundry.data.fields.BooleanField(), 52 | multiple: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 2, label: "EnhancedTerrainLayer.Multiple" }), 53 | elevation: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "EnhancedTerrainLayer.Elevation" }), 54 | depth: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0,label: "EnhancedTerrainLayer.Depth" }), 55 | opacity: new foundry.data.fields.AlphaField({ required: true, nullable: false, initial: 1, label: "EnhancedTerrainLayer.Opacity" }), 56 | drawcolor: new foundry.data.fields.StringField({ label: "EnhancedTerrainLayer.DrawColor" }), 57 | environment: new foundry.data.fields.StringField({ label: "EnhancedTerrainLayer.Environment" }), 58 | obstacle: new foundry.data.fields.StringField({ label: "EnhancedTerrainLayer.Obstacle" }), 59 | flags: new foundry.data.fields.ObjectField() 60 | } 61 | } 62 | 63 | _validateModel(data) { 64 | // Must have at least three points in the shape 65 | // (!(hasText || hasFill || hasLine)) { 66 | // throw new Error("Drawings must have visible text, a visible fill, or a visible line"); 67 | // 68 | } 69 | 70 | /** 71 | * Is a user able to update or delete an existing Drawing document?? 72 | * @protected 73 | */ 74 | static #canModify(user, doc, data) { 75 | if (user.isGM) return true; // GM users can do anything 76 | return false; 77 | } 78 | 79 | testUserPermission(user, permission, { exact = false } = {}) { 80 | return user.isGM; 81 | } 82 | 83 | static migrateData(data) { 84 | /** 85 | * V10 migration to ShapeData model 86 | * @deprecated since v10 87 | */ 88 | if (getProperty(data, "shape.type") == undefined) 89 | setProperty(data, "shape.type", "p"); 90 | this._addDataFieldMigration(data, "width", "shape.width"); 91 | this._addDataFieldMigration(data, "height", "shape.height"); 92 | this._addDataFieldMigration(data, "points", "shape.points", d => d.points.flat()); 93 | return super.migrateData(data); 94 | } 95 | 96 | static shimData(data, options) { 97 | this._addDataFieldShim(data, "width", "shape.width", { since: 10, until: 12 }); 98 | this._addDataFieldShim(data, "height", "shape.height", { since: 10, until: 12 }); 99 | this._addDataFieldShim(data, "points", "shape.points", { since: 10, until: 12 }); 100 | return super.shimData(data, options); 101 | } 102 | } 103 | 104 | export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { 105 | 106 | /* -------------------------------------------- */ 107 | /* Properties */ 108 | /* -------------------------------------------- */ 109 | #envobj = null; 110 | 111 | get layer() { 112 | return canvas.terrain; 113 | } 114 | 115 | get isEmbedded() { 116 | return true; 117 | } 118 | 119 | get fillType() { 120 | return CONST.DRAWING_FILL_TYPES.PATTERN; 121 | } 122 | 123 | get color() { 124 | return this.drawcolor || setting('environment-color')[this.environment] || getflag(canvas.scene, 'defaultcolor') || setting('environment-color')['_default'] || "#FFFFFF"; 125 | } 126 | 127 | get alpha() { 128 | return this.opacity ?? getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1; 129 | } 130 | 131 | get rotation() { 132 | return 0; 133 | } 134 | 135 | get bezierFactor() { 136 | return 0; 137 | } 138 | 139 | get strokeWidth() { 140 | return canvas.dimensions.size / 20; 141 | } 142 | 143 | static text(val) { 144 | return String.fromCharCode(215) + (val == 0.5 ? String.fromCharCode(189) : val); 145 | } 146 | 147 | get text() { 148 | let mult = Math.clamped(this.multiple, setting('minimum-cost'), setting('maximum-cost')); 149 | return this.constructor.text(mult); 150 | } 151 | 152 | get texture() { 153 | let image = setting('terrain-image'); 154 | 155 | if (image == "clear") 156 | return null; 157 | 158 | let mult = Math.clamped(this.multiple, setting('minimum-cost'), setting('maximum-cost')); 159 | if (mult > 4) 160 | mult = 4; 161 | if (mult >= 1) 162 | mult = parseInt(mult); 163 | if (mult < 1) 164 | mult = 0.5; 165 | 166 | if (mult == 1) 167 | return null; 168 | 169 | return `modules/enhanced-terrain-layer/img/${image}${mult}x.svg`; 170 | } 171 | 172 | get environmentObject() { 173 | if (this.#envobj?.id == this.environment) 174 | return this.#envobj; 175 | this.#envobj = canvas.terrain.getEnvironments().find(e => e.id == this.environment); 176 | return this.#envobj; 177 | } 178 | 179 | get width() { 180 | return this.shape.width; 181 | } 182 | 183 | get height() { 184 | return this.shape.height; 185 | } 186 | 187 | get top() { 188 | return (this.depth < 0 ? this.elevation : this.elevation + this.depth); 189 | } 190 | 191 | get bottom() { 192 | return (this.depth < 0 ? this.elevation + this.depth: this.elevation); 193 | } 194 | 195 | /* -------------------------------------------- */ 196 | 197 | /** 198 | * A flag for whether the current User has full ownership over the Drawing document. 199 | * @type {boolean} 200 | */ 201 | get isOwner() { 202 | return game.user.isGM || (this.data.author === game.user.id); 203 | } 204 | 205 | static async createDocuments(data = [], context = {}) { 206 | const { parent, pack, ...options } = context; 207 | 208 | let originals = []; 209 | let created = []; 210 | for (let terrain of data) { 211 | if (terrain instanceof TerrainDocument) 212 | terrain = terrain.toObject(); 213 | //update this object 214 | // mergeObject(terrainDoc.data, data); 215 | terrain._id = terrain._id || makeid(); 216 | 217 | //don't create a terrain that has less than 3 points 218 | if ((terrain.shape.type == CONST.DRAWING_TYPES.POLYGON || terrain.shape.type == CONST.DRAWING_TYPES.FREEHAND) && terrain.shape.points.length < 3) 219 | continue; 220 | 221 | let document = new TerrainDocument(terrain, context); 222 | 223 | /* 224 | if (terrain.update) 225 | terrain.update(terrain); 226 | 227 | if (terrain.document == undefined) { 228 | let document = new TerrainDocument(terrain, { parent: canvas.scene }); 229 | terrain.document = document; 230 | } 231 | */ 232 | 233 | //update the data and save it to the scene 234 | if (game.user.isGM) { 235 | let key = `flags.enhanced-terrain-layer.terrain${document._id}`; 236 | await canvas.scene.update({ [key]: document.toObject() }, { diff: false }); 237 | 238 | originals.push(terrain); 239 | } 240 | 241 | //add it to the terrain set 242 | canvas.scene.terrain.set(document._id, document); 243 | 244 | //if the multiple has changed then update the image 245 | if (document._object != undefined) 246 | document.object.draw(); 247 | else { 248 | document.object?._onCreate(terrain, options, game.user.id); 249 | //document.object.draw(); 250 | } 251 | 252 | created.push(document); 253 | } 254 | 255 | if(originals.length) 256 | canvas.terrain.storeHistory("create", originals); 257 | 258 | if (game.user.isGM) 259 | game.socket.emit('module.enhanced-terrain-layer', { action: '_createTerrain', arguments: [created] }); 260 | 261 | await this._onCreateDocuments(created, context); 262 | return created; 263 | } 264 | 265 | static async updateDocuments(updates = [], context = {}) { 266 | const { parent, pack, ...options } = context; 267 | /* 268 | const updated = await this.database.update(this.implementation, { updates, options, parent, pack }); 269 | await this._onUpdateDocuments(updated, context); 270 | return updated;*/ 271 | 272 | let originals = []; 273 | let updated = []; 274 | for (let update of updates) { 275 | let document = canvas.scene.terrain.get(update._id); 276 | 277 | if (game.user.isGM) { 278 | originals.push(document.toObject(false)); 279 | } 280 | 281 | delete update.submit; 282 | //update this object 283 | //mergeObject(this.data, data); 284 | //let changes = await terrain.update(update, { diff: (options.diff !== undefined ? options.diff : true)}); 285 | let changes = foundry.utils.diffObject(document.toObject(false), update, { deletionKeys: true }); 286 | if (foundry.utils.isEmpty(changes)) continue; 287 | 288 | if (document.object._original) { 289 | mergeObject(document.object._original, changes); 290 | } else 291 | document.alter(changes); 292 | 293 | //update the data and save it to the scene 294 | if (game.user.isGM) { 295 | let objectdata = duplicate(getflag(document.parent, `terrain${document.id}`)); 296 | mergeObject(objectdata, changes); 297 | let key = `flags.enhanced-terrain-layer.terrain${document.id}`; 298 | await document.parent.update({ [key]: objectdata }, { diff: false }); 299 | 300 | //document.updateSource(changes); 301 | } 302 | 303 | //if (changes.environment != undefined) 304 | // this.updateEnvironment(); 305 | 306 | //if the multiple has changed then update the image 307 | if (changes.multiple != undefined || changes.environment != undefined) { 308 | document.object.draw(); 309 | } else 310 | document.object.refresh(); 311 | 312 | updated.push(document); 313 | } 314 | 315 | if (originals.length && !options.isUndo) 316 | canvas.terrain.storeHistory("update", originals); 317 | 318 | if (game.user.isGM) 319 | game.socket.emit('module.enhanced-terrain-layer', { action: '_updateTerrain', arguments: [updated] }); 320 | 321 | await this._onUpdateDocuments(updated, context); 322 | return updated; 323 | } 324 | 325 | static async deleteDocuments(ids = [], context = {}) { 326 | const { parent, pack, ...options } = context; 327 | 328 | let updates = []; 329 | let originals = []; 330 | let deleted = []; 331 | const deleteIds = options.deleteAll ? canvas.scene.terrain.keys() : ids; 332 | for (let id of deleteIds) { 333 | let terrain = canvas.scene.terrain.find(t => t.id == id); 334 | 335 | if (terrain == undefined) 336 | continue; 337 | 338 | //remove this object from the terrain list 339 | canvas.scene.terrain.delete(id); 340 | 341 | if (game.user.isGM) { 342 | let key = `flags.enhanced-terrain-layer.-=terrain${id}`; 343 | updates[key] = null; 344 | 345 | if (!options.isUndo) 346 | originals.push(terrain); 347 | } 348 | 349 | //remove the PIXI object 350 | canvas.primary.removeTerrain(terrain); 351 | //canvas.terrain.objects.removeChild(terrain.object); 352 | delete canvas.terrain.controlled[id]; 353 | terrain.object.destroy({ children: true }); 354 | 355 | deleted.push(terrain); 356 | } 357 | 358 | if (!options.isUndo && originals.length) 359 | canvas.terrain.storeHistory("delete", originals); 360 | 361 | if (game.user.isGM) 362 | game.socket.emit('module.enhanced-terrain-layer', { action: '_deleteTerrain', arguments: [ids] }); 363 | 364 | //remove the deleted items from the scene 365 | if(Object.keys(updates).length) 366 | canvas.scene.update(updates); 367 | 368 | await this._onDeleteDocuments(deleted, context); 369 | return deleted; 370 | } 371 | 372 | //static async create(data, options) { 373 | 374 | /* 375 | data._id = data._id || makeid(); 376 | 377 | let userId = game.user._id; 378 | 379 | data = data instanceof Array ? data : [data]; 380 | for (let d of data) { 381 | const allowed = Hooks.call(`preCreateTerrain`, this, d, options, userId); 382 | if (allowed === false) { 383 | debug(`Terrain creation prevented by preCreate hook`); 384 | return null; 385 | } 386 | } 387 | 388 | let embedded = data.map(d => { 389 | let object = canvas.terrain.createObject(d); 390 | object._onCreate(options, userId); 391 | canvas["#scene"].terrain.push(d); 392 | canvas.scene.setFlag('enhanced-terrain-layer', 'terrain' + d._id, d); 393 | Hooks.callAll(`createTerrain`, canvas.terrain, d, options, userId); 394 | return d; 395 | }); 396 | 397 | return data.length === 1 ? embedded[0] : embedded; 398 | */ 399 | //} 400 | 401 | //async update(data = {}, context = {}) { 402 | //update this object 403 | /* 404 | mergeObject(this, data); 405 | if (options.save === true) { 406 | //update the data and save it to the scene 407 | let objectdata = duplicate(getflag(canvas.scene, `terrain${this.id}`)); 408 | mergeObject(objectdata, this.document.toObject()); 409 | //let updates = {}; 410 | //updates['flags.enhanced-terrain-layer.terrain' + this.document._id + '.multiple'] = data.multiple; 411 | let key = `flags.enhanced-terrain-layer.terrain${this.document._id}`; 412 | await canvas.scene.update({ [key]: objectdata }, { diff: false }); 413 | //canvas.terrain._costGrid = null; 414 | } 415 | 416 | if (data.environment != undefined) 417 | this.updateEnvironment(); 418 | //await canvas.scene.setFlag("enhanced-terrain-layer", "terrain" + this.document._id, objectdata, {diff: false}); 419 | //if the multiple has changed then update the image 420 | if (data.multiple != undefined || data.environment != undefined) { 421 | this.object.draw(); 422 | } else 423 | this.object.refresh(); 424 | return this; 425 | */ 426 | //} 427 | 428 | async delete(options) { 429 | let key = `flags.enhanced-terrain-layer.-=terrain${this.document._id}`; 430 | await canvas.scene.update({ [key]: null }, { diff: false }); 431 | return this; 432 | } 433 | 434 | alter(changes) { 435 | // 'cause I havn't found an easy way to mass update a document. Pretty sure Foundry does it somewhere.... but until I find it. 436 | for (let [k, v] of Object.entries(changes)) { 437 | if (k == "shape") { 438 | for (let [s_k, s_v] of Object.entries(v)) { 439 | this.shape[s_k] = s_v; 440 | } 441 | } else 442 | this[k] = v; 443 | } 444 | mergeObject(this._source, changes); 445 | } 446 | 447 | //updateEnvironment() { 448 | //this.environment = canvas.terrain.getEnvironments().find(e => e.id == this.environment); 449 | //if (this.environment == undefined && !setting('use-obstacles')) 450 | // this.environment = canvas.terrain.getObstacles().find(e => e.id == this.document.environment); 451 | //} 452 | } 453 | -------------------------------------------------------------------------------- /terrain-main.js: -------------------------------------------------------------------------------- 1 | import { TerrainLayer } from './classes/terrainlayer.js'; 2 | import { TerrainHUD } from './classes/terrainhud.js'; 3 | import { TerrainConfig } from './classes/terrainconfig.js'; 4 | import { Terrain } from './classes/terrain.js'; 5 | import { TerrainDocument } from './classes/terraindocument.js'; 6 | import { TerrainShape } from './classes/terrainshape.js'; 7 | import { registerSettings } from "./js/settings.js"; 8 | import { initApi, registerModule, registerSystem } from './js/api.js'; 9 | import { RuleProvider } from './classes/ruleprovider.js'; 10 | 11 | let debugEnabled = 2; 12 | export let debug = (...args) => { 13 | if (debugEnabled > 1) console.log("DEBUG: Enhanced Terrain Layer | ", ...args); 14 | }; 15 | export let log = (...args) => console.log("Enhanced Terrain Layer | ", ...args); 16 | export let warn = (...args) => { 17 | if (debugEnabled > 0) console.warn("Enhanced Terrain Layer | ", ...args); 18 | }; 19 | export let error = (...args) => console.error("Enhanced Terrain Layer | ", ...args); 20 | 21 | export let i18n = key => { 22 | return game.i18n.localize(key); 23 | }; 24 | 25 | export let setting = key => { 26 | if (canvas.terrain._setting[key] !== undefined) 27 | return canvas.terrain._setting[key]; 28 | else 29 | return game.settings.get("enhanced-terrain-layer", key); 30 | }; 31 | 32 | export let getflag = (obj, key) => { 33 | return getProperty(obj, `flags.enhanced-terrain-layer.${key}`); 34 | //const flags = obj.flags['enhanced-terrain-layer']; 35 | //return flags && flags[key]; 36 | } 37 | 38 | function registerLayer() { 39 | CONFIG.Canvas.layers.terrain = { group: "interface", layerClass: TerrainLayer }; 40 | CONFIG.Terrain = { 41 | documentClass: TerrainDocument, 42 | layerClass: TerrainLayer, 43 | //sheetClass: TerrainConfig, 44 | sheetClasses: { 45 | base: { 46 | "enhanced-terrain-layer.TerrainSheet": { 47 | id: "enhanced-terrain-layer.TerrainSheet", 48 | label: "Enhanced Terrain Sheet", 49 | "default": true, 50 | cls: TerrainConfig 51 | } 52 | } 53 | }, 54 | typeLabels: { base: 'EnhancedTerrainLayer.Terrain' }, 55 | objectClass: Terrain 56 | }; 57 | 58 | //canvas["#scene"] = {}; 59 | 60 | let createEmbeddedDocuments = async function (wrapped, ...args) { 61 | let [embeddedName, updates = [], context = {}] = args; 62 | if (embeddedName == 'Terrain') { 63 | context.parent = this; 64 | context.pack = this.pack; 65 | return TerrainDocument.createDocuments(updates, context); 66 | } else 67 | return wrapped(...args); 68 | } 69 | 70 | if (game.modules.get("lib-wrapper")?.active) { 71 | libWrapper.register("enhanced-terrain-layer", "Scene.prototype.createEmbeddedDocuments", createEmbeddedDocuments, "MIXED"); 72 | } else { 73 | const oldCreateEmbeddedDocuments = Scene.prototype.createEmbeddedDocuments; 74 | Scene.prototype.createEmbeddedDocuments = async function (event) { 75 | return createEmbeddedDocuments.call(this, oldCreateEmbeddedDocuments.bind(this), ...arguments); 76 | } 77 | } 78 | 79 | /* 80 | let oldCreateEmbeddedDocuments = Scene.prototype.createEmbeddedDocuments; 81 | Scene.prototype.createEmbeddedDocuments = async function (embeddedName, updates = [], context = {}) { 82 | if (embeddedName == 'Terrain') { 83 | context.parent = this; 84 | context.pack = this.pack; 85 | return TerrainDocument.createDocuments(updates, context); 86 | } else 87 | return oldCreateEmbeddedDocuments.call(this, embeddedName, updates, context); 88 | }*/ 89 | 90 | let updateEmbeddedDocuments = async function (wrapped, ...args) { 91 | let [embeddedName, updates = [], context = {}] = args; 92 | if (embeddedName == 'Terrain') { 93 | context.parent = this; 94 | context.pack = this.pack; 95 | return TerrainDocument.updateDocuments(updates, context); 96 | } else 97 | return wrapped(...args); 98 | } 99 | 100 | if (game.modules.get("lib-wrapper")?.active) { 101 | libWrapper.register("enhanced-terrain-layer", "Scene.prototype.updateEmbeddedDocuments", updateEmbeddedDocuments, "MIXED"); 102 | } else { 103 | const oldUpdateEmbeddedDocuments = Scene.prototype.updateEmbeddedDocuments; 104 | Scene.prototype.updateEmbeddedDocuments = async function (event) { 105 | return updateEmbeddedDocuments.call(this, oldUpdateEmbeddedDocuments.bind(this), ...arguments); 106 | } 107 | } 108 | 109 | /* 110 | let oldUpdateEmbeddedDocuments = Scene.prototype.updateEmbeddedDocuments; 111 | Scene.prototype.updateEmbeddedDocuments = async function (embeddedName, updates = [], context = {}) { 112 | if (embeddedName == 'Terrain') { 113 | context.parent = this; 114 | context.pack = this.pack; 115 | return TerrainDocument.updateDocuments(updates, context); 116 | } else 117 | return oldUpdateEmbeddedDocuments.call(this, embeddedName, updates, context); 118 | }*/ 119 | 120 | let deleteEmbeddedDocuments = async function (wrapped, ...args) { 121 | let [embeddedName, updates = [], context = {}] = args; 122 | if (embeddedName == 'Terrain') { 123 | context.parent = this; 124 | context.pack = this.pack; 125 | return TerrainDocument.deleteDocuments(updates, context); 126 | } else 127 | return wrapped(...args); 128 | } 129 | 130 | if (game.modules.get("lib-wrapper")?.active) { 131 | libWrapper.register("enhanced-terrain-layer", "Scene.prototype.deleteEmbeddedDocuments", deleteEmbeddedDocuments, "MIXED"); 132 | } else { 133 | const oldDeleteEmbeddedDocuments = Scene.prototype.deleteEmbeddedDocuments; 134 | Scene.prototype.deleteEmbeddedDocuments = async function (event) { 135 | return deleteEmbeddedDocuments.call(this, oldDeleteEmbeddedDocuments.bind(this), ...arguments); 136 | } 137 | } 138 | 139 | /* 140 | let oldDeleteEmbeddedDocuments = Scene.prototype.deleteEmbeddedDocuments; 141 | Scene.prototype.deleteEmbeddedDocuments = async function (embeddedName, ids, context = {}) { 142 | if (embeddedName == 'Terrain') { 143 | context.parent = this; 144 | context.pack = this.pack; 145 | return TerrainDocument.deleteDocuments(ids, context); 146 | } else 147 | return oldDeleteEmbeddedDocuments.call(this, embeddedName, ids, context); 148 | }*/ 149 | 150 | /* 151 | Object.defineProperty(Scene.prototype, "terrain", { 152 | get: function terrain() { 153 | return this.data.terrain; 154 | } 155 | }); 156 | */ 157 | } 158 | 159 | /* 160 | async function checkUpgrade() { 161 | let hasInformed = false; 162 | let inform = function () { 163 | if (!hasInformed) { 164 | ui.notifications.info('Converting old TerrainLayer data, please wait'); 165 | hasInformed = true; 166 | } 167 | } 168 | 169 | for (let scene of game.scenes.entries) { 170 | if (scene.data.flags?.TerrainLayer) { 171 | let gW = scene.data.grid; 172 | let gH = scene.data.grid; 173 | 174 | let data = duplicate(scene.data.flags?.TerrainLayer); 175 | for (let [k, v] of Object.entries(data)) { 176 | if (k == 'costGrid') { 177 | let grid = scene.getFlag('TerrainLayer', 'costGrid'); 178 | for (let y in grid) { 179 | for (let x in grid[y]) { 180 | //if (Object.values(data).find(t => { return t.x == (parseInt(x) * gW) && t.y == (parseInt(y) * gH); }) == undefined) { 181 | inform(); 182 | let id = makeid(); 183 | let data = { _id: id, x: parseInt(x) * gW, y: parseInt(y) * gH, points: [[0, 0], [gW, 0], [gW, gH], [0, gH], [0, 0]], width: gW, height: gH, multiple: grid[y][x].multiple }; 184 | await scene.setFlag('enhanced-terrain-layer', 'terrain' + id, data); 185 | //} 186 | } 187 | } 188 | } 189 | }; 190 | } 191 | } 192 | 193 | if (hasInformed) 194 | ui.notifications.info('TerrainLayer conversion complete.'); 195 | }*/ 196 | 197 | function registerKeybindings() { 198 | game.keybindings.register('enhanced-terrain-layer', 'toggle-view', { 199 | name: 'EnhancedTerrainLayer.ToggleView', 200 | restricted: true, 201 | editable: [{ key: 'KeyT', modifiers: [KeyboardManager.MODIFIER_KEYS?.ALT] }], 202 | onDown: (data) => { 203 | if (game.user.isGM) { 204 | canvas.terrain.toggle(null, true); 205 | } 206 | }, 207 | }); 208 | } 209 | 210 | export function makeid() { 211 | var result = ''; 212 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 213 | var charactersLength = characters.length; 214 | for (var i = 0; i < 16; i++) { 215 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 216 | } 217 | return result; 218 | } 219 | 220 | function addControls(app, html, addheader) { 221 | let multiple = getflag(app.object, "multiple") || 1; 222 | let cost = $('
').addClass('form-group') 223 | .append($('