Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c632a342d | |||
| 82b495f4c3 | |||
| f67899500e | |||
| 38204c7651 |
@@ -1,3 +1,11 @@
|
|||||||
|
## v1.2.0
|
||||||
|
### New features
|
||||||
|
- Draw outlines around Door Control icons to increase their visibility
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Secret doors are now tinted black instead of dark grey.
|
||||||
|
|
||||||
|
|
||||||
## v1.1.0
|
## v1.1.0
|
||||||
### New features
|
### New features
|
||||||
- Tint secret doors grey for the GM to differentiate them from regular doors
|
- Tint secret doors grey for the GM to differentiate them from regular doors
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ Makes doors smarter. Allows doors to synchronize across multiple scenes and send
|
|||||||
|
|
||||||
Door Control icons will be rendered the same size in every scene, regardless of the configured grid size. The size of the icons is configurable.
|
Door Control icons will be rendered the same size in every scene, regardless of the configured grid size. The size of the icons is configurable.
|
||||||
|
|
||||||
|
### Door Control Outline
|
||||||
|

|
||||||
|
|
||||||
|
Door Control icons will be rendered with an outline to improve their visibility on bright backgrounds.
|
||||||
|
|
||||||
### Tint Secret Doors
|
### Tint Secret Doors
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"smart-doors": {
|
"smart-doors": {
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"doorControlOutline": {
|
||||||
|
"name": "Door Control Outline",
|
||||||
|
"hint": "Draw outlines around Door Control icons to increase their visiblity"
|
||||||
|
},
|
||||||
"doorControlSizeFactor": {
|
"doorControlSizeFactor": {
|
||||||
"name": "Door Control Size Factor",
|
"name": "Door Control Size Factor",
|
||||||
"hint": "Defines by which factor the size of the door control icons should be scaled up"
|
"hint": "Defines by which factor the size of the door control icons should be scaled up"
|
||||||
|
|||||||
152
lib/outline_filter/outline_filter.js
Normal file
152
lib/outline_filter/outline_filter.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This is a modified version of the PIXI outline filter (https://github.com/pixijs/pixi-filters/tree/master/filters/outline)
|
||||||
|
and is licensed under the MIT license.
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2013-2017 Mathew Groves, Chad Engler
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const vertex = `attribute vec2 aVertexPosition;
|
||||||
|
attribute vec2 aTextureCoord;
|
||||||
|
|
||||||
|
uniform mat3 projectionMatrix;
|
||||||
|
|
||||||
|
varying vec2 vTextureCoord;
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
|
||||||
|
vTextureCoord = aTextureCoord;
|
||||||
|
}`
|
||||||
|
|
||||||
|
const fragment = `varying vec2 vTextureCoord;
|
||||||
|
uniform sampler2D uSampler;
|
||||||
|
|
||||||
|
uniform vec2 thickness;
|
||||||
|
uniform vec4 outlineColor;
|
||||||
|
uniform vec4 filterClamp;
|
||||||
|
|
||||||
|
const float DOUBLE_PI = 3.14159265358979323846264 * 2.;
|
||||||
|
|
||||||
|
void main(void) {
|
||||||
|
vec4 ownColor = texture2D(uSampler, vTextureCoord);
|
||||||
|
vec4 curColor;
|
||||||
|
float maxAlpha = 0.;
|
||||||
|
vec2 displaced;
|
||||||
|
for (float angle = 0.; angle <= DOUBLE_PI; angle += \${angleStep}) {
|
||||||
|
displaced.x = vTextureCoord.x + thickness.x * cos(angle);
|
||||||
|
displaced.y = vTextureCoord.y + thickness.y * sin(angle);
|
||||||
|
curColor = texture2D(uSampler, clamp(displaced, filterClamp.xy, filterClamp.zw));
|
||||||
|
maxAlpha = max(maxAlpha, curColor.a);
|
||||||
|
}
|
||||||
|
float resultAlpha = max(maxAlpha, ownColor.a);
|
||||||
|
// Original line:
|
||||||
|
// gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha);
|
||||||
|
gl_FragColor = vec4(ownColor.rgb + outlineColor.rgb * (resultAlpha - ownColor.a), resultAlpha);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OutlineFilter, originally by mishaa
|
||||||
|
* http://www.html5gamedevs.com/topic/10640-outline-a-sprite-change-certain-colors/?p=69966
|
||||||
|
* http://codepen.io/mishaa/pen/emGNRB<br>
|
||||||
|
* 
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @extends PIXI.Filter
|
||||||
|
* @memberof PIXI.filters
|
||||||
|
* @see {@link https://www.npmjs.com/package/@pixi/filter-outline|@pixi/filter-outline}
|
||||||
|
* @see {@link https://www.npmjs.com/package/pixi-filters|pixi-filters}
|
||||||
|
* @param {number} [thickness=1] The tickness of the outline. Make it 2 times more for resolution 2
|
||||||
|
* @param {number} [color=0x000000] The color of the outline.
|
||||||
|
* @param {number} [quality=0.1] The quality of the outline from `0` to `1`, using a higher quality
|
||||||
|
* setting will result in slower performance and more accuracy.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* someSprite.filters = [new OutlineFilter(2, 0x99ff99)];
|
||||||
|
*/
|
||||||
|
class OutlineFilter extends PIXI.Filter {
|
||||||
|
|
||||||
|
constructor(thickness = 1, color = 0x000000, quality = 0.1) {
|
||||||
|
const samples = Math.max(
|
||||||
|
quality * OutlineFilter.MAX_SAMPLES,
|
||||||
|
OutlineFilter.MIN_SAMPLES
|
||||||
|
);
|
||||||
|
const angleStep = (Math.PI * 2 / samples).toFixed(7);
|
||||||
|
|
||||||
|
super(vertex, fragment.replace(/\$\{angleStep\}/, angleStep));
|
||||||
|
this.uniforms.thickness = new Float32Array([0, 0]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The thickness of the outline.
|
||||||
|
* @member {number}
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
this.thickness = thickness;
|
||||||
|
|
||||||
|
this.uniforms.outlineColor = new Float32Array([0, 0, 0, 1]);
|
||||||
|
this.color = color;
|
||||||
|
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(filterManager, input, output, clear) {
|
||||||
|
this.uniforms.thickness[0] = this.thickness / input._frame.width;
|
||||||
|
this.uniforms.thickness[1] = this.thickness / input._frame.height;
|
||||||
|
|
||||||
|
filterManager.applyFilter(this, input, output, clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color of the glow.
|
||||||
|
* @member {number}
|
||||||
|
* @default 0x000000
|
||||||
|
*/
|
||||||
|
get color() {
|
||||||
|
return PIXI.utils.rgb2hex(this.uniforms.outlineColor);
|
||||||
|
}
|
||||||
|
set color(value) {
|
||||||
|
PIXI.utils.hex2rgb(value, this.uniforms.outlineColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum number of samples for rendering outline.
|
||||||
|
* @static
|
||||||
|
* @member {number} MIN_SAMPLES
|
||||||
|
* @memberof PIXI.filters.OutlineFilter
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
OutlineFilter.MIN_SAMPLES = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of samples for rendering outline.
|
||||||
|
* @static
|
||||||
|
* @member {number} MAX_SAMPLES
|
||||||
|
* @memberof PIXI.filters.OutlineFilter
|
||||||
|
* @default 100
|
||||||
|
*/
|
||||||
|
OutlineFilter.MAX_SAMPLES = 100;
|
||||||
|
|
||||||
|
export { OutlineFilter };
|
||||||
475
main.js
475
main.js
@@ -1,475 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const settingsKey = "smart-doors";
|
|
||||||
const currentDataVersion = "1.1.0"
|
|
||||||
|
|
||||||
Hooks.once("init", () => {
|
|
||||||
registerSettings()
|
|
||||||
hookDoorEvents()
|
|
||||||
hookWallConfigUpdate()
|
|
||||||
hookDoorControlDraw()
|
|
||||||
})
|
|
||||||
|
|
||||||
Hooks.once("ready", () => {
|
|
||||||
performMigrations()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Tint the source door red when a locked alert is hovered
|
|
||||||
Hooks.on("renderChatMessage", (message, html, data) => {
|
|
||||||
// Tint the door that generated this message
|
|
||||||
const source = message.data.flags.smartdoors?.source
|
|
||||||
if (!source)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Tint on mouse enter
|
|
||||||
const mouseEnter = function () {
|
|
||||||
const sourceDoor = canvas.controls.doors.children.find(door => door.wall.data._id === source.wall && door.wall.scene.id === source.scene);
|
|
||||||
if (sourceDoor)
|
|
||||||
sourceDoor.icon.tint = 0xff0000;
|
|
||||||
}
|
|
||||||
html.on("mouseenter", mouseEnter);
|
|
||||||
|
|
||||||
// Remove tint on mouse leave
|
|
||||||
const mouseLeave = function () {
|
|
||||||
const sourceDoor = canvas.controls.doors.children.find(door => door.wall.data._id === source.wall && door.wall.scene.id === source.scene);
|
|
||||||
if (sourceDoor)
|
|
||||||
sourceDoor.icon.tint = 0xffffff;
|
|
||||||
}
|
|
||||||
html.on("mouseleave", mouseLeave);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Adjust the repositioning formula for the door controls
|
|
||||||
DoorControl.prototype.reposition = function () {
|
|
||||||
let gridSize = this.wall.scene.data.grid
|
|
||||||
gridSize *= game.settings.get(settingsKey, "doorControlSizeFactor")
|
|
||||||
const pos = this.wall.midpoint.map(p => p - gridSize * 0.2)
|
|
||||||
this.position.set(...pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hookDoorControlDraw() {
|
|
||||||
const originalHandler = DoorControl.prototype.draw
|
|
||||||
DoorControl.prototype.draw = async function () {
|
|
||||||
const result = await originalHandler.call(this)
|
|
||||||
onDoorControlPostDraw.call(this)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the size of the door control in relation to the grid size so it'll have a constant percieved size
|
|
||||||
function onDoorControlPostDraw() {
|
|
||||||
// If the canvas isn't ready we'll do this after the "canvasReady" event is fired instead
|
|
||||||
if (!canvas.ready)
|
|
||||||
return
|
|
||||||
|
|
||||||
fixDoorControlSize(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the size of all door controls in relation to the grid size so it'll have a constant percieved size
|
|
||||||
Hooks.on("canvasReady", (currentCanvas, wall, update) => {
|
|
||||||
const doors = currentCanvas.controls.doors.children
|
|
||||||
doors.forEach(control => fixDoorControlSize(control))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Resizes the door control according to the grid size
|
|
||||||
function fixDoorControlSize(control) {
|
|
||||||
let gridSize = control.wall.scene.data.grid
|
|
||||||
gridSize *= game.settings.get(settingsKey, "doorControlSizeFactor")
|
|
||||||
control.icon.width = control.icon.height = gridSize * 0.4
|
|
||||||
control.hitArea = new PIXI.Rectangle(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44);
|
|
||||||
control.border.clear().lineStyle(1, 0xFF5500, 0.8).drawRoundedRect(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44, gridSize * 0.05).endFill();
|
|
||||||
control.bg.clear().beginFill(0x000000, 1.0).drawRoundedRect(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44, gridSize * 0.05).endFill();
|
|
||||||
}
|
|
||||||
|
|
||||||
const SECRET_DOOR_TINT = 0x222222
|
|
||||||
|
|
||||||
// Tint all secret doors dark grey
|
|
||||||
Hooks.on("canvasReady", () => {
|
|
||||||
if (game.settings.get(settingsKey, "highlightSecretDoors")) {
|
|
||||||
const types = CONST.WALL_DOOR_TYPES
|
|
||||||
const secretDoors = canvas.controls.doors.children.filter(control => control.wall.data.door == types.SECRET)
|
|
||||||
secretDoors.forEach(control => control.icon.tint = SECRET_DOOR_TINT)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// If door type has been changed, tint the door accordingly
|
|
||||||
Hooks.on("updateWall", (scene, wall, update) => {
|
|
||||||
if (!game.settings.get(settingsKey, "highlightSecretDoors"))
|
|
||||||
return
|
|
||||||
const types = CONST.WALL_DOOR_TYPES
|
|
||||||
if (wall.door === types.NONE)
|
|
||||||
return
|
|
||||||
// Find the door control corresponding to the changed door
|
|
||||||
const changedDoor = canvas.controls.doors.children.find(control => control.wall.data._id === wall._id);
|
|
||||||
// If the changed door doesn't have a control it's not on this scene - ignore it
|
|
||||||
if (!changedDoor)
|
|
||||||
return
|
|
||||||
// The wall object we got passed might be from another scene so we replace it with the door from the current scene
|
|
||||||
wall = changedDoor.wall.data
|
|
||||||
if (wall.door === types.DOOR)
|
|
||||||
changedDoor.icon.tint = 0xFFFFFF
|
|
||||||
else if (wall.door === types.SECRET)
|
|
||||||
changedDoor.icon.tint = SECRET_DOOR_TINT
|
|
||||||
else
|
|
||||||
console.warn("Smart Doors | Encountered unknown door type " + wall.door + " while highlighting secret doors.")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Inject our custom settings into the WallConfig dialog
|
|
||||||
Hooks.on("renderWallConfig", (wallConfig, html, data) => {
|
|
||||||
// Settings for synchronized doors
|
|
||||||
if (data.isDoor && game.settings.get(settingsKey, "synchronizedDoors")) {
|
|
||||||
// Inject settings
|
|
||||||
const synchronizedSettings = `
|
|
||||||
<p class="notes">${game.i18n.localize("smart-doors.ui.synchronizedDoors.description")}</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="synchronizationGroup">${game.i18n.localize("smart-doors.ui.synchronizedDoors.groupName")}</label>
|
|
||||||
<input type="text" name="synchronizationGroup"/>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
html.find(".form-group").last().after(synchronizedSettings)
|
|
||||||
|
|
||||||
const smartdoorsData = data.object.flags.smartdoors
|
|
||||||
// Fill the injected input fields with values
|
|
||||||
const input = (name) => html.find(`input[name="${name}"]`)
|
|
||||||
input("synchronizationGroup").prop("value", smartdoorsData?.synchronizationGroup)
|
|
||||||
|
|
||||||
// Recalculate config window height
|
|
||||||
wallConfig.setPosition({height: "auto"})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hook the update function of the WallConfig dialog so we can store our custom data
|
|
||||||
function hookWallConfigUpdate() {
|
|
||||||
// Replace the original function with our custom one
|
|
||||||
const originalHandler = WallConfig.prototype._updateObject;
|
|
||||||
WallConfig.prototype._updateObject = async function (event, formData) {
|
|
||||||
await originalHandler.call(this, event, formData)
|
|
||||||
return onWallConfigUpdate.call(this, event, formData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store our custom data from the WallConfig dialog
|
|
||||||
async function onWallConfigUpdate(event, formData) {
|
|
||||||
const updateData = {flags: {smartdoors: {synchronizationGroup: formData.synchronizationGroup}}}
|
|
||||||
let ids = this.options.editTargets;
|
|
||||||
if (ids.length == 0) {
|
|
||||||
ids = [this.object.data._id];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a synchronization group is set, get the state of existing doors and assume their state
|
|
||||||
if (formData.synchronizationGroup) {
|
|
||||||
// Search for other doors in the synchronization group that aren't in the list of edited doors
|
|
||||||
const doorInGroup = findInAllWalls(wall => {
|
|
||||||
// We only search for doors
|
|
||||||
if (!wall.door)
|
|
||||||
return false
|
|
||||||
// We only want doors in the same synchronization group
|
|
||||||
if (wall.flags.smartdoors?.synchronizationGroup !== formData.synchronizationGroup)
|
|
||||||
return false
|
|
||||||
// Doors on this scene that have their id included in `ids` are currently being changed. Ignore them.
|
|
||||||
if (wall.scene === canvas.scene && ids.includes(wall._id))
|
|
||||||
return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if (doorInGroup)
|
|
||||||
updateData.ds = doorInGroup.ds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update all the edited walls
|
|
||||||
const updateDataset = ids.reduce((dataset, id) => {
|
|
||||||
dataset.push({_id: id, ...updateData})
|
|
||||||
return dataset
|
|
||||||
}, [])
|
|
||||||
return canvas.scene.updateEmbeddedEntity("Wall", updateDataset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook mouse events on DoorControls to perform our logic.
|
|
||||||
// If we successfully handled the event block the original handler. Forward the event otherwise.
|
|
||||||
function hookDoorEvents() {
|
|
||||||
// Replace the original mousedown handler with our custom one
|
|
||||||
const originalMouseDownHandler = DoorControl.prototype._onMouseDown
|
|
||||||
DoorControl.prototype._onMouseDown = function (event) {
|
|
||||||
// Call our handler first. Only allow the original handler to run if our handler returns true
|
|
||||||
const eventHandled = onDoorMouseDown.call(this, event)
|
|
||||||
if (eventHandled)
|
|
||||||
return
|
|
||||||
return originalMouseDownHandler.call(this, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the original rightdown handler with our custom one
|
|
||||||
const originalRightDownHandler = DoorControl.prototype._onRightDown
|
|
||||||
DoorControl.prototype._onRightDown = function (event) {
|
|
||||||
// Call our handler first. Only allow the original handler to run if our handler returns true
|
|
||||||
const eventHandled = onDoorRightDown.call(this, event)
|
|
||||||
if (eventHandled)
|
|
||||||
return
|
|
||||||
return originalRightDownHandler.call(this, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Searches through all scenes for walls and returns those that match the given filter criteria.
|
|
||||||
function filterAllWalls(filterFn) {
|
|
||||||
// Find all walls that match the filter criteria
|
|
||||||
const scenes = game.scenes.map((scene) => {return {scene: scene, walls: scene.data.walls.filter(filterFn)}})
|
|
||||||
// Drop all scenes that don't contain any results
|
|
||||||
return scenes.filter(scene => scene.walls.length > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Searches through all scenes for a wall that matches the given filter criteria
|
|
||||||
function findInAllWalls(filterFn) {
|
|
||||||
// TODO The performance of this could be increased by stopping the search on the first hit
|
|
||||||
const scenes = filterAllWalls(filterFn)
|
|
||||||
// If results were found take the first wall from the first scene.
|
|
||||||
return scenes[0]?.walls[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our custom handler for mousedown events on doors
|
|
||||||
function onDoorMouseDown(event) {
|
|
||||||
// If the user doesn't have the "door" permission we don't do anything.
|
|
||||||
if (!game.user.can("WALL_DOORS"))
|
|
||||||
return false
|
|
||||||
// If the game is paused don't do anything if the current player isn't the gm
|
|
||||||
if ( game.paused && !game.user.isGM )
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (toggleSecretDoorLeftClick.call(this, event))
|
|
||||||
return true
|
|
||||||
|
|
||||||
if (lockedDoorAlertLeftClick.call(this))
|
|
||||||
return true
|
|
||||||
|
|
||||||
if (synchronizedDoorsLeftClick.call(this))
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our custom handler for rightdown events on doors
|
|
||||||
function onDoorRightDown(event) {
|
|
||||||
|
|
||||||
if (synchronizedDoorsRightClick.call(this))
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggles between normal and secret doors
|
|
||||||
function toggleSecretDoorLeftClick(event) {
|
|
||||||
if (event.data.originalEvent.ctrlKey && game.user.isGM && game.settings.get(settingsKey, "toggleSecretDoors")) {
|
|
||||||
const types = CONST.WALL_DOOR_TYPES
|
|
||||||
const newtype = this.wall.data.door === types.DOOR ? types.SECRET : types.DOOR
|
|
||||||
this.wall.update({door: newtype})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a chat message stating that a player tried to open a locked door
|
|
||||||
function lockedDoorAlertLeftClick() {
|
|
||||||
const state = this.wall.data.ds
|
|
||||||
const states = CONST.WALL_DOOR_STATES
|
|
||||||
|
|
||||||
// Check if this feature is enabled
|
|
||||||
if (!game.settings.get(settingsKey, "lockedDoorAlert"))
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Only create messages when the door is locked.
|
|
||||||
if (state != states.LOCKED)
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Generate no message if the gm attempts to open the door
|
|
||||||
if (game.user.isGM)
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Create and send the chat message
|
|
||||||
const message = {}
|
|
||||||
message.user = game.user
|
|
||||||
if (game.user.character)
|
|
||||||
message.speaker = {actor: game.user.character}
|
|
||||||
message.content = "Just tried to open a locked door"
|
|
||||||
message.sound = CONFIG.sounds.lock
|
|
||||||
message.flags = {smartdoors: {source: {wall: this.wall.data._id, scene: this.wall.scene.id}}}
|
|
||||||
ChatMessage.create(message)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates all doors in the specified synchronization group with the provided data
|
|
||||||
function updateSynchronizedDoors(updateData, synchronizationGroup) {
|
|
||||||
// Search for doors belonging to the synchronization group in all scenes
|
|
||||||
let scenes = filterAllWalls(wall => wall.door && wall.flags.smartdoors?.synchronizationGroup === synchronizationGroup);
|
|
||||||
|
|
||||||
// Update all doors in the synchronization group
|
|
||||||
scenes.forEach((scene) => {
|
|
||||||
scene.scene.updateEmbeddedEntity("Wall", scene.walls.map((wall) => {return {_id: wall._id, ...updateData}}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the state of all synchronized doors
|
|
||||||
function synchronizedDoorsLeftClick() {
|
|
||||||
const state = this.wall.data.ds
|
|
||||||
const states = CONST.WALL_DOOR_STATES
|
|
||||||
|
|
||||||
// Check if this feature is enabled
|
|
||||||
if (!game.settings.get(settingsKey, "synchronizedDoors"))
|
|
||||||
return false
|
|
||||||
|
|
||||||
const synchronizationGroup = this.wall.data.flags.smartdoors?.synchronizationGroup
|
|
||||||
|
|
||||||
// Does this door have a synchronization group? If not there is nothing to do
|
|
||||||
if (!synchronizationGroup)
|
|
||||||
return false
|
|
||||||
|
|
||||||
// If the door is locked there is nothing to synchronize
|
|
||||||
if (state === states.LOCKED)
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Calculate new door state
|
|
||||||
const newstate = state === states.CLOSED ? states.OPEN : states.CLOSED
|
|
||||||
|
|
||||||
// Update all doors belonging to the synchronization group
|
|
||||||
const updateData = {ds: newstate}
|
|
||||||
updateSynchronizedDoors(updateData, synchronizationGroup)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function synchronizedDoorsRightClick() {
|
|
||||||
const state = this.wall.data.ds
|
|
||||||
const states = CONST.WALL_DOOR_STATES
|
|
||||||
|
|
||||||
// Check if this feature is enabled
|
|
||||||
if (!game.settings.get(settingsKey, "synchronizedDoors"))
|
|
||||||
return false
|
|
||||||
|
|
||||||
const synchronizationGroup = this.wall.data.flags.smartdoors?.synchronizationGroup
|
|
||||||
|
|
||||||
// Does this door have a synchronization group? If not there is nothing to do
|
|
||||||
if (!synchronizationGroup)
|
|
||||||
return false
|
|
||||||
|
|
||||||
// Only the gm is allowed to lock/unlock doors
|
|
||||||
if ( !game.user.isGM )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// If the door is currently opened we cannot lock the door
|
|
||||||
if ( state === states.OPEN )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Calculate new door state
|
|
||||||
const newstate = state === states.LOCKED ? states.CLOSED : states.LOCKED;
|
|
||||||
|
|
||||||
// Update all doors belonging to the synchronization group
|
|
||||||
const updateData = {ds: newstate}
|
|
||||||
updateSynchronizedDoors(updateData, synchronizationGroup)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function performMigrations() {
|
|
||||||
if (!game.user.isGM)
|
|
||||||
return
|
|
||||||
|
|
||||||
let dataVersion = game.settings.get(settingsKey, "dataVersion")
|
|
||||||
if (dataVersion === "fresh install")
|
|
||||||
{
|
|
||||||
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataVersion === "1.0.0") {
|
|
||||||
dataVersion = "1.1.0"
|
|
||||||
ui.notifications.info(game.i18n.format("smart-doors.ui.messages.migrating", {version: dataVersion}))
|
|
||||||
|
|
||||||
// Make a dictionary that maps all door ids to their scenes
|
|
||||||
const walls = game.scenes.reduce((dict, scene) => {
|
|
||||||
scene.data.walls.forEach(wall => {
|
|
||||||
if (!wall.door)
|
|
||||||
return
|
|
||||||
dict[wall._id] = scene.id
|
|
||||||
})
|
|
||||||
return dict
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
// Migrate all messages that have a (wall) source id
|
|
||||||
game.messages.forEach(async message => {
|
|
||||||
const wallId = message.data.flags.smartdoors?.sourceId
|
|
||||||
if (!wallId)
|
|
||||||
return
|
|
||||||
const flags = message.data.flags
|
|
||||||
delete flags.smartdoors.sourceId
|
|
||||||
const scene = walls[wallId]
|
|
||||||
// If there is no wall with this id anymore we can drop the value. It has no purpose anymore
|
|
||||||
if (!scene) {
|
|
||||||
if (!message.data.flags.smartdoors)
|
|
||||||
delete flags.smartdoors
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Assign the id and the scene id to the new data structure
|
|
||||||
flags.smartdoors.source = {wall: wallId, scene: scene}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have to disable recursive here so deleting keys will actually work
|
|
||||||
message.update({flags: flags}, {diff: false, recursive: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
game.settings.set(settingsKey, "dataVersion", dataVersion)
|
|
||||||
ui.notifications.info(game.i18n.format("smart-doors.ui.messages.migrationDone", {version: dataVersion}))
|
|
||||||
}
|
|
||||||
if (dataVersion != currentDataVersion)
|
|
||||||
ui.notifications.error(game.i18n.format("smart-doors.ui.messages.unknownVersion", {version: dataVersion}), {permanent: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadGM() {
|
|
||||||
if (game.user.isGM)
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerSettings() {
|
|
||||||
game.settings.register(settingsKey, "dataVersion", {
|
|
||||||
scope: "world",
|
|
||||||
config: false,
|
|
||||||
type: String,
|
|
||||||
default: "fresh install"
|
|
||||||
})
|
|
||||||
game.settings.register(settingsKey, "doorControlSizeFactor", {
|
|
||||||
name: "smart-doors.settings.doorControlSizeFactor.name",
|
|
||||||
hint: "smart-doors.settings.doorControlSizeFactor.hint",
|
|
||||||
scope: "client",
|
|
||||||
config: true,
|
|
||||||
type: Number,
|
|
||||||
default: 1.5,
|
|
||||||
onChange: () => location.reload()
|
|
||||||
})
|
|
||||||
game.settings.register(settingsKey, "highlightSecretDoors", {
|
|
||||||
name: "smart-doors.settings.highlightSecretDoors.name",
|
|
||||||
hint: "smart-doors.settings.highlightSecretDoors.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
onChange: reloadGM,
|
|
||||||
})
|
|
||||||
game.settings.register(settingsKey, "toggleSecretDoors", {
|
|
||||||
name: "smart-doors.settings.toggleSecretDoors.name",
|
|
||||||
hint: "smart-doors.settings.toggleSecretDoors.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
})
|
|
||||||
game.settings.register(settingsKey, "lockedDoorAlert", {
|
|
||||||
name: "smart-doors.settings.lockedDoorAlert.name",
|
|
||||||
hint: "smart-doors.settings.lockedDoorAlert.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
})
|
|
||||||
game.settings.register(settingsKey, "synchronizedDoors", {
|
|
||||||
name: "smart-doors.settings.synchronizedDoors.name",
|
|
||||||
hint: "smart-doors.settings.synchronizedDoors.hint",
|
|
||||||
scope: "world",
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
"name": "smart-doors",
|
"name": "smart-doors",
|
||||||
"title": "Smart Doors",
|
"title": "Smart Doors",
|
||||||
"description": "Makes doors smarter. Allows doors to synchronize across multiple scenes and sends chat messages when players try to open locked doors.",
|
"description": "Makes doors smarter. Allows doors to synchronize across multiple scenes and sends chat messages when players try to open locked doors.",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"minimumCoreVersion" : "0.7.7",
|
"minimumCoreVersion" : "0.7.7",
|
||||||
"compatibleCoreVersion" : "0.7.8",
|
"compatibleCoreVersion" : "0.7.8",
|
||||||
"author": "Manuel Vögele",
|
"author": "Manuel Vögele",
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"./main.js"
|
"src/main.js"
|
||||||
],
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
],
|
],
|
||||||
"url": "https://github.com/manuelVo/foundryvtt-smart-doors",
|
"url": "https://github.com/manuelVo/foundryvtt-smart-doors",
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-smart-doors/master/module.json",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-smart-doors/master/module.json",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-smart-doors/archive/v1.1.0.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-smart-doors/archive/v1.2.0.zip",
|
||||||
"readme": "https://github.com/manuelVo/foundryvtt-smart-doors/blob/master/README.md",
|
"readme": "https://github.com/manuelVo/foundryvtt-smart-doors/blob/master/README.md",
|
||||||
"changelog": "https://github.com/manuelVo/foundryvtt-smart-doors/blob/master/CHANGELOG.md",
|
"changelog": "https://github.com/manuelVo/foundryvtt-smart-doors/blob/master/CHANGELOG.md",
|
||||||
"bugs": "https://github.com/manuelVo/foundryvtt-smart-doors/issues"
|
"bugs": "https://github.com/manuelVo/foundryvtt-smart-doors/issues"
|
||||||
|
|||||||
36
src/features/door_control_icon_scale.js
Normal file
36
src/features/door_control_icon_scale.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
|
||||||
|
// Adjust the repositioning formula for the door controls
|
||||||
|
export function hookDoorControlReposition() {
|
||||||
|
DoorControl.prototype.reposition = function () {
|
||||||
|
let gridSize = this.wall.scene.data.grid
|
||||||
|
gridSize *= game.settings.get(settingsKey, "doorControlSizeFactor")
|
||||||
|
const pos = this.wall.midpoint.map(p => p - gridSize * 0.2)
|
||||||
|
this.position.set(...pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the size of all door controls in relation to the grid size so it'll have a constant percieved size
|
||||||
|
export function onCanvasReady(currentCanvas) {
|
||||||
|
const doors = currentCanvas.controls.doors.children
|
||||||
|
doors.forEach(control => fixDoorControlSize(control))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the size of the door control in relation to the grid size so it'll have a constant percieved size
|
||||||
|
export function onDoorControlPostDraw() {
|
||||||
|
// If the canvas isn't ready we'll do this after the "canvasReady" event is fired instead
|
||||||
|
if (!canvas.ready)
|
||||||
|
return
|
||||||
|
|
||||||
|
fixDoorControlSize(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resizes the door control according to the grid size
|
||||||
|
function fixDoorControlSize(control) {
|
||||||
|
let gridSize = control.wall.scene.data.grid
|
||||||
|
gridSize *= game.settings.get(settingsKey, "doorControlSizeFactor")
|
||||||
|
control.icon.width = control.icon.height = gridSize * 0.4
|
||||||
|
control.hitArea = new PIXI.Rectangle(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44);
|
||||||
|
control.border.clear().lineStyle(1, 0xFF5500, 0.8).drawRoundedRect(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44, gridSize * 0.05).endFill();
|
||||||
|
control.bg.clear().beginFill(0x000000, 1.0).drawRoundedRect(gridSize * -0.02, gridSize * -0.02, gridSize * 0.44, gridSize * 0.44, gridSize * 0.05).endFill();
|
||||||
|
}
|
||||||
24
src/features/door_control_outline.js
Normal file
24
src/features/door_control_outline.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
import {OutlineFilter} from "../../lib/outline_filter/outline_filter.js"
|
||||||
|
|
||||||
|
export function onDoorControlPostDraw() {
|
||||||
|
if (!game.settings.get(settingsKey, "doorControlOutline"))
|
||||||
|
return
|
||||||
|
|
||||||
|
const types = CONST.WALL_DOOR_TYPES
|
||||||
|
if (this.wall.data.door === types.NONE)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Remove all OutlineFilters from current filters
|
||||||
|
let pixiFilters = this.icon.filters || []
|
||||||
|
pixiFilters = pixiFilters.filter(pixiFilter => !(pixiFilter instanceof OutlineFilter))
|
||||||
|
|
||||||
|
let outlineFilter;
|
||||||
|
if (this.wall.data.door === types.SECRET && game.settings.get(settingsKey, "highlightSecretDoors"))
|
||||||
|
outlineFilter = new OutlineFilter(1, 0xFFFFFF)
|
||||||
|
else
|
||||||
|
outlineFilter = new OutlineFilter(1, 0x000000)
|
||||||
|
|
||||||
|
pixiFilters.push(outlineFilter)
|
||||||
|
this.icon.filters = pixiFilters
|
||||||
|
}
|
||||||
34
src/features/highlight_secret_doors.js
Normal file
34
src/features/highlight_secret_doors.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
|
||||||
|
const SECRET_DOOR_TINT = 0x000000
|
||||||
|
|
||||||
|
// Tint all secret doors dark grey
|
||||||
|
export function onCanvasReady(currentCanvas) {
|
||||||
|
if (game.settings.get(settingsKey, "highlightSecretDoors")) {
|
||||||
|
const types = CONST.WALL_DOOR_TYPES
|
||||||
|
const secretDoors = canvas.controls.doors.children.filter(control => control.wall.data.door == types.SECRET)
|
||||||
|
secretDoors.forEach(control => control.icon.tint = SECRET_DOOR_TINT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If door type has been changed, tint the door accordingly
|
||||||
|
export function onUpdateWall(scene, wall, update) {
|
||||||
|
if (!game.settings.get(settingsKey, "highlightSecretDoors"))
|
||||||
|
return
|
||||||
|
const types = CONST.WALL_DOOR_TYPES
|
||||||
|
if (wall.door === types.NONE)
|
||||||
|
return
|
||||||
|
// Find the door control corresponding to the changed door
|
||||||
|
const changedDoor = canvas.controls.doors.children.find(control => control.wall.data._id === wall._id);
|
||||||
|
// If the changed door doesn't have a control it's not on this scene - ignore it
|
||||||
|
if (!changedDoor)
|
||||||
|
return
|
||||||
|
// The wall object we got passed might be from another scene so we replace it with the door from the current scene
|
||||||
|
wall = changedDoor.wall.data
|
||||||
|
if (wall.door === types.DOOR)
|
||||||
|
changedDoor.icon.tint = 0xFFFFFF
|
||||||
|
else if (wall.door === types.SECRET)
|
||||||
|
changedDoor.icon.tint = SECRET_DOOR_TINT
|
||||||
|
else
|
||||||
|
console.warn("Smart Doors | Encountered unknown door type " + wall.door + " while highlighting secret doors.")
|
||||||
|
}
|
||||||
54
src/features/locked_door_alert.js
Normal file
54
src/features/locked_door_alert.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
|
||||||
|
// Tint the source door red when a locked alert is hovered
|
||||||
|
export function onRenderChatMessage(message, html, data) {
|
||||||
|
// Tint the door that generated this message
|
||||||
|
const source = message.data.flags.smartdoors?.source
|
||||||
|
if (!source)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Tint on mouse enter
|
||||||
|
const mouseEnter = function () {
|
||||||
|
const sourceDoor = canvas.controls.doors.children.find(door => door.wall.data._id === source.wall && door.wall.scene.id === source.scene);
|
||||||
|
if (sourceDoor)
|
||||||
|
sourceDoor.icon.tint = 0xff0000;
|
||||||
|
}
|
||||||
|
html.on("mouseenter", mouseEnter);
|
||||||
|
|
||||||
|
// Remove tint on mouse leave
|
||||||
|
const mouseLeave = function () {
|
||||||
|
const sourceDoor = canvas.controls.doors.children.find(door => door.wall.data._id === source.wall && door.wall.scene.id === source.scene);
|
||||||
|
if (sourceDoor)
|
||||||
|
sourceDoor.icon.tint = 0xffffff;
|
||||||
|
}
|
||||||
|
html.on("mouseleave", mouseLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a chat message stating that a player tried to open a locked door
|
||||||
|
export function onDoorLeftClick() {
|
||||||
|
const state = this.wall.data.ds
|
||||||
|
const states = CONST.WALL_DOOR_STATES
|
||||||
|
|
||||||
|
// Check if this feature is enabled
|
||||||
|
if (!game.settings.get(settingsKey, "lockedDoorAlert"))
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Only create messages when the door is locked.
|
||||||
|
if (state != states.LOCKED)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Generate no message if the gm attempts to open the door
|
||||||
|
if (game.user.isGM)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Create and send the chat message
|
||||||
|
const message = {}
|
||||||
|
message.user = game.user
|
||||||
|
if (game.user.character)
|
||||||
|
message.speaker = {actor: game.user.character}
|
||||||
|
message.content = "Just tried to open a locked door"
|
||||||
|
message.sound = CONFIG.sounds.lock
|
||||||
|
message.flags = {smartdoors: {source: {wall: this.wall.data._id, scene: this.wall.scene.id}}}
|
||||||
|
ChatMessage.create(message)
|
||||||
|
return true
|
||||||
|
}
|
||||||
132
src/features/synchronized_doors.js
Normal file
132
src/features/synchronized_doors.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
import * as Util from "../util.js"
|
||||||
|
|
||||||
|
// Inject settings for synchronized doors
|
||||||
|
export function onRederWallConfig(wallConfig, html, data) {
|
||||||
|
if (data.isDoor && game.settings.get(settingsKey, "synchronizedDoors")) {
|
||||||
|
// Inject settings
|
||||||
|
const synchronizedSettings = `
|
||||||
|
<p class="notes">${game.i18n.localize("smart-doors.ui.synchronizedDoors.description")}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="synchronizationGroup">${game.i18n.localize("smart-doors.ui.synchronizedDoors.groupName")}</label>
|
||||||
|
<input type="text" name="synchronizationGroup"/>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
html.find(".form-group").last().after(synchronizedSettings)
|
||||||
|
|
||||||
|
const smartdoorsData = data.object.flags.smartdoors
|
||||||
|
// Fill the injected input fields with values
|
||||||
|
const input = (name) => html.find(`input[name="${name}"]`)
|
||||||
|
input("synchronizationGroup").prop("value", smartdoorsData?.synchronizationGroup)
|
||||||
|
|
||||||
|
// Recalculate config window height
|
||||||
|
wallConfig.setPosition({height: "auto"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store our custom data from the WallConfig dialog
|
||||||
|
export async function onWallConfigUpdate(event, formData) {
|
||||||
|
const updateData = {flags: {smartdoors: {synchronizationGroup: formData.synchronizationGroup}}}
|
||||||
|
let ids = this.options.editTargets;
|
||||||
|
if (ids.length == 0) {
|
||||||
|
ids = [this.object.data._id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a synchronization group is set, get the state of existing doors and assume their state
|
||||||
|
if (formData.synchronizationGroup) {
|
||||||
|
// Search for other doors in the synchronization group that aren't in the list of edited doors
|
||||||
|
const doorInGroup = Util.findInAllWalls(wall => {
|
||||||
|
// We only search for doors
|
||||||
|
if (!wall.door)
|
||||||
|
return false
|
||||||
|
// We only want doors in the same synchronization group
|
||||||
|
if (wall.flags.smartdoors?.synchronizationGroup !== formData.synchronizationGroup)
|
||||||
|
return false
|
||||||
|
// Doors on this scene that have their id included in `ids` are currently being changed. Ignore them.
|
||||||
|
if (wall.scene === canvas.scene && ids.includes(wall._id))
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if (doorInGroup)
|
||||||
|
updateData.ds = doorInGroup.ds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all the edited walls
|
||||||
|
const updateDataset = ids.reduce((dataset, id) => {
|
||||||
|
dataset.push({_id: id, ...updateData})
|
||||||
|
return dataset
|
||||||
|
}, [])
|
||||||
|
return canvas.scene.updateEmbeddedEntity("Wall", updateDataset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the state of all synchronized doors
|
||||||
|
export function onDoorLeftClick() {
|
||||||
|
const state = this.wall.data.ds
|
||||||
|
const states = CONST.WALL_DOOR_STATES
|
||||||
|
|
||||||
|
// Check if this feature is enabled
|
||||||
|
if (!game.settings.get(settingsKey, "synchronizedDoors"))
|
||||||
|
return false
|
||||||
|
|
||||||
|
const synchronizationGroup = this.wall.data.flags.smartdoors?.synchronizationGroup
|
||||||
|
|
||||||
|
// Does this door have a synchronization group? If not there is nothing to do
|
||||||
|
if (!synchronizationGroup)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// If the door is locked there is nothing to synchronize
|
||||||
|
if (state === states.LOCKED)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Calculate new door state
|
||||||
|
const newstate = state === states.CLOSED ? states.OPEN : states.CLOSED
|
||||||
|
|
||||||
|
// Update all doors belonging to the synchronization group
|
||||||
|
const updateData = {ds: newstate}
|
||||||
|
updateSynchronizedDoors(updateData, synchronizationGroup)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onDoorRightClick() {
|
||||||
|
const state = this.wall.data.ds
|
||||||
|
const states = CONST.WALL_DOOR_STATES
|
||||||
|
|
||||||
|
// Check if this feature is enabled
|
||||||
|
if (!game.settings.get(settingsKey, "synchronizedDoors"))
|
||||||
|
return false
|
||||||
|
|
||||||
|
const synchronizationGroup = this.wall.data.flags.smartdoors?.synchronizationGroup
|
||||||
|
|
||||||
|
// Does this door have a synchronization group? If not there is nothing to do
|
||||||
|
if (!synchronizationGroup)
|
||||||
|
return false
|
||||||
|
|
||||||
|
// Only the gm is allowed to lock/unlock doors
|
||||||
|
if ( !game.user.isGM )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If the door is currently opened we cannot lock the door
|
||||||
|
if ( state === states.OPEN )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Calculate new door state
|
||||||
|
const newstate = state === states.LOCKED ? states.CLOSED : states.LOCKED;
|
||||||
|
|
||||||
|
// Update all doors belonging to the synchronization group
|
||||||
|
const updateData = {ds: newstate}
|
||||||
|
updateSynchronizedDoors(updateData, synchronizationGroup)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates all doors in the specified synchronization group with the provided data
|
||||||
|
function updateSynchronizedDoors(updateData, synchronizationGroup) {
|
||||||
|
// Search for doors belonging to the synchronization group in all scenes
|
||||||
|
let scenes = Util.filterAllWalls(wall => wall.door && wall.flags.smartdoors?.synchronizationGroup === synchronizationGroup);
|
||||||
|
|
||||||
|
// Update all doors in the synchronization group
|
||||||
|
scenes.forEach((scene) => {
|
||||||
|
scene.scene.updateEmbeddedEntity("Wall", scene.walls.map((wall) => {return {_id: wall._id, ...updateData}}))
|
||||||
|
})
|
||||||
|
}
|
||||||
12
src/features/toggle_secret_door.js
Normal file
12
src/features/toggle_secret_door.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {settingsKey} from "../settings.js"
|
||||||
|
|
||||||
|
// Toggles between normal and secret doors
|
||||||
|
export function onDoorLeftClick(event) {
|
||||||
|
if (event.data.originalEvent.ctrlKey && game.user.isGM && game.settings.get(settingsKey, "toggleSecretDoors")) {
|
||||||
|
const types = CONST.WALL_DOOR_TYPES
|
||||||
|
const newtype = this.wall.data.door === types.DOOR ? types.SECRET : types.DOOR
|
||||||
|
this.wall.update({door: newtype})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
107
src/main.js
Normal file
107
src/main.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import * as DoorControlIconScale from "./features/door_control_icon_scale.js"
|
||||||
|
import * as DoorControlOutline from "./features/door_control_outline.js"
|
||||||
|
import * as HighlightSecretDoors from "./features/highlight_secret_doors.js"
|
||||||
|
import * as LockedDoorAlert from "./features/locked_door_alert.js"
|
||||||
|
import * as SynchronizedDoors from "./features/synchronized_doors.js"
|
||||||
|
import * as ToggleSecretDoor from "./features/toggle_secret_door.js"
|
||||||
|
|
||||||
|
import {performMigrations} from "./migration.js"
|
||||||
|
import {registerSettings} from "./settings.js"
|
||||||
|
|
||||||
|
Hooks.once("init", () => {
|
||||||
|
registerSettings()
|
||||||
|
hookDoorEvents()
|
||||||
|
hookWallConfigUpdate()
|
||||||
|
hookDoorControlDraw()
|
||||||
|
DoorControlIconScale.hookDoorControlReposition()
|
||||||
|
})
|
||||||
|
|
||||||
|
Hooks.once("ready", () => {
|
||||||
|
performMigrations()
|
||||||
|
})
|
||||||
|
|
||||||
|
Hooks.on("renderChatMessage", LockedDoorAlert.onRenderChatMessage)
|
||||||
|
|
||||||
|
Hooks.on("canvasReady", DoorControlIconScale.onCanvasReady)
|
||||||
|
Hooks.on("canvasReady", HighlightSecretDoors.onCanvasReady)
|
||||||
|
|
||||||
|
Hooks.on("updateWall", HighlightSecretDoors.onUpdateWall)
|
||||||
|
|
||||||
|
// Inject our custom settings into the WallConfig dialog
|
||||||
|
Hooks.on("renderWallConfig", SynchronizedDoors.onRederWallConfig)
|
||||||
|
|
||||||
|
// Hook the update function of the WallConfig dialog so we can store our custom data
|
||||||
|
function hookWallConfigUpdate() {
|
||||||
|
// Replace the original function with our custom one
|
||||||
|
const originalHandler = WallConfig.prototype._updateObject;
|
||||||
|
WallConfig.prototype._updateObject = async function (event, formData) {
|
||||||
|
await originalHandler.call(this, event, formData)
|
||||||
|
return SynchronizedDoors.onWallConfigUpdate.call(this, event, formData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookDoorControlDraw() {
|
||||||
|
const originalHandler = DoorControl.prototype.draw
|
||||||
|
DoorControl.prototype.draw = async function () {
|
||||||
|
const result = await originalHandler.call(this)
|
||||||
|
DoorControlIconScale.onDoorControlPostDraw.call(this)
|
||||||
|
DoorControlOutline.onDoorControlPostDraw.call(this)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook mouse events on DoorControls to perform our logic.
|
||||||
|
// If we successfully handled the event block the original handler. Forward the event otherwise.
|
||||||
|
function hookDoorEvents() {
|
||||||
|
// Replace the original mousedown handler with our custom one
|
||||||
|
const originalMouseDownHandler = DoorControl.prototype._onMouseDown
|
||||||
|
DoorControl.prototype._onMouseDown = function (event) {
|
||||||
|
// Call our handler first. Only allow the original handler to run if our handler returns true
|
||||||
|
const eventHandled = onDoorMouseDown.call(this, event)
|
||||||
|
if (eventHandled)
|
||||||
|
return
|
||||||
|
return originalMouseDownHandler.call(this, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the original rightdown handler with our custom one
|
||||||
|
const originalRightDownHandler = DoorControl.prototype._onRightDown
|
||||||
|
DoorControl.prototype._onRightDown = function (event) {
|
||||||
|
// Call our handler first. Only allow the original handler to run if our handler returns true
|
||||||
|
const eventHandled = onDoorRightDown.call(this, event)
|
||||||
|
if (eventHandled)
|
||||||
|
return
|
||||||
|
return originalRightDownHandler.call(this, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our custom handler for mousedown events on doors
|
||||||
|
function onDoorMouseDown(event) {
|
||||||
|
// If the user doesn't have the "door" permission we don't do anything.
|
||||||
|
if (!game.user.can("WALL_DOORS"))
|
||||||
|
return false
|
||||||
|
// If the game is paused don't do anything if the current player isn't the gm
|
||||||
|
if ( game.paused && !game.user.isGM )
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (ToggleSecretDoor.onDoorLeftClick.call(this, event))
|
||||||
|
return true
|
||||||
|
|
||||||
|
if (LockedDoorAlert.onDoorLeftClick.call(this))
|
||||||
|
return true
|
||||||
|
|
||||||
|
if (SynchronizedDoors.onDoorLeftClick.call(this))
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our custom handler for rightdown events on doors
|
||||||
|
function onDoorRightDown(event) {
|
||||||
|
|
||||||
|
if (SynchronizedDoors.onDoorRightClick.call(this))
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
57
src/migration.js
Normal file
57
src/migration.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {settingsKey} from "./settings.js"
|
||||||
|
|
||||||
|
const currentDataVersion = "1.1.0"
|
||||||
|
|
||||||
|
export function performMigrations() {
|
||||||
|
if (!game.user.isGM)
|
||||||
|
return
|
||||||
|
|
||||||
|
let dataVersion = game.settings.get(settingsKey, "dataVersion")
|
||||||
|
if (dataVersion === "fresh install")
|
||||||
|
{
|
||||||
|
game.settings.set(settingsKey, "dataVersion", currentDataVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataVersion === "1.0.0") {
|
||||||
|
dataVersion = "1.1.0"
|
||||||
|
ui.notifications.info(game.i18n.format("smart-doors.ui.messages.migrating", {version: dataVersion}))
|
||||||
|
|
||||||
|
// Make a dictionary that maps all door ids to their scenes
|
||||||
|
const walls = game.scenes.reduce((dict, scene) => {
|
||||||
|
scene.data.walls.forEach(wall => {
|
||||||
|
if (!wall.door)
|
||||||
|
return
|
||||||
|
dict[wall._id] = scene.id
|
||||||
|
})
|
||||||
|
return dict
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// Migrate all messages that have a (wall) source id
|
||||||
|
game.messages.forEach(async message => {
|
||||||
|
const wallId = message.data.flags.smartdoors?.sourceId
|
||||||
|
if (!wallId)
|
||||||
|
return
|
||||||
|
const flags = message.data.flags
|
||||||
|
delete flags.smartdoors.sourceId
|
||||||
|
const scene = walls[wallId]
|
||||||
|
// If there is no wall with this id anymore we can drop the value. It has no purpose anymore
|
||||||
|
if (!scene) {
|
||||||
|
if (!message.data.flags.smartdoors)
|
||||||
|
delete flags.smartdoors
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Assign the id and the scene id to the new data structure
|
||||||
|
flags.smartdoors.source = {wall: wallId, scene: scene}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to disable recursive here so deleting keys will actually work
|
||||||
|
message.update({flags: flags}, {diff: false, recursive: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
game.settings.set(settingsKey, "dataVersion", dataVersion)
|
||||||
|
ui.notifications.info(game.i18n.format("smart-doors.ui.messages.migrationDone", {version: dataVersion}))
|
||||||
|
}
|
||||||
|
if (dataVersion != currentDataVersion)
|
||||||
|
ui.notifications.error(game.i18n.format("smart-doors.ui.messages.unknownVersion", {version: dataVersion}), {permanent: true})
|
||||||
|
}
|
||||||
66
src/settings.js
Normal file
66
src/settings.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export const settingsKey = "smart-doors";
|
||||||
|
|
||||||
|
function reloadGM() {
|
||||||
|
if (game.user.isGM)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerSettings() {
|
||||||
|
game.settings.register(settingsKey, "dataVersion", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: String,
|
||||||
|
default: "fresh install"
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "doorControlSizeFactor", {
|
||||||
|
name: "smart-doors.settings.doorControlSizeFactor.name",
|
||||||
|
hint: "smart-doors.settings.doorControlSizeFactor.hint",
|
||||||
|
scope: "client",
|
||||||
|
config: true,
|
||||||
|
type: Number,
|
||||||
|
default: 1.5,
|
||||||
|
onChange: () => location.reload()
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "doorControlOutline", {
|
||||||
|
name: "smart-doors.settings.doorControlOutline.name",
|
||||||
|
hint: "smart-doors.settings.doorControlOutline.hint",
|
||||||
|
scope: "client",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
onChange: () => location.reload(),
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "highlightSecretDoors", {
|
||||||
|
name: "smart-doors.settings.highlightSecretDoors.name",
|
||||||
|
hint: "smart-doors.settings.highlightSecretDoors.hint",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
onChange: reloadGM,
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "toggleSecretDoors", {
|
||||||
|
name: "smart-doors.settings.toggleSecretDoors.name",
|
||||||
|
hint: "smart-doors.settings.toggleSecretDoors.hint",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "lockedDoorAlert", {
|
||||||
|
name: "smart-doors.settings.lockedDoorAlert.name",
|
||||||
|
hint: "smart-doors.settings.lockedDoorAlert.hint",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
game.settings.register(settingsKey, "synchronizedDoors", {
|
||||||
|
name: "smart-doors.settings.synchronizedDoors.name",
|
||||||
|
hint: "smart-doors.settings.synchronizedDoors.hint",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/util.js
Normal file
15
src/util.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Searches through all scenes for walls and returns those that match the given filter criteria.
|
||||||
|
export function filterAllWalls(filterFn) {
|
||||||
|
// Find all walls that match the filter criteria
|
||||||
|
const scenes = game.scenes.map((scene) => {return {scene: scene, walls: scene.data.walls.filter(filterFn)}})
|
||||||
|
// Drop all scenes that don't contain any results
|
||||||
|
return scenes.filter(scene => scene.walls.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searches through all scenes for a wall that matches the given filter criteria
|
||||||
|
export function findInAllWalls(filterFn) {
|
||||||
|
// TODO The performance of this could be increased by stopping the search on the first hit
|
||||||
|
const scenes = filterAllWalls(filterFn)
|
||||||
|
// If results were found take the first wall from the first scene.
|
||||||
|
return scenes[0]?.walls[0]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user