diff --git a/README.md b/README.md index cfa830e..e4213d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TODO -- macro button design + package - UI button +- macro button design + package - # Actor Sheet Locker for Foundry VTT diff --git a/src/actor-sheet-locker/lang/de.json b/src/actor-sheet-locker/lang/de.json index 07c0144..ffbf06f 100644 --- a/src/actor-sheet-locker/lang/de.json +++ b/src/actor-sheet-locker/lang/de.json @@ -54,6 +54,9 @@ "gmMsgSheet": "[{userName}] versucht, den Akteur [{sheetName}] zu verändern.", "gmMsgItem": "[{userName}] versucht, auf einem Charakterbogen den Item [{itemName}] hinzuzufügen oder zu ändern.", "gmMsgItemDeleted": "[{userName}] versucht, auf einem Charakterbogen den Item [{itemName}] zu löschen." + }, + "controlButton": { + "label": "Actor Sheet Locker ein- und ausschalten" } } } diff --git a/src/actor-sheet-locker/lang/en.json b/src/actor-sheet-locker/lang/en.json index d98e626..fdb66df 100644 --- a/src/actor-sheet-locker/lang/en.json +++ b/src/actor-sheet-locker/lang/en.json @@ -54,6 +54,9 @@ "gmMsgSheet": "[{userName}] is attempting to modify actor [{sheetName}].", "gmMsgItem": "[{userName}] is attempting to create or change item [{itemName}] in character sheet.", "gmMsgItemDeleted": "[{userName}] is attempting to delete item [{itemName}] from character sheet." + }, + "controlButton": { + "label": "Toggle Actor Sheet Locker on/off" } } } diff --git a/src/actor-sheet-locker/scripts/main.js b/src/actor-sheet-locker/scripts/main.js index e2d49f4..eff362e 100644 --- a/src/actor-sheet-locker/scripts/main.js +++ b/src/actor-sheet-locker/scripts/main.js @@ -27,34 +27,38 @@ let permanentUIMsgID; Logger.info(Config.data.modDescription); if (Config.setting('isActive')) { ActorSheetLocker.switchOn(); - ActorSheetLocker.stateChangeUIMessage(); + stateChangeUIMessage(); } else { ActorSheetLocker.switchOff(); } }); Hooks.on("preCreateItem", function (item, data, options, userid) { - return ActorSheetLocker.onItemChangedInSheet(item, data, options, userid); + return onItemChangedInSheet(item, data, options, userid); }); Hooks.on("preCreateActiveEffect", function (item, data, options, userid) { - return ActorSheetLocker.onItemChangedInSheet(item, data, options, userid); + return onItemChangedInSheet(item, data, options, userid); }); Hooks.on("preUpdateActor", function (actor, data, options, userid) { - return ActorSheetLocker.onSheetChanged(actor, data, options, userid); + return onSheetChanged(actor, data, options, userid); }); Hooks.on("preUpdateItem", function (item, data, options, userid) { - return ActorSheetLocker.onItemChangedInSheet(item, data, options, userid); + return onItemChangedInSheet(item, data, options, userid); }); Hooks.on("preUpdateActiveEffect", function (item, data, options, userid) { - return ActorSheetLocker.onItemChangedInSheet(item, data, options, userid); + return onItemChangedInSheet(item, data, options, userid); }); Hooks.on("preDeleteItem", function (item, options, userid) { - return ActorSheetLocker.onItemDeletedFromSheet(item, options, userid); + return onItemDeletedFromSheet(item, options, userid); }); Hooks.on("preDeleteActiveEffect", function (item, options, userid) { - return ActorSheetLocker.onItemDeletedFromSheet(item, options, userid); + return onItemDeletedFromSheet(item, options, userid); + }); + + Hooks.on("getSceneControlButtons", (controls) => { + renderControlButton(controls); }); } ) @@ -94,7 +98,7 @@ async function initExposedClasses() { window.ActorSheetLocker = ActorSheetLocker; Hooks.on("updateSetting", async function (setting) { if (setting.key.startsWith(Config.data.modID)) { - ActorSheetLocker.onGameSettingChanged(); + onGameSettingChanged(); } }); Logger.debug("Exposed classes are ready"); @@ -102,20 +106,200 @@ async function initExposedClasses() { async function initSocketlib() { socket = socketlib.registerModule(Config.data.modID); - socket.register("stateChangeUIMessage", ActorSheetLocker.stateChangeUIMessage); - socket.register("sheetEditGMAlertUIMessage", ActorSheetLocker.sheetEditGMAlertUIMessage); - socket.register("itemChangedGMAlertUIMessage", ActorSheetLocker.itemChangedGMAlertUIMessage); - socket.register("itemDeletedGMAlertUIMessage", ActorSheetLocker.itemDeletedGMAlertUIMessage); + socket.register("stateChangeUIMessage", stateChangeUIMessage); + socket.register("sheetEditGMAlertUIMessage", sheetEditGMAlertUIMessage); + socket.register("itemChangedGMAlertUIMessage", itemChangedGMAlertUIMessage); + socket.register("itemDeletedGMAlertUIMessage", itemDeletedGMAlertUIMessage); Logger.debug(`Module ${Config.data.modID} registered in socketlib.`); } +function renderControlButton(controls) { + if (game.user.isGM) { + let tokenControls = controls.find(control => control.name === "token") + tokenControls.tools.push({ + name: "toggleActorSheetLocker", + title: Config.localize('controlButton.label'), + icon: "fa-solid fa-user-lock", // see https://fontawesome.com/search?o=r&m=free + toggle: true, + active: Config.setting('isActive'), + onClick: (active) => { + Config.modifySetting('isActive', active); + } + }); + } +} + +function onSheetChanged(actorOrItem, data, options, userid) { + Logger.debug("actorOrItem:", actorOrItem); + Logger.debug("data: ", data); + Logger.debug("options: ", options); + Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); + if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { + if (!ActorSheetLocker.isSilentMode) { + ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { + permanent: false, + localize: false, + console: false + }); + if (Config.setting('alertGMOnReject')) { + socket.executeForAllGMs("sheetEditGMAlertUIMessage", game.users.get(userid)?.name, actorOrItem.name); + } + } else { + ActorSheetLocker.isSilentMode = false; + } + return false; + } +} + +function onItemChangedInSheet(item, data, options, userid) { + Logger.debug("item:", item); + Logger.debug("data: ", data); + Logger.debug("options: ", options); + Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); + if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { + + // check if event is allowed + if ( + data?.system?.worn != null + ) return true; + + if (!ActorSheetLocker.isSilentMode) { + ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { + permanent: false, + localize: false, + console: false + }); + if (Config.setting('alertGMOnReject')) { + socket.executeForAllGMs("itemChangedGMAlertUIMessage", game.users.get(userid)?.name, item.name); + } + } else { + ActorSheetLocker.isSilentMode = false; + } + return false; + } +} + +function onItemDeletedFromSheet(item, options, userid) { + Logger.debug("item:", item); + Logger.debug("options: ", options); + Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); + if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { + if (!ActorSheetLocker.isSilentMode) { + ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { + permanent: false, + localize: false, + console: false + }); + if (Config.setting('alertGMOnReject')) { + socket.executeForAllGMs("itemDeletedGMAlertUIMessage", game.users.get(userid)?.name, item.name); + } + } else { + ActorSheetLocker.isSilentMode = false; + } + return false; + } +} + +function onGameSettingChanged() { + ActorSheetLocker.isActive = Config.setting('isActive'); + + if (game.user.isGM && Config.setting('notifyOnChange')) { + // UI messages should only be triggered by the GM via sockets. + // This seems to be the only way to suppress them if needed. + if (!ActorSheetLocker.isSilentMode) { + socket.executeForEveryone("stateChangeUIMessage"); + } else { + ActorSheetLocker.isSilentMode = false; + } + } + + // Refresh scene control button to reflect new state of "isActive". + ui.controls.controls.find(control => control.name === "token").tools.find(tool => tool.name === "toggleActorSheetLocker").active = ActorSheetLocker.isActive; + ui.controls.render(); +} + +function stateChangeUIMessage() { + let message = + (ActorSheetLocker.isActive ? Config.localize('onOffUIMessage.whenON') : Config.localize('onOffUIMessage.whenOFF')); + + if (Config.setting('notifyOnChange')) { + + let isPermanent = ( + ActorSheetLocker.isActive && Config.setting('notifyPermanentlyWhileLOCKED') + || + !ActorSheetLocker.isActive && Config.setting('notifyPermanentlyWhileUNLOCKED') + ); + + // Clear previous permanent msg (if any) + if (permanentUIMsgID != null) { + ui.notifications.remove(permanentUIMsgID); + } + + if (ActorSheetLocker.isActive) { + permanentUIMsgID = ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { + permanent: isPermanent, + localize: false, + console: false + }); + } else { + permanentUIMsgID = ui.notifications.info(`[${Config.data.modTitle}] ${message}`, { + permanent: isPermanent, + localize: false, + console: false + }); + }; + if (!isPermanent) permanentUIMsgID = null; + } + Logger.info(message); +} + +function sheetEditGMAlertUIMessage(userName, sheetName) { + let message = + Config.localize('sheetEditRejected.gmMsgSheet') + .replace('{userName}', userName) + .replace('{sheetName}', sheetName); + ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { + permanent: false, + localize: false, + console: false + }); + Logger.warn(message); +} + +function itemChangedGMAlertUIMessage(userName, itemName) { + let message = + Config.localize('sheetEditRejected.gmMsgItem') + .replace('{userName}', userName) + .replace('{itemName}', itemName); + ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { + permanent: false, + localize: false, + console: false + }); + Logger.warn(message); +} + +function itemDeletedGMAlertUIMessage(userName, itemName) { + let message = + Config.localize('sheetEditRejected.gmMsgItemDeleted') + .replace('{userName}', userName) + .replace('{itemName}', itemName); + ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { + permanent: false, + localize: false, + console: false + }); + Logger.warn(message); +} + + /** * Public class for accessing this module through macro code */ export class ActorSheetLocker { - static #isActive = false; + static isActive = false; static #previousState; - static #isSilentMode; + static isSilentMode; static healthCheck() { alert(`Module '${Config.data.modTitle}' says: '${ready2play ? `I am alive!` : `I am NOT ready - something went wrong:(`}'` ); @@ -142,11 +326,11 @@ export class ActorSheetLocker { } static isOn() { - return this.#isActive; + return this.isActive; } static isOff() { - return !this.#isActive; + return !this.isActive; } /** @@ -156,170 +340,11 @@ export class ActorSheetLocker { * @returns {Promise} */ static async #switch(newStateIsON, silentMode = false) { - this.#isSilentMode = silentMode; - this.#previousState = this.#isActive; + ActorSheetLocker.isSilentMode = silentMode; + this.#previousState = this.isActive; // propagate change to the game settings, and wait for it to complete // It turned out to be much more stable here by waiting for game.settings to be updated. // Might be an ugly workaround, better ideas welcome! await Config.modifySetting('isActive', newStateIsON); } - - static onSheetChanged(actorOrItem, data, options, userid) { - Logger.debug("actorOrItem:", actorOrItem); - Logger.debug("data: ", data); - Logger.debug("options: ", options); - Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); - if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { - if (!this.#isSilentMode) { - ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { - permanent: false, - localize: false, - console: false - }); - if (Config.setting('alertGMOnReject')) { - socket.executeForAllGMs("sheetEditGMAlertUIMessage", game.users.get(userid)?.name, actorOrItem.name); - } - } else { - this.#isSilentMode = false; - } - return false; - } - } - - static onItemChangedInSheet(item, data, options, userid) { - Logger.debug("item:", item); - Logger.debug("data: ", data); - Logger.debug("options: ", options); - Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); - if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { - - // check if event is allowed - if ( - data?.system?.worn != null - ) return true; - - if (!this.#isSilentMode) { - ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { - permanent: false, - localize: false, - console: false - }); - if (Config.setting('alertGMOnReject')) { - socket.executeForAllGMs("itemChangedGMAlertUIMessage", game.users.get(userid)?.name, item.name); - } - } else { - this.#isSilentMode = false; - } - return false; - } - } - - static onItemDeletedFromSheet(item, options, userid) { - Logger.debug("item:", item); - Logger.debug("options: ", options); - Logger.debug("userid: ", userid, "game.user.id: ", game.user.id); - if (Config.setting('isActive') && (!game.user.isGM || Config.setting('lockForGM'))) { - if (!this.#isSilentMode) { - ui.notifications.error("[" + Config.data.modTitle + "] " + Config.localize('sheetEditRejected.playerMsg'), { - permanent: false, - localize: false, - console: false - }); - if (Config.setting('alertGMOnReject')) { - socket.executeForAllGMs("itemDeletedGMAlertUIMessage", game.users.get(userid)?.name, item.name); - } - } else { - this.#isSilentMode = false; - } - return false; - } - } - - static onGameSettingChanged() { - this.#isActive = Config.setting('isActive'); - - if (game.user.isGM && Config.setting('notifyOnChange')) { - // UI messages should only be triggered by the GM via sockets. - // This seems to be the only way to suppress them if needed. - if (!this.#isSilentMode) { - socket.executeForEveryone("stateChangeUIMessage"); - } else { - this.#isSilentMode = false; - } - } - } - - static stateChangeUIMessage() { - let message = - (ActorSheetLocker.#isActive ? Config.localize('onOffUIMessage.whenON') : Config.localize('onOffUIMessage.whenOFF')); - - if (Config.setting('notifyOnChange')) { - - let isPermanent = ( - ActorSheetLocker.#isActive && Config.setting('notifyPermanentlyWhileLOCKED') - || - !ActorSheetLocker.#isActive && Config.setting('notifyPermanentlyWhileUNLOCKED') - ); - - // Clear previous permanent msg (if any) - if (permanentUIMsgID != null) { - ui.notifications.remove(permanentUIMsgID); - } - - if (ActorSheetLocker.#isActive) { - permanentUIMsgID = ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { - permanent: isPermanent, - localize: false, - console: false - }); - } else { - permanentUIMsgID = ui.notifications.info(`[${Config.data.modTitle}] ${message}`, { - permanent: isPermanent, - localize: false, - console: false - }); - }; - if (!isPermanent) permanentUIMsgID = null; - } - Logger.info(message); - } - - static sheetEditGMAlertUIMessage(userName, sheetName) { - let message = - Config.localize('sheetEditRejected.gmMsgSheet') - .replace('{userName}', userName) - .replace('{sheetName}', sheetName); - ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { - permanent: false, - localize: false, - console: false - }); - Logger.warn(message); - } - - static itemChangedGMAlertUIMessage(userName, itemName) { - let message = - Config.localize('sheetEditRejected.gmMsgItem') - .replace('{userName}', userName) - .replace('{itemName}', itemName); - ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { - permanent: false, - localize: false, - console: false - }); - Logger.warn(message); - } - - static itemDeletedGMAlertUIMessage(userName, itemName) { - let message = - Config.localize('sheetEditRejected.gmMsgItemDeleted') - .replace('{userName}', userName) - .replace('{itemName}', itemName); - ui.notifications.error(`[${Config.data.modTitle}] ${message}`, { - permanent: false, - localize: false, - console: false - }); - Logger.warn(message); - } }