├── 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 | -------------------------------------------------------------------------------- /img/solid3x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/solid4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/solid0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/oldschool2x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/oldschool3x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/oldschool4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/triangle2x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/oldschool0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/triangle0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /img/triangle4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/diagonal3x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/diagonal4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/diagonal2x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/diagonal0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/horizontal2x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/vertical4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/horizontal0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/horizontal3x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/horizontal4x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/vertical0.5x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/vertical2x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/vertical3x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 |
Set the default settings when terrain is created on this scene
3 |
22 |
23 |
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 = $('