diff --git a/main.js b/main.js deleted file mode 100644 index 5b4404b..0000000 --- a/main.js +++ /dev/null @@ -1,475 +0,0 @@ -"use strict"; - -const settingsKey = "smart-doors"; -const currentDataVersion = "1.1.0" - -Hooks.once("init", () => { - registerSettings() - hookDoorEvents() - hookWallConfigUpdate() - hookDoorControlDraw() -}) - -Hooks.once("ready", () => { - performMigrations() -}) - -// 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 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); -}) - -// Adjust the repositioning formula for the door controls -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) -} - -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 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) -} - -// Set the size of all door controls in relation to the grid size so it'll have a constant percieved size -Hooks.on("canvasReady", (currentCanvas, wall, update) => { - const doors = currentCanvas.controls.doors.children - doors.forEach(control => fixDoorControlSize(control)) -}) - -// 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(); -} - -const SECRET_DOOR_TINT = 0x222222 - -// Tint all secret doors dark grey -Hooks.on("canvasReady", () => { - 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 -Hooks.on("updateWall", (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.") -}) - -// 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) { - 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 = ` +

${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"}) + } +} + +// 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] +}