Add door synchronization feature

This commit is contained in:
2020-12-05 00:16:09 +01:00
parent fd8248696b
commit 177326dd61
5 changed files with 216 additions and 24 deletions

View File

@@ -4,3 +4,4 @@ root = true
end_of_line = lf end_of_line = lf
indent_style = tab indent_style = tab
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1,7 +1,6 @@
# Smart Doors # 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 ## Planned features
- Attach macros to doors that are being executed when the door is being opened/closed - 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 - Give out keys to players, that allow them to lock/unlock associated doors

View File

@@ -4,6 +4,16 @@
"lockedDoorAlert": { "lockedDoorAlert": {
"name": "Locked Door Alert", "name": "Locked Door Alert",
"hint": "Send a message in chat when a player tried to open a locked door" "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"
} }
} }
} }

204
main.js
View File

@@ -5,8 +5,10 @@ const settingsKey = "smart-doors";
Hooks.once("init", () => { Hooks.once("init", () => {
registerSettings() registerSettings()
hookDoorEvents() hookDoorEvents()
hookWallConfigUpdate()
}) })
// Tint the source door red when a locked alert is hovered
Hooks.on("renderChatMessage", (message, html, data) => { Hooks.on("renderChatMessage", (message, html, data) => {
// Tint the door that generated this message // Tint the door that generated this message
const sourceId = message.data.flags.smartdoors?.sourceId const sourceId = message.data.flags.smartdoors?.sourceId
@@ -26,43 +28,215 @@ Hooks.on("renderChatMessage", (message, html, data) => {
html.on("mouseleave", mouseLeave); 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 = `
<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) {
// 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() { 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 const originalMouseDownHandler = DoorControl.prototype._onMouseDown
DoorControl.prototype._onMouseDown = function (event) { DoorControl.prototype._onMouseDown = function (event) {
// Call our handler first. Only allow the original handler to run if our handler returns true // Call our handler first. Only allow the original handler to run if our handler returns true
const continuePropagation = onDoorMousedown.call(this, event) const eventHandled = onDoorMouseDown.call(this, event)
if (!continuePropagation) if (eventHandled)
return false return
return originalMouseDownHandler.call(this, event) 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 the user doesn't have the "door" permission we don't do anything.
if (!game.user.can("WALL_DOORS")) 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 the game is paused don't do anything if the current player isn't the gm
if ( game.paused && !game.user.isGM ) if ( game.paused && !game.user.isGM )
return false
if (lockedDoorAlertLeftClick.call(this))
return true return true
// Create a chat message stating that a player tried to open a locked door if (synchronizedDoorsLeftClick.call(this))
if (game.settings.get(settingsKey, "lockedDoorAlert")) { return true
if (this.wall.data.ds == CONST.WALL_DOOR_STATES.LOCKED && !game.user.isGM) {
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 = {} const message = {}
message.user = game.user; message.user = game.user;
message.content = "Just tried to open a locked door" message.content = "Just tried to open a locked door"
message.sound = CONFIG.sounds.lock message.sound = CONFIG.sounds.lock
message.flags = {smartdoors: {sourceId: this.wall.data._id}} message.flags = {smartdoors: {sourceId: this.wall.data._id}}
ChatMessage.create(message) 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 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 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() { function registerSettings() {
game.settings.register(settingsKey, "lockedDoorAlert", { game.settings.register(settingsKey, "lockedDoorAlert", {
name: "smart-doors.settings.lockedDoorAlert.name", name: "smart-doors.settings.lockedDoorAlert.name",
@@ -72,4 +246,12 @@ function registerSettings() {
type: Boolean, type: Boolean,
default: true, 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,
})
} }

View File

@@ -1,10 +1,10 @@
{ {
"name": "smart-doors", "name": "smart-doors",
"title": "Smart Doors", "title": "Smart Doors",
"description": "Sends a message to chat when a player tries to open a locked door", "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.alpha0", "version": "0.1.alpha1",
"minimumCoreVersion" : "0.7.7", "minimumCoreVersion" : "0.7.7",
"compatibleCoreVersion" : "0.7.7", "compatibleCoreVersion" : "0.7.8",
"author": "Manuel Vögele", "author": "Manuel Vögele",
"esmodules": [ "esmodules": [
"./main.js" "./main.js"