From 6e7b8923ece0ce035129df029e5e65176547402e Mon Sep 17 00:00:00 2001 From: ironmonk88 <75920956+ironmonk88@users.noreply.github.com> Date: Sat, 27 Aug 2022 16:54:56 -0700 Subject: [PATCH] 1.0.43 changes --- .gitattributes | 3 + CHANGELOG.md | 4 + classes/terrain.js | 1076 ++++++++++++++++++--------------- classes/terrainconfig.js | 55 +- classes/terraindocument.js | 344 ++++++++--- classes/terrainhud.js | 44 +- classes/terraininfo.js | 4 +- classes/terrainlayer.js | 220 ++++--- classes/terrainshape.js | 265 ++++++++ css/terrainlayer.css | 5 + js/controls.js | 21 +- js/settings.js | 2 +- lang/en.json | 2 +- module.json | 117 ++-- templates/terrain-config.html | 38 +- templates/terrain-hud.html | 16 +- terrain-main.js | 91 ++- 17 files changed, 1521 insertions(+), 786 deletions(-) create mode 100644 .gitattributes create mode 100644 classes/terrainshape.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b596487 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# exclude .gitignore and similar from the generated tarball +.git* export-ignore +/screenshots export-ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bef632..78dfb4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Version 1.0.43 + +Updates to support v10 + # Version 1.0.42 Fixed icon sizing for the HUD Terrain cost. diff --git a/classes/terrain.js b/classes/terrain.js index 8b99ff0..b342253 100644 --- a/classes/terrain.js +++ b/classes/terrain.js @@ -1,109 +1,436 @@ import { makeid, log, setting, debug, getflag } from '../terrain-main.js'; import { TerrainLayer } from './terrainlayer.js'; +import { TerrainShape } from './terrainshape.js'; export class Terrain extends PlaceableObject { - constructor(...args) { - super(...args); - - /** - * The Terrain image container - * @type {PIXI.Container|null} - */ - this.terrain = null; - - /** - * The tiling texture used for this template, if any - * @type {PIXI.Texture} - */ - this.texture = null; - - /** - * The primary drawing shape - * @type {PIXI.Graphics} - */ - this.drawing = null; - - /** - * The terrain shape used for testing point intersection - * @type {PIXI.Polygon} - */ - this.shape = null; - - /** - * The Terrain border frame - * @type {PIXI.Container|null} - */ - this.frame = null; - - /** - * Internal flag for the permanent points of the polygon - * @type {boolean} - * @private - */ - this._fixedPoints = duplicate(this.data.points || []); + + /** + * The border frame and resizing handles for the drawing. + * @type {PIXI.Container} + */ + frame; + + /** + * The overlay for the icon and text. + * @type {PIXI.Container} + */ + overlay; + + /** + * A text label that may be displayed as part of the interface layer for the Drawing. + * @type {PreciseText|null} + */ + text = null; + + /** + * The icon to display the environment. + * @type {PIXI.Container} + */ + container; + + /** + * The icon to display the environment. + * @type {PIXI.Container} + */ + icon; + + /** + * The drawing shape which is rendered as a PIXI.Graphics subclass in the PrimaryCanvasGroup. + * @type {DrawingShape} + */ + shape; + + /** + * An internal timestamp for the previous freehand draw time, to limit sampling. + * @type {number} + * @private + */ + _drawTime = 0; + + /** + * An internal flag for the permanent points of the polygon. + * @type {number[]} + * @private + */ + _fixedPoints = foundry.utils.deepClone(this.document.shape.points); + + /* -------------------------------------------- */ + + /** @inheritdoc */ + static embeddedName = "Terrain"; + + /* -------------------------------------------- */ + + /** + * The rate at which points are sampled (in milliseconds) during a freehand drawing workflow + * @type {number} + */ + static FREEHAND_SAMPLE_RATE = 75; + + /** + * A convenience reference to the possible shape types. + * @enum {string} + */ + static SHAPE_TYPES = foundry.data.ShapeData.TYPES; + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** @override */ + get bounds() { + const { x, y, shape } = this.document; + return new PIXI.Rectangle(x, y, shape.width, shape.height).normalize(); + } + + /* -------------------------------------------- */ + + /** @override */ + get center() { + const { x, y, shape } = this.document; + if (this.isPolygon) + return this.centerPolygon; + else + return new PIXI.Point(x + (shape.width / 2), y + (shape.height / 2)); + } + + get centerPolygon() { + const { x, y, shape } = this.document; + //center overlay + var points = shape.points; + var tx = 0, + ty = 0, + i, + j, + f; + + let s = canvas.dimensions.size; + + var area = function (points) { + var area = 0, + i, + j; + + for (i = 0, j = points.length - 2; i < points.length - 1; j = i, i += 2) { + var point1 = { x: points[i], y: points[i + 1] }; + var point2 = { x: points[j], y: points[j + 1] }; + area += point1.x * point2.y; + area -= point1.y * point2.x; + } + area /= 2; + + return area; + } + + for (i = 0, j = points.length - 2; i < points.length - 1; j = i, i += 2) { + var point1 = { x: points[i], y: points[i + 1] }; + var point2 = { x: points[j], y: points[j + 1] }; + f = point1.x * point2.y - point2.x * point1.y; + tx += (point1.x + point2.x) * f; + ty += (point1.y + point2.y) * f; + } + + f = area(points) * 6; + + return new PIXI.Point(x + parseInt(tx / f), y + parseInt(ty / f)); + //this.overlay.anchor.set(0.5, 0.5); + //this.overlay.x = parseInt(x / f); + //this.overlay.y = parseInt(y / f);// - (s / 5.2); + } + + /* -------------------------------------------- */ + + /** + * A Boolean flag for whether the Drawing utilizes a tiled texture background? + * @type {boolean} + */ + get isTiled() { + return true; + } + + /* -------------------------------------------- */ + + /** + * A Boolean flag for whether the Drawing is a Polygon type (either linear or freehand)? + * @type {boolean} + */ + get isPolygon() { + return this.type === Drawing.SHAPE_TYPES.POLYGON; + } + + /* -------------------------------------------- */ + + /** + * Does the Drawing have text that is displayed? + * @type {boolean} + */ + get hasText() { + return this.true; + } + + /* -------------------------------------------- */ + + /** + * The shape type that this Drawing represents. A value in Drawing.SHAPE_TYPES. + * @see {@link Drawing.SHAPE_TYPES} + * @type {string} + */ + get type() { + return this.document.shape.type; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _destroy(options) { + canvas.primary.removeTerrain(this); + this.texture?.destroy(); + } + + /** @override */ + async _draw() { + + // Load the background texture, if one is defined + const texture = this.document.texture; + if (this.isPreview) this.texture = this._original.texture?.clone(); + else this.texture = texture ? await loadTexture(texture, { fallback: "icons/svg/hazard.svg" }) : null; + + //this.document.updateEnvironment(); + + // Create the primary group drawing container + //this.container = canvas.primary.addTerrain(this); //this.addChild(this.#drawTerrain()); + //this.container.removeChildren(); + + this.shape = canvas.primary.addTerrain(this);//this.container.addChild(this.#drawTerrain()); + + // Control Border + this.frame = this.addChild(this.#drawFrame()); + + //this.overlay = this.addChild(new PIXI.Graphics()); + //this.overlay.anchor.set(0.5, 0.5); + + // Terrain text + this.text = this.addChild(this.#drawText()); + + // Terrain icon + this.icon = this.addChild(this.#drawIcon()); + + // Enable Interactivity, if this is a true Terrain + //if (this.id) this.activateListeners(); + return this; + } + + #drawTerrain() { + let shape = new TerrainShape(this); + shape.texture = this.texture ?? null; + shape.object = this; + return shape; + } + + /* -------------------------------------------- */ + + /** + * Create elements for the Terrain border and handles + * @private + */ + #drawFrame() { + const frame = new PIXI.Container(); + frame.border = frame.addChild(new PIXI.Graphics()); + frame.handle = frame.addChild(new ResizeHandle([1, 1])); + return frame; + } + + /** + * Create elements for the foreground text + * @private + */ + #drawText() { + const { text, shape } = this.document; + + let s = canvas.dimensions.size; + let fontSize = (s / 3); + + const stroke = Math.max(Math.round(fontSize / 32), 2); + + // Define the text style + const textStyle = PreciseText.getTextStyle({ + fontFamily: 'Arial', + fontSize: fontSize, + fill: "#FFFFFF", + stroke: "#111111", + strokeThickness: stroke, + dropShadow: true, + dropShadowColor: "#000000", + dropShadowBlur: Math.max(Math.round(fontSize / 16), 2), + dropShadowAngle: 0, + dropShadowDistance: 0, + align: "center", + wordWrap: false, + wordWrapWidth: shape.width, + padding: stroke + }); + + return new PreciseText(text || undefined, textStyle); + } + + #drawIcon() { + const { environmentObject, color } = this.document; + + let icon = new PIXI.Container(); + + if (environmentObject?.icon == undefined) + return icon; + + let s = canvas.dimensions.size; + const size = Math.max(Math.round(s / 2.5), 5); + + let sc = Color.from(color); + + icon.border = icon.addChild(new PIXI.Graphics()); + icon.border.clear().lineStyle(3, 0x000000).drawRoundedRect(0, 0, size, size, 4).beginFill(0x000000, 0.5).lineStyle(2, sc).drawRoundedRect(0, 0, size, size, 4).endFill(); + + icon.background = icon.addChild(new PIXI.Sprite.from(environmentObject?.icon)); + icon.background.x = icon.background.y = 1; + icon.background.width = icon.background.height = size - 2; + + return icon; + } + + _refresh() { + // Refresh the shape bounds and the displayed frame + const { x, y, hidden, shape } = this.document; + + // Refresh the primary drawing container + this.shape.refresh(); + //this.shape.position.set(shape.width / 2, shape.height / 2); + + const bounds = new PIXI.Rectangle(0, 0, shape.width, shape.height).normalize(); + this.hitArea = this.controlled ? bounds.clone().pad(50) : bounds; // Pad to include resize handle + this.buttonMode = true; + if (this.id && this.controlled) this.#refreshFrame(bounds); + else this.frame.visible = false; + + //const center = this.center; + //this.overlay.visible = this.id && !this._original; + //this.overlay.alpha = opacity; + //this.overlay.position.set(center.x - x, center.y - y); + + // Refresh the display of text + this.#refreshText(); + this.#refreshIcon(); + + // Set position and visibility + this.position.set(x, y); + this.visible = this.isVisible; + } + + #refreshFrame(rect) { + const { x, y, alpha } = this.document; + + // Determine the border color + const colors = CONFIG.Canvas.dispositionColors; + let bc = colors.INACTIVE; + if (this.controlled) { + bc = this.document.locked ? colors.HOSTILE : colors.CONTROLLED; + } + + // Draw the border + const pad = 6; + const t = CONFIG.Canvas.objectBorderThickness; + const h = Math.round(t / 2); + const o = Math.round(h / 2) + pad; + const border = rect.clone().pad(o); + this.frame.border.clear().lineStyle(t, 0x000000).drawShape(border).lineStyle(h, bc).drawShape(border); + + // Draw the handle + this.frame.handle.refresh(border); + this.frame.visible = true; + } + + #refreshText() { + + if (!this.document.text) return; + const { x, y, alpha } = this.document; + this.text.alpha = (ui.controls.activeControl == 'terrain' ? 1.0 : alpha) ?? 1.0; + this.text.visible = this.id && (setting('show-text') || ui.controls.activeControl == 'terrain'); + const padding = (this.document.environmentObject?.icon && (setting('show-icon') || ui.controls.activeControl == 'terrain') ? 0 : (this.text.width / 2)); + const center = this.center; + this.text.position.set( + (center.x - x) - padding, + (center.y - y) - (this.text.height / 2) + ); + } + + #refreshIcon() { + + if (!this.document.environmentObject?.icon) return; + const { x, y, alpha } = this.document; + this.icon.alpha = (ui.controls.activeControl == 'terrain' ? 1.0 : alpha) ?? 1.0; + this.icon.visible = this.id && (setting('show-icon') || ui.controls.activeControl == 'terrain'); + const padding = (this.document.text && (setting('show-text') || ui.controls.activeControl == 'terrain') ? this.icon.width : (this.icon.width / 2)); + const center = this.center; + this.icon.position.set( + (center.x - x) - padding, + (center.y - y) - (this.icon.height / 2) + ); } static get defaults() { - const sceneFlags = canvas.scene.data.flags['enhanced-terrain-layer']; + const sceneFlags = canvas.scene.flags['enhanced-terrain-layer']; let sceneMult = sceneFlags?.multiple; let sceneElev = sceneFlags?.elevation; let sceneDepth = sceneFlags?.depth; + let sceneEnv = sceneFlags?.environment; return { - width: 0, - height: 0, //rotation:0, locked: false, hidden: false, - points: [], + //drawcolor: setting('environment-color')[sceneEnv] || getflag(canvas.scene, 'defaultcolor') || setting('environment-color')['_default'] || "#FFFFFF", + //opacity: getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1, multiple: (sceneMult == undefined || sceneMult == "" ? this.layer.defaultmultiple : Math.clamped(parseInt(sceneMult), setting('minimum-cost'), setting('maximum-cost'))), elevation: (sceneElev == undefined || sceneElev == "" ? 0 : sceneElev), depth: (sceneDepth == undefined || sceneDepth == "" ? 0 : sceneDepth), - environment: sceneFlags?.environment || null, - obstacle: null + environment: sceneEnv || null, + obstacle: null, + shape: {}, + bezierFactor: 0 } } - /* -------------------------------------------- */ - - /** @override */ - static get embeddedName() { - return "Terrain"; - } - static get layer() { return canvas.terrain; } - get multiple() { - return this.data.multiple ?? Terrain.defaults.multiple; - } + get isVisible() { + const { x, y, shape, hidden } = this.document; - get terraintype() { - warn('terraintype is deprecated, please use environment'); - return ''; - } + if (ui.controls.activeControl == 'terrain') + return true; - get terrainheight() { - warn('terrainheight is deprecated, please use min/max'); - return { min: this.bottom, max: this.top }; - } + if (hidden && (!game.user.isGM || setting("only-show-active"))) + return false; - get min() { - warn('min is deprecated, please use elevation'); - return this.elevation; + log("isVisible", canvas.terrain._tokenDrag, this.layer._tokenDrag); + + if (!this.layer.showterrain && !(this.layer.showOnDrag && this.layer._tokenDrag)) + return false; + + const point = this.center; + const tolerance = canvas.grid.size; + return canvas.effects.visibility.testVisibility(point, { tolerance, object: this }); } - get max() { - warn('max is deprecated, please use top'); - return this.data.max || Terrain.defaults.max; + /* + get multiple() { + return this.document.multiple ?? Terrain.defaults.multiple; } get elevation() { - return this.data.elevation ?? Terrain.defaults.elevation; + return this.document.elevation ?? Terrain.defaults.elevation; } get depth() { - return this.data.depth ?? 0; + return this.document.depth ?? 0; } get top() { @@ -115,184 +442,64 @@ export class Terrain extends PlaceableObject { } get color() { - return this.data.drawcolor || setting('environment-color')[this.environment?.id] || this.environment?.color || getflag(canvas.scene, 'defaultcolor') || setting('environment-color')['_default'] || "#FFFFFF"; + return this.document.drawcolor || setting('environment-color')[this.environment?.id] || this.environment?.color || getflag(canvas.scene, 'defaultcolor') || setting('environment-color')['_default'] || "#FFFFFF"; } get opacity() { - return this.data.opacity ?? getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1; + return this.document.opacity ?? getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1; } + */ /* get environment() { - return this.data.environment; + return this.document.environment; }*/ + /* get obstacle() { - return this.data.obstacle; + return this.document.obstacle; } + */ contains(x, y) { - if (!(x < 0 || y < 0 || x > this.data.width || y > this.data.height)) { + if (!(x < 0 || y < 0 || x > this.document.width || y > this.document.height)) { return this.shape?.contains(x, y); } return false; } - static async create(data, options) { - debugger; - //super.create(data, options); - //canvas.scene._data.terrain - data._id = data._id || makeid(); + cost() { + if (this.document.hidden) { + return 1; + } else + return this.document.multiple; + } - let userId = game.user._id; - data = data instanceof Array ? data : [data]; - for (let d of data) { - const allowed = Hooks.call(`preCreateTerrain`, this, d, options, userId); - if (allowed === false) { - debug(`Terrain creation prevented by preCreate hook`); - return null; - } - } - let embedded = data.map(d => { - let object = canvas.terrain.createObject(d); - object._onCreate(options, userId); - canvas.scene.data.terrain.push(d); - canvas.scene.setFlag('enhanced-terrain-layer', 'terrain' + d._id, d); - Hooks.callAll(`createTerrain`, canvas.terrain, d, options, userId); - return d; - }); - //+++layer.storeHistory("create", result); - return data.length === 1 ? embedded[0] : embedded; - /* - const created = await canvas.scene.createEmbeddedEntity(this.embeddedName, data, options); - if (!created) return; - if (created instanceof Array) { - return created.map(c => this.layer.get(c._id)); - } else { - return this.layer.get(created._id); - }*/ - //canvas.scene.data.terrain.push(data); - //await canvas.scene.setFlag('enhanced-terrain-layer', 'terrain' + data._id, data); - //return this; - } - /* -------------------------------------------- */ - /* Properties */ - /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ - drawDashedPolygon(polygons, x, y, rotation, dash, gap, offsetPercentage) { - var i; - var p1; - var p2; - var dashLeft = 0; - var gapLeft = 0; - if (offsetPercentage > 0) { - var progressOffset = (dash + gap) * offsetPercentage; - if (progressOffset < dash) dashLeft = dash - progressOffset; - else gapLeft = gap - (progressOffset - dash); - } - var rotatedPolygons = []; - for (i = 0; i < polygons.length; i++) { - var p = { x: polygons[i][0], y: polygons[i][1] }; - var cosAngle = Math.cos(rotation); - var sinAngle = Math.sin(rotation); - var dx = p.x; - var dy = p.y; - p.x = (dx * cosAngle - dy * sinAngle); - p.y = (dx * sinAngle + dy * cosAngle); - rotatedPolygons.push(p); - } - for (i = 0; i < rotatedPolygons.length; i++) { - p1 = rotatedPolygons[i]; - if (i == rotatedPolygons.length - 1) p2 = rotatedPolygons[0]; - else p2 = rotatedPolygons[i + 1]; - var dx = p2.x - p1.x; - var dy = p2.y - p1.y; - if (dx == 0 && dy == 0) - continue; - var len = Math.sqrt(dx * dx + dy * dy); - var normal = { x: dx / len, y: dy / len }; - var progressOnLine = 0; - let mx = x + p1.x + gapLeft * normal.x; - let my = y + p1.y + gapLeft * normal.y; - this.moveTo(mx, my); - while (progressOnLine <= len) { - progressOnLine += gapLeft; - if (dashLeft > 0) progressOnLine += dashLeft; - else progressOnLine += dash; - if (progressOnLine > len) { - dashLeft = progressOnLine - len; - progressOnLine = len; - } else { - dashLeft = 0; - } - let lx = x + p1.x + progressOnLine * normal.x; - let ly = y + p1.y + progressOnLine * normal.y; - this.lineTo(lx, ly); - progressOnLine += gap; - if (progressOnLine > len && dashLeft == 0) { - gapLeft = progressOnLine - len; - //console.log(progressOnLine, len, gap); - } else { - gapLeft = 0; - let mx = x + p1.x + progressOnLine * normal.x; - let my = y + p1.y + progressOnLine * normal.y; - this.moveTo(mx, my); - } - } - } - } - /** @override */ - async draw() { - this.clear(); - - let mult = Math.clamped(this.data.multiple, setting('minimum-cost'), setting('maximum-cost')); - if (mult > 4) - mult = 4; - if (mult >= 1) - mult = parseInt(mult); - if (mult < 1) - mult = 0.5; - - let image = setting('terrain-image'); - if (image != 'clear') - this.texture = (mult != 1 ? await loadTexture(`modules/enhanced-terrain-layer/img/${image}${mult}x.svg`) : null); - else - this.texture = null; - this.updateEnvironment(); - // Create the inner Terrain container - this._createTerrain(); - // Control Border - this._createFrame(); - // Render Appearance - this.refresh(); - // Enable Interactivity, if this is a true Terrain - if (this.id) this.activateListeners(); - return this; - } - /* -------------------------------------------- */ + + + /** * Create the components of the terrain element, the terrain container, the drawn shape, and the overlay text */ + /* _createTerrain() { // Terrain container @@ -307,45 +514,10 @@ export class Terrain extends PlaceableObject { this._createIcon(); this._positionOverlay(); } + */ /* -------------------------------------------- */ - - /** - * Create elements for the foreground text - * @private - */ - _createText() { - if (this.text && !this.text._destroyed) { - this.text.destroy(); - this.text = null; - } - let s = canvas.dimensions.size; - let fontsize = (s / 3); - let mult = Math.clamped(this.data.multiple, setting('minimum-cost'), setting('maximum-cost')); - - const stroke = Math.max(Math.round(fontsize / 32), 2); - - // Define the text style - const textStyle = new PIXI.TextStyle({ - fontFamily: 'Arial', - fontSize: fontsize, - fill: "#FFFFFF", - stroke: "#111111", - strokeThickness: stroke, - dropShadow: true, - dropShadowColor: "#000000", - dropShadowBlur: Math.max(Math.round(fontsize / 16), 2), - dropShadowAngle: 0, - dropShadowDistance: 0, - align: "center", - wordWrap: false, - wordWrapWidth: 1.5 * this.data.width, - padding: stroke - }); - - return new PreciseText(String.fromCharCode(215) + (mult == 0.5 ? String.fromCharCode(189) : mult), textStyle); - } - + /* _createIcon() { if (this.icon && !this.icon._destroyed) { this.icon.destroy(); @@ -369,65 +541,10 @@ export class Terrain extends PlaceableObject { this.icon.background.x = this.icon.background.y = 1; this.icon.background.width = this.icon.background.height = size - 2; } + */ - _positionOverlay() { - //center overlay - var points = this.data.points; - var x = 0, - y = 0, - i, - j, - f; - - let s = canvas.dimensions.size; - - var area = function (points) { - var area = 0, - i, - j; - - for (i = 0, j = points.length - 1; i < points.length; j = i, i++) { - var point1 = points[i]; - var point2 = points[j]; - area += point1[0] * point2[1]; - area -= point1[1] * point2[0]; - } - area /= 2; - - return area; - } - - for (i = 0, j = points.length - 1; i < points.length; j = i, i++) { - var point1 = points[i]; - var point2 = points[j]; - f = point1[0] * point2[1] - point2[0] * point1[1]; - x += (point1[0] + point2[0]) * f; - y += (point1[1] + point2[1]) * f; - } - - f = area(points) * 6; - - //this.overlay.anchor.set(0.5, 0.5); - this.overlay.x = parseInt(x / f); - this.overlay.y = parseInt(y / f);// - (s / 5.2); - } - - /* -------------------------------------------- */ - - /** - * Create elements for the Terrain border and handles - * @private - */ - _createFrame() { - this.frame = this.addChild(new PIXI.Container()); - this.frame.border = this.frame.addChild(new PIXI.Graphics()); - this.frame.handle = this.frame.addChild(new ResizeHandle([1, 1])); - } - - /* -------------------------------------------- */ - - /** @override */ - refresh(icons) { + /* + _refresh() { if (this._destroyed || this.drawing?._destroyed || this.drawing == undefined) return; this.drawing.clear(); @@ -436,7 +553,7 @@ export class Terrain extends PlaceableObject { // Outer Stroke //const colors = CONFIG.Canvas.dispositionColors; - let sc = colorStringToHex(this.color); //this.data.hidden ? colorStringToHex("#C0C0C0") : + let sc = Color.from(this.color); //this.document.hidden ? colorStringToHex("#C0C0C0") : let lStyle = new PIXI.LineStyle(); mergeObject(lStyle, { width: s / 20, color: sc, alpha: (setting('draw-border') ? 1 : 0), cap: PIXI.LINE_CAP.ROUND, join: PIXI.LINE_JOIN.ROUND, visible: true }); this.drawing.lineStyle(lStyle); @@ -445,8 +562,8 @@ export class Terrain extends PlaceableObject { // Fill Color or Texture if (this.texture && sc != 'transparent') { - let sW = (canvas.grid.w / (this.texture.width * (setting('terrain-image') == 'diagonal' ? 2 : 1))); - let sH = (canvas.grid.h / (this.texture.height * (setting('terrain-image') == 'diagonal' ? 2 : 1))); + let sW = (canvas.dimensions.size / (this.texture.width * (setting('terrain-image') == 'diagonal' ? 2 : 1))); + let sH = (canvas.dimensions.size / (this.texture.height * (setting('terrain-image') == 'diagonal' ? 2 : 1))); this.drawing.beginTextureFill({ texture: this.texture, color: sc, @@ -456,14 +573,14 @@ export class Terrain extends PlaceableObject { } // Draw polygon - let points = this.data.points || []; + let points = this.document.points || []; if (points.length >= 2) { if (points.length === 2) this.drawing.endFill(); this.shape = new PIXI.Polygon(points.deepFlatten()); } if (this.shape && sc != 'transparent') { - if (this.data.hidden) { + if (this.document.hidden) { this.drawDashedPolygon.call(this.drawing, points, 0, 0, 0, 1, 5, 0); lStyle.width = 0; this.drawing.lineStyle(lStyle); @@ -496,14 +613,14 @@ export class Terrain extends PlaceableObject { // Determine drawing bounds and update the frame const bounds = this.terrain.getLocalBounds(); - if (this.id && this._controlled) this._refreshFrame(bounds); + if (this.id && this.controlled) this._refreshFrame(bounds); else this.frame.visible = false; // Toggle visibility - this.position.set(this.data.x, this.data.y); + this.position.set(this.document.x, this.document.y); this.terrain.hitArea = bounds; this.alpha = 1; - this.visible = !this.data.hidden || (game.user.isGM && (ui.controls.activeControl == 'terrain' || !setting('only-show-active'))); + this.visible = !this.document.hidden || (game.user.isGM && (ui.controls.activeControl == 'terrain' || !setting('only-show-active'))); if (this.visible && game.modules.get("levels")?.active && canvas.tokens.controlled[0]) { const token = canvas.tokens.controlled[0]; @@ -513,38 +630,11 @@ export class Terrain extends PlaceableObject { return this; } + */ /* -------------------------------------------- */ - /** - * Refresh the boundary frame which outlines the Terrain shape - * @private - */ - _refreshFrame({ x, y, width, height }) { - // Determine the border color - const colors = CONFIG.Canvas.dispositionColors; - let bc = colors.INACTIVE; - if (this._controlled) { - bc = this.data.locked ? colors.HOSTILE : colors.CONTROLLED; - } - - // Draw the border - const pad = 6; - const t = CONFIG.Canvas.objectBorderThickness; - const h = Math.round(t / 2); - const o = Math.round(h / 2) + pad; - this.frame.border.clear() - .lineStyle(t, 0x000000).drawRect(x - o, y - o, width + (2 * o), height + (2 * o)) - .lineStyle(h, bc).drawRect(x - o, y - o, width + (2 * o), height + (2 * o)) - - // Draw the handle - this.frame.handle.position.set(x + width + o, y + height + o); - this.frame.handle.clear() - .beginFill(0x000000, 1.0).lineStyle(h, 0x000000).drawCircle(0, 0, pad + h) - .lineStyle(h, bc).drawCircle(0, 0, pad); - this.frame.visible = true; - } /* -------------------------------------------- */ @@ -552,11 +642,23 @@ export class Terrain extends PlaceableObject { * Add a new polygon point to the terrain, ensuring it differs from the last one * @private */ - _addPoint(position, temporary = true) { - const point = [position.x - this.data.x, position.y - this.data.y]; - this.data.points = this._fixedPoints.concat([point]); + _addPoint(position, { round = false, snap = false, temporary = false } = {}) { + if (snap) position = canvas.grid.getSnappedPosition(position.x, position.y, this.layer.gridPrecision); + else if (round) { + position.x = Math.roundFast(position.x); + position.y = Math.roundFast(position.y); + } + + // Avoid adding duplicate points + const last = this._fixedPoints.slice(-2); + const next = [position.x - this.document.x, position.y - this.document.y]; + if (next.equals(last)) return; + + // Append the new point and update the shape + const points = this._fixedPoints.concat(next); + this.document.shape.updateSource({ points }); if (!temporary) { - this._fixedPoints = this.data.points; + this._fixedPoints = points; this._drawTime = Date.now(); } } @@ -568,28 +670,8 @@ export class Terrain extends PlaceableObject { * @private */ _removePoint() { - if (this._fixedPoints.length) this._fixedPoints.pop(); - this.data.points = this._fixedPoints; - } - - /* -------------------------------------------- */ - - cost() { - if (this.data.hidden) { - return 1; - } else - return this.data.multiple; - } - - /** @override */ - activateListeners() { - super.activateListeners(); - /* - this.frame.handle.off("mouseover").off("mouseout").off("mousedown") - .on("mouseover", this._onHandleHoverIn.bind(this)) - .on("mouseout", this._onHandleHoverOut.bind(this)) - .on("mousedown", this._onHandleMouseDown.bind(this)); - this.frame.handle.interactive = true;*/ + this._fixedPoints.splice(-2); + this.document.shape.updateSource({ points: this._fixedPoints }); } /* -------------------------------------------- */ @@ -597,27 +679,22 @@ export class Terrain extends PlaceableObject { /* -------------------------------------------- */ /** @override */ - _onUpdate(data) { - const changed = new Set(Object.keys(data)); - if (changed.has("z")) { - this.zIndex = parseInt(data.z) || 0; - } + _onUpdate(changed, options, userId) { + //if (changed.environment != undefined) + // this.document.updateEnvironment(); - if (data.environment != undefined) - this.updateEnvironment(); - - data.multiple = Math.clamped(data.multiple, setting('minimum-cost'), setting('maximum-cost')); + changed.multiple = Math.clamped(changed.multiple, setting('minimum-cost'), setting('maximum-cost')); // Full re-draw or partial refresh if (changed.has("multiple") || changed.has("environment")) - this.draw().then(() => super._onUpdate(data)); + this.draw().then(() => super._onUpdate(changed, options, userId)); else { this.refresh(); - super._onUpdate(data); + super._onUpdate(changed, options, userId); } // Update the sheet, if it's visible - if (this._sheet && this._sheet.rendered) this.sheet.render(); + //if (this._sheet && this._sheet.rendered) this.sheet.render(); } /* -------------------------------------------- */ @@ -630,21 +707,21 @@ export class Terrain extends PlaceableObject { delete this._creating; return true; } - if (this._controlled) return true; + if (this.controlled) return true; if (game.activeTool !== "select") return false; return user.isGM; } /** @override */ _canHUD(user, event) { - return this._controlled; + return this.controlled; } /* -------------------------------------------- */ /** @override */ _canConfigure(user, event) { - if (!this._controlled) return false; + if (!this.controlled) return false; return super._canConfigure(user); } @@ -670,19 +747,48 @@ export class Terrain extends PlaceableObject { * @private */ _onMouseDraw(event) { - const { destination, originalEvent } = event.data; + const { destination, origin, originalEvent } = event.data; const isShift = originalEvent.shiftKey; const isAlt = originalEvent.altKey; - - // Determine position let position = destination; - if (!isShift) { - position = canvas.grid.getSnappedPosition(position.x, position.y, this.layer.gridPrecision); - } else { - position = { x: parseInt(position.x), y: parseInt(position.y) }; - } - this._addPoint(position, true); + switch (this.type) { + + // Polygon Shapes + case Drawing.SHAPE_TYPES.POLYGON: + const isFreehand = game.activeTool === "freehand"; + let temporary = true; + if (isFreehand) { + const now = Date.now(); + temporary = (now - this._drawTime) < this.constructor.FREEHAND_SAMPLE_RATE; + } + const snap = !(isShift || isFreehand); + this._addPoint(position, { snap, temporary }); + break; + + // Other Shapes + default: + const shape = this.shape; + const minSize = canvas.dimensions.size * 0.5; + let dx = position.x - origin.x; + let dy = position.y - origin.y; + if (Math.abs(dx) < minSize) dx = minSize * Math.sign(shape.width); + if (Math.abs(dy) < minSize) dy = minSize * Math.sign(shape.height); + if (isAlt) { + dx = Math.abs(dy) < Math.abs(dx) ? Math.abs(dy) * Math.sign(dx) : dx; + dy = Math.abs(dx) < Math.abs(dy) ? Math.abs(dx) * Math.sign(dy) : dy; + } + const r = new PIXI.Rectangle(origin.x, origin.y, dx, dy).normalize(); + this.document.updateSource({ + x: r.x, + y: r.y, + shape: { + width: r.width, + height: r.height + } + }); + break; + } // Refresh the display this.refresh(); @@ -715,24 +821,24 @@ export class Terrain extends PlaceableObject { // Update each dragged Terrain const clones = event.data.clones || []; const updates = clones.map(c => { - let dest = { x: c.data.x, y: c.data.y }; + let dest = { x: c.document.x, y: c.document.y }; if (!event.data.originalEvent.shiftKey) { - dest = canvas.grid.getSnappedPosition(c.data.x, c.data.y, this.layer.gridPrecision); + dest = canvas.grid.getSnappedPosition(dest.x, dest.y, this.layer.gridPrecision); } // Define the update const update = { _id: c._original.id, x: dest.x, - y: dest.y//, - //rotation: c.data.rotation + y: dest.y }; // Hide the original until after the update processes + c.visible = false; c._original.visible = false; return update; }); - return canvas.scene.updateEmbeddedDocuments("Terrain", updates).then(() => { + return canvas.scene.updateEmbeddedDocuments("Terrain", updates, { diff: false }).then(() => { for (let clone of clones) { clone._original.visible = true; } @@ -747,6 +853,21 @@ export class Terrain extends PlaceableObject { return super._onDragLeftCancel(event); } + /** @inheritDoc */ + _onDragStart() { + super._onDragStart(); + const o = this._original; + o.shape.alpha = o.alpha; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _onDragEnd() { + super._onDragEnd(); + if (this.isPreview) this._original.shape.alpha = 1.0; + } + /* -------------------------------------------- */ /* Resize Handling */ /* -------------------------------------------- */ @@ -771,6 +892,9 @@ export class Terrain extends PlaceableObject { */ _onHandleHoverOut(event) { event.data.handle.scale.set(1.0, 1.0); + if (this.mouseInteractionManager.state < MouseInteractionManager.INTERACTION_STATES.CLICKED) { + this._dragHandle = false; + } } /* -------------------------------------------- */ @@ -781,9 +905,9 @@ export class Terrain extends PlaceableObject { * @private */ _onHandleMouseDown(event) { - if (!this.data.locked) { + if (!this.document.locked) { this._dragHandle = true; - this._original = this.document.toJSON(); + this._original = this.document.toObject(); } } @@ -795,13 +919,16 @@ export class Terrain extends PlaceableObject { * @private */ _onHandleDragStart(event) { + /* const handle = event.data.handle; - const aw = Math.abs(this.data.width); - const ah = Math.abs(this.data.height); - const x0 = this.data.x + (handle.offset[0] * aw); - const y0 = this.data.y + (handle.offset[1] * ah); + const aw = Math.abs(this.document.width); + const ah = Math.abs(this.document.height); + const x0 = this.document.x + (handle.offset[0] * aw); + const y0 = this.document.y + (handle.offset[1] * ah); event.data.origin = { x: x0, y: y0, width: aw, height: ah }; this.resizing = true; + */ + event.data.origin = { x: this.bounds.right, y: this.bounds.bottom }; } /* -------------------------------------------- */ @@ -820,9 +947,11 @@ export class Terrain extends PlaceableObject { // Update Terrain dimensions const dx = destination.x - origin.x; const dy = destination.y - origin.y; - const update = this._rescaleDimensions(this._original, dx, dy); - this.document.data.update(update); - this.refresh(); + const normalized = this._rescaleDimensions(this._original, dx, dy); + try { + this.document.updateSource(normalized); + this.refresh(); + } catch (err) { } } /* -------------------------------------------- */ @@ -842,14 +971,14 @@ export class Terrain extends PlaceableObject { const dx = destination.x - origin.x; const dy = destination.y - origin.y; const update = this._rescaleDimensions(this._original, dx, dy); - this.document.data.update(update); - this.resizing = false; - delete this._original; //delete the original so that the drag cancel doesn't erase our changes. + return this.document.update(update, { diff: false }); + //this.resizing = false; + //delete this._original; //delete the original so that the drag cancel doesn't erase our changes. - this._positionOverlay(); + //this._positionOverlay(); // Commit the update - return this.document.update(this.document.data, { diff: false }); + //return this.document.update(this.document.data, { diff: false }); } /* -------------------------------------------- */ @@ -859,8 +988,9 @@ export class Terrain extends PlaceableObject { * @private */ _onHandleDragCancel(event) { - if (this._original) - this.document.data.update(this._original); + //if (this._original) + // this.document.data.update(this._original); + this.document.updateSource(this._original); this._dragHandle = false; delete this._original; this.refresh(); @@ -876,24 +1006,23 @@ export class Terrain extends PlaceableObject { * @private */ _rescaleDimensions(original, dx, dy) { - let { points, width, height } = original; + let { points, width, height } = original.shape; width += dx; height += dy; + points = points || []; // Rescale polygon points - const scaleX = 1 + (dx / original.width); - const scaleY = 1 + (dy / original.height); - points = points.map(p => [p[0] * scaleX, p[1] * scaleY]); + if (this.isPolygon) { + const scaleX = 1 + (dx / original.shape.width); + const scaleY = 1 + (dy / original.shape.height); + points = points.map((p, i) => p * (i % 2 ? scaleY : scaleX)); + } - // Normalize the shape - const update = this.constructor.normalizeShape({ + return this.constructor.normalizeShape({ x: original.x, y: original.y, - width: width, - height: height, - points: points + shape: { width: Math.roundFast(width), height: Math.roundFast(height), points } }); - return update; } /* -------------------------------------------- */ @@ -906,81 +1035,50 @@ export class Terrain extends PlaceableObject { */ static normalizeShape(data) { // Adjust shapes with an explicit points array - let points = data.points; - if (points && points.length) { - //Close the shape - points.push([points[0][0], points[0][1]]); - - // De-dupe any points which were repeated in sequence - points = points.reduce((arr, p1) => { - let p0 = arr.length ? arr[arr.length - 1] : null; - if (!p0 || !p1.equals(p0)) arr.push(p1); - return arr; - }, []); - - // Adjust points for the minimal x and y values - const [xs, ys] = data.points.reduce((arr, p) => { - arr[0].push(p[0]); - arr[1].push(p[1]); - return arr; - }, [[], []]); + const rawPoints = data.shape.points; + if (rawPoints?.length) { + + // Organize raw points and de-dupe any points which repeated in sequence + const xs = []; + const ys = []; + for (let i = 1; i < rawPoints.length; i += 2) { + const x0 = rawPoints[i - 3]; + const y0 = rawPoints[i - 2]; + const x1 = rawPoints[i - 1]; + const y1 = rawPoints[i]; + if ((x1 === x0) && (y1 === y0)) { + continue; + } + xs.push(x1); + ys.push(y1); + } // Determine minimal and maximal points - let minX = Math.min(...xs); - let maxX = Math.max(...xs); - let minY = Math.min(...ys); - let maxY = Math.max(...ys); - - // Normalize points - points = points.map(p => [p[0] - minX, p[1] - minY]); + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + + // Normalize points relative to minX and minY + const points = []; + for (let i = 0; i < xs.length; i++) { + points.push(xs[i] - minX, ys[i] - minY); + } // Update data data.x += minX; data.y += minY; - data.width = parseInt(maxX - minX); - data.height = parseInt(maxY - minY); - data.points = points; + data.shape.width = maxX - minX; + data.shape.height = maxY - minY; + data.shape.points = points; + }// Adjust rectangles + else { + const normalized = new PIXI.Rectangle(data.x, data.y, data.shape.width, data.shape.height).normalize(); + data.x = normalized.x; + data.y = normalized.y; + data.shape.width = normalized.width; + data.shape.height = normalized.height; } return data; } - - async update(data, options = {save: true}) { - //update this object - mergeObject(this.data, data); - delete this.data.id; //remove the id if I've accidentally added it. We should be using _id - if (options.save === true) { - //update the data and save it to the scene - let objectdata = duplicate(getflag(canvas.scene, `terrain${this.data._id}`)); - mergeObject(objectdata, this.data); - //let updates = {}; - //updates['flags.enhanced-terrain-layer.terrain' + this.data._id + '.multiple'] = data.multiple; - let key = `flags.enhanced-terrain-layer.terrain${this.data._id}`; - await canvas.scene.update({ [key]: objectdata }, { diff: false }); - //canvas.terrain._costGrid = null; - } - - if (data.environment != undefined) - this.updateEnvironment(); - //await canvas.scene.setFlag("enhanced-terrain-layer", "terrain" + this.data._id, objectdata, {diff: false}); - //if the multiple has changed then update the image - if (data.multiple != undefined || data.environment != undefined) { - this.draw(); - }else - this.refresh(); - return this; - } - - async delete(options) { - let layerdata = duplicate(getflag(this.scene, "data")); - let idx = layerdata.findIndex(t => { return t._id == this.id }); - layerdata.splice(idx, 1); - await this.scene.setFlag("enhanced-terrain-layer", "data", layerdata); - return this; - } - - updateEnvironment() { - this.environment = canvas.terrain.getEnvironments().find(e => e.id == this.data.environment); - //if (this.environment == undefined && !setting('use-obstacles')) - // this.environment = canvas.terrain.getObstacles().find(e => e.id == this.data.environment); - } } \ No newline at end of file diff --git a/classes/terrainconfig.js b/classes/terrainconfig.js index a297f7f..bc74760 100644 --- a/classes/terrainconfig.js +++ b/classes/terrainconfig.js @@ -12,6 +12,8 @@ export class TerrainConfig extends DocumentSheet { //title: i18n("EnhancedTerrainLayer.Configuration"), template: "modules/enhanced-terrain-layer/templates/terrain-config.html", width: 400, + height: "auto", + configureDefault: false, submitOnChange: false }); } @@ -22,7 +24,6 @@ export class TerrainConfig extends DocumentSheet { getData(options) { var _obstacles = {}; var _environments = canvas.terrain.getEnvironments().reduce(function (map, obj) { - if (obj.obstacle === true) { _obstacles[obj.id] = i18n(obj.text); }else @@ -30,22 +31,14 @@ export class TerrainConfig extends DocumentSheet { return map; }, {}); - /*var _obstacles = canvas.terrain.getObstacles().reduce(function (map, obj) { - map[obj.id] = i18n(obj.text); - return map; - }, {});*/ - - let object = duplicate(this.object.data); - object.opacity = this.document.object.opacity; - - return { - object: object, - options: this.options, + const data = super.getData(); + return mergeObject(data, { + author: game.users.get(this.document.author)?.name || "", environments: _environments, obstacles: _obstacles, useObstacles: setting('use-obstacles'), - submitText: this.options.preview ? "Create" : "Update" - } + submitText: this.document.id ? "Update" : "Create" + }) } /* -------------------------------------------- */ @@ -54,7 +47,7 @@ export class TerrainConfig extends DocumentSheet { _onChangeInput(event) { if ($(event.target).attr('name') == 'multiple') { let val = $(event.target).val(); - $(event.target).next().html(TerrainLayer.multipleText(val)); + $(event.target).next().html(TerrainDocument.text(val)); } super._onChangeInput.call(this, event); } @@ -65,6 +58,19 @@ export class TerrainConfig extends DocumentSheet { async _updateObject(event, formData) { if (!game.user.isGM) throw "You do not have the ability to configure a Terrain object."; + // Un-scale the bezier factor + //formData.bezierFactor /= 2; + + if (formData.width != this.object.width || formData.height != this.object.height) { + let reshape = this.object.object._rescaleDimensions(this.object, formData.width - this.object.width, formData.height - this.object.height); + formData["shape.width"] = reshape.shape.width; + formData["shape.height"] = reshape.shape.height; + if (this.object.object.isPolygon) + formData["shape.points"] = reshape.shape.points; + } + delete formData.width; + delete formData.height; + let data = expandObject(formData); data.multiple = Math.clamped(data.multiple, setting('minimum-cost'), setting('maximum-cost')); @@ -72,20 +78,23 @@ export class TerrainConfig extends DocumentSheet { if (data.opacity == defaultOpacity) data.opacity = null; - if (this.document.id) { - /* - if (game.user.isGM) { - game.socket.emit('module.enhanced-terrain-layer', { action: 'updateTerrain', arguments: [data] }); - }*/ - return this.document.update(data); + if (this.object.id) { + return this.object.update(data); + } + return this.object.constructor.create(data); + } + + async close(options) { + await super.close(options); + if (this.preview) { + this.preview.removeChildren(); + this.preview = null; } - return this.document.constructor.create(data); } activateListeners(html) { super.activateListeners(html); - if (setting('use-obstacles')) { $('select[name="environment"], select[name="obstacle"]', html).on('change', function () { //make sure that the environment is always set if using obstacles diff --git a/classes/terraindocument.js b/classes/terraindocument.js index 1b38139..f1b7205 100644 --- a/classes/terraindocument.js +++ b/classes/terraindocument.js @@ -1,8 +1,7 @@ -import * as fields from "../../../common/data/fields.mjs"; -import { Document, DocumentData } from "../../../common/abstract/module.mjs"; import { makeid, log, error, i18n, setting, getflag } from '../terrain-main.js'; import { Terrain } from './terrain.js'; +/* export class TerrainData extends DocumentData { static defineSchema() { return { @@ -25,36 +24,80 @@ export class TerrainData extends DocumentData { } } } +*/ -export class BaseTerrain extends Document { - +export class BaseTerrain extends foundry.abstract.Document { /** @inheritdoc */ - static get schema() { - return TerrainData; + static metadata = Object.freeze(mergeObject(super.metadata, { + name: "Terrain", + collection: "terrain", + label: "EnhancedTerrainLayer.terrain", + isEmbedded: true, + permissions: { + create: "TEMPLATE_CREATE", + update: this.#canModify, + delete: this.#canModify + } + }, { inplace: false })); + + static defineSchema() { + return { + _id: new foundry.data.fields.DocumentIdField(), + //author: new foundry.data.fields.ForeignDocumentField(BaseUser, { nullable: false, initial: () => game.user?.id }), + shape: new foundry.data.fields.EmbeddedDataField(foundry.data.ShapeData), + x: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "XCoord" }), + y: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "YCoord" }), + hidden: new foundry.data.fields.BooleanField(), + locked: new foundry.data.fields.BooleanField(), + multiple: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 2, label: "EnhancedTerrainLayer.Multiple" }), + elevation: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0, label: "EnhancedTerrainLayer.Elevation" }), + depth: new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0,label: "EnhancedTerrainLayer.Depth" }), + opacity: new foundry.data.fields.AlphaField({ required: true, nullable: false, initial: 1, label: "EnhancedTerrainLayer.Opacity" }), + drawcolor: new foundry.data.fields.ColorField({ label: "EnhancedTerrainLayer.DrawColor" }), + environment: new foundry.data.fields.StringField({ label: "EnhancedTerrainLayer.Environment" }), + obstacle: new foundry.data.fields.StringField({ label: "EnhancedTerrainLayer.Obstacle" }), + flags: new foundry.data.fields.ObjectField() + } } - /** @inheritdoc */ - static get metadata() { - return mergeObject(super.metadata, { - name: "Terrain", - collection: "terrain", - label: "EnhancedTerrainLayer.terrain", - isEmbedded: true, - permissions: { - create: "TEMPLATE_CREATE", - update: this._canModify, - delete: this._canModify - } - }); - }; + _validateModel(data) { + // Must have at least three points in the shape + // (!(hasText || hasFill || hasLine)) { + // throw new Error("Drawings must have visible text, a visible fill, or a visible line"); + // + } /** * Is a user able to update or delete an existing Drawing document?? * @protected */ - static _canModify(user, doc, data) { + static #canModify(user, doc, data) { if (user.isGM) return true; // GM users can do anything - return doc.data.author === user.id; // Users may only update their own created drawings + return false; + } + + testUserPermission(user, permission, { exact = false } = {}) { + return user.isGM; + } + + static migrateData(data) { + /** + * V10 migration to ShapeData model + * @deprecated since v10 + */ + this._addDataFieldMigration(data, "type", "shape.type", "p"); + this._addDataFieldMigration(data, "width", "shape.width"); + this._addDataFieldMigration(data, "height", "shape.height"); + this._addDataFieldMigration(data, "points", "shape.points", d => d.points.flat()); + return super.migrateData(data); + } + + static shimData(data, options) { + this._addDataFieldShim(data, "type", "shape.type", { since: 10, until: 12 }); + this._addDataFieldShim(data, "width", "shape.width", { since: 10, until: 12 }); + this._addDataFieldShim(data, "height", "shape.height", { since: 10, until: 12 }); + this._addDataFieldShim(data, "points", "shape.points", { since: 10, until: 12 }); + return super.shimData(data, options); } } @@ -62,7 +105,8 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { /* -------------------------------------------- */ /* Properties */ -/* -------------------------------------------- */ + /* -------------------------------------------- */ + #envobj = null; get layer() { return canvas.terrain; @@ -72,12 +116,80 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { return true; } - /** - * A reference to the User who created the Drawing document. - * @type {User} - */ - get author() { - return game.users.get(this.data.author); + get fillType() { + return CONST.DRAWING_FILL_TYPES.PATTERN; + } + + get color() { + return this.drawcolor || setting('environment-color')[this.environment] || getflag(canvas.scene, 'defaultcolor') || setting('environment-color')['_default'] || "#FFFFFF"; + } + + get alpha() { + return this.opacity ?? getflag(canvas.scene, 'opacity') ?? setting('opacity') ?? 1; + } + + get rotation() { + return 0; + } + + get bezierFactor() { + return 0; + } + + get strokeWidth() { + return canvas.dimensions.size / 20; + } + + static text(val) { + return String.fromCharCode(215) + (val == 0.5 ? String.fromCharCode(189) : val); + } + + get text() { + let mult = Math.clamped(this.multiple, setting('minimum-cost'), setting('maximum-cost')); + return this.constructor.text(mult); + } + + get texture() { + let image = setting('terrain-image'); + + if (image == "clear") + return null; + + let mult = Math.clamped(this.multiple, setting('minimum-cost'), setting('maximum-cost')); + if (mult > 4) + mult = 4; + if (mult >= 1) + mult = parseInt(mult); + if (mult < 1) + mult = 0.5; + + if (mult == 1) + return null; + + return `modules/enhanced-terrain-layer/img/${image}${mult}x.svg`; + } + + get environmentObject() { + if (this.#envobj?.id == this.environment) + return this.#envobj; + this.#envobj = canvas.terrain.getEnvironments().find(e => e.id == this.environment); + return this.#envobj; + } + + get width() { + return this.shape.width; + } + + get height() { + return this.shape.height; + } + + get top() { + return this.elevation + this.depth; + } + + get bottom() { + return this.elevation; } /* -------------------------------------------- */ @@ -92,10 +204,6 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { static async createDocuments(data = [], context = {}) { const { parent, pack, ...options } = context; - /* - const created = await this.database.create(this.implementation, { data, options, parent, pack }); - await this._onCreateDocuments(created, context); - return created;*/ let originals = []; let created = []; @@ -105,38 +213,41 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { terrain._id = terrain._id || makeid(); //don't create a terrain that has less than 3 points - if (terrain.points.length < 3) + if ((terrain.shape.type == CONST.DRAWING_TYPES.POLYGON || terrain.shape.type == CONST.DRAWING_TYPES.FREEHAND) && terrain.shape.points.length < 3) continue; - if(terrain.update) + let document = new TerrainDocument(terrain, context); + + /* + if (terrain.update) terrain.update(terrain); if (terrain.document == undefined) { let document = new TerrainDocument(terrain, { parent: canvas.scene }); - terrain = document.data; + terrain.document = document; } + */ //update the data and save it to the scene if (game.user.isGM) { - let key = `flags.enhanced-terrain-layer.terrain${terrain._id}`; - await canvas.scene.update({ [key]: terrain.toJSON() }, { diff: false }); + let key = `flags.enhanced-terrain-layer.terrain${document._id}`; + await canvas.scene.update({ [key]: document.toObject() }, { diff: false }); originals.push(terrain); } //add it to the terrain set - canvas.scene.data.terrain.set(terrain._id, terrain.document); + canvas["#scene"].terrain.set(document._id, document); //if the multiple has changed then update the image - if (terrain.document._object != undefined) - terrain.document.object.draw(); + if (document._object != undefined) + document.object.draw(); else { - terrain.document._object = new Terrain(terrain.document); - canvas.terrain.objects.addChild(terrain.document._object); - terrain.document._object.draw(); + document.object?._onCreate(terrain, options, game.user.id); + //document.object.draw(); } - created.push(terrain); + created.push(document); } if(originals.length) @@ -159,35 +270,44 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { let originals = []; let updated = []; for (let update of updates) { - let terrain = canvas.scene.data.terrain.get(update._id); + let document = canvas["#scene"].terrain.get(update._id); if (game.user.isGM) { - originals.push(terrain.toObject()); + originals.push(document.toObject(false)); } + + delete update.submit; //update this object //mergeObject(this.data, data); - let changes = terrain.data.update(update, { diff: (options.diff !== undefined ? options.diff : true)}); - - if (Object.keys(changes).length) { - //update the data and save it to the scene - if (game.user.isGM) { - let objectdata = duplicate(getflag(canvas.scene, `terrain${terrain.id}`)); - mergeObject(objectdata, changes); - let key = `flags.enhanced-terrain-layer.terrain${terrain.id}`; - await canvas.scene.update({ [key]: objectdata }, { diff: false }); - } + //let changes = await terrain.update(update, { diff: (options.diff !== undefined ? options.diff : true)}); + let changes = foundry.utils.diffObject(document.toObject(false), update, { deletionKeys: true }); + if (foundry.utils.isEmpty(changes)) continue; - //if (data.environment != undefined) - // this.updateEnvironment(); + if (document.object._original) { + mergeObject(document.object._original, changes); + } else + document.alter(changes); - //if the multiple has changed then update the image - if (changes.multiple != undefined || changes.environment != undefined) { - terrain.object.draw(); - } else - terrain.object.refresh(); + //update the data and save it to the scene + if (game.user.isGM) { + let objectdata = duplicate(getflag(document.parent, `terrain${document.id}`)); + mergeObject(objectdata, changes); + let key = `flags.enhanced-terrain-layer.terrain${document.id}`; + await document.parent.update({ [key]: objectdata }, { diff: false }); - updated.push(terrain); + //document.updateSource(changes); } + + //if (changes.environment != undefined) + // this.updateEnvironment(); + + //if the multiple has changed then update the image + if (changes.multiple != undefined || changes.environment != undefined) { + document.object.draw(); + } else + document.object.refresh(); + + updated.push(document); } if (originals.length && !options.isUndo) @@ -206,27 +326,28 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { let updates = []; let originals = []; let deleted = []; - const deleteIds = options.deleteAll ? canvas.scene.data.terrain.keys() : ids; + const deleteIds = options.deleteAll ? canvas["#scene"].terrain.keys() : ids; for (let id of deleteIds) { - let terrain = canvas.scene.data.terrain.find(t => t.id == id); + let terrain = canvas["#scene"].terrain.find(t => t.id == id); if (terrain == undefined) continue; //remove this object from the terrain list - canvas.scene.data.terrain.delete(id); + canvas["#scene"].terrain.delete(id); if (game.user.isGM) { let key = `flags.enhanced-terrain-layer.-=terrain${id}`; updates[key] = null; if (!options.isUndo) - originals.push(terrain.data); + originals.push(terrain); } //remove the PIXI object - canvas.terrain.objects.removeChild(terrain.object); - delete canvas.terrain._controlled[id]; + canvas.primary.removeTerrain(terrain); + //canvas.terrain.objects.removeChild(terrain.object); + delete canvas.terrain.controlled[id]; terrain.object.destroy({ children: true }); deleted.push(terrain); @@ -245,4 +366,85 @@ export class TerrainDocument extends CanvasDocumentMixin(BaseTerrain) { await this._onDeleteDocuments(deleted, context); return deleted; } + + //static async create(data, options) { + + /* + data._id = data._id || makeid(); + + let userId = game.user._id; + + data = data instanceof Array ? data : [data]; + for (let d of data) { + const allowed = Hooks.call(`preCreateTerrain`, this, d, options, userId); + if (allowed === false) { + debug(`Terrain creation prevented by preCreate hook`); + return null; + } + } + + let embedded = data.map(d => { + let object = canvas.terrain.createObject(d); + object._onCreate(options, userId); + canvas["#scene"].terrain.push(d); + canvas.scene.setFlag('enhanced-terrain-layer', 'terrain' + d._id, d); + Hooks.callAll(`createTerrain`, canvas.terrain, d, options, userId); + return d; + }); + + return data.length === 1 ? embedded[0] : embedded; + */ + //} + + //async update(data = {}, context = {}) { + //update this object + /* + mergeObject(this, data); + if (options.save === true) { + //update the data and save it to the scene + let objectdata = duplicate(getflag(canvas.scene, `terrain${this.id}`)); + mergeObject(objectdata, this.document.toObject()); + //let updates = {}; + //updates['flags.enhanced-terrain-layer.terrain' + this.document._id + '.multiple'] = data.multiple; + let key = `flags.enhanced-terrain-layer.terrain${this.document._id}`; + await canvas.scene.update({ [key]: objectdata }, { diff: false }); + //canvas.terrain._costGrid = null; + } + + if (data.environment != undefined) + this.updateEnvironment(); + //await canvas.scene.setFlag("enhanced-terrain-layer", "terrain" + this.document._id, objectdata, {diff: false}); + //if the multiple has changed then update the image + if (data.multiple != undefined || data.environment != undefined) { + this.object.draw(); + } else + this.object.refresh(); + return this; + */ + //} + + async delete(options) { + let key = `flags.enhanced-terrain-layer.-=terrain${this.document._id}`; + await canvas.scene.update({ [key]: null }, { diff: false }); + return this; + } + + alter(changes) { + // 'cause I havn't found an easy way to mass update a document. Pretty sure Foundry does it somewhere.... but until I find it. + for (let [k, v] of Object.entries(changes)) { + if (k == "shape") { + for (let [s_k, s_v] of Object.entries(v)) { + this.shape[s_k] = s_v; + } + } else + this[k] = v; + } + mergeObject(this._source, changes); + } + + //updateEnvironment() { + //this.environment = canvas.terrain.getEnvironments().find(e => e.id == this.environment); + //if (this.environment == undefined && !setting('use-obstacles')) + // this.environment = canvas.terrain.getObstacles().find(e => e.id == this.document.environment); + //} } diff --git a/classes/terrainhud.js b/classes/terrainhud.js index 8a2b8ee..6bacf33 100644 --- a/classes/terrainhud.js +++ b/classes/terrainhud.js @@ -23,7 +23,7 @@ export class TerrainHUD extends BasePlaceableHUD { getData() { var _environments = canvas.terrain.getEnvironments().map(obj => { obj.text = i18n(obj.text); - obj.active = (this.object.data.environment == obj.id); + obj.active = (this.object.document.environment == obj.id); return obj; }); @@ -39,10 +39,8 @@ export class TerrainHUD extends BasePlaceableHUD { return mergeObject(data, { lockedClass: data.locked ? "active" : "", visibilityClass: data.hidden ? "active" : "", - cost: TerrainLayer.multipleText(this.object.multiple), - elevation: this.object.elevation, - depth: this.object.depth, - environment: this.object.environment, + text: TerrainLayer.multipleText(data.multiple), + environment: this.object.document.environmentObject, environments: _environments }); } @@ -88,20 +86,15 @@ export class TerrainHUD extends BasePlaceableHUD { let ctrl = event.currentTarget; let id = ctrl.dataset.environmentId; $('.environment-list .environment-container.active', this.element).removeClass('active'); - if (id != this.object.data.environment) + if (id != this.object.document.environment) $('.environment-list .environment-container[data-environment-id="' + id + '"]', this.element).addClass('active'); const updates = this.layer.controlled.map(o => { - return { _id: o.id, environment: (id != this.object.data.environment ? id : '') }; + return { _id: o.id, environment: (id != this.object.document.environment ? id : '') }; }); return canvas.scene.updateEmbeddedDocuments("Terrain", updates).then(() => { - for (let terrain of this.layer.controlled) { - let data = updates.find(u => { return u._id == terrain.data._id }); - terrain.update(data, { save: false }).then(() => { - $('.environments > img', this.element).attr('src', terrain?.environment?.icon || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); - }); - } + $('.environments > img', this.element).attr('src', this.object.document.environmentObject?.icon || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); }); } @@ -123,7 +116,7 @@ export class TerrainHUD extends BasePlaceableHUD { _onHandleClick(increase, event) { const updates = this.layer.controlled.map(o => { - let mult = TerrainLayer.alterMultiple(o.data.multiple, increase); + let mult = TerrainLayer.alterMultiple(o.document.multiple, increase); //let idx = TerrainLayer.multipleOptions.indexOf(mult); //idx = Math.clamped((increase ? idx + 1 : idx - 1), 0, TerrainLayer.multipleOptions.length - 1); return { _id: o.id, multiple: mult }; //TerrainLayer.multipleOptions[idx] }; @@ -131,7 +124,7 @@ export class TerrainHUD extends BasePlaceableHUD { let that = this; return canvas.scene.updateEmbeddedDocuments("Terrain", updates).then(() => { - $('.terrain-cost', that.element).html(`${TerrainLayer.multipleText(that.object.multiple)}`); + $('.terrain-cost', that.element).html(`${TerrainLayer.multipleText(that.object.document.multiple)}`); }); /* @@ -155,7 +148,7 @@ export class TerrainHUD extends BasePlaceableHUD { async _onToggleVisibility(event) { event.preventDefault(); - const isHidden = this.object.data.hidden; + const isHidden = this.object.document.hidden; event.currentTarget.classList.toggle("active", !isHidden); @@ -167,7 +160,7 @@ export class TerrainHUD extends BasePlaceableHUD { async _onToggleLocked(event) { event.preventDefault(); - const isLocked = this.object.data.locked; + const isLocked = this.object.document.locked; event.currentTarget.classList.toggle("active", !isLocked); @@ -182,14 +175,25 @@ export class TerrainHUD extends BasePlaceableHUD { /** @override */ setPosition() { $('#hud').append(this.element); - let { x, y, width, height } = this.object.terrain.hitArea; + let { x, y, width, height } = this.object.hitArea; + /* const c = 70; const p = 0; const position = { width: width + (c * 2), // + (p * 2), height: height + (p * 2), - left: x + this.object.data.x - c - p, - top: y + this.object.data.y - p + left: x + this.object.document.x - c - p, + top: y + this.object.document.y - p + }; + this.element.css(position); + */ + const c = 70; + const p = -10; + const position = { + width: width + (c * 2) + (p * 2), + height: height + (p * 2), + left: x + this.object.document.x - c - p, + top: y + this.object.document.y - p }; this.element.css(position); } diff --git a/classes/terraininfo.js b/classes/terraininfo.js index d39e635..6cce1ad 100644 --- a/classes/terraininfo.js +++ b/classes/terraininfo.js @@ -98,8 +98,8 @@ export class TokenTerrainInfo extends TerrainInfo { get shape() { const left = 0; const top = 0; - const right = left + this.token.data.width * canvas.grid.w; - const bottom = top + this.token.data.height * canvas.grid.h; + const right = left + this.token.width * canvas.dimensions.size; + const bottom = top + this.token.height * canvas.dimensions.size; return new PIXI.Polygon(left, top, right, top, right, bottom, left, bottom); } } \ No newline at end of file diff --git a/classes/terrainlayer.js b/classes/terrainlayer.js index 56a5e81..0654f22 100644 --- a/classes/terrainlayer.js +++ b/classes/terrainlayer.js @@ -1,7 +1,7 @@ import { Terrain } from './terrain.js'; import { TerrainConfig } from './terrainconfig.js'; import { TerrainHUD } from './terrainhud.js'; -import { TerrainDocument, TerrainData } from './terraindocument.js'; +import { TerrainDocument } from './terraindocument.js'; import { PolygonTerrainInfo, TemplateTerrainInfo, TokenTerrainInfo } from './terraininfo.js'; import { makeid, log, debug, warn, error, i18n, setting, getflag } from '../terrain-main.js'; import EmbeddedCollection from "../../../common/abstract/embedded-collection.mjs"; @@ -24,12 +24,14 @@ export class TerrainLayer extends PlaceablesLayer { /** @override */ static get layerOptions() { return mergeObject(super.layerOptions, { - zIndex: 35, //15, + name: "terrain", + zIndex: 5, canDragCreate: game.user.isGM, canDelete: game.user.isGM, controllableObjects: game.user.isGM, rotatableObjects: false, objectClass: Terrain, + sortActiveTop: true, //sheetClass: TerrainConfig, sheetClasses: { base: { @@ -45,7 +47,7 @@ export class TerrainLayer extends PlaceablesLayer { } getDocuments() { - return canvas.scene?.data.terrain || null; + return canvas["#scene"].terrain || null; } get gridPrecision() { @@ -118,17 +120,24 @@ export class TerrainLayer extends PlaceablesLayer { set showterrain(value) { this._showterrain = value; - canvas.terrain.visible = this._showterrain; + canvas.terrain.visible = canvas.terrain.objects.visible = (this._showterrain || ui.controls.activeControl == 'terrain'); + this.refreshVisibility(); if (game.user.isGM) game.settings.set("enhanced-terrain-layer", "showterrain", this._showterrain); } + refreshVisibility() { + for (let t of canvas.terrain.placeables) { + t.visible = t.isVisible; + } + } + elevation(pts, options = {}) { pts = pts instanceof Array ? pts : [pts]; let results = []; - const hx = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.grid.w / 2); - const hy = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.grid.h / 2); + const hx = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.dimensions.size / 2); + const hy = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.dimensions.size / 2); for (let pt of pts) { let [gx, gy] = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? [pt.x, pt.y] : canvas.grid.grid.getPixelsFromGridPosition(pt.y, pt.x)); @@ -138,10 +147,10 @@ export class TerrainLayer extends PlaceablesLayer { //get the cost for the terrain layer for (let terrain of this.placeables) { - const testX = tx - terrain.data.x; - const testY = ty - terrain.data.y; - if (terrain?.shape?.contains(testX, testY)) { - results.push({ top: terrain.top, bottom: terrain.bottom, terrain: terrain }); + const testX = tx - terrain.document.x; + const testY = ty - terrain.document.y; + if (terrain?.document?.shape?.contains(testX, testY)) { + results.push({ top: terrain.document.top, bottom: terrain.document.bottom, terrain: terrain }); } } } @@ -150,7 +159,7 @@ export class TerrainLayer extends PlaceablesLayer { } calcElevationFromOptions(options) { - return (options.elevation === false ? null : (options.elevation != undefined ? options.elevation : options?.token?.data?.elevation)); + return (options.elevation === false ? null : (options.elevation != undefined ? options.elevation : options?.token?.document?.elevation)); } listTerrain(options = {}) { @@ -159,13 +168,13 @@ export class TerrainLayer extends PlaceablesLayer { const terrainInfos = options.list || []; for (const terrain of this.placeables) { - if (elevation < terrain.bottom || elevation > terrain.top) + if (elevation < terrain.document.bottom || elevation > terrain.document.top) continue; - if (terrain.multiple == 1) + if (terrain.document.multiple == 1) continue; - if (options.ignore?.includes(terrain.data.environment)) + if (options.ignore?.includes(terrain.document.environment)) continue; - let reducers = options.reduce?.filter(e => e.id == terrain.data.environment || (useObstacles && e.id == terrain.obstacle)); + let reducers = options.reduce?.filter(e => e.id == terrain.document.environment || (useObstacles && e.id == terrain.document.obstacle)); terrainInfos.push(new PolygonTerrainInfo(terrain, reducers)); } return terrainInfos; @@ -177,20 +186,21 @@ export class TerrainLayer extends PlaceablesLayer { const terrainInfos = options.list || []; for (const template of canvas.templates.placeables) { - const terrainFlag = template.data.flags['enhanced-terrain-layer']; + const terrainFlag = template.flags['enhanced-terrain-layer']; if (!terrainFlag) continue; const terraincost = terrainFlag.multiple ?? 1; const terrainbottom = terrainFlag.elevation ?? Terrain.defaults.elevation; const terraintop = terrainbottom + (terrainFlag.depth ?? Terrain.defaults.depth); const environment = terrainFlag.environment || ''; + const obstacle = terrainFlag.obstacle || ''; if (elevation < terrainbottom || elevation > terraintop) continue; if (terraincost == 1) continue; if (options.ignore?.includes(environment)) continue; - let reducers = options.reduce?.filter(e => e.id == terrain.data.environment || (useObstacles && e.id == terrain.obstacle)); + let reducers = options.reduce?.filter(e => e.id == environment || (useObstacles && e.id == obstacle)); terrainInfos.push(new TemplateTerrainInfo(template, reducers)); } return terrainInfos; @@ -201,8 +211,8 @@ export class TerrainLayer extends PlaceablesLayer { let isDead = options.isDead || function (token) { return !!token.actor?.effects?.find(e => { - const core = e.data.flags["core"]; - return core && core["statusId"] === CONFIG.Combat.defeatedStatusId; + const core = e.flags["core"]; + return core && core["statusId"] === "dead"; }); } @@ -212,9 +222,9 @@ export class TerrainLayer extends PlaceablesLayer { for (const token of canvas.tokens.placeables) { if (token.id == tokenId) continue; - if (token.data.hidden) + if (token.hidden) continue; - if (elevation != undefined && token.data.elevation != elevation) + if (elevation != undefined && token.elevation != elevation) continue; let dead = isDead(token); if ((setting("dead-cause-difficult") && dead) || (setting("tokens-cause-difficult") && !dead)) { @@ -234,8 +244,8 @@ export class TerrainLayer extends PlaceablesLayer { costWithTerrain(pts, terrain, options = {}) { pts = pts instanceof Array ? pts : [pts]; - const hx = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.grid.w / 2); - const hy = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.grid.h / 2); + const hx = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.dimensions.size / 2); + const hy = (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || options.ignoreGrid === true ? 0 : canvas.dimensions.size / 2); let calculate = options.calculate || 'maximum'; let calculateFn; @@ -262,8 +272,8 @@ export class TerrainLayer extends PlaceablesLayer { const ty = (gy + hy); for (const terrainInfo of terrain) { - const testX = tx - terrainInfo.object.data.x; - const testY = ty - terrainInfo.object.data.y; + const testX = tx - terrainInfo.object.document.x; + const testY = ty - terrainInfo.object.document.y; if (!terrainInfo.shape.contains(testX, testY)) continue; @@ -301,12 +311,12 @@ export class TerrainLayer extends PlaceablesLayer { } terrainFromPixels(x, y) { - const hx = (x + (canvas.grid.w / 2)); - const hy = (y + (canvas.grid.h / 2)); + const hx = (x + (canvas.dimensions.size / 2)); + const hy = (y + (canvas.dimensions.size / 2)); let terrains = this.placeables.filter(t => { - const testX = hx - t.data.x; - const testY = hy - t.data.y; + const testX = hx - t.x; + const testY = hy - t.y; return t.shape.contains(testX, testY); }); @@ -323,13 +333,19 @@ export class TerrainLayer extends PlaceablesLayer { return canvas.hud.terrain; } + configureDefault() { + //const defaults = game.settings.get("core", TerrainLayer.DEFAULT_CONFIG_SETTING); + //const d = TerrainDocument.fromSource(defaults); + //new TerrainConfig(d).render(true); + } + async draw() { - canvas.scene.data.terrain = new EmbeddedCollection(canvas.scene.data, [], Terrain); - let etl = canvas.scene.data.flags['enhanced-terrain-layer']; + canvas["#scene"].terrain = new foundry.abstract.EmbeddedCollection(canvas.scene, [], Terrain); + let etl = canvas.scene.flags['enhanced-terrain-layer']; if (etl) { for (let [k, v] of Object.entries(etl)) { if (k.startsWith('terrain')) { - if (k != 'terrainundefined' && v != undefined && v.x != undefined && v.y != undefined && v._id != undefined && v.points != undefined) { + if (k != 'terrainundefined' && v != undefined && v.x != undefined && v.y != undefined && v._id != undefined && v.shape?.points != undefined) { //lets correct any changes let change = false; if (v.environment == '' && v.obstacle != '') { @@ -346,12 +362,14 @@ export class TerrainLayer extends PlaceablesLayer { change = true; } + change = !!TerrainDocument.migrateData(v); + if (change) await canvas.scene.setFlag('enhanced-terrain-layer', k, v); //add this the the terrain collection let document = new TerrainDocument(v, { parent: canvas.scene }); - canvas.scene.data.terrain.set(v._id, document); + canvas["#scene"].terrain.set(v._id, document); } else await canvas.scene.unsetFlag('enhanced-terrain-layer', k); @@ -374,7 +392,9 @@ export class TerrainLayer extends PlaceablesLayer { this.preview = this.addChild(new PIXI.Container()); const documents = this.getDocuments(); - const promises = documents.map(doc => { return doc.object.draw(); }); + const promises = documents.map(doc => { + return doc.object?.draw(); + }); // Wait for all objects to draw this.visible = true; @@ -390,22 +410,60 @@ export class TerrainLayer extends PlaceablesLayer { game.socket.emit('module.enhanced-terrain-layer', { action: 'toggle', arguments: [this._showterrain] }); } - deactivate() { - //if (this.objects) { - super.deactivate(); - if (this.objects) this.objects.visible = true; - //} + _deactivate() { + super._deactivate(); + this.visible = this._showterrain; + if (this.objects) this.objects.visible = this._showterrain; + this.refreshVisibility(); } _getNewTerrainData(origin) { + const tool = game.activeTool; + const data = mergeObject(Terrain.defaults, { x: origin.x, y: origin.y, - points: [[0,0]] + author: game.user.id }); - return data; + + // Mandatory additions + delete data._id; + if (tool !== "freehand") { + origin = canvas.grid.getSnappedPosition(origin.x, origin.y, this.gridPrecision); + data.x = origin.x; + data.y = origin.y; + } + + switch (tool) { + case "rect": + data.shape.type = Drawing.SHAPE_TYPES.RECTANGLE; + data.shape.width = 1; + data.shape.height = 1; + break; + case "ellipse": + data.shape.type = Drawing.SHAPE_TYPES.ELLIPSE; + data.shape.width = 1; + data.shape.height = 1; + break; + case "freehand": + data.shape.type = Drawing.SHAPE_TYPES.POLYGON; + data.shape.points = [0, 0]; + data.bezierFactor = data.bezierFactor ?? 0.5; + break; + case "polygon": + data.shape.type = Drawing.SHAPE_TYPES.POLYGON; + data.shape.points = [0, 0]; + data.bezierFactor = 0; + break; + } + + return TerrainDocument.cleanData(data); } + //get documentCollection() { + // return canvas + //} + /* -------------------------------------------- */ /* Event Listeners and Handlers */ /* -------------------------------------------- */ @@ -415,10 +473,10 @@ export class TerrainLayer extends PlaceablesLayer { const { preview, createState, originalEvent } = event.data; // Continue polygon point placement - if (createState >= 1 && preview instanceof Terrain) { + if (createState >= 1 && preview.isPolygon) { let point = event.data.destination; - if (!originalEvent.shiftKey) point = canvas.grid.getSnappedPosition(point.x, point.y, this.gridPrecision); - preview._addPoint(point, false); + const snap = !originalEvent.shiftKey; + preview._addPoint(point, { snap, round: true }); preview._chain = true; // Note that we are now in chain mode return preview.refresh(); } @@ -434,13 +492,13 @@ export class TerrainLayer extends PlaceablesLayer { const { createState, preview } = event.data; // Conclude polygon placement with double-click - if (createState >= 1) { + if (createState >= 1 && preview.isPolygon) { event.data.createState = 2; return this._onDragLeftDrop(event); } else if (createState == 0 || createState == undefined) { //add a default square - let gW = canvas.grid.w; - let gH = canvas.grid.h; + let gW = canvas.dimensions.size; + let gH = canvas.dimensions.size; //let pos = canvas.grid.getSnappedPosition(event.data.origin.x, event.data.origin.y, 1); let [tX, tY] = canvas.grid.grid.getGridPositionFromPixels(event.data.origin.x, event.data.origin.y); @@ -448,23 +506,25 @@ export class TerrainLayer extends PlaceablesLayer { let points = []; if (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS || canvas.grid.type == CONST.GRID_TYPES.SQUARE) - points = [[0, 0], [gW, 0], [gW, gH], [0, gH], [0, 0]]; + points = [0, 0, gW, 0, gW, gH, 0, gH, 0, 0]; else if (canvas.grid.type == CONST.GRID_TYPES.HEXEVENR || canvas.grid.type == CONST.GRID_TYPES.HEXODDR) - points = [[gW / 2, 0], [gW, gH * 0.25], [gW, gH * 0.75], [gW / 2, gH], [0, gH * 0.75], [0, gH * 0.25], [gW / 2, 0]]; + points = [gW / 2, 0, gW, gH * 0.25, gW, gH * 0.75, gW / 2, gH, 0, gH * 0.75, 0, gH * 0.25, gW / 2, 0]; else if (canvas.grid.type == CONST.GRID_TYPES.HEXEVENQ || canvas.grid.type == CONST.GRID_TYPES.HEXODDQ) - points = [[0, gH / 2], [gW * 0.25, 0], [gW * 0.75, 0], [gW, gH / 2], [gW * 0.75, gH], [gW * 0.25, gH], [0, gH / 2]]; + points = [0, gH / 2, gW * 0.25, 0, gW * 0.75, 0, gW, gH / 2, gW * 0.75, gH, gW * 0.25, gH, 0, gH / 2]; const data = mergeObject(Terrain.defaults, { x: gX - (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS ? (gW / 2) : 0), y: gY - (canvas.grid.type == CONST.GRID_TYPES.GRIDLESS ? (gH / 2) : 0), - points: points, - width: gW, - height: gH + shape: { + points: points, + width: gW, + height: gH + } }); - const document = new TerrainDocument(data, { parent: canvas.scene }); + //const document = new TerrainDocument(data, { parent: canvas.scene }); - this.createTerrain(document.data); + this.createTerrain(data); } // Standard double-click handling @@ -474,8 +534,8 @@ export class TerrainLayer extends PlaceablesLayer { /* -------------------------------------------- */ /** @override */ - _onDragLeftStart(event) { - super._onDragLeftStart(event); + async _onDragLeftStart(event) { + await super._onDragLeftStart(event); const data = this._getNewTerrainData(event.data.origin); const document = new TerrainDocument(data, { parent: canvas.scene }); @@ -489,12 +549,14 @@ export class TerrainLayer extends PlaceablesLayer { /** @override */ _onDragLeftMove(event) { const { preview, createState } = event.data; - if (!preview) return; - //if (preview.parent === null) { // In theory this should never happen, but rarely does - // this.preview.addChild(preview); - //} + if (!preview || preview._destroyed ) return; + if (preview.parent === null) { // In theory this should never happen, but rarely does + this.preview.addChild(preview); + } if (createState >= 1) { preview._onMouseDraw(event); + const isFreehand = game.activeTool === "freehand"; + if (!preview.isPolygon || isFreehand) event.data.createState = 2; } } @@ -504,23 +566,28 @@ export class TerrainLayer extends PlaceablesLayer { * Handling of mouse-up events which conclude a new object creation after dragging * @private */ - _onDragLeftDrop(event) { - const { createState, destination, origin, preview } = event.data; + async _onDragLeftDrop(event) { + const { createState, destination, origin, originalEvent, preview } = event.data; // Successful drawing completion if (createState === 2) { - const distance = Math.hypot(destination.x - origin.x, destination.y - origin.y); + const distance = Math.hypot(destination.x - preview.x, destination.y - preview.y); const minDistance = distance >= (canvas.dimensions.size / this.gridPrecision); - const completePolygon = (preview.data.points.length > 2); + const completePolygon = preview.isPolygon && (preview.document.shape.points.length > 4); // Create a completed terrain if (minDistance || completePolygon) { event.data.createState = 0; - const data = preview.data; + const data = preview.document.toObject(false); // Adjust the final data - this.createTerrain(data); preview._chain = false; + //const createData = this.constructor.placeableClass.normalizeShape(data); + let terrain = await this.createTerrain(data); + + const o = terrain.object; + o._creating = true; + if (game.activeTool !== "freehand") o.control({ isNew: true }); } // Cancel the preview @@ -546,7 +613,7 @@ export class TerrainLayer extends PlaceablesLayer { if (preview?._chain) { preview._removePoint(); preview.refresh(); - if (preview.data.points.length) return event.preventDefault(); + if (preview.document.shape.points.length) return event.preventDefault(); } super._onDragLeftCancel(event); } @@ -595,7 +662,7 @@ export class TerrainLayer extends PlaceablesLayer { // Iterate over objects const toCreate = []; for (let c of this._copy) { - let data = c.document.toObject(); + let data = c.document.toObject(false); delete data._id; // Constrain the destination position @@ -650,7 +717,7 @@ export class TerrainLayer extends PlaceablesLayer { toRelease.forEach(obj => obj.release(releaseOptions)); // Control new objects - if (isObjectEmpty(controlOptions)) controlOptions.releaseOthers = false; + if (isEmpty(controlOptions)) controlOptions.releaseOthers = false; const toControl = newSet.filter(obj => !oldSet.includes(obj)); toControl.forEach(obj => obj.control(controlOptions)); @@ -662,9 +729,8 @@ export class TerrainLayer extends PlaceablesLayer { createTerrain(data) { //data = mergeObject(Terrain.defaults, data); - const createData = Terrain.normalizeShape(data); - const cls = getDocumentClass("Terrain"); + const createData = this.constructor.placeableClass.normalizeShape(data); // Create the object return cls.create(createData, { parent: canvas.scene }); /*.then(d => { @@ -681,8 +747,8 @@ export class TerrainLayer extends PlaceablesLayer { //This is used for players, to add an remove on the fly _createTerrain(data, options = {}) { - let toCreate = data.map(d => new TerrainData(d)); - TerrainDocument.createDocuments(toCreate, { parent: canvas.scene }); + //let toCreate = data.map(d => new TerrainData(d)); + TerrainDocument.createDocuments(data, { parent: canvas.scene }); /* let toCreate = data.map(d => { @@ -695,7 +761,7 @@ export class TerrainLayer extends PlaceablesLayer { let userId = game.user._id; let object = canvas.terrain.createObject(data); object._onCreate(options, userId); - canvas.scene.data.terrain.push(data);*/ + canvas["#scene"].terrain.push(data);*/ } _updateTerrain(data, options = {}) { @@ -707,9 +773,9 @@ export class TerrainLayer extends PlaceablesLayer { } //refresh all the terrain on this layer - refresh(icons) { + refresh() { for (let terrain of this.placeables) { - terrain.refresh(icons); + terrain.refresh(); } } diff --git a/classes/terrainshape.js b/classes/terrainshape.js new file mode 100644 index 0000000..a3e0e28 --- /dev/null +++ b/classes/terrainshape.js @@ -0,0 +1,265 @@ +import { makeid, log, setting, debug, getflag } from '../terrain-main.js'; + +export class TerrainShape extends DrawingShape { + refresh() { + if (this._destroyed) return; + const doc = this.document; + this.clear(); + + let drawAlpha = (ui.controls.activeControl == 'terrain' ? 1.0 : doc.alpha); + + // Outer Stroke + let sc = Color.from(doc.color || "#FFFFFF"); + let lStyle = new PIXI.LineStyle(); + 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 }); + this.lineStyle(lStyle); + + // Fill Color or Texture + if (doc.fillType) { + const fc = Color.from(doc.color || "#FFFFFF"); + if ((doc.fillType === CONST.DRAWING_FILL_TYPES.PATTERN)) { + if (this.object.texture) { + let sW = (canvas.dimensions.size / (this.object.texture.width * (setting('terrain-image') == 'diagonal' ? 2 : 1))); + let sH = (canvas.dimensions.size / (this.object.texture.height * (setting('terrain-image') == 'diagonal' ? 2 : 1))); + this.beginTextureFill({ + texture: this.object.texture, + color: fc || 0xFFFFFF, + alpha: drawAlpha, + matrix: new PIXI.Matrix().scale(sW, sH) + }); + } + } else this.beginFill(fc, doc.fillAlpha); + } + + // Draw the shape + switch (doc.shape.type) { + case Drawing.SHAPE_TYPES.RECTANGLE: + this.#drawRectangle(); + break; + case Drawing.SHAPE_TYPES.ELLIPSE: + this.#drawEllipse(); + break; + case Drawing.SHAPE_TYPES.POLYGON: + if (this.document.bezierFactor) this.#drawFreehand(); + else this.#drawPolygon(); + break; + } + + // Conclude fills + this.lineStyle(0x000000, 0.0).closePath().endFill(); + + // Set the drawing position + this.setPosition(); + } + + /* -------------------------------------------- */ + + /** + * Draw rectangular shapes. + * @private + */ + #drawRectangle() { + const { shape, strokeWidth } = this.document; + const hs = strokeWidth / 2; + + if (this.document.hidden) { + this.drawDashedPolygon([0, 0, shape.width, 0, shape.width, shape.height, 0, shape.height,0, 0], 0, 0, 0, strokeWidth * 2, strokeWidth * 3, 0); + this._lineStyle.width = 0; + } + this.drawRect(hs, hs, shape.width - (2 * hs), shape.height - (2 * hs)); + + this._lineStyle.width = strokeWidth; + } + + /* -------------------------------------------- */ + + /** + * Draw ellipsoid shapes. + * @private + */ + #drawEllipse() { + const { shape, strokeWidth } = this.document; + const hw = shape.width / 2; + const hh = shape.height / 2; + const hs = strokeWidth / 2; + const width = Math.max(Math.abs(hw) - hs, 0); + const height = Math.max(Math.abs(hh) - hs, 0); + this.drawEllipse(hw, hh, width, height); + } + + /* -------------------------------------------- */ + + /** + * Draw polygonal shapes. + * @private + */ + #drawPolygon() { + const { shape, strokeWidth } = this.document; + const points = shape.points; + if (points.length < 4) return; + else if (points.length === 4) this.endFill(); + + if (this.document.hidden) { + this.drawDashedPolygon(points, 0, 0, 0, strokeWidth * 2, strokeWidth * 3, 0); + this._lineStyle.width = 0; + } + this.drawPolygon(points); + this._lineStyle.width = strokeWidth; + } + + /* -------------------------------------------- */ + + /** + * Draw freehand shapes with bezier spline smoothing. + * @private + */ + #drawFreehand() { + const { bezierFactor, fillType, shape } = this.document; + + // Get drawing points + let points = shape.points; + + // Draw simple polygons if only 2 points are present + if (points.length <= 4) return this.#drawPolygon(); + + // Set initial conditions + const factor = bezierFactor ?? 0.5; + let previous = first; + let point = points.slice(2, 4); + points = points.concat(last); // Repeat the final point so the bezier control points know how to finish + let cp0 = this.#getBezierControlPoints(factor, last, previous, point).nextCP; + let cp1; + let nextCP; + + // Begin iteration + this.moveTo(first[0], first[1]); + for (let i = 4; i < points.length - 1; i += 2) { + const next = [points[i], points[i + 1]]; + if (next) { + let bp = this.#getBezierControlPoints(factor, previous, point, next); + cp1 = bp.cp1; + nextCP = bp.nextCP; + } + + // First point + if ((i === 4) && !isClosed) { + this.quadraticCurveTo(cp1.x, cp1.y, point[0], point[1]); + } + + // Last Point + else if ((i === points.length - 2) && !isClosed) { + this.quadraticCurveTo(cp0.x, cp0.y, point[0], point[1]); + } + + // Bezier points + else { + this.bezierCurveTo(cp0.x, cp0.y, cp1.x, cp1.y, point[0], point[1]); + } + + // Increment + previous = point; + point = next; + cp0 = nextCP; + } + } + + /* -------------------------------------------- */ + + /** + * Attribution: The equations for how to calculate the bezier control points are derived from Rob Spencer's article: + * http://scaledinnovation.com/analytics/splines/aboutSplines.html + * @param {number} factor The smoothing factor + * @param {number[]} previous The prior point + * @param {number[]} point The current point + * @param {number[]} next The next point + * @returns {{cp1: Point, nextCP: Point}} The bezier control points + * @private + */ + #getBezierControlPoints(factor, previous, point, next) { + + // Calculate distance vectors + const vector = { x: next[0] - previous[0], y: next[1] - previous[1] }; + const preDistance = Math.hypot(point[0] - previous[0], point[1] - previous[1]); + const postDistance = Math.hypot(next[0] - point[0], next[1] - point[1]); + const distance = preDistance + postDistance; + + // Compute control point locations + const cp0d = distance === 0 ? 0 : factor * (preDistance / distance); + const cp1d = distance === 0 ? 0 : factor * (postDistance / distance); + + // Return points + return { + cp1: { + x: point[0] - (vector.x * cp0d), + y: point[1] - (vector.y * cp0d) + }, + nextCP: { + x: point[0] + (vector.x * cp1d), + y: point[1] + (vector.y * cp1d) + } + }; + } + + drawDashedPolygon(points, x, y, rotation, dash, gap, offsetPercentage) { + var i; + var p1; + var p2; + var dashLeft = 0; + var gapLeft = 0; + if (offsetPercentage > 0) { + var progressOffset = (dash + gap) * offsetPercentage; + if (progressOffset < dash) dashLeft = dash - progressOffset; + else gapLeft = gap - (progressOffset - dash); + } + var rotatedPolygons = []; + for (i = 0; i < points.length - 1; i += 2) { + var p = { x: points[i], y: points[i + 1] }; + var cosAngle = Math.cos(rotation); + var sinAngle = Math.sin(rotation); + var dx = p.x; + var dy = p.y; + p.x = (dx * cosAngle - dy * sinAngle); + p.y = (dx * sinAngle + dy * cosAngle); + rotatedPolygons.push(p); + } + for (i = 0; i < rotatedPolygons.length; i++) { + p1 = rotatedPolygons[i]; + if (i == rotatedPolygons.length - 1) p2 = rotatedPolygons[0]; + else p2 = rotatedPolygons[i + 1]; + var dx = p2.x - p1.x; + var dy = p2.y - p1.y; + if (dx == 0 && dy == 0) + continue; + var len = Math.sqrt(dx * dx + dy * dy); + var normal = { x: dx / len, y: dy / len }; + var progressOnLine = 0; + let mx = x + p1.x + gapLeft * normal.x; + let my = y + p1.y + gapLeft * normal.y; + this.moveTo(mx, my); + while (progressOnLine <= len) { + progressOnLine += gapLeft; + if (dashLeft > 0) progressOnLine += dashLeft; + else progressOnLine += dash; + if (progressOnLine > len) { + dashLeft = progressOnLine - len; + progressOnLine = len; + } else { + dashLeft = 0; + } + let lx = x + p1.x + progressOnLine * normal.x; + let ly = y + p1.y + progressOnLine * normal.y; + this.lineTo(lx, ly); + progressOnLine += gap; + if (progressOnLine > len && dashLeft == 0) { + gapLeft = progressOnLine - len; + //console.log(progressOnLine, len, gap); + } else { + gapLeft = 0; + let mx = x + p1.x + progressOnLine * normal.x; + let my = y + p1.y + progressOnLine * normal.y; + this.moveTo(mx, my); + } + } + } + } +} \ No newline at end of file diff --git a/css/terrainlayer.css b/css/terrainlayer.css index f1417ba..891c7ea 100644 --- a/css/terrainlayer.css +++ b/css/terrainlayer.css @@ -10,6 +10,11 @@ text-align: center; }*/ +#controls ol.control-tools.sub-controls { + flex-grow: 0; + margin-right: 6px; +} + #terrainlayer-tools.control-tools { list-style: none; padding: 0; diff --git a/js/controls.js b/js/controls.js index 4d8bbc7..e3967ce 100644 --- a/js/controls.js +++ b/js/controls.js @@ -19,9 +19,24 @@ Hooks.on('getSceneControlButtons', (controls) => { icon: 'fas fa-expand' }, { - name: 'addterrain', - title: game.i18n.localize('EnhancedTerrainLayer.add'), - icon: 'fas fa-marker' + name: "rect", + title: "CONTROLS.DrawingRect", + icon: "fa-solid fa-square" + }, + { + name: "ellipse", + title: "CONTROLS.DrawingEllipse", + icon: "fa-solid fa-circle" + }, + { + name: "polygon", + title: "CONTROLS.DrawingPoly", + icon: "fa-solid fa-draw-polygon" + }, + { + name: "freehand", + title: "CONTROLS.DrawingFree", + icon: "fa-solid fa-signature" }, { name: 'terraintoggle', diff --git a/js/settings.js b/js/settings.js index 078a126..6720a1c 100644 --- a/js/settings.js +++ b/js/settings.js @@ -74,7 +74,7 @@ export const registerSettings = function () { hint: "EnhancedTerrainLayer.show-icon.hint", scope: "world", config: true, - default: true, + default: false, type: Boolean, onChange: debouncedRefresh }); diff --git a/lang/en.json b/lang/en.json index 9762852..cdc6043 100644 --- a/lang/en.json +++ b/lang/en.json @@ -8,7 +8,7 @@ "EnhancedTerrainLayer.DecreaseCost": "Decrease Cost", "EnhancedTerrainLayer.TerrainCost": "Terrain Cost", "EnhancedTerrainLayer.Cost": "Cost", - "EnhancedTerrainLayer.Configure": "Configure this scene terrain.", + "EnhancedTerrainLayer.Configure": "Configure this terrain.", "EnhancedTerrainLayer.Configuration": "Terrain Configuration", "EnhancedTerrainLayer.UpdateTerrain": "Update Terrain", "EnhancedTerrainLayer.MovementCost": "Movement Cost", diff --git a/module.json b/module.json index 65eb75d..fa85f18 100644 --- a/module.json +++ b/module.json @@ -1,55 +1,62 @@ -{ - "name": "enhanced-terrain-layer", - "title": "Enhanced Terrain Layer", - "description": "A base module that adds a Terrain Layer to Foundry. Used as a library for Rulers and other modules", - "version": "1.0.42", - "minimumCoreVersion": "9", - "compatibleCoreVersion": "9", - "author": "IronMonk", - "authors": [{ - "name": "IronMonk", - "discord": "ironmonk88#4075", - "patreon": "https://www.patreon.com/ironmonk", - "ko-fi": "https://ko-fi.com/ironmonk88" - }], - "socket": true, - "languages": [ - { - "lang": "en", - "name": "English", - "path": "lang/en.json" - }, - { - "lang": "es", - "name": "Spanish", - "path": "lang/es.json" - }, - { - "lang": "de", - "name": "Deutsch", - "path": "lang/de.json" - }, - { - "lang": "ko", - "name": "Korean", - "path": "lang/ko.json" - }, - { - "lang": "zh-tw", - "name": "正體中文", - "path": "lang/zh-tw.json" - } - ], - "esmodules": [ - "terrain-main.js", - "js/controls.js", - "js/settings.js" - ], - "styles": [ "css/terrainlayer.css" ], - "packs": [], - "url" : "https://github.com/ironmonk88/enhanced-terrain-layer", - "download" : "https://github.com/ironmonk88/enhanced-terrain-layer/archive/1.0.42.zip", - "manifest" : "https://github.com/ironmonk88/enhanced-terrain-layer/releases/latest/download/module.json", - "bugs": "https://github.com/ironmonk88/enhanced-terrain-layer/issues", - "allowBugReporter": true -} +{ + "title": "Enhanced Terrain Layer", + "description": "A base module that adds a Terrain Layer to Foundry. Used as a library for Rulers and other modules", + "version": "1.0.43", + "authors": [ + { + "name": "IronMonk", + "discord": "ironmonk88#4075", + "patreon": "https://www.patreon.com/ironmonk", + "ko-fi": "https://ko-fi.com/ironmonk88" + } + ], + "socket": true, + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + }, + { + "lang": "es", + "name": "Spanish", + "path": "lang/es.json" + }, + { + "lang": "de", + "name": "Deutsch", + "path": "lang/de.json" + }, + { + "lang": "ko", + "name": "Korean", + "path": "lang/ko.json" + }, + { + "lang": "zh-tw", + "name": "正體中文", + "path": "lang/zh-tw.json" + } + ], + "esmodules": [ + "terrain-main.js", + "js/controls.js", + "js/settings.js" + ], + "styles": [ + "css/terrainlayer.css" + ], + "url": "https://github.com/ironmonk88/enhanced-terrain-layer", + "download": "https://github.com/ironmonk88/enhanced-terrain-layer/archive/1.0.43.zip", + "manifest": "https://github.com/ironmonk88/enhanced-terrain-layer/releases/latest/download/module.json", + "bugs": "https://github.com/ironmonk88/enhanced-terrain-layer/issues", + "allowBugReporter": true, + "id": "enhanced-terrain-layer", + "compatibility": { + "minimum": "10", + "verified": "10" + }, + "name": "enhanced-terrain-layer", + "minimumCoreVersion": "10", + "compatibleCoreVersion": "10" +} \ No newline at end of file diff --git a/templates/terrain-config.html b/templates/terrain-config.html index 75cafe8..8460b45 100644 --- a/templates/terrain-config.html +++ b/templates/terrain-config.html @@ -3,54 +3,54 @@ {{localize "EnhancedTerrainLayer.Configure"}}

