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 = ` +

${game.i18n.localize("smart-doors.ui.synchronizedDoors.description")}

+
+ + +
+ ` + 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"