1039 lines
31 KiB
JavaScript
1039 lines
31 KiB
JavaScript
|
|
/**
|
||
|
|
* 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<number>}
|
||
|
|
*/
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|