- - + +
- - + +
- - + +
- - + +
- +
- {{ colorPicker name="drawcolor" value=object.drawcolor}} + {{ colorPicker name="drawcolor" value=data.drawcolor}}
- - {{object.opacity}} + + {{data.opacity}}
- +
- +
@@ -58,10 +58,10 @@
@@ -69,18 +69,18 @@
{{/if}}
- +
- +
diff --git a/templates/terrain-hud.html b/templates/terrain-hud.html index 808fe80..2170d65 100644 --- a/templates/terrain-hud.html +++ b/templates/terrain-hud.html @@ -1,12 +1,12 @@
-
+
- {{{cost}}} + {{{text}}}
-
- +
+
{{#each environments}}
{{this.text}}
@@ -14,13 +14,13 @@
-
+
-
{{this.elevation}}
+
{{elevation}}
-
+
-
{{this.depth}}
+
{{depth}}
diff --git a/terrain-main.js b/terrain-main.js index 1fe0a83..289e522 100644 --- a/terrain-main.js +++ b/terrain-main.js @@ -2,7 +2,8 @@ import { TerrainLayer } from './classes/terrainlayer.js'; import { TerrainHUD } from './classes/terrainhud.js'; import { TerrainConfig } from './classes/terrainconfig.js'; import { Terrain } from './classes/terrain.js'; -import { BaseTerrain, TerrainDocument } from './classes/terraindocument.js'; +import { TerrainDocument } from './classes/terraindocument.js'; +import { TerrainShape } from './classes/terrainshape.js'; import { registerSettings } from "./js/settings.js"; let debugEnabled = 2; @@ -27,15 +28,13 @@ export let setting = key => { }; export let getflag = (obj, key) => { - const flags = obj.data.flags['enhanced-terrain-layer']; - return flags && flags[key]; + return getProperty(obj, `flags.enhanced-terrain-layer.${key}`); + //const flags = obj.flags['enhanced-terrain-layer']; + //return flags && flags[key]; } function registerLayer() { - if (isNewerVersion(game.version, "9")) - CONFIG.Canvas.layers.terrain = { group: "primary", layerClass: TerrainLayer }; - else - CONFIG.Canvas.layers.terrain = TerrainLayer; + CONFIG.Canvas.layers.terrain = { group: "interface", layerClass: TerrainLayer }; CONFIG.Terrain = { documentClass: TerrainDocument, layerClass: TerrainLayer, @@ -50,9 +49,12 @@ function registerLayer() { } } }, + typeLabels: { base: 'EnhancedTerrainLayer.Terrain' }, objectClass: Terrain }; + canvas["#scene"] = {}; + let createEmbeddedDocuments = async function (wrapped, ...args) { let [embeddedName, updates = [], context = {}] = args; if (embeddedName == 'Terrain') { @@ -282,7 +284,7 @@ async function addControlsv9(app, dest, full) { let template = "modules/enhanced-terrain-layer/templates/terrain-form.html"; let data = { - data: duplicate(app.object.data.flags['enhanced-terrain-layer'] || {}), + data: duplicate(app.object.flags['enhanced-terrain-layer'] || {}), environments: env, obstacles: obs, full: full @@ -331,6 +333,36 @@ Hooks.on('init', async () => { canvas.terrain[data.action].apply(canvas.terrain, data.arguments); }); + PrimaryCanvasGroup.prototype.addTerrain = function (terrain) { + let shape = this.terrain.get(terrain.objectId); + if (!shape) shape = this.addChild(new TerrainShape(terrain)); + else shape.object = terrain; + shape.texture = terrain.texture ?? null; + this.terrain.set(terrain.objectId, shape); + return shape; + } + + PrimaryCanvasGroup.prototype.removeTerrain = function(terrain) { + const shape = this.terrain.get(terrain.objectId); + if (shape) { + this.removeChild(shape); + this.terrain.delete(terrain.objectId); + } + } + + let oldTearDown = PrimaryCanvasGroup.prototype.tearDown; + PrimaryCanvasGroup.prototype.tearDown = async function () { + oldTearDown.call(this); + this.terrain.clear(); + } + + let oldDraw = PrimaryCanvasGroup.prototype.draw; + PrimaryCanvasGroup.prototype.draw = async function () { + if (!this.terrain) + this.terrain = new foundry.utils.Collection(); + oldDraw.call(this); + } + registerSettings(); registerLayer(); registerKeybindings(); @@ -354,9 +386,14 @@ Hooks.on('init', async () => { let onDragLeftStart = async function (wrapped, ...args) { wrapped(...args); if (canvas != null) { + canvas.terrain._tokenDrag = true; + log("drag start", canvas.terrain._tokenDrag); + canvas.terrain.refreshVisibility(); + const isVisible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain' || canvas.terrain.showOnDrag); - canvas.terrain.visible = isVisible; - //log('Terrain visible: Start', canvas.terrain.visible); + canvas.terrain.visible = canvas.terrain.objects.visible = isVisible; + //canvas.terrain.toggleShapes(isVisible); + //log('Terrain visible: Start', canvas.terrain.objects.visible); } } @@ -372,9 +409,14 @@ Hooks.on('init', async () => { let onDragLeftDrop = async function (wrapped, ...args) { wrapped(...args); if (canvas != null) { + canvas.terrain._tokenDrag = false; + log("left drop", canvas.terrain._tokenDrag); + canvas.terrain.refreshVisibility(); + const isVisible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain' || canvas.terrain.showOnDrag); - canvas.terrain.visible = isVisible; - //log('Terrain visible: Drop', canvas.terrain.visible); + canvas.terrain.visible = canvas.terrain.objects.visible = isVisible; + //canvas.terrain.toggleShapes(isVisible); + //log('Terrain visible: Drop', canvas.terrain.objects.visible); } } @@ -390,8 +432,15 @@ Hooks.on('init', async () => { let onDragLeftCancel = async function (wrapped, ...args) { const ruler = canvas.controls.ruler; - if (canvas != null && ruler._state !== Ruler.STATES.MEASURING) - canvas.terrain.visible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain'); + if (canvas != null && ruler._state !== Ruler.STATES.MEASURING) { + canvas.terrain._tokenDrag = false; + log("left cancel", canvas.terrain._tokenDrag); + canvas.terrain.refreshVisibility(); + + let isVisible = (canvas.terrain.showterrain || ui.controls.activeControl == 'terrain'); + canvas.terrain.visible = canvas.terrain.objects.visible = isVisible; + //canvas.terrain.toggleShapes(isVisible); + } wrapped(...args); } @@ -524,7 +573,9 @@ Hooks.on("renderSceneConfig", async (app, html, data) => { }); Hooks.on("updateScene", (scene, data) => { - canvas.terrain.refresh(true); //refresh the terrain to respond to default terrain color + if (getProperty(data, "flags.enhanced-terrain-layer.opacity") || getProperty(data, "flags.enhanced-terrain-layer.drawcolor")) { + canvas.terrain.refresh(true); //refresh the terrain to respond to default terrain color + } if (canvas.terrain.toolbar) canvas.terrain.toolbar.render(true); }); @@ -556,6 +607,12 @@ Hooks.on("controlToken", (app, html) => { Hooks.on("updateSetting", (setting, data, options, userid) => { if (setting.key.startsWith("enhanced-terrain-layer")) { const key = setting.key.replace("enhanced-terrain-layer.", ""); - canvas.terrain._setting[key] = (key == "environment-color" ? JSON.parse(data.value) : data.value); + canvas.terrain._setting[key] = (key == "terrain-image" ? data.value : JSON.parse(data.value)); + } +}); + +Hooks.on("sightRefresh", () => { + for (let t of canvas.terrain.placeables) { + t.visible = t.isVisible; } -}); \ No newline at end of file +}) \ No newline at end of file