/** * A Wall is an implementation of PlaceableObject which represents a physical or visual barrier within the Scene. * Walls are used to restrict Token movement or visibility as well as to define the areas of effect for ambient lights * and sounds. * @category - Canvas * @see {@link WallDocument} * @see {@link WallsLayer} */ class Wall extends PlaceableObject { constructor(document) { super(document); this.#edge = this.#createEdge(); this.#priorDoorState = this.document.ds; } /** @inheritdoc */ static embeddedName = "Wall"; /** @override */ static RENDER_FLAGS = { redraw: {propagate: ["refresh"]}, refresh: {propagate: ["refreshState", "refreshLine"], alias: true}, refreshState: {propagate: ["refreshEndpoints", "refreshHighlight"]}, refreshLine: {propagate: ["refreshEndpoints", "refreshHighlight", "refreshDirection"]}, refreshEndpoints: {}, refreshDirection: {}, refreshHighlight: {} }; /** * A reference the Door Control icon associated with this Wall, if any * @type {DoorControl|null} */ doorControl; /** * The line segment that represents the Wall. * @type {PIXI.Graphics} */ line; /** * The endpoints of the Wall line segment. * @type {PIXI.Graphics} */ endpoints; /** * The icon that indicates the direction of the Wall. * @type {PIXI.Sprite|null} */ directionIcon; /** * A Graphics object used to highlight this wall segment. Only used when the wall is controlled. * @type {PIXI.Graphics} */ highlight; /** * Cache the prior door state so that we can identify changes in the door state. * @type {number} */ #priorDoorState; /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /** * A convenience reference to the coordinates Array for the Wall endpoints, [x0,y0,x1,y1]. * @type {number[]} */ get coords() { return this.document.c; } /* -------------------------------------------- */ /** * The Edge instance which represents this Wall. * The Edge is re-created when data for the Wall changes. * @type {Edge} */ get edge() { return this.#edge; } #edge; /* -------------------------------------------- */ /** @inheritdoc */ get bounds() { const [x0, y0, x1, y1] = this.document.c; return new PIXI.Rectangle(x0, y0, x1-x0, y1-y0).normalize(); } /* -------------------------------------------- */ /** * A boolean for whether this wall contains a door * @type {boolean} */ get isDoor() { return this.document.door > CONST.WALL_DOOR_TYPES.NONE; } /* -------------------------------------------- */ /** * A boolean for whether the wall contains an open door * @returns {boolean} */ get isOpen() { return this.isDoor && (this.document.ds === CONST.WALL_DOOR_STATES.OPEN); } /* -------------------------------------------- */ /** * Return the coordinates [x,y] at the midpoint of the wall segment * @returns {Array} */ get midpoint() { return [(this.coords[0] + this.coords[2]) / 2, (this.coords[1] + this.coords[3]) / 2]; } /* -------------------------------------------- */ /** @inheritdoc */ get center() { const [x, y] = this.midpoint; return new PIXI.Point(x, y); } /* -------------------------------------------- */ /** * Get the direction of effect for a directional Wall * @type {number|null} */ get direction() { let d = this.document.dir; if ( !d ) return null; let c = this.coords; let angle = Math.atan2(c[3] - c[1], c[2] - c[0]); if ( d === CONST.WALL_DIRECTIONS.LEFT ) return angle + (Math.PI / 2); else return angle - (Math.PI / 2); } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /** @override */ getSnappedPosition(position) { throw new Error("Wall#getSnappedPosition is not supported: WallDocument does not have a (x, y) position"); } /* -------------------------------------------- */ /** * Initialize the edge which represents this Wall. * @param {object} [options] Options which modify how the edge is initialized * @param {boolean} [options.deleted] Has the edge been deleted? */ initializeEdge({deleted=false}={}) { // The wall has been deleted if ( deleted ) { this.#edge = null; canvas.edges.delete(this.id); return; } // Re-create the Edge for the wall this.#edge = this.#createEdge(); canvas.edges.set(this.id, this.#edge); } /* -------------------------------------------- */ /** * Create an Edge from the Wall placeable. * @returns {Edge} */ #createEdge() { let {c, light, sight, sound, move, dir, threshold} = this.document; if ( this.isOpen ) light = sight = sound = move = CONST.WALL_SENSE_TYPES.NONE; const dpx = this.scene.dimensions.distancePixels; return new foundry.canvas.edges.Edge({x: c[0], y: c[1]}, {x: c[2], y: c[3]}, { id: this.id, object: this, type: "wall", direction: dir, light, sight, sound, move, threshold: { light: threshold.light * dpx, sight: threshold.sight * dpx, sound: threshold.sound * dpx, attenuation: threshold.attenuation } }); } /* -------------------------------------------- */ /** * This helper converts the wall segment to a Ray * @returns {Ray} The wall in Ray representation */ toRay() { return Ray.fromArrays(this.coords.slice(0, 2), this.coords.slice(2)); } /* -------------------------------------------- */ /** @override */ async _draw(options) { this.line = this.addChild(new PIXI.Graphics()); this.line.eventMode = "auto"; this.directionIcon = this.addChild(this.#drawDirection()); this.directionIcon.eventMode = "none"; this.endpoints = this.addChild(new PIXI.Graphics()); this.endpoints.eventMode = "auto"; this.cursor = "pointer"; } /* -------------------------------------------- */ /** @override */ clear() { this.clearDoorControl(); return super.clear(); } /* -------------------------------------------- */ /** * Draw a control icon that is used to manipulate the door's open/closed state * @returns {DoorControl} */ createDoorControl() { if ((this.document.door === CONST.WALL_DOOR_TYPES.SECRET) && !game.user.isGM) return null; this.doorControl = canvas.controls.doors.addChild(new CONFIG.Canvas.doorControlClass(this)); this.doorControl.draw(); return this.doorControl; } /* -------------------------------------------- */ /** * Clear the door control if it exists. */ clearDoorControl() { if ( this.doorControl ) { this.doorControl.destroy({children: true}); this.doorControl = null; } } /* -------------------------------------------- */ /** * Draw a directional prompt icon for one-way walls to illustrate their direction of effect. * @returns {PIXI.Sprite|null} The drawn icon */ #drawDirection() { if ( this.directionIcon ) return null; // Create the icon const tex = getTexture(CONFIG.controlIcons.wallDirection); const icon = new PIXI.Sprite(tex); // Set icon initial state icon.width = icon.height = 32; icon.anchor.set(0.5, 0.5); icon.visible = false; return icon; } /* -------------------------------------------- */ /** * Compute an approximate Polygon which encloses the line segment providing a specific hitArea for the line * @param {number} pad The amount of padding to apply * @returns {PIXI.Polygon} A constructed Polygon for the line */ #getHitPolygon(pad) { const c = this.document.c; // Identify wall orientation const dx = c[2] - c[0]; const dy = c[3] - c[1]; // Define the array of polygon points let points; if ( Math.abs(dx) >= Math.abs(dy) ) { const sx = Math.sign(dx); points = [ c[0]-(pad*sx), c[1]-pad, c[2]+(pad*sx), c[3]-pad, c[2]+(pad*sx), c[3]+pad, c[0]-(pad*sx), c[1]+pad ]; } else { const sy = Math.sign(dy); points = [ c[0]-pad, c[1]-(pad*sy), c[2]-pad, c[3]+(pad*sy), c[2]+pad, c[3]+(pad*sy), c[0]+pad, c[1]-(pad*sy) ]; } // Return a Polygon which pads the line return new PIXI.Polygon(points); } /* -------------------------------------------- */ /** @inheritDoc */ control({chain=false, ...options}={}) { const controlled = super.control(options); if ( controlled && chain ) { const links = this.getLinkedSegments(); for ( let l of links.walls ) { l.control({releaseOthers: false}); this.layer.controlledObjects.set(l.id, l); } } return controlled; } /* -------------------------------------------- */ /** @override */ _destroy(options) { this.clearDoorControl(); } /* -------------------------------------------- */ /** * Test whether the Wall direction lies between two provided angles * This test is used for collision and vision checks against one-directional walls * @param {number} lower The lower-bound limiting angle in radians * @param {number} upper The upper-bound limiting angle in radians * @returns {boolean} */ isDirectionBetweenAngles(lower, upper) { let d = this.direction; if ( d < lower ) { while ( d < lower ) d += (2 * Math.PI); } else if ( d > upper ) { while ( d > upper ) d -= (2 * Math.PI); } return ( d > lower && d < upper ); } /* -------------------------------------------- */ /** * A simple test for whether a Ray can intersect a directional wall * @param {Ray} ray The ray to test * @returns {boolean} Can an intersection occur? */ canRayIntersect(ray) { if ( this.direction === null ) return true; return this.isDirectionBetweenAngles(ray.angle - (Math.PI/2), ray.angle + (Math.PI/2)); } /* -------------------------------------------- */ /** * Get an Array of Wall objects which are linked by a common coordinate * @returns {Object} An object reporting ids and endpoints of the linked segments */ getLinkedSegments() { const test = new Set(); const done = new Set(); const ids = new Set(); const objects = []; // Helper function to add wall points to the set const _addPoints = w => { let p0 = w.coords.slice(0, 2).join("."); if ( !done.has(p0) ) test.add(p0); let p1 = w.coords.slice(2).join("."); if ( !done.has(p1) ) test.add(p1); }; // Helper function to identify other walls which share a point const _getWalls = p => { return canvas.walls.placeables.filter(w => { if ( ids.has(w.id) ) return false; let p0 = w.coords.slice(0, 2).join("."); let p1 = w.coords.slice(2).join("."); return ( p === p0 ) || ( p === p1 ); }); }; // Seed the initial search with this wall's points _addPoints(this); // Begin recursively searching while ( test.size > 0 ) { const testIds = [...test]; for ( let p of testIds ) { let walls = _getWalls(p); walls.forEach(w => { _addPoints(w); if ( !ids.has(w.id) ) objects.push(w); ids.add(w.id); }); test.delete(p); done.add(p); } } // Return the wall IDs and their endpoints return { ids: [...ids], walls: objects, endpoints: [...done].map(p => p.split(".").map(Number)) }; } /* -------------------------------------------- */ /* Incremental Refresh */ /* -------------------------------------------- */ /** @override */ _applyRenderFlags(flags) { if ( flags.refreshState ) this._refreshState(); if ( flags.refreshLine ) this._refreshLine(); if ( flags.refreshEndpoints ) this._refreshEndpoints(); if ( flags.refreshDirection ) this._refreshDirection(); if ( flags.refreshHighlight ) this._refreshHighlight(); } /* -------------------------------------------- */ /** * Refresh the displayed position of the wall which refreshes when the wall coordinates or type changes. * @protected */ _refreshLine() { const c = this.document.c; const wc = this._getWallColor(); const lw = Wall.#getLineWidth(); // Draw line this.line.clear() .lineStyle(lw * 3, 0x000000, 1.0) // Background black .moveTo(c[0], c[1]) .lineTo(c[2], c[3]); this.line.lineStyle(lw, wc, 1.0) // Foreground color .lineTo(c[0], c[1]); // Tint direction icon if ( this.directionIcon ) { this.directionIcon.position.set((c[0] + c[2]) / 2, (c[1] + c[3]) / 2); this.directionIcon.tint = wc; } // Re-position door control icon if ( this.doorControl ) this.doorControl.reposition(); // Update hit area for interaction const priorHitArea = this.line.hitArea; this.line.hitArea = this.#getHitPolygon(lw * 3); if ( !priorHitArea || (this.line.hitArea.x !== priorHitArea.x) || (this.line.hitArea.y !== priorHitArea.y) || (this.line.hitArea.width !== priorHitArea.width) || (this.line.hitArea.height !== priorHitArea.height) ) { MouseInteractionManager.emulateMoveEvent(); } } /* -------------------------------------------- */ /** * Refresh the display of wall endpoints which refreshes when the wall position or state changes. * @protected */ _refreshEndpoints() { const c = this.coords; const wc = this._getWallColor(); const lw = Wall.#getLineWidth(); const cr = (this.hover || this.layer.highlightObjects) ? lw * 4 : lw * 3; this.endpoints.clear() .lineStyle(lw, 0x000000, 1.0) .beginFill(wc, 1.0) .drawCircle(c[0], c[1], cr) .drawCircle(c[2], c[3], cr) .endFill(); } /* -------------------------------------------- */ /** * Draw a directional prompt icon for one-way walls to illustrate their direction of effect. * @protected */ _refreshDirection() { if ( !this.document.dir ) return this.directionIcon.visible = false; // Set icon state and rotation const icon = this.directionIcon; const iconAngle = -Math.PI / 2; const angle = this.direction; icon.rotation = iconAngle + angle; icon.visible = true; } /* -------------------------------------------- */ /** * Refresh the appearance of the wall control highlight graphic. Occurs when wall control or position changes. * @protected */ _refreshHighlight() { // Remove highlight if ( !this.controlled ) { if ( this.highlight ) { this.removeChild(this.highlight).destroy(); this.highlight = undefined; } return; } // Add highlight if ( !this.highlight ) { this.highlight = this.addChildAt(new PIXI.Graphics(), 0); this.highlight.eventMode = "none"; } else this.highlight.clear(); // Configure highlight const c = this.coords; const lw = Wall.#getLineWidth(); const cr = lw * 2; let cr2 = cr * 2; let cr4 = cr * 4; // Draw highlight this.highlight.lineStyle({width: cr, color: 0xFF9829}) .drawRoundedRect(c[0] - cr2, c[1] - cr2, cr4, cr4, cr) .drawRoundedRect(c[2] - cr2, c[3] - cr2, cr4, cr4, cr) .lineStyle({width: cr2, color: 0xFF9829}) .moveTo(c[0], c[1]).lineTo(c[2], c[3]); } /* -------------------------------------------- */ /** * Refresh the displayed state of the Wall. * @protected */ _refreshState() { this.alpha = this._getTargetAlpha(); this.zIndex = this.controlled ? 2 : this.hover ? 1 : 0; } /* -------------------------------------------- */ /** * Given the properties of the wall - decide upon a color to render the wall for display on the WallsLayer * @returns {number} * @protected */ _getWallColor() { const senses = CONST.WALL_SENSE_TYPES; // Invisible Walls if ( this.document.sight === senses.NONE ) return 0x77E7E8; // Terrain Walls else if ( this.document.sight === senses.LIMITED ) return 0x81B90C; // Windows (Sight Proximity) else if ( [senses.PROXIMITY, senses.DISTANCE].includes(this.document.sight) ) return 0xc7d8ff; // Ethereal Walls else if ( this.document.move === senses.NONE ) return 0xCA81FF; // Doors else if ( this.document.door === CONST.WALL_DOOR_TYPES.DOOR ) { let ds = this.document.ds || CONST.WALL_DOOR_STATES.CLOSED; if ( ds === CONST.WALL_DOOR_STATES.CLOSED ) return 0x6666EE; else if ( ds === CONST.WALL_DOOR_STATES.OPEN ) return 0x66CC66; else if ( ds === CONST.WALL_DOOR_STATES.LOCKED ) return 0xEE4444; } // Secret Doors else if ( this.document.door === CONST.WALL_DOOR_TYPES.SECRET ) { let ds = this.document.ds || CONST.WALL_DOOR_STATES.CLOSED; if ( ds === CONST.WALL_DOOR_STATES.CLOSED ) return 0xA612D4; else if ( ds === CONST.WALL_DOOR_STATES.OPEN ) return 0x7C1A9b; else if ( ds === CONST.WALL_DOOR_STATES.LOCKED ) return 0xEE4444; } // Standard Walls return 0xFFFFBB; } /* -------------------------------------------- */ /** * Adapt the width that the wall should be rendered based on the grid size. * @returns {number} */ static #getLineWidth() { const s = canvas.dimensions.size; if ( s > 150 ) return 4; else if ( s > 100 ) return 3; return 2; } /* -------------------------------------------- */ /* Socket Listeners and Handlers */ /* -------------------------------------------- */ /** @inheritDoc */ _onCreate(data, options, userId) { super._onCreate(data, options, userId); this.layer._cloneType = this.document.toJSON(); this.initializeEdge(); this.#onModifyWall(this.document.door !== CONST.WALL_DOOR_TYPES.NONE); } /* -------------------------------------------- */ /** @inheritDoc */ _onUpdate(changed, options, userId) { super._onUpdate(changed, options, userId); // Update the clone tool wall data this.layer._cloneType = this.document.toJSON(); // Handle wall changes which require perception changes. const edgeChange = ("c" in changed) || CONST.WALL_RESTRICTION_TYPES.some(k => k in changed) || ("dir" in changed) || ("threshold" in changed); const doorChange = ["door", "ds"].some(k => k in changed); if ( edgeChange || doorChange ) { this.initializeEdge(); this.#onModifyWall(doorChange); } // Trigger door interaction sounds if ( "ds" in changed ) { const states = CONST.WALL_DOOR_STATES; let interaction; if ( changed.ds === states.LOCKED ) interaction = "lock"; else if ( changed.ds === states.OPEN ) interaction = "open"; else if ( changed.ds === states.CLOSED ) { if ( this.#priorDoorState === states.OPEN ) interaction = "close"; else if ( this.#priorDoorState === states.LOCKED ) interaction = "unlock"; } if ( options.sound !== false ) this._playDoorSound(interaction); this.#priorDoorState = changed.ds; } // Incremental Refresh this.renderFlags.set({ refreshLine: edgeChange || doorChange, refreshDirection: "dir" in changed }); } /* -------------------------------------------- */ /** @inheritDoc */ _onDelete(options, userId) { super._onDelete(options, userId); this.clearDoorControl(); this.initializeEdge({deleted: true}); this.#onModifyWall(false); } /* -------------------------------------------- */ /** * Callback actions when a wall that contains a door is moved or its state is changed * @param {boolean} doorChange Update vision and sound restrictions */ #onModifyWall(doorChange=false) { canvas.perception.update({ refreshEdges: true, // Recompute edge intersections initializeLighting: true, // Recompute light sources initializeVision: true, // Recompute vision sources initializeSounds: true // Recompute sound sources }); // Re-draw door icons if ( doorChange ) { const dt = this.document.door; const hasCtrl = (dt === CONST.WALL_DOOR_TYPES.DOOR) || ((dt === CONST.WALL_DOOR_TYPES.SECRET) && game.user.isGM); if ( hasCtrl ) { if ( this.doorControl ) this.doorControl.draw(); // Asynchronous else this.createDoorControl(); } else this.clearDoorControl(); } else if ( this.doorControl ) this.doorControl.reposition(); } /* -------------------------------------------- */ /** * Play a door interaction sound. * This plays locally, each client independently applies this workflow. * @param {string} interaction The door interaction: "open", "close", "lock", "unlock", or "test". * @protected * @internal */ _playDoorSound(interaction) { if ( !CONST.WALL_DOOR_INTERACTIONS.includes(interaction) ) { throw new Error(`"${interaction}" is not a valid door interaction type`); } if ( !this.isDoor ) return; // Identify which door sound effect to play const doorSound = CONFIG.Wall.doorSounds[this.document.doorSound]; let sounds = doorSound?.[interaction]; if ( sounds && !Array.isArray(sounds) ) sounds = [sounds]; else if ( !sounds?.length ) { if ( interaction !== "test" ) return; sounds = [CONFIG.sounds.lock]; } const src = sounds[Math.floor(Math.random() * sounds.length)]; // Play the door sound as a localized sound effect canvas.sounds.playAtPosition(src, this.center, this.soundRadius, { volume: 1.0, easing: true, walls: false, gmAlways: true, muffledEffect: {type: "lowpass", intensity: 5} }); } /* -------------------------------------------- */ /** * Customize the audible radius of sounds emitted by this wall, for example when a door opens or closes. * @type {number} */ get soundRadius() { return canvas.dimensions.distance * 12; // 60 feet on a 5ft grid } /* -------------------------------------------- */ /* Interactivity */ /* -------------------------------------------- */ /** @inheritdoc */ _canControl(user, event) { if ( !this.layer.active || this.isPreview ) return false; // If the User is chaining walls, we don't want to control the last one const isChain = this.hover && (game.keyboard.downKeys.size === 1) && game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.CONTROL); return !isChain; } /* -------------------------------------------- */ /** @inheritdoc */ _onHoverIn(event, options) { // contrary to hover out, hover in is prevented in chain mode to avoid distracting the user if ( this.layer._chain ) return false; const dest = event.getLocalPosition(this.layer); this.layer.last = { point: WallsLayer.getClosestEndpoint(dest, this) }; return super._onHoverIn(event, options); } /* -------------------------------------------- */ /** @inheritdoc */ _onHoverOut(event) { const mgr = canvas.mouseInteractionManager; if ( this.hover && !this.layer._chain && (mgr.state < mgr.states.CLICKED) ) this.layer.last = {point: null}; return super._onHoverOut(event); } /* -------------------------------------------- */ /** @override */ _overlapsSelection(rectangle) { const [ax, ay, bx, by] = this.document.c; const {left, right, top, bottom} = rectangle; let tmin = -Infinity; let tmax = Infinity; const dx = bx - ax; if ( dx !== 0 ) { const tx1 = (left - ax) / dx; const tx2 = (right - ax) / dx; tmin = Math.max(tmin, Math.min(tx1, tx2)); tmax = Math.min(tmax, Math.max(tx1, tx2)); } else if ( (ax < left) || (ax > right) ) return false; const dy = by - ay; if ( dy !== 0 ) { const ty1 = (top - ay) / dy; const ty2 = (bottom - ay) / dy; tmin = Math.max(tmin, Math.min(ty1, ty2)); tmax = Math.min(tmax, Math.max(ty1, ty2)); } else if ( (ay < top) || (ay > bottom) ) return false; if ( (tmin > 1) || (tmax < 0) || (tmax < tmin) ) return false; return true; } /* -------------------------------------------- */ /** @inheritdoc */ _onClickLeft(event) { if ( this.layer._chain ) return false; event.stopPropagation(); const alt = game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.ALT); const shift = game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT); if ( this.controlled && !alt ) { if ( shift ) return this.release(); else if ( this.layer.controlled.length > 1 ) return this.layer._onDragLeftStart(event); } return this.control({releaseOthers: !shift, chain: alt}); } /* -------------------------------------------- */ /** @override */ _onClickLeft2(event) { event.stopPropagation(); const sheet = this.sheet; sheet.render(true, {walls: this.layer.controlled}); } /* -------------------------------------------- */ /** @override */ _onClickRight2(event) { event.stopPropagation(); const sheet = this.sheet; sheet.render(true, {walls: this.layer.controlled}); } /* -------------------------------------------- */ /** @inheritdoc */ _onDragLeftStart(event) { const origin = event.interactionData.origin; const dLeft = Math.hypot(origin.x - this.coords[0], origin.y - this.coords[1]); const dRight = Math.hypot(origin.x - this.coords[2], origin.y - this.coords[3]); event.interactionData.fixed = dLeft < dRight ? 1 : 0; // Affix the opposite point return super._onDragLeftStart(event); } /* -------------------------------------------- */ /** @override */ _onDragLeftMove(event) { // Pan the canvas if the drag event approaches the edge canvas._onDragCanvasPan(event); // Group movement const {destination, fixed, origin} = event.interactionData; let clones = event.interactionData.clones || []; const snap = !event.shiftKey; if ( clones.length > 1 ) { // Drag a group of walls - snap to the end point maintaining relative positioning const p0 = fixed ? this.coords.slice(0, 2) : this.coords.slice(2, 4); // Get the snapped final point const pt = this.layer._getWallEndpointCoordinates({ x: destination.x + (p0[0] - origin.x), y: destination.y + (p0[1] - origin.y) }, {snap}); const dx = pt[0] - p0[0]; const dy = pt[1] - p0[1]; for ( let c of clones ) { c.document.c = c._original.document.c.map((p, i) => i % 2 ? p + dy : p + dx); } } // Single-wall pivot else if ( clones.length === 1 ) { const w = clones[0]; const pt = this.layer._getWallEndpointCoordinates(destination, {snap}); w.document.c = fixed ? pt.concat(this.coords.slice(2, 4)) : this.coords.slice(0, 2).concat(pt); } // Refresh display clones.forEach(c => c.renderFlags.set({refreshLine: true})); } /* -------------------------------------------- */ /** @override */ _prepareDragLeftDropUpdates(event) { const {clones, destination, fixed, origin} = event.interactionData; const snap = !event.shiftKey; const updates = []; // Pivot a single wall if ( clones.length === 1 ) { // Get the snapped final point const pt = this.layer._getWallEndpointCoordinates(destination, {snap}); const p0 = fixed ? this.coords.slice(2, 4) : this.coords.slice(0, 2); const coords = fixed ? pt.concat(p0) : p0.concat(pt); // If we collapsed the wall, delete it if ( (coords[0] === coords[2]) && (coords[1] === coords[3]) ) { this.document.delete().finally(() => this.layer.clearPreviewContainer()); return null; // No further updates } // Otherwise shift the last point this.layer.last.point = pt; updates.push({_id: clones[0]._original.id, c: coords}); return updates; } // Drag a group of walls - snap to the end point maintaining relative positioning const p0 = fixed ? this.coords.slice(0, 2) : this.coords.slice(2, 4); const pt = this.layer._getWallEndpointCoordinates({ x: destination.x + (p0[0] - origin.x), y: destination.y + (p0[1] - origin.y) }, {snap}); const dx = pt[0] - p0[0]; const dy = pt[1] - p0[1]; for ( const clone of clones ) { const c = clone._original.document.c; updates.push({_id: clone._original.id, c: [c[0]+dx, c[1]+dy, c[2]+dx, c[3]+dy]}); } return updates; } /* -------------------------------------------- */ /* Deprecations and Compatibility */ /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get roof() { foundry.utils.logCompatibilityWarning("Wall#roof has been deprecated. There's no replacement", {since: 12, until: 14}); return null; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get hasActiveRoof() { foundry.utils.logCompatibilityWarning("Wall#hasActiveRoof has been deprecated. There's no replacement", {since: 12, until: 14}); return false; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ identifyInteriorState() { foundry.utils.logCompatibilityWarning("Wall#identifyInteriorState has been deprecated. " + "It has no effect anymore and there's no replacement.", {since: 12, until: 14}); } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ orientPoint(point) { foundry.utils.logCompatibilityWarning("Wall#orientPoint has been moved to foundry.canvas.edges.Edge#orientPoint", {since: 12, until: 14}); return this.edge.orientPoint(point); } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ applyThreshold(sourceType, sourceOrigin, externalRadius=0) { foundry.utils.logCompatibilityWarning("Wall#applyThreshold has been moved to" + " foundry.canvas.edges.Edge#applyThreshold", {since: 12, until: 14}); return this.edge.applyThreshold(sourceType, sourceOrigin, externalRadius); } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get vertices() { foundry.utils.logCompatibilityWarning("Wall#vertices is replaced by Wall#edge", {since: 12, until: 14}); return this.#edge; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get A() { foundry.utils.logCompatibilityWarning("Wall#A is replaced by Wall#edge#a", {since: 12, until: 14}); return this.#edge.a; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ get B() { foundry.utils.logCompatibilityWarning("Wall#A is replaced by Wall#edge#b", {since: 12, until: 14}); return this.#edge.b; } }