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.

460 lines
16 KiB

  1. import { showArtSelect } from '../token-variants.mjs';
  2. import {
  3. BASE_IMAGE_CATEGORIES,
  4. SEARCH_TYPE,
  5. updateActorImage,
  6. updateTokenImage,
  7. userRequiresImageCache,
  8. } from '../scripts/utils.js';
  9. import { addToQueue, ArtSelect, renderFromQueue } from './artSelect.js';
  10. import { getSearchOptions, TVA_CONFIG, updateSettings } from '../scripts/settings.js';
  11. import ConfigureSettings from './configureSettings.js';
  12. import MissingImageConfig from './missingImageConfig.js';
  13. import { cacheImages, doImageSearch } from '../scripts/search.js';
  14. async function autoApply(actor, image1, image2, formData, typeOverride) {
  15. let portraitFound = formData.ignorePortrait;
  16. let tokenFound = formData.ignoreToken;
  17. if (formData.diffImages) {
  18. let results = [];
  19. if (!formData.ignorePortrait) {
  20. results = await doImageSearch(actor.name, {
  21. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  22. simpleResults: true,
  23. searchOptions: formData.searchOptions,
  24. });
  25. if ((results ?? []).length != 0) {
  26. portraitFound = true;
  27. await updateActorImage(actor, results[0], false, formData.compendium);
  28. }
  29. }
  30. if (!formData.ignoreToken) {
  31. results = await doImageSearch(actor.prototypeToken.name, {
  32. searchType: SEARCH_TYPE.TOKEN,
  33. simpleResults: true,
  34. searchOptions: formData.searchOptions,
  35. });
  36. if ((results ?? []).length != 0) {
  37. tokenFound = true;
  38. updateTokenImage(results[0], {
  39. actor: actor,
  40. pack: formData.compendium,
  41. applyDefaultConfig: false,
  42. });
  43. }
  44. }
  45. } else {
  46. let results = await doImageSearch(actor.name, {
  47. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT_AND_TOKEN,
  48. simpleResults: true,
  49. searchOptions: formData.searchOptions,
  50. });
  51. if ((results ?? []).length != 0) {
  52. portraitFound = tokenFound = true;
  53. updateTokenImage(results[0], {
  54. actor: actor,
  55. actorUpdate: { img: results[0] },
  56. pack: formData.compendium,
  57. applyDefaultConfig: false,
  58. });
  59. }
  60. }
  61. if (!(tokenFound && portraitFound) && formData.autoDisplayArtSelect) {
  62. addToArtSelectQueue(actor, image1, image2, formData, typeOverride);
  63. }
  64. }
  65. function addToArtSelectQueue(actor, image1, image2, formData, typeOverride) {
  66. if (formData.diffImages) {
  67. if (!formData.ignorePortrait && !formData.ignoreToken) {
  68. addToQueue(actor.name, {
  69. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  70. object: actor,
  71. preventClose: true,
  72. image1: image1,
  73. image2: image2,
  74. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT,
  75. searchOptions: formData.searchOptions,
  76. callback: async function (imgSrc, _) {
  77. await updateActorImage(actor, imgSrc);
  78. showArtSelect(actor.prototypeToken.name, {
  79. searchType: typeOverride ?? SEARCH_TYPE.TOKEN,
  80. object: actor,
  81. force: true,
  82. image1: imgSrc,
  83. image2: image2,
  84. displayMode: ArtSelect.IMAGE_DISPLAY.TOKEN,
  85. searchOptions: formData.searchOptions,
  86. callback: (imgSrc, name) =>
  87. updateTokenImage(imgSrc, {
  88. actor: actor,
  89. imgName: name,
  90. applyDefaultConfig: false,
  91. }),
  92. });
  93. },
  94. });
  95. } else if (formData.ignorePortrait) {
  96. addToQueue(actor.name, {
  97. searchType: typeOverride ?? SEARCH_TYPE.TOKEN,
  98. object: actor,
  99. image1: image1,
  100. image2: image2,
  101. displayMode: ArtSelect.IMAGE_DISPLAY.TOKEN,
  102. searchOptions: formData.searchOptions,
  103. callback: async function (imgSrc, name) {
  104. updateTokenImage(imgSrc, {
  105. actor: actor,
  106. imgName: name,
  107. applyDefaultConfig: false,
  108. });
  109. },
  110. });
  111. } else if (formData.ignoreToken) {
  112. addToQueue(actor.name, {
  113. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT,
  114. object: actor,
  115. image1: image1,
  116. image2: image2,
  117. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT,
  118. searchOptions: formData.searchOptions,
  119. callback: async function (imgSrc, name) {
  120. await updateActorImage(actor, imgSrc);
  121. },
  122. });
  123. }
  124. } else {
  125. addToQueue(actor.name, {
  126. searchType: typeOverride ?? SEARCH_TYPE.PORTRAIT_AND_TOKEN,
  127. object: actor,
  128. image1: image1,
  129. image2: image2,
  130. displayMode: ArtSelect.IMAGE_DISPLAY.PORTRAIT_TOKEN,
  131. searchOptions: formData.searchOptions,
  132. callback: async function (imgSrc, name) {
  133. await updateActorImage(actor, imgSrc);
  134. updateTokenImage(imgSrc, {
  135. actor: actor,
  136. imgName: name,
  137. applyDefaultConfig: false,
  138. });
  139. },
  140. });
  141. }
  142. }
  143. export default class CompendiumMapConfig extends FormApplication {
  144. constructor() {
  145. super({}, {});
  146. this.searchOptions = deepClone(getSearchOptions());
  147. mergeObject(this.searchOptions, deepClone(TVA_CONFIG.compendiumMapper.searchOptions));
  148. this._fixSearchPaths();
  149. }
  150. static get defaultOptions() {
  151. return mergeObject(super.defaultOptions, {
  152. id: 'token-variants-compendium-map-config',
  153. classes: ['sheet'],
  154. template: 'modules/token-variants/templates/compendiumMap.html',
  155. resizable: false,
  156. minimizable: false,
  157. title: game.i18n.localize('token-variants.settings.compendium-mapper.Name'),
  158. width: 500,
  159. });
  160. }
  161. async getData(options) {
  162. let data = super.getData(options);
  163. data = mergeObject(data, TVA_CONFIG.compendiumMapper);
  164. const supportedPacks = ['Actor', 'Cards', 'Item', 'Macro', 'RollTable'];
  165. data.supportedPacks = supportedPacks.join(', ');
  166. const packs = [];
  167. game.packs.forEach((pack) => {
  168. if (!pack.locked && supportedPacks.includes(pack.documentName)) {
  169. packs.push({ title: pack.title, id: pack.collection, type: pack.documentName });
  170. }
  171. });
  172. data.compendiums = packs;
  173. data.compendium = TVA_CONFIG.compendiumMapper.compendium;
  174. data.categories = BASE_IMAGE_CATEGORIES.concat(TVA_CONFIG.customImageCategories);
  175. data.category = TVA_CONFIG.compendiumMapper.category;
  176. return data;
  177. }
  178. /**
  179. * @param {JQuery} html
  180. */
  181. activateListeners(html) {
  182. super.activateListeners(html);
  183. html.find('.token-variants-override-category').change(this._onCategoryOverride).trigger('change');
  184. html.find('.token-variants-auto-apply').change(this._onAutoApply);
  185. html.find('.token-variants-diff-images').change(this._onDiffImages);
  186. html.find(`.token-variants-search-options`).on('click', this._onSearchOptions.bind(this));
  187. html.find(`.token-variants-missing-images`).on('click', this._onMissingImages.bind(this));
  188. $(html).find('[name="compendium"]').change(this._onCompendiumSelect.bind(this)).trigger('change');
  189. }
  190. async _onAutoApply(event) {
  191. $(event.target).closest('form').find('.token-variants-auto-art-select').prop('disabled', !event.target.checked);
  192. }
  193. async _onCategoryOverride(event) {
  194. $(event.target).closest('form').find('.token-variants-category').prop('disabled', !event.target.checked);
  195. }
  196. async _onDiffImages(event) {
  197. $(event.target).closest('form').find('.token-variants-tp-ignore').prop('disabled', !event.target.checked);
  198. }
  199. async _onCompendiumSelect(event) {
  200. const compendium = game.packs.get($(event.target).val());
  201. if (compendium) {
  202. $(event.target)
  203. .closest('form')
  204. .find('.token-specific')
  205. .css('visibility', compendium.documentName === 'Actor' ? 'visible' : 'hidden');
  206. }
  207. }
  208. _fixSearchPaths() {
  209. if (!this.searchOptions.searchPaths?.length) {
  210. this.searchOptions.searchPaths = deepClone(TVA_CONFIG.searchPaths);
  211. }
  212. }
  213. async _onSearchOptions(event) {
  214. this._fixSearchPaths();
  215. new ConfigureSettings(this.searchOptions, {
  216. searchPaths: true,
  217. searchFilters: true,
  218. searchAlgorithm: true,
  219. randomizer: false,
  220. features: false,
  221. popup: false,
  222. permissions: false,
  223. worldHud: false,
  224. misc: false,
  225. activeEffects: false,
  226. }).render(true);
  227. }
  228. async _onMissingImages(event) {
  229. new MissingImageConfig().render(true);
  230. }
  231. async startMapping(formData) {
  232. if (formData.diffImages && formData.ignoreToken && formData.ignorePortrait) {
  233. return;
  234. }
  235. const originalSearchPaths = TVA_CONFIG.searchPaths;
  236. if (formData.searchOptions.searchPaths?.length) {
  237. TVA_CONFIG.searchPaths = formData.searchOptions.searchPaths;
  238. }
  239. if (formData.cache || !userRequiresImageCache() || formData.searchOptions.searchPaths?.length) {
  240. await cacheImages();
  241. }
  242. const endMapping = function () {
  243. if (formData.searchOptions.searchPaths?.length) {
  244. TVA_CONFIG.searchPaths = originalSearchPaths;
  245. cacheImages();
  246. }
  247. };
  248. const compendium = game.packs.get(formData.compendium);
  249. let missingImageList = TVA_CONFIG.compendiumMapper.missingImages
  250. .filter((mi) => mi.document === 'all' || mi.document === compendium.documentName)
  251. .map((mi) => mi.image);
  252. const typeOverride = formData.overrideCategory ? formData.category : null;
  253. let artSelectDisplayed = false;
  254. let processItem;
  255. if (compendium.documentName === 'Actor') {
  256. processItem = async function (item) {
  257. const actor = await compendium.getDocument(item._id);
  258. if (actor.name === '#[CF_tempEntity]') return; // Compendium Folders module's control entity
  259. let hasPortrait = actor.img !== CONST.DEFAULT_TOKEN && !missingImageList.includes(actor.img);
  260. let hasToken =
  261. actor.prototypeToken.texture.src !== CONST.DEFAULT_TOKEN &&
  262. !missingImageList.includes(actor.prototypeToken.texture.src);
  263. if (formData.syncImages && hasPortrait !== hasToken) {
  264. if (hasPortrait) {
  265. await updateTokenImage(actor.img, { actor: actor, applyDefaultConfig: false });
  266. } else {
  267. await updateActorImage(actor, actor.prototypeToken.texture.src);
  268. }
  269. hasPortrait = hasToken = true;
  270. }
  271. let includeThisActor = !(formData.missingOnly && hasPortrait) && !formData.ignorePortrait;
  272. let includeThisToken = !(formData.missingOnly && hasToken) && !formData.ignoreToken;
  273. const image1 = formData.showImages ? actor.img : '';
  274. const image2 = formData.showImages ? actor.prototypeToken.texture.src : '';
  275. if (includeThisActor || includeThisToken) {
  276. if (formData.autoApply) {
  277. await autoApply(actor, image1, image2, formData, typeOverride);
  278. } else {
  279. artSelectDisplayed = true;
  280. addToArtSelectQueue(actor, image1, image2, formData, typeOverride);
  281. }
  282. }
  283. };
  284. } else {
  285. processItem = async function (item) {
  286. const doc = await compendium.getDocument(item._id);
  287. if (doc.name === '#[CF_tempEntity]') return; // Compendium Folders module's control entity
  288. let defaultImg = '';
  289. if (doc.schema.fields.img || doc.schema.fields.texture) {
  290. defaultImg = (doc.schema.fields.img ?? doc.schema.fields.texture).initial();
  291. }
  292. const hasImage = doc.img != null && doc.img !== defaultImg && !missingImageList.includes(doc.img);
  293. let imageFound = false;
  294. if (formData.missingOnly && hasImage) return;
  295. if (formData.autoApply) {
  296. let results = await doImageSearch(doc.name, {
  297. searchType: typeOverride ?? compendium.documentName,
  298. simpleResults: true,
  299. searchOptions: formData.searchOptions,
  300. });
  301. if ((results ?? []).length != 0) {
  302. imageFound = true;
  303. await updateActorImage(doc, results[0], false, formData.compendium);
  304. }
  305. }
  306. if (!formData.autoApply || (formData.autoDisplayArtSelect && !imageFound)) {
  307. artSelectDisplayed = true;
  308. addToQueue(doc.name, {
  309. searchType: typeOverride ?? compendium.documentName,
  310. object: doc,
  311. image1: formData.showImages ? doc.img : '',
  312. displayMode: ArtSelect.IMAGE_DISPLAY.IMAGE,
  313. searchOptions: formData.searchOptions,
  314. callback: async function (imgSrc, name) {
  315. await updateActorImage(doc, imgSrc);
  316. },
  317. });
  318. }
  319. };
  320. }
  321. const allItems = [];
  322. compendium.index.forEach((k) => {
  323. allItems.push(k);
  324. });
  325. if (formData.autoApply) {
  326. let processing = true;
  327. let stopProcessing = false;
  328. let processed = 0;
  329. let counter = $(`<p>CACHING 0/${allItems.length}</p>`);
  330. let d;
  331. const startProcessing = async function () {
  332. while (processing && processed < allItems.length) {
  333. await new Promise((resolve, reject) => {
  334. setTimeout(async () => {
  335. await processItem(allItems[processed]);
  336. resolve();
  337. }, 10);
  338. });
  339. processed++;
  340. counter.html(`${processed}/${allItems.length}`);
  341. }
  342. if (stopProcessing || processed === allItems.length) {
  343. d?.close(true);
  344. addToQueue('DUMMY', { execute: endMapping });
  345. renderFromQueue();
  346. }
  347. };
  348. d = new Dialog({
  349. title: `Mapping: ${compendium.title}`,
  350. content: `
  351. <div style="text-align:center;" class="fa-3x"><i class="fas fa-spinner fa-pulse"></i></div>
  352. <div style="text-align:center;" class="counter"></div>
  353. <button style="width:100%;" class="pause"><i class="fas fa-play-circle"></i> Pause/Start</button>`,
  354. buttons: {
  355. cancel: {
  356. icon: '<i class="fas fa-stop-circle"></i>',
  357. label: 'Cancel',
  358. },
  359. },
  360. default: 'cancel',
  361. render: (html) => {
  362. html.find('.counter').append(counter);
  363. const spinner = html.find('.fa-spinner');
  364. html.find('.pause').on('click', () => {
  365. if (processing) {
  366. processing = false;
  367. spinner.removeClass('fa-pulse');
  368. } else {
  369. processing = true;
  370. startProcessing();
  371. spinner.addClass('fa-pulse');
  372. }
  373. });
  374. setTimeout(async () => startProcessing(), 1000);
  375. },
  376. close: () => {
  377. if (!stopProcessing) {
  378. stopProcessing = true;
  379. if (!processing) startProcessing();
  380. else processing = false;
  381. }
  382. },
  383. });
  384. d.render(true);
  385. } else {
  386. const tasks = allItems.map(processItem);
  387. Promise.all(tasks).then(() => {
  388. addToQueue('DUMMY', { execute: endMapping });
  389. renderFromQueue();
  390. if (formData.missingOnly && !artSelectDisplayed) {
  391. ui.notifications.warn('Token Variant Art: No documents found containing missing images.');
  392. }
  393. });
  394. }
  395. }
  396. /**
  397. * @param {Event} event
  398. * @param {Object} formData
  399. */
  400. async _updateObject(event, formData) {
  401. // If search paths are the same, remove them from searchOptions
  402. if (
  403. !this.searchOptions.searchPaths?.length ||
  404. isEmpty(diffObject(this.searchOptions.searchPaths, TVA_CONFIG.searchPaths))
  405. ) {
  406. this.searchOptions.searchPaths = [];
  407. }
  408. formData.searchOptions = this.searchOptions;
  409. await updateSettings({ compendiumMapper: formData });
  410. if (formData.compendium) {
  411. this.startMapping(formData);
  412. }
  413. }
  414. }