Add door synchronization feature
This commit is contained in:
@@ -3,4 +3,5 @@ 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
lang/en.json
10
lang/en.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
218
main.js
218
main.js
@@ -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) {
|
|
||||||
const message = {}
|
return false
|
||||||
message.user = game.user;
|
}
|
||||||
message.content = "Just tried to open a locked door"
|
|
||||||
message.sound = CONFIG.sounds.lock
|
// Our custom handler for rightdown events on doors
|
||||||
message.flags = {smartdoors: {sourceId: this.wall.data._id}}
|
function onDoorRightDown(event) {
|
||||||
ChatMessage.create(message)
|
if (synchronizedDoorsRightClick.call(this))
|
||||||
return false
|
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
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user