- `
- 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,
- })
-}
diff --git a/module.json b/module.json
index b6902fc..c3e8d27 100644
--- a/module.json
+++ b/module.json
@@ -7,7 +7,7 @@
"compatibleCoreVersion" : "0.7.8",
"author": "Manuel Vögele",
"esmodules": [
- "./main.js"
+ "src/main.js"
],
"languages": [
{
diff --git a/src/features/door_control_icon_scale.js b/src/features/door_control_icon_scale.js
new file mode 100644
index 0000000..918475e
--- /dev/null
+++ b/src/features/door_control_icon_scale.js
@@ -0,0 +1,45 @@
+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)
+ }
+}
+
+export 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 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
+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();
+}
diff --git a/src/features/highlight_secret_doors.js b/src/features/highlight_secret_doors.js
new file mode 100644
index 0000000..2ffdfa7
--- /dev/null
+++ b/src/features/highlight_secret_doors.js
@@ -0,0 +1,34 @@
+import {settingsKey} from "../settings.js"
+
+const SECRET_DOOR_TINT = 0x222222
+
+// 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.")
+}
diff --git a/src/features/locked_door_alert.js b/src/features/locked_door_alert.js
new file mode 100644
index 0000000..b14fccf
--- /dev/null
+++ b/src/features/locked_door_alert.js
@@ -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
+}
diff --git a/src/features/synchronized_doors.js b/src/features/synchronized_doors.js
new file mode 100644
index 0000000..0322c2b
--- /dev/null
+++ b/src/features/synchronized_doors.js
@@ -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 = `
+
+ `
+ 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}}))
+ })
+}
diff --git a/src/features/toggle_secret_door.js b/src/features/toggle_secret_door.js
new file mode 100644
index 0000000..4b32684
--- /dev/null
+++ b/src/features/toggle_secret_door.js
@@ -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
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..f49e5fb
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,97 @@
+"use strict";
+
+import * as DoorControlIconScale from "./features/door_control_icon_scale.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()
+ DoorControlIconScale.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)
+ }
+}
+
+// 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
+}
diff --git a/src/migration.js b/src/migration.js
new file mode 100644
index 0000000..fd36391
--- /dev/null
+++ b/src/migration.js
@@ -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})
+}
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..3bee4ad
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,57 @@
+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, "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,
+ })
+}
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 0000000..c052f2f
--- /dev/null
+++ b/src/util.js
@@ -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]
+}