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.

368 lines
16 KiB

1 year ago
1 year ago
1 year ago
  1. import { registerWrappers } from "./patches.js";
  2. import { getWallBounds,getSceneSettings,migrateData,getTokenLOSheight } from "./utils.js";
  3. import { WallHeightToolTip } from './tooltip.js';
  4. import { MODULE_SCOPE, TOP_KEY, BOTTOM_KEY, ENABLE_ADVANCED_VISION_KEY, ENABLE_ADVANCED_MOVEMENT_KEY } from "./const.js";
  5. const MODULE_ID = 'wall-height';
  6. Object.defineProperty(Token.prototype, "losHeight", {
  7. get: function myProperty() {
  8. return getTokenLOSheight(this);
  9. },
  10. });
  11. Hooks.once("init",()=>{
  12. registerWrappers();
  13. registerSettings();
  14. if(game.settings.get(MODULE_ID,'enableTooltip')){
  15. Hooks.on("renderHeadsUpDisplay", (app, html, data) => {
  16. canvas.hud.wallHeight?.close();
  17. html.find("#wall-height-tooltip").remove();
  18. html.append('<template id="wall-height-tooltip"></template>');
  19. canvas.hud.wallHeight = new WallHeightToolTip();
  20. });
  21. }
  22. WallHeight.cacheSettings();
  23. });
  24. Hooks.once("ready", ()=>{
  25. if(!game.user.isGM) return;
  26. if(game.settings.get(MODULE_ID, 'migrateOnStartup')) WallHeight.migrateAll();
  27. if(game.settings.get(MODULE_ID, 'migrateTokenHeight')) {
  28. WallHeight.migrateTokenHeight();
  29. game.settings.set(MODULE_ID, 'migrateTokenHeight',false)
  30. }
  31. })
  32. Hooks.on("hoverWall",(wall, hovered)=>{
  33. if (!canvas.hud?.wallHeight || canvas.walls._chain) return;
  34. const {advancedVision} = getSceneSettings(canvas.scene);
  35. if(advancedVision!=null && !advancedVision)
  36. return;
  37. if (hovered) {
  38. canvas.hud.wallHeight.bind(wall);
  39. } else {
  40. canvas.hud.wallHeight.clear();
  41. }
  42. });
  43. Hooks.on("renderSceneControls", () => {
  44. if (canvas.hud?.wallHeight) canvas.hud.wallHeight.clear();
  45. });
  46. Hooks.on("deleteWall", () => {
  47. if (canvas.hud?.wallHeight) canvas.hud.wallHeight.clear();
  48. });
  49. Hooks.on("createWall", () => {
  50. if (canvas.hud?.wallHeight) canvas.hud.wallHeight.clear();
  51. });
  52. Hooks.on("updateWall", () => {
  53. if (canvas.hud?.wallHeight) canvas.hud.wallHeight.clear();
  54. });
  55. function registerSettings() {
  56. game.settings.register(MODULE_ID, 'enableTooltip', {
  57. name: game.i18n.localize(`${MODULE_SCOPE}.settings.enableTooltip.name`),
  58. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.enableTooltip.hint`),
  59. scope: 'world',
  60. config: true,
  61. type: Boolean,
  62. default: false
  63. });
  64. game.settings.register(MODULE_ID, 'enableWallText', {
  65. name: game.i18n.localize(`${MODULE_SCOPE}.settings.enableWallText.name`),
  66. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.enableWallText.hint`),
  67. scope: 'world',
  68. config: true,
  69. type: Boolean,
  70. default: true,
  71. onChange: () => {
  72. WallHeight.cacheSettings();
  73. },
  74. });
  75. game.settings.register(MODULE_ID, "blockSightMovement", {
  76. name: game.i18n.localize(`${MODULE_SCOPE}.settings.blockSightMovement.name`),
  77. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.blockSightMovement.hint`),
  78. scope: "world",
  79. config: true,
  80. type: Boolean,
  81. default: true,
  82. onChange: () => {
  83. WallHeight.cacheSettings();
  84. },
  85. });
  86. game.settings.register(MODULE_ID, "autoLOSHeight", {
  87. name: game.i18n.localize(`${MODULE_SCOPE}.settings.autoLOSHeight.name`),
  88. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.autoLOSHeight.hint`),
  89. scope: "world",
  90. config: true,
  91. type: Boolean,
  92. default: true,
  93. onChange: () => {
  94. WallHeight.cacheSettings();
  95. },
  96. });
  97. game.settings.register(MODULE_ID, "defaultLosHeight", {
  98. name: game.i18n.localize(`${MODULE_SCOPE}.settings.defaultLosHeight.name`),
  99. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.defaultLosHeight.hint`),
  100. scope: "world",
  101. config: true,
  102. type: Number,
  103. default: 6,
  104. onChange: () => {
  105. WallHeight.cacheSettings();
  106. },
  107. });
  108. game.settings.register(MODULE_ID, "losHeightMulti", {
  109. name: game.i18n.localize(`${MODULE_SCOPE}.settings.losHeightMulti.name`),
  110. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.losHeightMulti.hint`),
  111. scope: "world",
  112. config: true,
  113. type: Number,
  114. default: 0.89,
  115. range: {
  116. min: 0.1,
  117. max: 2,
  118. step: 0.01,
  119. },
  120. onChange: () => {
  121. WallHeight.cacheSettings();
  122. },
  123. });
  124. game.settings.register(MODULE_ID, 'globalAdvancedLighting', {
  125. name: game.i18n.localize(`${MODULE_SCOPE}.settings.globalAdvancedLighting.name`),
  126. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.globalAdvancedLighting.hint`),
  127. scope: 'world',
  128. config: true,
  129. type: Boolean,
  130. default: true,
  131. });
  132. game.settings.register(MODULE_ID, 'migrateOnStartup', {
  133. name: game.i18n.localize(`${MODULE_SCOPE}.settings.migrateOnStartup.name`),
  134. hint: game.i18n.localize(`${MODULE_SCOPE}.settings.migrateOnStartup.hint`),
  135. scope: 'world',
  136. config: true,
  137. type: Boolean,
  138. default: false
  139. });
  140. game.settings.register(MODULE_ID, 'migrateTokenHeight', {
  141. scope: 'world',
  142. config: false,
  143. type: Boolean,
  144. default: false
  145. });
  146. }
  147. Hooks.on("renderWallConfig", (app, html, data) => {
  148. const {advancedVision} = getSceneSettings(canvas.scene);
  149. if(!advancedVision) return;
  150. let { top, bottom } = getWallBounds(app.object);
  151. top = parseFloat(top);
  152. bottom = parseFloat(bottom);
  153. const topLabel = game.i18n.localize(`${MODULE_SCOPE}.WallHeightTopLabel`);
  154. const bottomLabel = game.i18n.localize(`${MODULE_SCOPE}.WallHeightBottomLabel`);
  155. const moduleLabel = game.i18n.localize(`${MODULE_SCOPE}.ModuleLabel`);
  156. html.find(`.door-options`).after(`
  157. <fieldset>
  158. <legend>${moduleLabel}</legend>
  159. <div class="form-group">
  160. <label>${topLabel}</label>
  161. <input name="flags.${MODULE_SCOPE}.${TOP_KEY}" type="number" step="any" value="${Number.isFinite(top) ? top : ""}" placeholder="Infinity">
  162. </div>
  163. <div class="form-group">
  164. <label>${bottomLabel}</label>
  165. <input name="flags.${MODULE_SCOPE}.${BOTTOM_KEY}" type="number" step="any" value="${Number.isFinite(bottom) ? bottom : ""}" placeholder="-Infinity">
  166. </div>
  167. </legend>
  168. </fieldset>
  169. `);
  170. app.setPosition({ height: "auto" });
  171. });
  172. Hooks.on("renderAmbientLightConfig", (app, html, data) => {
  173. const {advancedVision} = getSceneSettings(canvas.scene);
  174. if(!advancedVision) return;
  175. const label = game.i18n.localize(`${MODULE_SCOPE}.advancedLightingLabel`);
  176. const notes = game.i18n.localize(`${MODULE_SCOPE}.advancedLightingNotes`);
  177. const rangeTop = game.i18n.localize(`${MODULE_SCOPE}.levelsRangeTop`);
  178. const rangeBottom = game.i18n.localize(`${MODULE_SCOPE}.levelsRangeBottom`);
  179. const distance = (app.object.parent?.grid.units ?? game.system.grid.units) || game.i18n.localize(`${MODULE_SCOPE}.distance`);
  180. const checked = app.object.getFlag(MODULE_SCOPE, "advancedLighting") ? "checked" : "";
  181. const globalAdvancedLighting = game.settings.get(MODULE_ID, 'globalAdvancedLighting');
  182. const warnEnabledGlobally = `<p class="hint" style="color: red;">${game.i18n.localize(`${MODULE_SCOPE}.ALGlobal`)}</p>`;
  183. const hint = globalAdvancedLighting ? warnEnabledGlobally : ""
  184. const _injectHTML = `<div class="form-group">
  185. <label>${label}</label>
  186. <input type="checkbox" name="flags.${MODULE_SCOPE}.advancedLighting" ${checked} ${globalAdvancedLighting ? "disabled" : ""}>
  187. ${hint}
  188. <p class="hint">${notes}</p>
  189. </div>`
  190. html.find(`input[name="walls"]`).closest(".form-group").after(_injectHTML);
  191. app.setPosition({ height: "auto" });
  192. if(WallHeight.isLevels) return
  193. const bottom = app.object.flags?.levels?.rangeBottom;
  194. const top = app.object.flags?.levels?.rangeTop;
  195. const elevationHtml = `
  196. <div class="form-group">
  197. <label>${rangeTop} <span class="units">(${distance})</span></label>
  198. <div class="form-fields">
  199. <input name="flags.levels.rangeTop" type="number" step="any" value="${Number.isFinite(top) ? top : ""}" placeholder="Infinity">
  200. </div>
  201. </div>
  202. <div class="form-group">
  203. <label>${rangeBottom} <span class="units">(${distance})</span></label>
  204. <div class="form-fields">
  205. <input name="flags.levels.rangeBottom" type="number" step="any" value="${Number.isFinite(bottom) ? bottom : ""}" placeholder="-Infinity">
  206. </div>
  207. </div>
  208. `
  209. html.find(`input[name="config.dim"]`).closest(".form-group").after(elevationHtml);
  210. app.setPosition({ height: "auto" });
  211. })
  212. Hooks.on("renderAmbientSoundConfig", (app, html, data) => {
  213. const {advancedVision} = getSceneSettings(canvas.scene);
  214. if(!advancedVision) return;
  215. const label = game.i18n.localize(`${MODULE_SCOPE}.advancedLightingLabel`);
  216. const notes = game.i18n.localize(`${MODULE_SCOPE}.advancedLightingNotes`);
  217. const checked = app.object.getFlag(MODULE_SCOPE, "advancedLighting") ? "checked" : "";
  218. const rangeTop = game.i18n.localize(`${MODULE_SCOPE}.levelsRangeTop`);
  219. const rangeBottom = game.i18n.localize(`${MODULE_SCOPE}.levelsRangeBottom`);
  220. const distance = (canvas.scene.grid.units ?? game.system?.grid?.units) || game.i18n.localize(`${MODULE_SCOPE}.distance`);
  221. const globalAdvancedLighting = game.settings.get(MODULE_ID, 'globalAdvancedLighting');
  222. const warnEnabledGlobally = `<p class="hint" style="color: red;">${game.i18n.localize(`${MODULE_SCOPE}.ALGlobal`)}</p>`;
  223. const hint = globalAdvancedLighting ? warnEnabledGlobally : ""
  224. const _injectHTML = `<div class="form-group">
  225. <label>${label}</label>
  226. <input type="checkbox" name="flags.${MODULE_SCOPE}.advancedLighting" ${checked} ${globalAdvancedLighting ? "disabled" : ""}>
  227. ${hint}
  228. <p class="hint">${notes}</p>
  229. </div>`
  230. html.find(`input[name="walls"]`).closest(".form-group").after(_injectHTML);
  231. app.setPosition({ height: "auto" });
  232. if(WallHeight.isLevels) return
  233. const bottom = app.object.flags?.levels?.rangeBottom;
  234. const top = app.object.flags?.levels?.rangeTop;
  235. const elevationHtml = `
  236. <div class="form-group">
  237. <label>${rangeTop} <span class="units">(${distance})</span></label>
  238. <div class="form-fields">
  239. <input name="flags.levels.rangeTop" type="number" step="any" value="${Number.isFinite(top) ? top : ""}" placeholder="Infinity">
  240. </div>
  241. </div>
  242. <div class="form-group">
  243. <label>${rangeBottom} <span class="units">(${distance})</span></label>
  244. <div class="form-fields">
  245. <input name="flags.levels.rangeBottom" type="number" step="any" value="${Number.isFinite(bottom) ? bottom : ""}" placeholder="-Infinity">
  246. </div>
  247. </div>
  248. `
  249. html.find(`input[name="radius"]`).closest(".form-group").after(elevationHtml);
  250. app.setPosition({ height: "auto" });
  251. })
  252. Hooks.on("renderTokenConfig", (app, html, data) => {
  253. const tokenHeight = app.token.getFlag(MODULE_SCOPE, "tokenHeight") || 0;
  254. const label = game.i18n.localize(`${MODULE_SCOPE}.tokenHeightLabel`);
  255. const losHeight = app.object?.object?.losHeight ?? 0;
  256. const height = losHeight - app.token.elevation;
  257. const hint = game.i18n.localize(`${MODULE_SCOPE}.tokenHeightHint`).replace("{{height}}", height).replace("{{losHeight}}", losHeight);
  258. const distance = (canvas.scene.grid.units ?? game.system?.grid?.units) || game.i18n.localize(`${MODULE_SCOPE}.distance`);
  259. let newHtml = `
  260. <div class="form-group slim">
  261. <label>${label} <span class="units">(${distance})</span></label>
  262. <div class="form-fields">
  263. <input type="number" step="any" name="flags.${MODULE_SCOPE}.tokenHeight" placeholder="units" value="${tokenHeight}">
  264. </div>
  265. ${app.object?.object?.losHeight ? `<p class="hint">${hint}</p>` : ""}
  266. </div>
  267. `;
  268. html.find('input[name="lockRotation"]').closest(".form-group").before(newHtml);
  269. app.setPosition({ height: "auto" });
  270. });
  271. Hooks.on("renderSceneConfig", (app, html, data) => {
  272. const {advancedVision} = getSceneSettings(app.object);
  273. const enableVisionKeyLabel = game.i18n.localize(`${MODULE_SCOPE}.AdvancedVisionLabel`);
  274. const moduleLabel = game.i18n.localize(`${MODULE_SCOPE}.ModuleLabel`);
  275. html.find(`input[name="globalLightThreshold"]`).closest(".form-group").after(`
  276. <fieldset>
  277. <legend>${moduleLabel}</legend>
  278. <div class="form-group">
  279. <li class="flexrow">
  280. <label>${enableVisionKeyLabel}</label>
  281. <input name="flags.${MODULE_SCOPE}.${ENABLE_ADVANCED_VISION_KEY}" type="checkbox" data-dtype="Boolean" `+ ((advancedVision || advancedVision==null)?`checked`:``)+`>
  282. </li>
  283. </div>
  284. </fieldset>`
  285. );
  286. app.setPosition({ height: "auto" });
  287. });
  288. Handlebars.registerHelper('if_null', function(a, opts) {
  289. if (a == null) {
  290. return opts.fn(this);
  291. } else {
  292. return opts.inverse(this);
  293. }
  294. });
  295. // First time message
  296. Hooks.once("ready", () => {
  297. if(game.modules.get("levels-3d-preview")?.active) return;
  298. // Module title
  299. const MODULE_TITLE = game.modules.get(MODULE_ID).title;
  300. const FALLBACK_MESSAGE_TITLE = "Welcome to Wall Height";
  301. const FALLBACK_MESSAGE = `<large>
  302. <p><strong>I (theripper93) am now taking over the development of Wall Height, be sure to stop by my <a href="https://theripper93.com/">Discord</a> for help and support from the wonderful community as well as many resources</strong></p>
  303. <p>Thanks to all the patreons supporting the development of my modules making continued updates possible!</p>
  304. <p>If you want to support the development of the module you can do so here : <a href="https://www.patreon.com/theripper93">Patreon</a> </p></large>
  305. <p><strong>Patreons</strong> get also access to <strong>15+ premium modules</strong></p>
  306. <p>Want even more verticality? Go Full 3D</p>
  307. <h1>3D Canvas</h1>
  308. <iframe width="385" height="225" src="https://www.youtube.com/embed/rziXLJEfxqI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
  309. <p>Check 3D Canvas and all my other <strong>15+ premium modules <a href="https://theripper93.com/">Here</a></strong></p>`;
  310. // Settings key used for the "Don't remind me again" setting
  311. const DONT_REMIND_AGAIN_KEY = "popup-dont-remind-again";
  312. // Dialog code
  313. game.settings.register(MODULE_ID, DONT_REMIND_AGAIN_KEY, {
  314. name: "",
  315. default: false,
  316. type: Boolean,
  317. scope: "world",
  318. config: false,
  319. });
  320. if (game.user.isGM && !game.settings.get(MODULE_ID, DONT_REMIND_AGAIN_KEY)) {
  321. new Dialog({
  322. title: FALLBACK_MESSAGE_TITLE,
  323. content: FALLBACK_MESSAGE,
  324. buttons: {
  325. dont_remind: {
  326. icon: '<i class="fas fa-times"></i>',
  327. label: "Don't remind me again",
  328. callback: () => game.settings.set(MODULE_ID, DONT_REMIND_AGAIN_KEY, true),
  329. },
  330. },
  331. default: "dont_remind",
  332. }).render(true);
  333. }
  334. });