/** * An icon representing a Door Control * @extends {PIXI.Container} */ class DoorControl extends PIXI.Container { constructor(wall) { super(); this.wall = wall; this.visible = false; // Door controls are not visible by default } /* -------------------------------------------- */ /** * The center of the wall which contains the door. * @type {PIXI.Point} */ get center() { return this.wall.center; } /* -------------------------------------------- */ /** * Draw the DoorControl icon, displaying its icon texture and border * @returns {Promise} */ async draw() { // Background this.bg = this.bg || this.addChild(new PIXI.Graphics()); this.bg.clear().beginFill(0x000000, 1.0).drawRoundedRect(-2, -2, 44, 44, 5).endFill(); this.bg.alpha = 0; // Control Icon this.icon = this.icon || this.addChild(new PIXI.Sprite()); this.icon.width = this.icon.height = 40; this.icon.alpha = 0.6; this.icon.texture = this._getTexture(); // Border this.border = this.border || this.addChild(new PIXI.Graphics()); this.border.clear().lineStyle(1, 0xFF5500, 0.8).drawRoundedRect(-2, -2, 44, 44, 5).endFill(); this.border.visible = false; // Add control interactivity this.eventMode = "static"; this.interactiveChildren = false; this.hitArea = new PIXI.Rectangle(-2, -2, 44, 44); this.cursor = "pointer"; // Set position this.reposition(); this.alpha = 1.0; // Activate listeners this.removeAllListeners(); this.on("pointerover", this._onMouseOver).on("pointerout", this._onMouseOut) .on("pointerdown", this._onMouseDown).on("rightdown", this._onRightDown); return this; } /* -------------------------------------------- */ /** * Get the icon texture to use for the Door Control icon based on the door state * @returns {PIXI.Texture} */ _getTexture() { // Determine displayed door state const ds = CONST.WALL_DOOR_STATES; let s = this.wall.document.ds; if ( !game.user.isGM && (s === ds.LOCKED) ) s = ds.CLOSED; // Determine texture path const icons = CONFIG.controlIcons; let path = { [ds.LOCKED]: icons.doorLocked, [ds.CLOSED]: icons.doorClosed, [ds.OPEN]: icons.doorOpen }[s] || icons.doorClosed; if ( (s === ds.CLOSED) && (this.wall.document.door === CONST.WALL_DOOR_TYPES.SECRET) ) path = icons.doorSecret; // Obtain the icon texture return getTexture(path); } /* -------------------------------------------- */ reposition() { let pos = this.wall.midpoint.map(p => p - 20); this.position.set(...pos); } /* -------------------------------------------- */ /** * Determine whether the DoorControl is visible to the calling user's perspective. * The control is always visible if the user is a GM and no Tokens are controlled. * @see {CanvasVisibility#testVisibility} * @type {boolean} */ get isVisible() { if ( !canvas.visibility.tokenVision ) return true; // Hide secret doors from players const w = this.wall; if ( (w.document.door === CONST.WALL_DOOR_TYPES.SECRET) && !game.user.isGM ) return false; // Test two points which are perpendicular to the door midpoint const ray = this.wall.toRay(); const [x, y] = w.midpoint; const [dx, dy] = [-ray.dy, ray.dx]; const t = 3 / (Math.abs(dx) + Math.abs(dy)); // Approximate with Manhattan distance for speed const points = [ {x: x + (t * dx), y: y + (t * dy)}, {x: x - (t * dx), y: y - (t * dy)} ]; // Test each point for visibility return points.some(p => { return canvas.visibility.testVisibility(p, {object: this, tolerance: 0}); }); } /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ /** * Handle mouse over events on a door control icon. * @param {PIXI.FederatedEvent} event The originating interaction event * @protected */ _onMouseOver(event) { event.stopPropagation(); const canControl = game.user.can("WALL_DOORS"); const blockPaused = game.paused && !game.user.isGM; if ( !canControl || blockPaused ) return false; this.border.visible = true; this.icon.alpha = 1.0; this.bg.alpha = 0.25; canvas.walls.hover = this.wall; } /* -------------------------------------------- */ /** * Handle mouse out events on a door control icon. * @param {PIXI.FederatedEvent} event The originating interaction event * @protected */ _onMouseOut(event) { event.stopPropagation(); if ( game.paused && !game.user.isGM ) return false; this.border.visible = false; this.icon.alpha = 0.6; this.bg.alpha = 0; canvas.walls.hover = null; } /* -------------------------------------------- */ /** * Handle left mouse down events on a door control icon. * This should only toggle between the OPEN and CLOSED states. * @param {PIXI.FederatedEvent} event The originating interaction event * @protected */ _onMouseDown(event) { if ( event.button !== 0 ) return; // Only support standard left-click event.stopPropagation(); const { ds } = this.wall.document; const states = CONST.WALL_DOOR_STATES; // Determine whether the player can control the door at this time if ( !game.user.can("WALL_DOORS") ) return false; if ( game.paused && !game.user.isGM ) { ui.notifications.warn("GAME.PausedWarning", {localize: true}); return false; } const sound = !(game.user.isGM && game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.ALT)); // Play an audio cue for testing locked doors, only for the current client if ( ds === states.LOCKED ) { if ( sound ) this.wall._playDoorSound("test"); return false; } // Toggle between OPEN and CLOSED states return this.wall.document.update({ds: ds === states.CLOSED ? states.OPEN : states.CLOSED}, {sound}); } /* -------------------------------------------- */ /** * Handle right mouse down events on a door control icon. * This should toggle whether the door is LOCKED or CLOSED. * @param {PIXI.FederatedEvent} event The originating interaction event * @protected */ _onRightDown(event) { event.stopPropagation(); if ( !game.user.isGM ) return; let state = this.wall.document.ds; const states = CONST.WALL_DOOR_STATES; if ( state === states.OPEN ) return; state = state === states.LOCKED ? states.CLOSED : states.LOCKED; const sound = !(game.user.isGM && game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.ALT)); return this.wall.document.update({ds: state}, {sound}); } }