All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 lines
6.6 KiB

1 year ago
  1. /*
  2. * This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
  3. * Copyright (c) 2021 Matthew Haentschke.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, version 3.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import { logger } from './logger.js'
  18. import { Gateway } from './gateway.js'
  19. import { MODULE } from './module.js'
  20. import {queueUpdate} from './update-queue.js'
  21. import { Mutator } from './mutator.js'
  22. export class UserInterface {
  23. static register() {
  24. this.hooks();
  25. this.settings();
  26. }
  27. static hooks() {
  28. Hooks.on("renderActorSheet", UserInterface._renderActorSheet);
  29. }
  30. static settings() {
  31. const config = true;
  32. const settingsData = {
  33. showDismissLabel : {
  34. scope: "client", config, default: true, type: Boolean,
  35. },
  36. showRevertLabel : {
  37. scope: "client", config, default: true, type: Boolean,
  38. },
  39. dismissButtonScope : {
  40. scope: "client", config, default: 'spawned', type: String, choices: {
  41. disabled: MODULE.localize('setting.option.disabled'),
  42. spawned: MODULE.localize('setting.option.spawnedOnly'),
  43. all: MODULE.localize('setting.option.all')
  44. }
  45. },
  46. revertButtonBehavior : {
  47. scope: 'client', config, default: 'pop', type: String, choices: {
  48. disabled: MODULE.localize('setting.option.disabled'),
  49. pop: MODULE.localize('setting.option.popLatestMutation'),
  50. menu: MODULE.localize('setting.option.showMutationList')
  51. }
  52. }
  53. };
  54. MODULE.applySettings(settingsData);
  55. }
  56. static _renderActorSheet(app, html, data) {
  57. logger.debug("app |", app);
  58. logger.debug("html |", html);
  59. logger.debug("data |", data);
  60. UserInterface.addDismissButton(app, html, data);
  61. UserInterface.addRevertMutation(app, html, data);
  62. }
  63. static _shouldAddDismiss(token) {
  64. if ( !(token instanceof TokenDocument) ) return false;
  65. switch (MODULE.setting('dismissButtonScope')){
  66. case 'disabled':
  67. return false;
  68. case 'spawned':
  69. const controlData = token?.actor.getFlag(MODULE.data.name, 'control');
  70. /** do not add the button if we are not the controlling actor AND we aren't the GM */
  71. if ( !(controlData?.user === game.user.id) &&
  72. !game.user.isGM) return false;
  73. return !!controlData;
  74. case 'all':
  75. return true;
  76. }
  77. }
  78. static addDismissButton(app, html/*, data*/) {
  79. const token = app.token;
  80. /** this is not a warpgate spawned actor */
  81. if (!UserInterface._shouldAddDismiss(token)) return;
  82. /* do not add duplicate buttons! */
  83. if(html.closest('.app').find('.dismiss-warpgate').length !== 0) {
  84. logger.debug(MODULE.localize('debug.dismissPresent'));
  85. return;
  86. }
  87. const label = MODULE.setting('showDismissLabel') ? MODULE.localize("display.dismiss") : ""
  88. let dismissButton = $(`<a class="dismiss-warpgate" title="${MODULE.localize('display.dismiss')}"><i class="fas fa-user-slash"></i>${label}</a>`);
  89. dismissButton.click( (/*event*/) => {
  90. if (!token) {
  91. logger.error(MODULE.localize('error.sheetNoToken'));
  92. return;
  93. }
  94. const {id, parent} = token;
  95. Gateway.dismissSpawn(id, parent?.id);
  96. /** close the actor sheet if provided */
  97. app?.close({submit: false});
  98. });
  99. let title = html.closest('.app').find('.window-title');
  100. dismissButton.insertAfter(title);
  101. }
  102. static _shouldAddRevert(token) {
  103. if ( !(token instanceof TokenDocument) ) return false;
  104. const mutateStack = warpgate.mutationStack(token).stack;
  105. /* this is not a warpgate mutated actor,
  106. * or there are no remaining stacks to peel */
  107. if (mutateStack.length == 0) return false;
  108. return MODULE.setting('revertButtonBehavior') !== 'disabled';
  109. }
  110. static _getTokenFromApp(app) {
  111. const {token, actor} = app;
  112. const hasToken = token instanceof TokenDocument;
  113. if( !hasToken ) {
  114. /* check if linked and has an active token on scene */
  115. const candidates = actor?.getActiveTokens() ?? [];
  116. const linkedToken = candidates.find( t => (MODULE.isV10?t.document:t.data).actorLink )?.document ?? null;
  117. return linkedToken;
  118. }
  119. return token;
  120. }
  121. static addRevertMutation(app, html, data) {
  122. /* do not add duplicate buttons! */
  123. let foundButton = html.closest('.app').find('.revert-warpgate')
  124. /* we remove the current button on each render
  125. * in case the render was triggered by a mutation
  126. * event and we need to update the tool tip
  127. * on the revert stack
  128. */
  129. if (foundButton) {
  130. foundButton.remove();
  131. }
  132. const token = UserInterface._getTokenFromApp(app);
  133. if(!UserInterface._shouldAddRevert(token)) return;
  134. const mutateStack = token?.actor?.getFlag(MODULE.data.name, 'mutate');
  135. /* construct the revert button */
  136. const label = MODULE.setting('showRevertLabel') ? MODULE.localize("display.revert") : ""
  137. const stackCount = mutateStack.length > 1 ? ` 1/${mutateStack.length}` : '';
  138. let revertButton = $(`<a class="revert-warpgate" title="${MODULE.localize('display.revert')}${stackCount}"><i class="fas fa-undo-alt"></i>${label}</a>`);
  139. revertButton.click( async (event) => {
  140. const shouldShow = (shiftKey) => {
  141. const mode = MODULE.setting('revertButtonBehavior')
  142. const show = mode == 'menu' ? !shiftKey : shiftKey;
  143. return show;
  144. }
  145. let name = undefined;
  146. const showMenu = shouldShow(event.shiftKey);
  147. if (showMenu) {
  148. const buttons = mutateStack.map( mutation => {return {label: mutation.name, value: mutation.name}} )
  149. name = await warpgate.buttonDialog({buttons, title: MODULE.localize('display.revertDialogTitle')}, 'column');
  150. if (name === false) return;
  151. }
  152. /* need to queue this since 'click' could
  153. * happen at any time.
  154. * Do not need to remove the button here
  155. * as it will be refreshed on the render call
  156. */
  157. queueUpdate( async () => {
  158. await Mutator.revertMutation(token, name);
  159. app?.render(false);
  160. });
  161. });
  162. let title = html.closest('.app').find('.window-title');
  163. revertButton.insertAfter(title);
  164. }
  165. }