diff --git a/.editorconfig b/.editorconfig
index e376ef8..35c5fb2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,4 +3,5 @@ root = true
[*]
end_of_line = lf
indent_style = tab
-insert_final_newline = true
\ No newline at end of file
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/README.md b/README.md
index c368173..0d51655 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# Smart Doors
-Sends a message to chat when a player tries to open a locked door
+Makes doors smarter. Allows doors to synchronize across multiple scenes and sends chat messages when players try to open locked doors (and also tells you which of the doors).
## Planned features
- Attach macros to doors that are being executed when the door is being opened/closed
-- Synchronize the opening/closing of doors
- Give out keys to players, that allow them to lock/unlock associated doors
diff --git a/lang/en.json b/lang/en.json
index 31b3c29..92299a2 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -4,6 +4,16 @@
"lockedDoorAlert": {
"name": "Locked Door Alert",
"hint": "Send a message in chat when a player tried to open a locked door"
+ },
+ "synchronizedDoors": {
+ "name": "Synchronized Doors",
+ "hint": "Synchronize the state of configured doors"
+ }
+ },
+ "ui": {
+ "synchronizedDoors": {
+ "description": "State changes of doors in the same synchronization group will be synchronized across scenes. Leave blank to disable synchronization for this door.",
+ "groupName": "Synchronization Group"
}
}
}
diff --git a/main.js b/main.js
index 8b20993..8e827a2 100644
--- a/main.js
+++ b/main.js
@@ -5,8 +5,10 @@ const settingsKey = "smart-doors";
Hooks.once("init", () => {
registerSettings()
hookDoorEvents()
+ hookWallConfigUpdate()
})
+// 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 sourceId = message.data.flags.smartdoors?.sourceId
@@ -26,43 +28,215 @@ Hooks.on("renderChatMessage", (message, html, data) => {
html.on("mouseleave", mouseLeave);
})
+// 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 = `
+
+ `
+ 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) {
+ // TODO Bring newly merged doors in sync
+ const updateData = {flags: {smartdoors: {synchronizationGroup: formData.synchronizationGroup}}}
+
+ const ids = this.options.editTargets;
+ if (ids.length > 0) {
+ // Multiple walls are edited at once. Update all of them
+ const updateDataset = ids.reduce((dataset, id) => {
+ dataset.push({_id: id, ...updateData})
+ return dataset
+ }, [])
+ return canvas.scene.updateEmbeddedEntity("Wall", updateDataset)
+ }
+ else {
+ // Only one wall is being edited
+ return this.object.update(updateData);
+ }
+}
+
+// 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 by our custom one
+ // 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 continuePropagation = onDoorMousedown.call(this, event)
- if (!continuePropagation)
- return false
+ 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) {
+ return game.scenes.map((scene) => {return {scene: scene, walls: scene.data.walls.filter(filterFn)}});
+}
-function onDoorMousedown(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 true
+ 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 (lockedDoorAlertLeftClick.call(this))
return true
- // Create a chat message stating that a player tried to open a locked door
- if (game.settings.get(settingsKey, "lockedDoorAlert")) {
- if (this.wall.data.ds == CONST.WALL_DOOR_STATES.LOCKED && !game.user.isGM) {
- const message = {}
- message.user = game.user;
- message.content = "Just tried to open a locked door"
- message.sound = CONFIG.sounds.lock
- message.flags = {smartdoors: {sourceId: this.wall.data._id}}
- ChatMessage.create(message)
- return false
- }
- }
+ 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
+}
+
+// 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;
+ message.content = "Just tried to open a locked door"
+ message.sound = CONFIG.sounds.lock
+ message.flags = {smartdoors: {sourceId: this.wall.data._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 (this.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 registerSettings() {
game.settings.register(settingsKey, "lockedDoorAlert", {
name: "smart-doors.settings.lockedDoorAlert.name",
@@ -72,4 +246,12 @@ function registerSettings() {
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 aeb2e27..50a3f7e 100644
--- a/module.json
+++ b/module.json
@@ -1,10 +1,10 @@
{
"name": "smart-doors",
"title": "Smart Doors",
- "description": "Sends a message to chat when a player tries to open a locked door",
- "version": "0.1.alpha0",
+ "description": "Makes doors smarter. Allows doors to synchronize across multiple scenes and sends chat messages when players try to open locked doors.",
+ "version": "0.1.alpha1",
"minimumCoreVersion" : "0.7.7",
- "compatibleCoreVersion" : "0.7.7",
+ "compatibleCoreVersion" : "0.7.8",
"author": "Manuel Vögele",
"esmodules": [
"./main.js"